source: subversion/trunk/roundcubemail/program/include/rcube_ui.php @ 6085

Last change on this file since 6085 was 6085, checked in by alec, 13 months ago
  • More public methods, code cleanup
  • Property svn:keywords set to Id Date Author
File size: 47.6 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_ui.php                                          |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
9 | Copyright (C) 2011-2012, Kolab Systems AG                             |
10 |                                                                       |
11 | Licensed under the GNU General Public License version 3 or            |
12 | any later version with exceptions for skins & plugins.                |
13 | See the README file for a full license statement.                     |
14 |                                                                       |
15 | PURPOSE:                                                              |
16 |   Provide basic functions for the webmail user interface              |
17 |                                                                       |
18 +-----------------------------------------------------------------------+
19 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
20 | Author: Aleksander Machniak <alec@alec.pl>                            |
21 +-----------------------------------------------------------------------+
22
23 $Id$
24
25*/
26
27/**
28 * Roundcube Webmail functions for user interface
29 *
30 * @package Core
31 * @author Thomas Bruederli <roundcube@gmail.com>
32 * @author Aleksander Machniak <alec@alec.pl>
33 */
34class rcube_ui
35{
36    // define constants for input reading
37    const INPUT_GET  = 0x0101;
38    const INPUT_POST = 0x0102;
39    const INPUT_GPC  = 0x0103;
40
41
42    /**
43     * Get localized text in the desired language
44     * It's a global wrapper for rcube::gettext()
45     *
46     * @param mixed  $p      Named parameters array or label name
47     * @param string $domain Domain to search in (e.g. plugin name)
48     *
49     * @return string Localized text
50     * @see rcube::gettext()
51     */
52    public static function label($p, $domain = null)
53    {
54        return rcube::get_instance()->gettext($p, $domain);
55    }
56
57
58    /**
59     * Global wrapper of rcube::text_exists()
60     * to check whether a text label is defined
61     *
62     * @see rcube::text_exists()
63     */
64    public static function label_exists($name, $domain = null, &$ref_domain = null)
65    {
66        return rcube::get_instance()->text_exists($name, $domain, $ref_domain);
67    }
68
69
70    /**
71     * Compose an URL for a specific action
72     *
73     * @param string  Request action
74     * @param array   More URL parameters
75     * @param string  Request task (omit if the same)
76     *
77     * @return The application URL
78     */
79    public static function url($action, $p = array(), $task = null)
80    {
81        return rcube::get_instance()->url((array)$p + array('_action' => $action, 'task' => $task));
82    }
83
84
85    /**
86     * Replacing specials characters to a specific encoding type
87     *
88     * @param  string  Input string
89     * @param  string  Encoding type: text|html|xml|js|url
90     * @param  string  Replace mode for tags: show|replace|remove
91     * @param  boolean Convert newlines
92     *
93     * @return string  The quoted string
94     */
95    public static function rep_specialchars_output($str, $enctype = '', $mode = '', $newlines = true)
96    {
97        static $html_encode_arr = false;
98        static $js_rep_table = false;
99        static $xml_rep_table = false;
100
101        // encode for HTML output
102        if ($enctype == 'html') {
103            if (!$html_encode_arr) {
104                $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);
105                unset($html_encode_arr['?']);
106            }
107
108            $encode_arr = $html_encode_arr;
109
110            // don't replace quotes and html tags
111            if ($mode == 'show' || $mode == '') {
112                $ltpos = strpos($str, '<');
113                if ($ltpos !== false && strpos($str, '>', $ltpos) !== false) {
114                    unset($encode_arr['"']);
115                    unset($encode_arr['<']);
116                    unset($encode_arr['>']);
117                    unset($encode_arr['&']);
118                }
119            }
120            else if ($mode == 'remove') {
121                $str = strip_tags($str);
122            }
123
124            $out = strtr($str, $encode_arr);
125
126            // avoid douple quotation of &
127            $out = preg_replace('/&amp;([A-Za-z]{2,6}|#[0-9]{2,4});/', '&\\1;', $out);
128
129            return $newlines ? nl2br($out) : $out;
130        }
131
132        // if the replace tables for XML and JS are not yet defined
133        if ($js_rep_table === false) {
134            $js_rep_table = $xml_rep_table = array();
135            $xml_rep_table['&'] = '&amp;';
136
137            // can be increased to support more charsets
138            for ($c=160; $c<256; $c++) {
139                $xml_rep_table[chr($c)] = "&#$c;";
140            }
141
142            $xml_rep_table['"'] = '&quot;';
143            $js_rep_table['"']  = '\\"';
144            $js_rep_table["'"]  = "\\'";
145            $js_rep_table["\\"] = "\\\\";
146            // Unicode line and paragraph separators (#1486310)
147            $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A8))] = '&#8232;';
148            $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A9))] = '&#8233;';
149        }
150
151        // encode for javascript use
152        if ($enctype == 'js') {
153            return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));
154        }
155
156        // encode for plaintext
157        if ($enctype == 'text') {
158            return str_replace("\r\n", "\n", $mode=='remove' ? strip_tags($str) : $str);
159        }
160
161        if ($enctype == 'url') {
162            return rawurlencode($str);
163        }
164
165        // encode for XML
166        if ($enctype == 'xml') {
167            return strtr($str, $xml_rep_table);
168        }
169
170        // no encoding given -> return original string
171        return $str;
172    }
173
174
175    /**
176     * Quote a given string.
177     * Shortcut function for self::rep_specialchars_output()
178     *
179     * @return string HTML-quoted string
180     * @see self::rep_specialchars_output()
181     */
182    public static function Q($str, $mode = 'strict', $newlines = true)
183    {
184        return self::rep_specialchars_output($str, 'html', $mode, $newlines);
185    }
186
187
188    /**
189     * Quote a given string for javascript output.
190     * Shortcut function for self::rep_specialchars_output()
191     *
192     * @return string JS-quoted string
193     * @see self::rep_specialchars_output()
194     */
195    public static function JQ($str)
196    {
197        return self::rep_specialchars_output($str, 'js');
198    }
199
200
201    /**
202     * Read input value and convert it for internal use
203     * Performs stripslashes() and charset conversion if necessary
204     *
205     * @param  string   Field name to read
206     * @param  int      Source to get value from (GPC)
207     * @param  boolean  Allow HTML tags in field value
208     * @param  string   Charset to convert into
209     *
210     * @return string   Field value or NULL if not available
211     */
212    public static function get_input_value($fname, $source, $allow_html=FALSE, $charset=NULL)
213    {
214        $value = NULL;
215
216        if ($source == self::INPUT_GET) {
217            if (isset($_GET[$fname])) {
218                $value = $_GET[$fname];
219            }
220        }
221        else if ($source == self::INPUT_POST) {
222            if (isset($_POST[$fname])) {
223                $value = $_POST[$fname];
224            }
225        }
226        else if ($source == self::INPUT_GPC) {
227            if (isset($_POST[$fname])) {
228                $value = $_POST[$fname];
229            }
230            else if (isset($_GET[$fname])) {
231                $value = $_GET[$fname];
232            }
233            else if (isset($_COOKIE[$fname])) {
234                $value = $_COOKIE[$fname];
235            }
236        }
237
238        return self::parse_input_value($value, $allow_html, $charset);
239    }
240
241    /**
242     * Parse/validate input value. See self::get_input_value()
243     * Performs stripslashes() and charset conversion if necessary
244     *
245     * @param  string   Input value
246     * @param  boolean  Allow HTML tags in field value
247     * @param  string   Charset to convert into
248     *
249     * @return string   Parsed value
250     */
251    public static function parse_input_value($value, $allow_html=FALSE, $charset=NULL)
252    {
253        global $OUTPUT;
254
255        if (empty($value)) {
256            return $value;
257        }
258
259        if (is_array($value)) {
260            foreach ($value as $idx => $val) {
261                $value[$idx] = self::parse_input_value($val, $allow_html, $charset);
262            }
263            return $value;
264        }
265
266        // strip single quotes if magic_quotes_sybase is enabled
267        if (ini_get('magic_quotes_sybase')) {
268            $value = str_replace("''", "'", $value);
269        }
270        // strip slashes if magic_quotes enabled
271        else if (get_magic_quotes_gpc() || get_magic_quotes_runtime()) {
272            $value = stripslashes($value);
273        }
274
275        // remove HTML tags if not allowed
276        if (!$allow_html) {
277            $value = strip_tags($value);
278        }
279
280        $output_charset = is_object($OUTPUT) ? $OUTPUT->get_charset() : null;
281
282        // remove invalid characters (#1488124)
283        if ($output_charset == 'UTF-8') {
284            $value = rcube_charset::clean($value);
285        }
286
287        // convert to internal charset
288        if ($charset && $output_charset) {
289            $value = rcube_charset::convert($value, $output_charset, $charset);
290        }
291
292        return $value;
293    }
294
295
296    /**
297     * Convert array of request parameters (prefixed with _)
298     * to a regular array with non-prefixed keys.
299     *
300     * @param int    $mode   Source to get value from (GPC)
301     * @param string $ignore PCRE expression to skip parameters by name
302     *
303     * @return array Hash array with all request parameters
304     */
305    public static function request2param($mode = null, $ignore = 'task|action')
306    {
307        $out = array();
308        $src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST);
309
310        foreach ($src as $key => $value) {
311            $fname = $key[0] == '_' ? substr($key, 1) : $key;
312            if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) {
313                $out[$fname] = self::get_input_value($key, $mode);
314            }
315        }
316
317        return $out;
318    }
319
320
321    /**
322     * Convert the given string into a valid HTML identifier
323     * Same functionality as done in app.js with rcube_webmail.html_identifier()
324     */
325    public static function html_identifier($str, $encode=false)
326    {
327        if ($encode) {
328            return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
329        }
330        else {
331            return asciiwords($str, true, '_');
332        }
333    }
334
335
336    /**
337     * Create a HTML table based on the given data
338     *
339     * @param  array  Named table attributes
340     * @param  mixed  Table row data. Either a two-dimensional array or a valid SQL result set
341     * @param  array  List of cols to show
342     * @param  string Name of the identifier col
343     *
344     * @return string HTML table code
345     */
346    public static function table_output($attrib, $table_data, $a_show_cols, $id_col)
347    {
348        global $RCMAIL;
349
350        $table = new html_table(/*array('cols' => count($a_show_cols))*/);
351
352        // add table header
353        if (!$attrib['noheader']) {
354            foreach ($a_show_cols as $col) {
355                $table->add_header($col, self::Q(self::label($col)));
356            }
357        }
358
359        if (!is_array($table_data)) {
360            $db = $RCMAIL->get_dbh();
361            while ($table_data && ($sql_arr = $db->fetch_assoc($table_data))) {
362                $table->add_row(array('id' => 'rcmrow' . self::html_identifier($sql_arr[$id_col])));
363
364                // format each col
365                foreach ($a_show_cols as $col) {
366                    $table->add($col, self::Q($sql_arr[$col]));
367                }
368            }
369        }
370        else {
371            foreach ($table_data as $row_data) {
372                $class = !empty($row_data['class']) ? $row_data['class'] : '';
373                $rowid = 'rcmrow' . self::html_identifier($row_data[$id_col]);
374
375                $table->add_row(array('id' => $rowid, 'class' => $class));
376
377                // format each col
378                foreach ($a_show_cols as $col) {
379                    $table->add($col, self::Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
380                }
381            }
382        }
383
384        return $table->show($attrib);
385    }
386
387
388    /**
389     * Create an edit field for inclusion on a form
390     *
391     * @param string col field name
392     * @param string value field value
393     * @param array attrib HTML element attributes for field
394     * @param string type HTML element type (default 'text')
395     *
396     * @return string HTML field definition
397     */
398    public static function get_edit_field($col, $value, $attrib, $type = 'text')
399    {
400        static $colcounts = array();
401
402        $fname = '_'.$col;
403        $attrib['name']  = $fname . ($attrib['array'] ? '[]' : '');
404        $attrib['class'] = trim($attrib['class'] . ' ff_' . $col);
405
406        if ($type == 'checkbox') {
407            $attrib['value'] = '1';
408            $input = new html_checkbox($attrib);
409        }
410        else if ($type == 'textarea') {
411            $attrib['cols'] = $attrib['size'];
412            $input = new html_textarea($attrib);
413        }
414        else if ($type == 'select') {
415            $input = new html_select($attrib);
416            $input->add('---', '');
417            $input->add(array_values($attrib['options']), array_keys($attrib['options']));
418        }
419        else if ($attrib['type'] == 'password') {
420            $input = new html_passwordfield($attrib);
421        }
422        else {
423            if ($attrib['type'] != 'text' && $attrib['type'] != 'hidden') {
424                $attrib['type'] = 'text';
425            }
426            $input = new html_inputfield($attrib);
427        }
428
429        // use value from post
430        if (isset($_POST[$fname])) {
431            $postvalue = self::get_input_value($fname, self::INPUT_POST, true);
432            $value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue;
433        }
434
435        $out = $input->show($value);
436
437        return $out;
438    }
439
440
441    /**
442     * Replace all css definitions with #container [def]
443     * and remove css-inlined scripting
444     *
445     * @param string CSS source code
446     * @param string Container ID to use as prefix
447     *
448     * @return string Modified CSS source
449     * @todo I'm not sure this should belong to rcube_ui class
450     */
451    public static function mod_css_styles($source, $container_id, $allow_remote=false)
452    {
453        $last_pos = 0;
454        $replacements = new rcube_string_replacer;
455
456        // ignore the whole block if evil styles are detected
457        $source   = self::xss_entity_decode($source);
458        $stripped = preg_replace('/[^a-z\(:;]/i', '', $source);
459        $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\(' : '');
460        if (preg_match("/$evilexpr/i", $stripped)) {
461            return '/* evil! */';
462        }
463
464        // cut out all contents between { and }
465        while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
466            $styles = substr($source, $pos+1, $pos2-($pos+1));
467
468            // check every line of a style block...
469            if ($allow_remote) {
470                $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY);
471                foreach ($a_styles as $line) {
472                    $stripped = preg_replace('/[^a-z\(:;]/i', '', $line);
473                    // ... and only allow strict url() values
474                    $regexp = '!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims';
475                    if (stripos($stripped, 'url(') && !preg_match($regexp, $line)) {
476                        $a_styles = array('/* evil! */');
477                        break;
478                    }
479                }
480                $styles = join(";\n", $a_styles);
481            }
482
483            $key = $replacements->add($styles);
484            $source = substr($source, 0, $pos+1)
485                . $replacements->get_replacement($key)
486                . substr($source, $pos2, strlen($source)-$pos2);
487            $last_pos = $pos+2;
488        }
489
490        // remove html comments and add #container to each tag selector.
491        // also replace body definition because we also stripped off the <body> tag
492        $styles = preg_replace(
493            array(
494                '/(^\s*<!--)|(-->\s*$)/',
495                '/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
496                '/'.preg_quote($container_id, '/').'\s+body/i',
497            ),
498            array(
499                '',
500                "\\1#$container_id \\2",
501                $container_id,
502            ),
503            $source);
504
505        // put block contents back in
506        $styles = $replacements->resolve($styles);
507
508        return $styles;
509    }
510
511
512    /**
513     * Convert the given date to a human readable form
514     * This uses the date formatting properties from config
515     *
516     * @param mixed  Date representation (string, timestamp or DateTime object)
517     * @param string Date format to use
518     * @param bool   Enables date convertion according to user timezone
519     *
520     * @return string Formatted date string
521     */
522    public static function format_date($date, $format = null, $convert = true)
523    {
524        global $RCMAIL, $CONFIG;
525
526        if (is_object($date) && is_a($date, 'DateTime')) {
527            $timestamp = $date->format('U');
528        }
529        else {
530            if (!empty($date)) {
531                $timestamp = rcube_strtotime($date);
532            }
533
534            if (empty($timestamp)) {
535                return '';
536            }
537
538            try {
539                $date = new DateTime("@".$timestamp);
540            }
541            catch (Exception $e) {
542                return '';
543            }
544        }
545
546        if ($convert) {
547            try {
548                // convert to the right timezone
549                $stz = date_default_timezone_get();
550                $tz = new DateTimeZone($RCMAIL->config->get('timezone'));
551                $date->setTimezone($tz);
552                date_default_timezone_set($tz->getName());
553
554                $timestamp = $date->format('U');
555            }
556            catch (Exception $e) {
557            }
558        }
559
560        // define date format depending on current time
561        if (!$format) {
562            $now         = time();
563            $now_date    = getdate($now);
564            $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
565            $week_limit  = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
566
567            if ($CONFIG['prettydate'] && $timestamp > $today_limit && $timestamp < $now) {
568                $format = $RCMAIL->config->get('date_today', $RCMAIL->config->get('time_format', 'H:i'));
569                $today  = true;
570            }
571            else if ($CONFIG['prettydate'] && $timestamp > $week_limit && $timestamp < $now) {
572                $format = $RCMAIL->config->get('date_short', 'D H:i');
573            }
574            else {
575                $format = $RCMAIL->config->get('date_long', 'Y-m-d H:i');
576            }
577        }
578
579        // strftime() format
580        if (preg_match('/%[a-z]+/i', $format)) {
581            $format = strftime($format, $timestamp);
582            if ($stz) {
583                date_default_timezone_set($stz);
584            }
585            return $today ? (self::label('today') . ' ' . $format) : $format;
586        }
587
588        // parse format string manually in order to provide localized weekday and month names
589        // an alternative would be to convert the date() format string to fit with strftime()
590        $out = '';
591        for ($i=0; $i<strlen($format); $i++) {
592            if ($format[$i] == "\\") {  // skip escape chars
593                continue;
594            }
595
596            // write char "as-is"
597            if ($format[$i] == ' ' || $format[$i-1] == "\\") {
598                $out .= $format[$i];
599            }
600            // weekday (short)
601            else if ($format[$i] == 'D') {
602                $out .= self::label(strtolower(date('D', $timestamp)));
603            }
604            // weekday long
605            else if ($format[$i] == 'l') {
606                $out .= self::label(strtolower(date('l', $timestamp)));
607            }
608            // month name (short)
609            else if ($format[$i] == 'M') {
610                $out .= self::label(strtolower(date('M', $timestamp)));
611            }
612            // month name (long)
613            else if ($format[$i] == 'F') {
614                $out .= self::label('long'.strtolower(date('M', $timestamp)));
615            }
616            else if ($format[$i] == 'x') {
617                $out .= strftime('%x %X', $timestamp);
618            }
619            else {
620                $out .= date($format[$i], $timestamp);
621            }
622        }
623
624        if ($today) {
625            $label = self::label('today');
626            // replcae $ character with "Today" label (#1486120)
627            if (strpos($out, '$') !== false) {
628                $out = preg_replace('/\$/', $label, $out, 1);
629            }
630            else {
631                $out = $label . ' ' . $out;
632            }
633        }
634
635        if ($stz) {
636            date_default_timezone_set($stz);
637        }
638
639        return $out;
640    }
641
642
643    /**
644     * Return folders list in HTML
645     *
646     * @param array $attrib Named parameters
647     *
648     * @return string HTML code for the gui object
649     */
650    public static function folder_list($attrib)
651    {
652        global $RCMAIL;
653        static $a_mailboxes;
654
655        $attrib += array('maxlength' => 100, 'realnames' => false, 'unreadwrap' => ' (%s)');
656
657        // add some labels to client
658        $RCMAIL->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
659
660        $type = $attrib['type'] ? $attrib['type'] : 'ul';
661        unset($attrib['type']);
662
663        if ($type == 'ul' && !$attrib['id']) {
664            $attrib['id'] = 'rcmboxlist';
665        }
666
667        if (empty($attrib['folder_name'])) {
668            $attrib['folder_name'] = '*';
669        }
670
671        // get current folder
672        $mbox_name = $RCMAIL->storage->get_folder();
673
674        // build the folders tree
675        if (empty($a_mailboxes)) {
676            // get mailbox list
677            $a_folders = $RCMAIL->storage->list_folders_subscribed(
678                '', $attrib['folder_name'], $attrib['folder_filter']);
679            $delimiter = $RCMAIL->storage->get_hierarchy_delimiter();
680            $a_mailboxes = array();
681
682            foreach ($a_folders as $folder) {
683                self::build_folder_tree($a_mailboxes, $folder, $delimiter);
684            }
685        }
686
687        // allow plugins to alter the folder tree or to localize folder names
688        $hook = $RCMAIL->plugins->exec_hook('render_mailboxlist', array(
689            'list'      => $a_mailboxes,
690            'delimiter' => $delimiter,
691            'type'      => $type,
692            'attribs'   => $attrib,
693        ));
694
695        $a_mailboxes = $hook['list'];
696        $attrib      = $hook['attribs'];
697
698        if ($type == 'select') {
699            $select = new html_select($attrib);
700
701            // add no-selection option
702            if ($attrib['noselection']) {
703                $select->add(self::label($attrib['noselection']), '');
704            }
705
706            self::render_folder_tree_select($a_mailboxes, $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
707            $out = $select->show($attrib['default']);
708        }
709        else {
710            $js_mailboxlist = array();
711            $out = html::tag('ul', $attrib, self::render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
712
713            $RCMAIL->output->add_gui_object('mailboxlist', $attrib['id']);
714            $RCMAIL->output->set_env('mailboxes', $js_mailboxlist);
715            $RCMAIL->output->set_env('unreadwrap', $attrib['unreadwrap']);
716            $RCMAIL->output->set_env('collapsed_folders', (string)$RCMAIL->config->get('collapsed_folders'));
717        }
718
719        return $out;
720    }
721
722
723    /**
724     * Return folders list as html_select object
725     *
726     * @param array $p  Named parameters
727     *
728     * @return html_select HTML drop-down object
729     */
730    public static function folder_selector($p = array())
731    {
732        global $RCMAIL;
733
734        $p += array('maxlength' => 100, 'realnames' => false);
735        $a_mailboxes = array();
736        $storage = $RCMAIL->get_storage();
737
738        if (empty($p['folder_name'])) {
739            $p['folder_name'] = '*';
740        }
741
742        if ($p['unsubscribed']) {
743            $list = $storage->list_folders('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
744        }
745        else {
746            $list = $storage->list_folders_subscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
747        }
748
749        $delimiter = $storage->get_hierarchy_delimiter();
750
751        foreach ($list as $folder) {
752            if (empty($p['exceptions']) || !in_array($folder, $p['exceptions'])) {
753                self::build_folder_tree($a_mailboxes, $folder, $delimiter);
754            }
755        }
756
757        $select = new html_select($p);
758
759        if ($p['noselection']) {
760            $select->add($p['noselection'], '');
761        }
762
763        self::render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
764
765        return $select;
766    }
767
768
769    /**
770     * Create a hierarchical array of the mailbox list
771     */
772    public static function build_folder_tree(&$arrFolders, $folder, $delm = '/', $path = '')
773    {
774        global $RCMAIL;
775
776        // Handle namespace prefix
777        $prefix = '';
778        if (!$path) {
779            $n_folder = $folder;
780            $folder = $RCMAIL->storage->mod_folder($folder);
781
782            if ($n_folder != $folder) {
783                $prefix = substr($n_folder, 0, -strlen($folder));
784            }
785        }
786
787        $pos = strpos($folder, $delm);
788
789        if ($pos !== false) {
790            $subFolders    = substr($folder, $pos+1);
791            $currentFolder = substr($folder, 0, $pos);
792
793            // sometimes folder has a delimiter as the last character
794            if (!strlen($subFolders)) {
795                $virtual = false;
796            }
797            else if (!isset($arrFolders[$currentFolder])) {
798                $virtual = true;
799            }
800            else {
801                $virtual = $arrFolders[$currentFolder]['virtual'];
802            }
803        }
804        else {
805            $subFolders    = false;
806            $currentFolder = $folder;
807            $virtual       = false;
808        }
809
810        $path .= $prefix . $currentFolder;
811
812        if (!isset($arrFolders[$currentFolder])) {
813            $arrFolders[$currentFolder] = array(
814                'id' => $path,
815                'name' => rcube_charset::convert($currentFolder, 'UTF7-IMAP'),
816                'virtual' => $virtual,
817                'folders' => array());
818        }
819        else {
820            $arrFolders[$currentFolder]['virtual'] = $virtual;
821        }
822
823        if (strlen($subFolders)) {
824            self::build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
825        }
826    }
827
828
829    /**
830     * Return html for a structured list &lt;ul&gt; for the mailbox tree
831     */
832    public static function render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel = 0)
833    {
834        global $RCMAIL;
835
836        $maxlength = intval($attrib['maxlength']);
837        $realnames = (bool)$attrib['realnames'];
838        $msgcounts = $RCMAIL->storage->get_cache('messagecount');
839        $collapsed = $RCMAIL->config->get('collapsed_folders');
840
841        $out = '';
842        foreach ($arrFolders as $key => $folder) {
843            $title        = null;
844            $folder_class = self::folder_classname($folder['id']);
845            $is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
846            $unread       = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
847
848            if ($folder_class && !$realnames) {
849                $foldername = $RCMAIL->gettext($folder_class);
850            }
851            else {
852                $foldername = $folder['name'];
853
854                // shorten the folder name to a given length
855                if ($maxlength && $maxlength > 1) {
856                    $fname = abbreviate_string($foldername, $maxlength);
857                    if ($fname != $foldername) {
858                        $title = $foldername;
859                    }
860                    $foldername = $fname;
861                }
862            }
863
864            // make folder name safe for ids and class names
865            $folder_id = self::html_identifier($folder['id'], true);
866            $classes   = array('mailbox');
867
868            // set special class for Sent, Drafts, Trash and Junk
869            if ($folder_class) {
870                $classes[] = $folder_class;
871            }
872
873            if ($folder['id'] == $mbox_name) {
874                $classes[] = 'selected';
875            }
876
877            if ($folder['virtual']) {
878                $classes[] = 'virtual';
879            }
880            else if ($unread) {
881                $classes[] = 'unread';
882            }
883
884            $js_name = self::JQ($folder['id']);
885            $html_name = self::Q($foldername) . ($unread ? html::span('unreadcount', sprintf($attrib['unreadwrap'], $unread)) : '');
886            $link_attrib = $folder['virtual'] ? array() : array(
887                'href' => self::url('', array('_mbox' => $folder['id'])),
888                'onclick' => sprintf("return %s.command('list','%s',this)", JS_OBJECT_NAME, $js_name),
889                'rel' => $folder['id'],
890                'title' => $title,
891            );
892
893            $out .= html::tag('li', array(
894                'id' => "rcmli".$folder_id,
895                'class' => join(' ', $classes),
896                'noclose' => true),
897                html::a($link_attrib, $html_name) .
898                (!empty($folder['folders']) ? html::div(array(
899                    'class' => ($is_collapsed ? 'collapsed' : 'expanded'),
900                    'style' => "position:absolute",
901                    'onclick' => sprintf("%s.command('collapse-folder', '%s')", JS_OBJECT_NAME, $js_name)
902                ), '&nbsp;') : ''));
903
904            $jslist[$folder_id] = array(
905                'id'      => $folder['id'],
906                'name'    => $foldername,
907                'virtual' => $folder['virtual']
908            );
909
910            if (!empty($folder['folders'])) {
911                $out .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
912                    self::render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
913            }
914
915            $out .= "</li>\n";
916        }
917
918        return $out;
919    }
920
921
922    /**
923     * Return html for a flat list <select> for the mailbox tree
924     */
925    public static function render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames = false, $nestLevel = 0, $opts = array())
926    {
927        global $RCMAIL;
928
929        $out = '';
930
931        foreach ($arrFolders as $key => $folder) {
932            // skip exceptions (and its subfolders)
933            if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
934                continue;
935            }
936
937            // skip folders in which it isn't possible to create subfolders
938            if (!empty($opts['skip_noinferiors'])) {
939                $attrs = $RCMAIL->storage->folder_attributes($folder['id']);
940                if ($attrs && in_array('\\Noinferiors', $attrs)) {
941                    continue;
942                }
943            }
944
945            if (!$realnames && ($folder_class = self::folder_classname($folder['id']))) {
946                $foldername = self::label($folder_class);
947            }
948            else {
949                $foldername = $folder['name'];
950
951                // shorten the folder name to a given length
952                if ($maxlength && $maxlength > 1) {
953                    $foldername = abbreviate_string($foldername, $maxlength);
954                }
955
956                 $select->add(str_repeat('&nbsp;', $nestLevel*4) . $foldername, $folder['id']);
957
958                if (!empty($folder['folders'])) {
959                    $out .= self::render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
960                        $select, $realnames, $nestLevel+1, $opts);
961                }
962            }
963        }
964
965        return $out;
966    }
967
968
969    /**
970     * Return internal name for the given folder if it matches the configured special folders
971     */
972    public static function folder_classname($folder_id)
973    {
974        global $CONFIG;
975
976        if ($folder_id == 'INBOX') {
977            return 'inbox';
978        }
979
980        // for these mailboxes we have localized labels and css classes
981        foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
982        {
983            if ($folder_id == $CONFIG[$smbx.'_mbox']) {
984                return $smbx;
985            }
986        }
987    }
988
989
990    /**
991     * Try to localize the given IMAP folder name.
992     * UTF-7 decode it in case no localized text was found
993     *
994     * @param string $name  Folder name
995     *
996     * @return string Localized folder name in UTF-8 encoding
997     */
998    public static function localize_foldername($name)
999    {
1000        if ($folder_class = self::folder_classname($name)) {
1001            return self::label($folder_class);
1002        }
1003        else {
1004            return rcube_charset::convert($name, 'UTF7-IMAP');
1005        }
1006    }
1007
1008
1009    public static function localize_folderpath($path)
1010    {
1011        global $RCMAIL;
1012
1013        $protect_folders = $RCMAIL->config->get('protect_default_folders');
1014        $default_folders = (array) $RCMAIL->config->get('default_folders');
1015        $delimiter       = $RCMAIL->storage->get_hierarchy_delimiter();
1016        $path            = explode($delimiter, $path);
1017        $result          = array();
1018
1019        foreach ($path as $idx => $dir) {
1020            $directory = implode($delimiter, array_slice($path, 0, $idx+1));
1021            if ($protect_folders && in_array($directory, $default_folders)) {
1022                unset($result);
1023                $result[] = self::localize_foldername($directory);
1024            }
1025            else {
1026                $result[] = rcube_charset::convert($dir, 'UTF7-IMAP');
1027            }
1028        }
1029
1030        return implode($delimiter, $result);
1031    }
1032
1033
1034    public static function quota_display($attrib)
1035    {
1036        global $OUTPUT;
1037
1038        if (!$attrib['id']) {
1039            $attrib['id'] = 'rcmquotadisplay';
1040        }
1041
1042        $_SESSION['quota_display'] = !empty($attrib['display']) ? $attrib['display'] : 'text';
1043
1044        $OUTPUT->add_gui_object('quotadisplay', $attrib['id']);
1045
1046        $quota = self::quota_content($attrib);
1047
1048        $OUTPUT->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready');
1049
1050        return html::span($attrib, '');
1051    }
1052
1053
1054    public static function quota_content($attrib = null)
1055    {
1056        global $RCMAIL;
1057
1058        $quota = $RCMAIL->storage->get_quota();
1059        $quota = $RCMAIL->plugins->exec_hook('quota', $quota);
1060
1061        $quota_result = (array) $quota;
1062        $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
1063
1064        if (!$quota['total'] && $RCMAIL->config->get('quota_zero_as_unlimited')) {
1065            $quota_result['title']   = self::label('unlimited');
1066            $quota_result['percent'] = 0;
1067        }
1068        else if ($quota['total']) {
1069            if (!isset($quota['percent'])) {
1070                $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
1071            }
1072
1073            $title = sprintf('%s / %s (%.0f%%)',
1074                self::show_bytes($quota['used'] * 1024), self::show_bytes($quota['total'] * 1024),
1075                $quota_result['percent']);
1076
1077            $quota_result['title'] = $title;
1078
1079            if ($attrib['width']) {
1080                $quota_result['width'] = $attrib['width'];
1081            }
1082            if ($attrib['height']) {
1083                $quota_result['height'] = $attrib['height'];
1084            }
1085        }
1086        else {
1087            $quota_result['title']   = self::label('unknown');
1088            $quota_result['percent'] = 0;
1089        }
1090
1091        return $quota_result;
1092    }
1093
1094
1095    /**
1096     * Outputs error message according to server error/response codes
1097     *
1098     * @param string $fallback       Fallback message label
1099     * @param array  $fallback_args  Fallback message label arguments
1100     */
1101    public static function display_server_error($fallback = null, $fallback_args = null)
1102    {
1103        global $RCMAIL;
1104
1105        $err_code = $RCMAIL->storage->get_error_code();
1106        $res_code = $RCMAIL->storage->get_response_code();
1107
1108        if ($err_code < 0) {
1109            $RCMAIL->output->show_message('storageerror', 'error');
1110        }
1111        else if ($res_code == rcube_storage::NOPERM) {
1112            $RCMAIL->output->show_message('errornoperm', 'error');
1113        }
1114        else if ($res_code == rcube_storage::READONLY) {
1115            $RCMAIL->output->show_message('errorreadonly', 'error');
1116        }
1117        else if ($err_code && ($err_str = $RCMAIL->storage->get_error_str())) {
1118            // try to detect access rights problem and display appropriate message
1119            if (stripos($err_str, 'Permission denied') !== false) {
1120                $RCMAIL->output->show_message('errornoperm', 'error');
1121            }
1122            else {
1123                $RCMAIL->output->show_message('servererrormsg', 'error', array('msg' => $err_str));
1124            }
1125        }
1126        else if ($fallback) {
1127            $RCMAIL->output->show_message($fallback, 'error', $fallback_args);
1128        }
1129    }
1130
1131
1132    /**
1133     * Generate CSS classes from mimetype and filename extension
1134     *
1135     * @param string $mimetype  Mimetype
1136     * @param string $filename  Filename
1137     *
1138     * @return string CSS classes separated by space
1139     */
1140    public static function file2class($mimetype, $filename)
1141    {
1142        list($primary, $secondary) = explode('/', $mimetype);
1143
1144        $classes = array($primary ? $primary : 'unknown');
1145        if ($secondary) {
1146            $classes[] = $secondary;
1147        }
1148        if (preg_match('/\.([a-z0-9]+)$/i', $filename, $m)) {
1149            $classes[] = $m[1];
1150        }
1151
1152        return strtolower(join(" ", $classes));
1153    }
1154
1155
1156    /**
1157     * Output HTML editor scripts
1158     *
1159     * @param string $mode  Editor mode
1160     */
1161    public static function html_editor($mode = '')
1162    {
1163        global $RCMAIL;
1164
1165        $hook = $RCMAIL->plugins->exec_hook('html_editor', array('mode' => $mode));
1166
1167        if ($hook['abort']) {
1168            return;
1169        }
1170
1171        $lang = strtolower($_SESSION['language']);
1172
1173        // TinyMCE uses two-letter lang codes, with exception of Chinese
1174        if (strpos($lang, 'zh_') === 0) {
1175            $lang = str_replace('_', '-', $lang);
1176        }
1177        else {
1178            $lang = substr($lang, 0, 2);
1179        }
1180
1181        if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js')) {
1182            $lang = 'en';
1183        }
1184
1185        $script = json_encode(array(
1186            'mode'       => $mode,
1187            'lang'       => $lang,
1188            'skin_path'  => $RCMAIL->output->get_skin_path(),
1189            'spellcheck' => intval($RCMAIL->config->get('enable_spellcheck')),
1190            'spelldict'  => intval($RCMAIL->config->get('spellcheck_dictionary'))
1191        ));
1192
1193        $RCMAIL->output->include_script('tiny_mce/tiny_mce.js');
1194        $RCMAIL->output->include_script('editor.js');
1195        $RCMAIL->output->add_script("rcmail_editor_init($script)", 'docready');
1196    }
1197
1198
1199    /**
1200     * Replaces TinyMCE's emoticon images with plain-text representation
1201     *
1202     * @param string $html  HTML content
1203     *
1204     * @return string HTML content
1205     */
1206    public static function replace_emoticons($html)
1207    {
1208        $emoticons = array(
1209            '8-)' => 'smiley-cool',
1210            ':-#' => 'smiley-foot-in-mouth',
1211            ':-*' => 'smiley-kiss',
1212            ':-X' => 'smiley-sealed',
1213            ':-P' => 'smiley-tongue-out',
1214            ':-@' => 'smiley-yell',
1215            ":'(" => 'smiley-cry',
1216            ':-(' => 'smiley-frown',
1217            ':-D' => 'smiley-laughing',
1218            ':-)' => 'smiley-smile',
1219            ':-S' => 'smiley-undecided',
1220            ':-$' => 'smiley-embarassed',
1221            'O:-)' => 'smiley-innocent',
1222            ':-|' => 'smiley-money-mouth',
1223            ':-O' => 'smiley-surprised',
1224            ';-)' => 'smiley-wink',
1225        );
1226
1227        foreach ($emoticons as $idx => $file) {
1228            // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
1229            $search[]  = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
1230            $replace[] = $idx;
1231        }
1232
1233        return preg_replace($search, $replace, $html);
1234    }
1235
1236
1237    /**
1238     * File upload progress handler.
1239     */
1240    public static function upload_progress()
1241    {
1242        global $RCMAIL;
1243
1244        $prefix = ini_get('apc.rfc1867_prefix');
1245        $params = array(
1246            'action' => $RCMAIL->action,
1247            'name' => self::get_input_value('_progress', self::INPUT_GET),
1248        );
1249
1250        if (function_exists('apc_fetch')) {
1251            $status = apc_fetch($prefix . $params['name']);
1252
1253            if (!empty($status)) {
1254                $status['percent'] = round($status['current']/$status['total']*100);
1255                $params = array_merge($status, $params);
1256            }
1257        }
1258
1259        if (isset($params['percent']))
1260            $params['text'] = self::label(array('name' => 'uploadprogress', 'vars' => array(
1261                'percent' => $params['percent'] . '%',
1262                'current' => self::show_bytes($params['current']),
1263                'total'   => self::show_bytes($params['total'])
1264        )));
1265
1266        $RCMAIL->output->command('upload_progress_update', $params);
1267        $RCMAIL->output->send();
1268    }
1269
1270
1271    /**
1272     * Initializes file uploading interface.
1273     */
1274    public static function upload_init()
1275    {
1276        global $RCMAIL;
1277
1278        // Enable upload progress bar
1279        if (($seconds = $RCMAIL->config->get('upload_progress')) && ini_get('apc.rfc1867')) {
1280            if ($field_name = ini_get('apc.rfc1867_name')) {
1281                $RCMAIL->output->set_env('upload_progress_name', $field_name);
1282                $RCMAIL->output->set_env('upload_progress_time', (int) $seconds);
1283            }
1284        }
1285
1286        // find max filesize value
1287        $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
1288        $max_postsize = parse_bytes(ini_get('post_max_size'));
1289        if ($max_postsize && $max_postsize < $max_filesize) {
1290            $max_filesize = $max_postsize;
1291        }
1292
1293        $RCMAIL->output->set_env('max_filesize', $max_filesize);
1294        $max_filesize = self::show_bytes($max_filesize);
1295        $RCMAIL->output->set_env('filesizeerror', self::label(array(
1296            'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
1297
1298        return $max_filesize;
1299    }
1300
1301
1302    /**
1303     * Initializes client-side autocompletion.
1304     */
1305    public static function autocomplete_init()
1306    {
1307        global $RCMAIL;
1308        static $init;
1309
1310        if ($init) {
1311            return;
1312        }
1313
1314        $init = 1;
1315
1316        if (($threads = (int)$RCMAIL->config->get('autocomplete_threads')) > 0) {
1317            $book_types = (array) $RCMAIL->config->get('autocomplete_addressbooks', 'sql');
1318            if (count($book_types) > 1) {
1319                $RCMAIL->output->set_env('autocomplete_threads', $threads);
1320                $RCMAIL->output->set_env('autocomplete_sources', $book_types);
1321            }
1322        }
1323
1324        $RCMAIL->output->set_env('autocomplete_max', (int)$RCMAIL->config->get('autocomplete_max', 15));
1325        $RCMAIL->output->set_env('autocomplete_min_length', $RCMAIL->config->get('autocomplete_min_length'));
1326        $RCMAIL->output->add_label('autocompletechars', 'autocompletemore');
1327    }
1328
1329
1330    /**
1331     * Returns supported font-family specifications
1332     *
1333     * @param string $font  Font name
1334     *
1335     * @param string|array Font-family specification array or string (if $font is used)
1336     */
1337    public static function font_defs($font = null)
1338    {
1339        $fonts = array(
1340            'Andale Mono'   => '"Andale Mono",Times,monospace',
1341            'Arial'         => 'Arial,Helvetica,sans-serif',
1342            'Arial Black'   => '"Arial Black","Avant Garde",sans-serif',
1343            'Book Antiqua'  => '"Book Antiqua",Palatino,serif',
1344            'Courier New'   => '"Courier New",Courier,monospace',
1345            'Georgia'       => 'Georgia,Palatino,serif',
1346            'Helvetica'     => 'Helvetica,Arial,sans-serif',
1347            'Impact'        => 'Impact,Chicago,sans-serif',
1348            'Tahoma'        => 'Tahoma,Arial,Helvetica,sans-serif',
1349            'Terminal'      => 'Terminal,Monaco,monospace',
1350            'Times New Roman' => '"Times New Roman",Times,serif',
1351            'Trebuchet MS'  => '"Trebuchet MS",Geneva,sans-serif',
1352            'Verdana'       => 'Verdana,Geneva,sans-serif',
1353        );
1354
1355        if ($font) {
1356            return $fonts[$font];
1357        }
1358
1359        return $fonts;
1360    }
1361
1362
1363    /**
1364     * Create a human readable string for a number of bytes
1365     *
1366     * @param int Number of bytes
1367     *
1368     * @return string Byte string
1369     */
1370    public static function show_bytes($bytes)
1371    {
1372        if ($bytes >= 1073741824) {
1373            $gb  = $bytes/1073741824;
1374            $str = sprintf($gb>=10 ? "%d " : "%.1f ", $gb) . self::label('GB');
1375        }
1376        else if ($bytes >= 1048576) {
1377            $mb  = $bytes/1048576;
1378            $str = sprintf($mb>=10 ? "%d " : "%.1f ", $mb) . self::label('MB');
1379        }
1380        else if ($bytes >= 1024) {
1381            $str = sprintf("%d ",  round($bytes/1024)) . self::label('KB');
1382        }
1383        else {
1384            $str = sprintf('%d ', $bytes) . self::label('B');
1385        }
1386
1387        return $str;
1388    }
1389
1390
1391    /**
1392     * Decode escaped entities used by known XSS exploits.
1393     * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
1394     *
1395     * @param string CSS content to decode
1396     *
1397     * @return string Decoded string
1398     * @todo I'm not sure this should belong to rcube_ui class
1399     */
1400    public static function xss_entity_decode($content)
1401    {
1402        $out = html_entity_decode(html_entity_decode($content));
1403        $out = preg_replace_callback('/\\\([0-9a-f]{4})/i',
1404            array(self, 'xss_entity_decode_callback'), $out);
1405        $out = preg_replace('#/\*.*\*/#Ums', '', $out);
1406
1407        return $out;
1408    }
1409
1410
1411    /**
1412     * preg_replace_callback callback for xss_entity_decode
1413     *
1414     * @param array $matches Result from preg_replace_callback
1415     *
1416     * @return string Decoded entity
1417     */
1418    public static function xss_entity_decode_callback($matches)
1419    {
1420        return chr(hexdec($matches[1]));
1421    }
1422
1423
1424    /**
1425     * Check if we can process not exceeding memory_limit
1426     *
1427     * @param integer Required amount of memory
1428     *
1429     * @return boolean True if memory won't be exceeded, False otherwise
1430     */
1431    public static function mem_check($need)
1432    {
1433        $mem_limit = parse_bytes(ini_get('memory_limit'));
1434        $memory    = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
1435
1436        return $mem_limit > 0 && $memory + $need > $mem_limit ? false : true;
1437    }
1438
1439
1440    /**
1441     * Check if working in SSL mode
1442     *
1443     * @param integer $port      HTTPS port number
1444     * @param boolean $use_https Enables 'use_https' option checking
1445     *
1446     * @return boolean
1447     */
1448    public static function https_check($port=null, $use_https=true)
1449    {
1450        global $RCMAIL;
1451
1452        if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') {
1453            return true;
1454        }
1455        if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') {
1456            return true;
1457        }
1458        if ($port && $_SERVER['SERVER_PORT'] == $port) {
1459            return true;
1460        }
1461        if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https')) {
1462            return true;
1463        }
1464
1465        return false;
1466    }
1467
1468}
Note: See TracBrowser for help on using the repository browser.