]> git.donarmstrong.com Git - roundcube.git/blob - program/include/main.inc
b341b18c4a81847164fa8c69f3f772dc93a6a62c
[roundcube.git] / program / include / main.inc
1 <?php
2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/main.inc                                              |
6  |                                                                       |
7  | This file is part of the Roundcube Webmail client                     |
8  | Copyright (C) 2005-2009, Roundcube Dev, - Switzerland                 |
9  | Licensed under the GNU GPL                                            |
10  |                                                                       |
11  | PURPOSE:                                                              |
12  |   Provide basic functions for the webmail package                     |
13  |                                                                       |
14  +-----------------------------------------------------------------------+
15  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16  +-----------------------------------------------------------------------+
17
18  $Id: main.inc 4830 2011-06-02 12:36:32Z alec $
19
20 */
21
22 /**
23  * Roundcube Webmail common functions
24  *
25  * @package Core
26  * @author Thomas Bruederli <roundcube@gmail.com>
27  */
28
29 require_once('lib/utf7.inc');
30 require_once('include/rcube_shared.inc');
31
32 // define constannts for input reading
33 define('RCUBE_INPUT_GET', 0x0101);
34 define('RCUBE_INPUT_POST', 0x0102);
35 define('RCUBE_INPUT_GPC', 0x0103);
36
37
38
39 /**
40  * Return correct name for a specific database table
41  *
42  * @param string Table name
43  * @return string Translated table name
44  */
45 function get_table_name($table)
46   {
47   global $CONFIG;
48
49   // return table name if configured
50   $config_key = 'db_table_'.$table;
51
52   if (strlen($CONFIG[$config_key]))
53     return $CONFIG[$config_key];
54
55   return $table;
56   }
57
58
59 /**
60  * Return correct name for a specific database sequence
61  * (used for Postgres only)
62  *
63  * @param string Secuence name
64  * @return string Translated sequence name
65  */
66 function get_sequence_name($sequence)
67   {
68   // return sequence name if configured
69   $config_key = 'db_sequence_'.$sequence;
70   $opt = rcmail::get_instance()->config->get($config_key);
71
72   if (!empty($opt))
73     return $opt;
74     
75   return $sequence;
76   }
77
78
79 /**
80  * Get localized text in the desired language
81  * It's a global wrapper for rcmail::gettext()
82  *
83  * @param mixed Named parameters array or label name
84  * @return string Localized text
85  * @see rcmail::gettext()
86  */
87 function rcube_label($p, $domain=null)
88 {
89   return rcmail::get_instance()->gettext($p, $domain);
90 }
91
92 /**
93  * Global wrapper of rcmail::text_exists()
94  * to check whether a text label is defined
95  *
96  * @see rcmail::text_exists()
97  */
98 function rcube_label_exists($name, $domain=null)
99 {
100   return rcmail::get_instance()->text_exists($name, $domain);
101 }
102
103 /**
104  * Overwrite action variable
105  *
106  * @param string New action value
107  */
108 function rcmail_overwrite_action($action)
109   {
110   $app = rcmail::get_instance();
111   $app->action = $action;
112   $app->output->set_env('action', $action);
113   }
114
115
116 /**
117  * Compose an URL for a specific action
118  *
119  * @param string  Request action
120  * @param array   More URL parameters
121  * @param string  Request task (omit if the same)
122  * @return The application URL
123  */
124 function rcmail_url($action, $p=array(), $task=null)
125 {
126   $app = rcmail::get_instance();
127   return $app->url((array)$p + array('_action' => $action, 'task' => $task));
128 }
129
130
131 /**
132  * Garbage collector function for temp files.
133  * Remove temp files older than two days
134  */
135 function rcmail_temp_gc()
136   {
137   $rcmail = rcmail::get_instance();
138
139   $tmp = unslashify($rcmail->config->get('temp_dir'));
140   $expire = mktime() - 172800;  // expire in 48 hours
141
142   if ($dir = opendir($tmp))
143     {
144     while (($fname = readdir($dir)) !== false)
145       {
146       if ($fname{0} == '.')
147         continue;
148
149       if (filemtime($tmp.'/'.$fname) < $expire)
150         @unlink($tmp.'/'.$fname);
151       }
152
153     closedir($dir);
154     }
155   }
156
157
158 /**
159  * Garbage collector for cache entries.
160  * Remove all expired message cache records
161  * @return void
162  */
163 function rcmail_cache_gc()
164   {
165   $rcmail = rcmail::get_instance();
166   $db = $rcmail->get_dbh();
167   
168   // get target timestamp
169   $ts = get_offset_time($rcmail->config->get('message_cache_lifetime', '30d'), -1);
170   
171   $db->query("DELETE FROM ".get_table_name('messages')."
172              WHERE  created < " . $db->fromunixtime($ts));
173
174   $db->query("DELETE FROM ".get_table_name('cache')."
175               WHERE  created < " . $db->fromunixtime($ts));
176   }
177
178
179 /**
180  * Catch an error and throw an exception.
181  *
182  * @param  int    Level of the error
183  * @param  string Error message
184  */ 
185 function rcube_error_handler($errno, $errstr)
186   {
187   throw new ErrorException($errstr, 0, $errno);
188   }
189
190
191 /**
192  * Convert a string from one charset to another.
193  * Uses mbstring and iconv functions if possible
194  *
195  * @param  string Input string
196  * @param  string Suspected charset of the input string
197  * @param  string Target charset to convert to; defaults to RCMAIL_CHARSET
198  * @return string Converted string
199  */
200 function rcube_charset_convert($str, $from, $to=NULL)
201   {
202   static $iconv_options = null;
203   static $mbstring_loaded = null;
204   static $mbstring_list = null;
205   static $convert_warning = false;
206   static $conv = null;
207
208   $error = false;
209
210   $to = empty($to) ? strtoupper(RCMAIL_CHARSET) : rcube_parse_charset($to);
211   $from = rcube_parse_charset($from);
212
213   if ($from == $to || empty($str) || empty($from))
214     return $str;
215
216   // convert charset using iconv module
217   if (function_exists('iconv') && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') {
218     if ($iconv_options === null) {
219       // ignore characters not available in output charset
220       $iconv_options = '//IGNORE';
221       if (iconv('', $iconv_options, '') === false) {
222         // iconv implementation does not support options
223         $iconv_options = '';
224       }
225     }
226
227     // throw an exception if iconv reports an illegal character in input
228     // it means that input string has been truncated
229     set_error_handler('rcube_error_handler', E_NOTICE);
230     try {
231       $_iconv = iconv($from, $to . $iconv_options, $str);
232     } catch (ErrorException $e) {
233       $_iconv = false;
234     }
235     restore_error_handler();
236     if ($_iconv !== false) {
237       return $_iconv;
238     }
239   }
240
241   if ($mbstring_loaded === null)
242     $mbstring_loaded = extension_loaded('mbstring');
243     
244   // convert charset using mbstring module
245   if ($mbstring_loaded) {
246     $aliases['WINDOWS-1257'] = 'ISO-8859-13';
247     
248     if ($mbstring_list === null) {
249       $mbstring_list = mb_list_encodings();
250       $mbstring_list = array_map('strtoupper', $mbstring_list);
251     }
252
253     $mb_from = $aliases[$from] ? $aliases[$from] : $from;
254     $mb_to = $aliases[$to] ? $aliases[$to] : $to;
255     
256     // return if encoding found, string matches encoding and convert succeeded
257     if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) {
258       if (mb_check_encoding($str, $mb_from) && ($out = mb_convert_encoding($str, $mb_to, $mb_from)))
259         return $out;
260     }
261   }
262
263   // convert charset using bundled classes/functions
264   if ($to == 'UTF-8') {
265     if ($from == 'UTF7-IMAP') {
266       if ($_str = utf7_to_utf8($str))
267         return $_str;
268     }
269     else if ($from == 'UTF-7') {
270       if ($_str = rcube_utf7_to_utf8($str))
271         return $_str;
272     }
273     else if (($from == 'ISO-8859-1') && function_exists('utf8_encode')) {
274       return utf8_encode($str);
275     }
276     else if (class_exists('utf8')) {
277       if (!$conv)
278         $conv = new utf8($from);
279       else
280         $conv->loadCharset($from);
281
282       if($_str = $conv->strToUtf8($str))
283         return $_str;
284     }
285     $error = true;
286   }
287   
288   // encode string for output
289   if ($from == 'UTF-8') {
290     // @TODO: we need a function for UTF-7 (RFC2152) conversion
291     if ($to == 'UTF7-IMAP' || $to == 'UTF-7') {
292       if ($_str = utf8_to_utf7($str))
293         return $_str;
294     }
295     else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) {
296       return utf8_decode($str);
297     }
298     else if (class_exists('utf8')) {
299       if (!$conv)
300         $conv = new utf8($to);
301       else
302         $conv->loadCharset($from);
303
304       if ($_str = $conv->strToUtf8($str))
305         return $_str;
306     }
307     $error = true;
308   }
309   
310   // report error
311   if ($error && !$convert_warning) {
312     raise_error(array(
313       'code' => 500,
314       'type' => 'php',
315       'file' => __FILE__,
316       'line' => __LINE__,
317       'message' => "Could not convert string from $from to $to. Make sure iconv/mbstring is installed or lib/utf8.class is available."
318       ), true, false);
319     
320     $convert_warning = true;
321   }
322   
323   // return UTF-8 or original string
324   return $str;
325   }
326
327
328 /**
329  * Parse and validate charset name string (see #1485758).
330  * Sometimes charset string is malformed, there are also charset aliases 
331  * but we need strict names for charset conversion (specially utf8 class)
332  *
333  * @param  string Input charset name
334  * @return string The validated charset name
335  */
336 function rcube_parse_charset($input)
337   {
338   static $charsets = array();
339   $charset = strtoupper($input);
340
341   if (isset($charsets[$input]))
342     return $charsets[$input];
343
344   $charset = preg_replace(array(
345     '/^[^0-9A-Z]+/',    // e.g. _ISO-8859-JP$SIO
346     '/\$.*$/',          // e.g. _ISO-8859-JP$SIO
347     '/UNICODE-1-1-*/',  // RFC1641/1642
348     '/^X-/',            // X- prefix (e.g. X-ROMAN8 => ROMAN8)
349     ), '', $charset);
350
351   if ($charset == 'BINARY')
352     return $charsets[$input] = null;
353
354   # Aliases: some of them from HTML5 spec.
355   $aliases = array(
356     'USASCII'       => 'WINDOWS-1252',
357     'ANSIX31101983' => 'WINDOWS-1252',
358     'ANSIX341968'   => 'WINDOWS-1252',
359     'UNKNOWN8BIT'   => 'ISO-8859-15',
360     'UNKNOWN'       => 'ISO-8859-15',
361     'USERDEFINED'   => 'ISO-8859-15',
362     'KSC56011987'   => 'EUC-KR',
363     'GB2312'        => 'GBK',
364     'GB231280'      => 'GBK',
365     'UNICODE'       => 'UTF-8',
366     'UTF7IMAP'      => 'UTF7-IMAP',
367     'TIS620'        => 'WINDOWS-874',
368     'ISO88599'      => 'WINDOWS-1254',
369     'ISO885911'     => 'WINDOWS-874',
370     'MACROMAN'      => 'MACINTOSH',
371     '77'            => 'MAC',
372     '128'           => 'SHIFT-JIS',
373     '129'           => 'CP949',
374     '130'           => 'CP1361',
375     '134'           => 'GBK',
376     '136'           => 'BIG5',
377     '161'           => 'WINDOWS-1253',
378     '162'           => 'WINDOWS-1254',
379     '163'           => 'WINDOWS-1258',
380     '177'           => 'WINDOWS-1255',
381     '178'           => 'WINDOWS-1256',
382     '186'           => 'WINDOWS-1257',
383     '204'           => 'WINDOWS-1251',
384     '222'           => 'WINDOWS-874',
385     '238'           => 'WINDOWS-1250',
386     'MS950'         => 'CP950',
387     'WINDOWS949'    => 'UHC',
388   );
389
390   // allow A-Z and 0-9 only
391   $str = preg_replace('/[^A-Z0-9]/', '', $charset);
392
393   if (isset($aliases[$str]))
394     $result = $aliases[$str];
395   // UTF
396   else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m))
397     $result = 'UTF-' . $m[1] . $m[2];
398   // ISO-8859
399   else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) {
400     $iso = 'ISO-8859-' . ($m[1] ? $m[1] : 1);
401     // some clients sends windows-1252 text as latin1,
402     // it is safe to use windows-1252 for all latin1
403     $result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso;
404     }
405   // handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE
406   else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) {
407     $result = 'WINDOWS-' . $m[2];
408     }
409   // LATIN
410   else if (preg_match('/LATIN(.*)/', $str, $m)) {
411     $aliases = array('2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10,
412         '7' => 13, '8' => 14, '9' => 15, '10' => 16,
413         'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8);
414
415     // some clients sends windows-1252 text as latin1,
416     // it is safe to use windows-1252 for all latin1
417     if ($m[1] == 1) {
418       $result = 'WINDOWS-1252';
419       }
420     // if iconv is not supported we need ISO labels, it's also safe for iconv
421     else if (!empty($aliases[$m[1]])) {
422       $result = 'ISO-8859-'.$aliases[$m[1]];
423       }
424     // iconv requires convertion of e.g. LATIN-1 to LATIN1
425     else {
426       $result = $str;
427       }
428     }
429   else {
430     $result = $charset;
431     }
432
433   $charsets[$input] = $result;
434
435   return $result;
436   }
437
438
439 /**
440  * Converts string from standard UTF-7 (RFC 2152) to UTF-8.
441  *
442  * @param  string  Input string
443  * @return string  The converted string
444  */
445 function rcube_utf7_to_utf8($str)
446 {
447   $Index_64 = array(
448     0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
449     0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
450     0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0,
451     1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0,
452     0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
453     1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
454     0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
455     1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
456   );
457
458   $u7len = strlen($str);
459   $str = strval($str);
460   $res = '';
461
462   for ($i=0; $u7len > 0; $i++, $u7len--)
463   {
464     $u7 = $str[$i];
465     if ($u7 == '+')
466     {
467       $i++;
468       $u7len--;
469       $ch = '';
470
471       for (; $u7len > 0; $i++, $u7len--)
472       {
473         $u7 = $str[$i];
474
475         if (!$Index_64[ord($u7)])
476           break;
477
478         $ch .= $u7;
479       }
480
481       if ($ch == '') {
482         if ($u7 == '-')
483           $res .= '+';
484         continue;
485       }
486
487       $res .= rcube_utf16_to_utf8(base64_decode($ch));
488     }
489     else
490     {
491       $res .= $u7;
492     }
493   }
494
495   return $res;
496 }
497
498 /**
499  * Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion)
500  *
501  * @param  string  Input string
502  * @return string  The converted string
503  */
504 function rcube_utf16_to_utf8($str)
505 {
506   $len = strlen($str);
507   $dec = '';
508
509   for ($i = 0; $i < $len; $i += 2) {
510     $c = ord($str[$i]) << 8 | ord($str[$i + 1]);
511     if ($c >= 0x0001 && $c <= 0x007F) {
512       $dec .= chr($c);
513     } else if ($c > 0x07FF) {
514       $dec .= chr(0xE0 | (($c >> 12) & 0x0F));
515       $dec .= chr(0x80 | (($c >>  6) & 0x3F));
516       $dec .= chr(0x80 | (($c >>  0) & 0x3F));
517     } else {
518       $dec .= chr(0xC0 | (($c >>  6) & 0x1F));
519       $dec .= chr(0x80 | (($c >>  0) & 0x3F));
520     }
521   }
522   return $dec;
523 }
524
525
526 /**
527  * Replacing specials characters to a specific encoding type
528  *
529  * @param  string  Input string
530  * @param  string  Encoding type: text|html|xml|js|url
531  * @param  string  Replace mode for tags: show|replace|remove
532  * @param  boolean Convert newlines
533  * @return string  The quoted string
534  */
535 function rep_specialchars_output($str, $enctype='', $mode='', $newlines=TRUE)
536   {
537   static $html_encode_arr = false;
538   static $js_rep_table = false;
539   static $xml_rep_table = false;
540
541   if (!$enctype)
542     $enctype = $OUTPUT->type;
543
544   // encode for HTML output
545   if ($enctype=='html')
546     {
547     if (!$html_encode_arr)
548       {
549       $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);
550       unset($html_encode_arr['?']);
551       }
552
553     $ltpos = strpos($str, '<');
554     $encode_arr = $html_encode_arr;
555
556     // don't replace quotes and html tags
557     if (($mode=='show' || $mode=='') && $ltpos!==false && strpos($str, '>', $ltpos)!==false)
558       {
559       unset($encode_arr['"']);
560       unset($encode_arr['<']);
561       unset($encode_arr['>']);
562       unset($encode_arr['&']);
563       }
564     else if ($mode=='remove')
565       $str = strip_tags($str);
566
567     $out = strtr($str, $encode_arr);
568
569     // avoid douple quotation of &
570     $out = preg_replace('/&amp;([A-Za-z]{2,6}|#[0-9]{2,4});/', '&\\1;', $out);
571
572     return $newlines ? nl2br($out) : $out;
573     }
574
575   // if the replace tables for XML and JS are not yet defined
576   if ($js_rep_table===false)
577     {
578     $js_rep_table = $xml_rep_table = array();
579     $xml_rep_table['&'] = '&amp;';
580
581     for ($c=160; $c<256; $c++)  // can be increased to support more charsets
582       $xml_rep_table[chr($c)] = "&#$c;";
583
584     $xml_rep_table['"'] = '&quot;';
585     $js_rep_table['"'] = '\\"';
586     $js_rep_table["'"] = "\\'";
587     $js_rep_table["\\"] = "\\\\";
588     // Unicode line and paragraph separators (#1486310)
589     $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A8))] = '&#8232;';
590     $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A9))] = '&#8233;';
591     }
592
593   // encode for javascript use
594   if ($enctype=='js')
595     return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));
596
597   // encode for plaintext
598   if ($enctype=='text')
599     return str_replace("\r\n", "\n", $mode=='remove' ? strip_tags($str) : $str);
600
601   if ($enctype=='url')
602     return rawurlencode($str);
603
604   // encode for XML
605   if ($enctype=='xml')
606     return strtr($str, $xml_rep_table);
607
608   // no encoding given -> return original string
609   return $str;
610   }
611   
612 /**
613  * Quote a given string.
614  * Shortcut function for rep_specialchars_output
615  *
616  * @return string HTML-quoted string
617  * @see rep_specialchars_output()
618  */
619 function Q($str, $mode='strict', $newlines=TRUE)
620   {
621   return rep_specialchars_output($str, 'html', $mode, $newlines);
622   }
623
624 /**
625  * Quote a given string for javascript output.
626  * Shortcut function for rep_specialchars_output
627  * 
628  * @return string JS-quoted string
629  * @see rep_specialchars_output()
630  */
631 function JQ($str)
632   {
633   return rep_specialchars_output($str, 'js');
634   }
635
636
637 /**
638  * Read input value and convert it for internal use
639  * Performs stripslashes() and charset conversion if necessary
640  * 
641  * @param  string   Field name to read
642  * @param  int      Source to get value from (GPC)
643  * @param  boolean  Allow HTML tags in field value
644  * @param  string   Charset to convert into
645  * @return string   Field value or NULL if not available
646  */
647 function get_input_value($fname, $source, $allow_html=FALSE, $charset=NULL)
648 {
649   $value = NULL;
650   
651   if ($source==RCUBE_INPUT_GET && isset($_GET[$fname]))
652     $value = $_GET[$fname];
653   else if ($source==RCUBE_INPUT_POST && isset($_POST[$fname]))
654     $value = $_POST[$fname];
655   else if ($source==RCUBE_INPUT_GPC)
656     {
657     if (isset($_POST[$fname]))
658       $value = $_POST[$fname];
659     else if (isset($_GET[$fname]))
660       $value = $_GET[$fname];
661     else if (isset($_COOKIE[$fname]))
662       $value = $_COOKIE[$fname];
663     }
664
665   return parse_input_value($value, $allow_html, $charset);
666 }
667
668 /**
669  * Parse/validate input value. See get_input_value()
670  * Performs stripslashes() and charset conversion if necessary
671  * 
672  * @param  string   Input value
673  * @param  boolean  Allow HTML tags in field value
674  * @param  string   Charset to convert into
675  * @return string   Parsed value
676  */
677 function parse_input_value($value, $allow_html=FALSE, $charset=NULL)
678 {
679   global $OUTPUT;
680
681   if (empty($value))
682     return $value;
683
684   if (is_array($value)) {
685     foreach ($value as $idx => $val)
686       $value[$idx] = parse_input_value($val, $allow_html, $charset);
687     return $value;
688   }
689
690   // strip single quotes if magic_quotes_sybase is enabled
691   if (ini_get('magic_quotes_sybase'))
692     $value = str_replace("''", "'", $value);
693   // strip slashes if magic_quotes enabled
694   else if (get_magic_quotes_gpc() || get_magic_quotes_runtime())
695     $value = stripslashes($value);
696
697   // remove HTML tags if not allowed    
698   if (!$allow_html)
699     $value = strip_tags($value);
700   
701   // convert to internal charset
702   if (is_object($OUTPUT) && $charset)
703     return rcube_charset_convert($value, $OUTPUT->get_charset(), $charset);
704   else
705     return $value;
706 }
707
708 /**
709  * Convert array of request parameters (prefixed with _)
710  * to a regular array with non-prefixed keys.
711  *
712  * @param  int   Source to get value from (GPC)
713  * @return array Hash array with all request parameters
714  */
715 function request2param($mode = RCUBE_INPUT_GPC)
716 {
717   $out = array();
718   $src = $mode == RCUBE_INPUT_GET ? $_GET : ($mode == RCUBE_INPUT_POST ? $_POST : $_REQUEST);
719   foreach ($src as $key => $value) {
720     $fname = $key[0] == '_' ? substr($key, 1) : $key;
721     $out[$fname] = get_input_value($key, $mode);
722   }
723   
724   return $out;
725 }
726
727 /**
728  * Remove all non-ascii and non-word chars
729  * except ., -, _
730  */
731 function asciiwords($str, $css_id = false, $replace_with = '')
732 {
733   $allowed = 'a-z0-9\_\-' . (!$css_id ? '\.' : '');
734   return preg_replace("/[^$allowed]/i", $replace_with, $str);
735 }
736
737 /**
738  * Remove single and double quotes from given string
739  *
740  * @param string Input value
741  * @return string Dequoted string
742  */
743 function strip_quotes($str)
744 {
745   return str_replace(array("'", '"'), '', $str);
746 }
747
748
749 /**
750  * Remove new lines characters from given string
751  *
752  * @param string Input value
753  * @return string Stripped string
754  */
755 function strip_newlines($str)
756 {
757   return preg_replace('/[\r\n]/', '', $str);
758 }
759
760
761 /**
762  * Create a HTML table based on the given data
763  *
764  * @param  array  Named table attributes
765  * @param  mixed  Table row data. Either a two-dimensional array or a valid SQL result set
766  * @param  array  List of cols to show
767  * @param  string Name of the identifier col
768  * @return string HTML table code
769  */
770 function rcube_table_output($attrib, $table_data, $a_show_cols, $id_col)
771   {
772   global $RCMAIL;
773   
774   $table = new html_table(/*array('cols' => count($a_show_cols))*/);
775     
776   // add table header
777   if (!$attrib['noheader'])
778     foreach ($a_show_cols as $col)
779       $table->add_header($col, Q(rcube_label($col)));
780   
781   $c = 0;
782   if (!is_array($table_data)) 
783   {
784     $db = $RCMAIL->get_dbh();
785     while ($table_data && ($sql_arr = $db->fetch_assoc($table_data)))
786     {
787       $zebra_class = $c % 2 ? 'even' : 'odd';
788       $table->add_row(array('id' => 'rcmrow' . $sql_arr[$id_col], 'class' => $zebra_class));
789
790       // format each col
791       foreach ($a_show_cols as $col)
792         $table->add($col, Q($sql_arr[$col]));
793       
794       $c++;
795     }
796   }
797   else 
798   {
799     foreach ($table_data as $row_data)
800     {
801       $zebra_class = $c % 2 ? 'even' : 'odd';
802       if (!empty($row_data['class']))
803         $zebra_class .= ' '.$row_data['class'];
804
805       $table->add_row(array('id' => 'rcmrow' . $row_data[$id_col], 'class' => $zebra_class));
806
807       // format each col
808       foreach ($a_show_cols as $col)
809         $table->add($col, Q($row_data[$col]));
810         
811       $c++;
812     }
813   }
814
815   return $table->show($attrib);
816   }
817
818
819 /**
820  * Create an edit field for inclusion on a form
821  * 
822  * @param string col field name
823  * @param string value field value
824  * @param array attrib HTML element attributes for field
825  * @param string type HTML element type (default 'text')
826  * @return string HTML field definition
827  */
828 function rcmail_get_edit_field($col, $value, $attrib, $type='text')
829   {
830   $fname = '_'.$col;
831   $attrib['name'] = $fname;
832   
833   if ($type=='checkbox')
834     {
835     $attrib['value'] = '1';
836     $input = new html_checkbox($attrib);
837     }
838   else if ($type=='textarea')
839     {
840     $attrib['cols'] = $attrib['size'];
841     $input = new html_textarea($attrib);
842     }
843   else
844     $input = new html_inputfield($attrib);
845
846   // use value from post
847   if (!empty($_POST[$fname]))
848     $value = get_input_value($fname, RCUBE_INPUT_POST,
849             $type == 'textarea' && strpos($attrib['class'], 'mce_editor')!==false ? true : false);
850
851   $out = $input->show($value);
852          
853   return $out;
854   }
855
856
857 /**
858  * Replace all css definitions with #container [def]
859  * and remove css-inlined scripting
860  *
861  * @param string CSS source code
862  * @param string Container ID to use as prefix
863  * @return string Modified CSS source
864  */
865 function rcmail_mod_css_styles($source, $container_id)
866   {
867   $last_pos = 0;
868   $replacements = new rcube_string_replacer;
869
870   // ignore the whole block if evil styles are detected
871   $stripped = preg_replace('/[^a-z\(:;]/', '', rcmail_xss_entity_decode($source));
872   if (preg_match('/expression|behavior|url\(|import[^a]/', $stripped))
873     return '/* evil! */';
874
875   // remove css comments (sometimes used for some ugly hacks)
876   $source = preg_replace('!/\*(.+)\*/!Ums', '', $source);
877
878   // cut out all contents between { and }
879   while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos)))
880   {
881     $key = $replacements->add(substr($source, $pos+1, $pos2-($pos+1)));
882     $source = substr($source, 0, $pos+1) . $replacements->get_replacement($key) . substr($source, $pos2, strlen($source)-$pos2);
883     $last_pos = $pos+2;
884   }
885
886   // remove html comments and add #container to each tag selector.
887   // also replace body definition because we also stripped off the <body> tag
888   $styles = preg_replace(
889     array(
890       '/(^\s*<!--)|(-->\s*$)/',
891       '/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
892       '/'.preg_quote($container_id, '/').'\s+body/i',
893     ),
894     array(
895       '',
896       "\\1#$container_id \\2",
897       $container_id,
898     ),
899     $source);
900
901   // put block contents back in
902   $styles = $replacements->resolve($styles);
903
904   return $styles;
905   }
906
907
908 /**
909  * Decode escaped entities used by known XSS exploits.
910  * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
911  *
912  * @param string CSS content to decode
913  * @return string Decoded string
914  */
915 function rcmail_xss_entity_decode($content)
916 {
917   $out = html_entity_decode(html_entity_decode($content));
918   $out = preg_replace_callback('/\\\([0-9a-f]{4})/i', 'rcmail_xss_entity_decode_callback', $out);
919   $out = preg_replace('#/\*.*\*/#Um', '', $out);
920   return $out;
921 }
922
923
924 /**
925  * preg_replace_callback callback for rcmail_xss_entity_decode_callback
926  *
927  * @param array matches result from preg_replace_callback
928  * @return string decoded entity
929  */ 
930 function rcmail_xss_entity_decode_callback($matches)
931
932   return chr(hexdec($matches[1]));
933 }
934
935 /**
936  * Compose a valid attribute string for HTML tags
937  *
938  * @param array Named tag attributes
939  * @param array List of allowed attributes
940  * @return string HTML formatted attribute string
941  */
942 function create_attrib_string($attrib, $allowed_attribs=array('id', 'class', 'style'))
943   {
944   // allow the following attributes to be added to the <iframe> tag
945   $attrib_str = '';
946   foreach ($allowed_attribs as $a)
947     if (isset($attrib[$a]))
948       $attrib_str .= sprintf(' %s="%s"', $a, str_replace('"', '&quot;', $attrib[$a]));
949
950   return $attrib_str;
951   }
952
953
954 /**
955  * Convert a HTML attribute string attributes to an associative array (name => value)
956  *
957  * @param string Input string
958  * @return array Key-value pairs of parsed attributes
959  */
960 function parse_attrib_string($str)
961   {
962   $attrib = array();
963   preg_match_all('/\s*([-_a-z]+)=(["\'])??(?(2)([^\2]*)\2|(\S+?))/Ui', stripslashes($str), $regs, PREG_SET_ORDER);
964
965   // convert attributes to an associative array (name => value)
966   if ($regs) {
967     foreach ($regs as $attr) {
968       $attrib[strtolower($attr[1])] = html_entity_decode($attr[3] . $attr[4]);
969     }
970   }
971
972   return $attrib;
973   }
974
975
976 /**
977  * Convert the given date to a human readable form
978  * This uses the date formatting properties from config
979  *
980  * @param mixed Date representation (string or timestamp)
981  * @param string Date format to use
982  * @return string Formatted date string
983  */
984 function format_date($date, $format=NULL)
985 {
986   global $CONFIG;
987   
988   $ts = NULL;
989
990   if (is_numeric($date))
991     $ts = $date;
992   else if (!empty($date))
993     {
994     // support non-standard "GMTXXXX" literal
995     $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
996     // if date parsing fails, we have a date in non-rfc format.
997     // remove token from the end and try again
998     while ((($ts = @strtotime($date))===false) || ($ts < 0))
999       {
1000         $d = explode(' ', $date);
1001         array_pop($d);
1002         if (!$d) break;
1003         $date = implode(' ', $d);
1004       }
1005     }
1006
1007   if (empty($ts))
1008     return '';
1009
1010   // get user's timezone
1011   if ($CONFIG['timezone'] === 'auto')
1012     $tz = isset($_SESSION['timezone']) ? $_SESSION['timezone'] : date('Z')/3600;
1013   else {
1014     $tz = $CONFIG['timezone'];
1015     if ($CONFIG['dst_active'])
1016       $tz++;
1017   }
1018
1019   // convert time to user's timezone
1020   $timestamp = $ts - date('Z', $ts) + ($tz * 3600);
1021
1022   // get current timestamp in user's timezone
1023   $now = time();  // local time
1024   $now -= (int)date('Z'); // make GMT time
1025   $now += ($tz * 3600); // user's time
1026   $now_date = getdate($now);
1027
1028   $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
1029   $week_limit  = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
1030
1031   // define date format depending on current time
1032   if (!$format) {
1033     if ($CONFIG['prettydate'] && $timestamp > $today_limit && $timestamp < $now) {
1034       $format = $CONFIG['date_today'] ? $CONFIG['date_today'] : 'H:i';
1035       $today  = true;
1036     }
1037     else if ($CONFIG['prettydate'] && $timestamp > $week_limit && $timestamp < $now)
1038       $format = $CONFIG['date_short'] ? $CONFIG['date_short'] : 'D H:i';
1039     else
1040       $format = $CONFIG['date_long'] ? $CONFIG['date_long'] : 'd.m.Y H:i';
1041   }
1042
1043   // strftime() format
1044   if (preg_match('/%[a-z]+/i', $format)) {
1045     $format = strftime($format, $timestamp);
1046     return $today ? (rcube_label('today') . ' ' . $format) : $format;
1047   }
1048
1049   // parse format string manually in order to provide localized weekday and month names
1050   // an alternative would be to convert the date() format string to fit with strftime()
1051   $out = '';
1052   for($i=0; $i<strlen($format); $i++) {
1053     if ($format{$i}=='\\')  // skip escape chars
1054       continue;
1055
1056     // write char "as-is"
1057     if ($format{$i}==' ' || $format{$i-1}=='\\')
1058       $out .= $format{$i};
1059     // weekday (short)
1060     else if ($format{$i}=='D')
1061       $out .= rcube_label(strtolower(date('D', $timestamp)));
1062     // weekday long
1063     else if ($format{$i}=='l')
1064       $out .= rcube_label(strtolower(date('l', $timestamp)));
1065     // month name (short)
1066     else if ($format{$i}=='M')
1067       $out .= rcube_label(strtolower(date('M', $timestamp)));
1068     // month name (long)
1069     else if ($format{$i}=='F')
1070       $out .= rcube_label('long'.strtolower(date('M', $timestamp)));
1071     else if ($format{$i}=='x')
1072       $out .= strftime('%x %X', $timestamp);
1073     else
1074       $out .= date($format{$i}, $timestamp);
1075   }
1076
1077   if ($today) {
1078     $label = rcube_label('today');
1079     // replcae $ character with "Today" label (#1486120)
1080     if (strpos($out, '$') !== false) {
1081       $out = preg_replace('/\$/', $label, $out, 1);
1082     }
1083     else {
1084       $out = $label . ' ' . $out;
1085     }
1086   }
1087
1088   return $out;
1089 }
1090
1091
1092 /**
1093  * Compose a valid representation of name and e-mail address
1094  *
1095  * @param string E-mail address
1096  * @param string Person name
1097  * @return string Formatted string
1098  */
1099 function format_email_recipient($email, $name='')
1100   {
1101   if ($name && $name != $email)
1102     {
1103     // Special chars as defined by RFC 822 need to in quoted string (or escaped).
1104     return sprintf('%s <%s>', preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name) ? '"'.addcslashes($name, '"').'"' : $name, trim($email));
1105     }
1106   else
1107     return trim($email);
1108   }
1109
1110
1111
1112 /****** debugging functions ********/
1113
1114
1115 /**
1116  * Print or write debug messages
1117  *
1118  * @param mixed Debug message or data
1119  * @return void
1120  */
1121 function console()
1122   {
1123   $args = func_get_args();
1124
1125   if (class_exists('rcmail', false)) {
1126     $rcmail = rcmail::get_instance();
1127     if (is_object($rcmail->plugins))
1128       $rcmail->plugins->exec_hook('console', $args);
1129   }
1130
1131   $msg = array();
1132   foreach ($args as $arg)
1133     $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
1134
1135   if (!($GLOBALS['CONFIG']['debug_level'] & 4))
1136     write_log('console', join(";\n", $msg));
1137   else if ($GLOBALS['OUTPUT']->ajax_call)
1138     print "/*\n " . join(";\n", $msg) . " \n*/\n";
1139   else
1140     {
1141     print '<div style="background:#eee; border:1px solid #ccc; margin-bottom:3px; padding:6px"><pre>';
1142     print join(";<br/>\n", $msg);
1143     print "</pre></div>\n";
1144     }
1145   }
1146
1147
1148 /**
1149  * Append a line to a logfile in the logs directory.
1150  * Date will be added automatically to the line.
1151  *
1152  * @param $name name of log file
1153  * @param line Line to append
1154  * @return void
1155  */
1156 function write_log($name, $line)
1157   {
1158   global $CONFIG, $RCMAIL;
1159
1160   if (!is_string($line))
1161     $line = var_export($line, true);
1162  
1163   if (empty($CONFIG['log_date_format']))
1164     $CONFIG['log_date_format'] = 'd-M-Y H:i:s O';
1165   
1166   $date = date($CONFIG['log_date_format']);
1167   
1168   // trigger logging hook
1169   if (is_object($RCMAIL) && is_object($RCMAIL->plugins)) {
1170     $log = $RCMAIL->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line));
1171     $name = $log['name'];
1172     $line = $log['line'];
1173     $date = $log['date'];
1174     if ($log['abort'])
1175       return true;
1176   }
1177  
1178   if ($CONFIG['log_driver'] == 'syslog') {
1179     $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
1180     syslog($prio, $line);
1181     return true;
1182   }
1183   else {
1184     $line = sprintf("[%s]: %s\n", $date, $line);
1185
1186     // log_driver == 'file' is assumed here
1187     if (empty($CONFIG['log_dir']))
1188       $CONFIG['log_dir'] = INSTALL_PATH.'logs';
1189
1190     // try to open specific log file for writing
1191     $logfile = $CONFIG['log_dir'].'/'.$name;
1192     if ($fp = @fopen($logfile, 'a')) {
1193       fwrite($fp, $line);
1194       fflush($fp);
1195       fclose($fp);
1196       return true;
1197     }
1198     else
1199       trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
1200   }
1201   return false;
1202 }
1203
1204
1205 /**
1206  * Write login data (name, ID, IP address) to the 'userlogins' log file.
1207  *
1208  * @return void
1209  */
1210 function rcmail_log_login()
1211 {
1212   global $RCMAIL;
1213
1214   if (!$RCMAIL->config->get('log_logins') || !$RCMAIL->user)
1215     return;
1216
1217   write_log('userlogins', sprintf('Successful login for %s (ID: %d) from %s',
1218     $RCMAIL->user->get_username(), $RCMAIL->user->ID, rcmail_remote_ip()));
1219 }
1220
1221
1222 /**
1223  * Returns remote IP address and forwarded addresses if found
1224  *
1225  * @return string Remote IP address(es)
1226  */
1227 function rcmail_remote_ip()
1228 {
1229     $address = $_SERVER['REMOTE_ADDR'];
1230
1231     // append the NGINX X-Real-IP header, if set
1232     if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
1233         $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
1234     }
1235     // append the X-Forwarded-For header, if set
1236     if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1237         $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
1238     }
1239
1240     if (!empty($remote_ip))
1241         $address .= '(' . implode(',', $remote_ip) . ')';
1242
1243     return $address;
1244 }
1245
1246
1247 /**
1248  * Check whether the HTTP referer matches the current request
1249  *
1250  * @return boolean True if referer is the same host+path, false if not
1251  */
1252 function rcube_check_referer()
1253 {
1254   $uri = parse_url($_SERVER['REQUEST_URI']);
1255   $referer = parse_url(rc_request_header('Referer'));
1256   return $referer['host'] == rc_request_header('Host') && $referer['path'] == $uri['path'];
1257 }
1258
1259
1260 /**
1261  * @access private
1262  * @return mixed
1263  */
1264 function rcube_timer()
1265 {
1266   return microtime(true);
1267 }
1268
1269
1270 /**
1271  * @access private
1272  * @return void
1273  */
1274 function rcube_print_time($timer, $label='Timer', $dest='console')
1275 {
1276   static $print_count = 0;
1277   
1278   $print_count++;
1279   $now = rcube_timer();
1280   $diff = $now-$timer;
1281   
1282   if (empty($label))
1283     $label = 'Timer '.$print_count;
1284   
1285   write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
1286 }
1287
1288
1289 /**
1290  * Return the mailboxlist in HTML
1291  *
1292  * @param array Named parameters
1293  * @return string HTML code for the gui object
1294  */
1295 function rcmail_mailbox_list($attrib)
1296 {
1297   global $RCMAIL;
1298   static $a_mailboxes;
1299
1300   $attrib += array('maxlength' => 100, 'realnames' => false);
1301
1302   // add some labels to client
1303   $RCMAIL->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
1304
1305   $type = $attrib['type'] ? $attrib['type'] : 'ul';
1306   unset($attrib['type']);
1307
1308   if ($type=='ul' && !$attrib['id'])
1309     $attrib['id'] = 'rcmboxlist';
1310
1311   // get mailbox list
1312   $mbox_name = $RCMAIL->imap->get_mailbox_name();
1313
1314   // build the folders tree
1315   if (empty($a_mailboxes)) {
1316     // get mailbox list
1317     $a_folders = $RCMAIL->imap->list_mailboxes();
1318     $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1319     $a_mailboxes = array();
1320
1321     foreach ($a_folders as $folder)
1322       rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
1323   }
1324
1325   // allow plugins to alter the folder tree or to localize folder names
1326   $hook = $RCMAIL->plugins->exec_hook('render_mailboxlist', array('list' => $a_mailboxes, 'delimiter' => $delimiter));
1327
1328   if ($type == 'select') {
1329     $select = new html_select($attrib);
1330
1331     // add no-selection option
1332     if ($attrib['noselection'])
1333       $select->add(rcube_label($attrib['noselection']), '');
1334
1335     rcmail_render_folder_tree_select($hook['list'], $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
1336     $out = $select->show();
1337   }
1338   else {
1339     $js_mailboxlist = array();
1340     $out = html::tag('ul', $attrib, rcmail_render_folder_tree_html($hook['list'], $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
1341
1342     $RCMAIL->output->add_gui_object('mailboxlist', $attrib['id']);
1343     $RCMAIL->output->set_env('mailboxes', $js_mailboxlist);
1344     $RCMAIL->output->set_env('collapsed_folders', $RCMAIL->config->get('collapsed_folders'));
1345   }
1346
1347   return $out;
1348 }
1349
1350
1351 /**
1352  * Return the mailboxlist as html_select object
1353  *
1354  * @param array Named parameters
1355  * @return html_select HTML drop-down object
1356  */
1357 function rcmail_mailbox_select($p = array())
1358 {
1359   global $RCMAIL;
1360
1361   $p += array('maxlength' => 100, 'realnames' => false);
1362   $a_mailboxes = array();
1363
1364   if ($p['unsubscribed'])
1365     $list = $RCMAIL->imap->list_unsubscribed();
1366   else
1367     $list = $RCMAIL->imap->list_mailboxes();
1368
1369   foreach ($list as $folder)
1370     if (empty($p['exceptions']) || !in_array($folder, $p['exceptions']))
1371       rcmail_build_folder_tree($a_mailboxes, $folder, $RCMAIL->imap->get_hierarchy_delimiter());
1372
1373   $select = new html_select($p);
1374   
1375   if ($p['noselection'])
1376     $select->add($p['noselection'], '');
1377     
1378   rcmail_render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames']);
1379   
1380   return $select;
1381 }
1382
1383
1384 /**
1385  * Create a hierarchical array of the mailbox list
1386  * @access private
1387  * @return void
1388  */
1389 function rcmail_build_folder_tree(&$arrFolders, $folder, $delm='/', $path='')
1390 {
1391   global $RCMAIL;
1392
1393   $pos = strpos($folder, $delm);
1394
1395   if ($pos !== false) {
1396     $subFolders = substr($folder, $pos+1);
1397     $currentFolder = substr($folder, 0, $pos);
1398
1399     // sometimes folder has a delimiter as the last character
1400     if (!strlen($subFolders))
1401       $virtual = false;
1402     else if (!isset($arrFolders[$currentFolder]))
1403       $virtual = true;
1404     else
1405       $virtual = $arrFolders[$currentFolder]['virtual'];
1406   }
1407   else {
1408     $subFolders = false;
1409     $currentFolder = $folder;
1410     $virtual = false;
1411   }
1412
1413   $path .= $currentFolder;
1414
1415   // Check \Noselect option (if options are in cache)
1416   if (!$virtual && ($opts = $RCMAIL->imap->mailbox_options($path))) {
1417     $virtual = in_array('\\Noselect', $opts);
1418   }
1419
1420   if (!isset($arrFolders[$currentFolder])) {
1421     $arrFolders[$currentFolder] = array(
1422       'id' => $path,
1423       'name' => rcube_charset_convert($currentFolder, 'UTF7-IMAP'),
1424       'virtual' => $virtual,
1425       'folders' => array());
1426   }
1427   else
1428     $arrFolders[$currentFolder]['virtual'] = $virtual;
1429
1430   if (strlen($subFolders))
1431     rcmail_build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
1432 }
1433
1434
1435 /**
1436  * Return html for a structured list &lt;ul&gt; for the mailbox tree
1437  * @access private
1438  * @return string
1439  */
1440 function rcmail_render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel=0)
1441 {
1442   global $RCMAIL, $CONFIG;
1443
1444   $maxlength = intval($attrib['maxlength']);
1445   $realnames = (bool)$attrib['realnames'];
1446   $msgcounts = $RCMAIL->imap->get_cache('messagecount');
1447
1448   $idx = 0;
1449   $out = '';
1450   foreach ($arrFolders as $key => $folder) {
1451     $zebra_class = (($nestLevel+1)*$idx) % 2 == 0 ? 'even' : 'odd';
1452     $title = null;
1453
1454     if (($folder_class = rcmail_folder_classname($folder['id'])) && !$realnames) {
1455       $foldername = rcube_label($folder_class);
1456     }
1457     else {
1458       $foldername = $folder['name'];
1459
1460       // shorten the folder name to a given length
1461       if ($maxlength && $maxlength > 1) {
1462         $fname = abbreviate_string($foldername, $maxlength);
1463         if ($fname != $foldername)
1464           $title = $foldername;
1465         $foldername = $fname;
1466       }
1467     }
1468
1469     // make folder name safe for ids and class names
1470     $folder_id = asciiwords($folder['id'], true, '_');
1471     $classes = array('mailbox');
1472
1473     // set special class for Sent, Drafts, Trash and Junk
1474     if ($folder['id']==$CONFIG['sent_mbox'])
1475       $classes[] = 'sent';
1476     else if ($folder['id']==$CONFIG['drafts_mbox'])
1477       $classes[] = 'drafts';
1478     else if ($folder['id']==$CONFIG['trash_mbox'])
1479       $classes[] = 'trash';
1480     else if ($folder['id']==$CONFIG['junk_mbox'])
1481       $classes[] = 'junk';
1482     else if ($folder['id']=='INBOX')
1483       $classes[] = 'inbox';
1484     else
1485       $classes[] = '_'.asciiwords($folder_class ? $folder_class : strtolower($folder['id']), true);
1486
1487     $classes[] = $zebra_class;
1488
1489     if ($folder['id'] == $mbox_name)
1490       $classes[] = 'selected';
1491
1492     $collapsed = preg_match('/&'.rawurlencode($folder['id']).'&/', $RCMAIL->config->get('collapsed_folders'));
1493     $unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
1494
1495     if ($folder['virtual'])
1496       $classes[] = 'virtual';
1497     else if ($unread)
1498       $classes[] = 'unread';
1499
1500     $js_name = JQ($folder['id']);
1501     $html_name = Q($foldername . ($unread ? " ($unread)" : ''));
1502     $link_attrib = $folder['virtual'] ? array() : array(
1503       'href' => rcmail_url('', array('_mbox' => $folder['id'])),
1504       'onclick' => sprintf("return %s.command('list','%s',this)", JS_OBJECT_NAME, $js_name),
1505       'title' => $title,
1506     );
1507
1508     $out .= html::tag('li', array(
1509         'id' => "rcmli".$folder_id,
1510         'class' => join(' ', $classes),
1511         'noclose' => true),
1512       html::a($link_attrib, $html_name) .
1513       (!empty($folder['folders']) ? html::div(array(
1514         'class' => ($collapsed ? 'collapsed' : 'expanded'),
1515         'style' => "position:absolute",
1516         'onclick' => sprintf("%s.command('collapse-folder', '%s')", JS_OBJECT_NAME, $js_name)
1517       ), '&nbsp;') : ''));
1518
1519     $jslist[$folder_id] = array('id' => $folder['id'], 'name' => $foldername, 'virtual' => $folder['virtual']);
1520
1521     if (!empty($folder['folders'])) {
1522       $out .= html::tag('ul', array('style' => ($collapsed ? "display:none;" : null)),
1523         rcmail_render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
1524     }
1525
1526     $out .= "</li>\n";
1527     $idx++;
1528   }
1529
1530   return $out;
1531 }
1532
1533
1534 /**
1535  * Return html for a flat list <select> for the mailbox tree
1536  * @access private
1537  * @return string
1538  */
1539 function rcmail_render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames=false, $nestLevel=0)
1540 {
1541   $out = '';
1542
1543   foreach ($arrFolders as $key=>$folder) {
1544     if (!$realnames && ($folder_class = rcmail_folder_classname($folder['id'])))
1545       $foldername = rcube_label($folder_class);
1546     else {
1547       $foldername = $folder['name'];
1548
1549       // shorten the folder name to a given length
1550       if ($maxlength && $maxlength>1)
1551         $foldername = abbreviate_string($foldername, $maxlength);
1552     }
1553
1554     $select->add(str_repeat('&nbsp;', $nestLevel*4) . $foldername, $folder['id']);
1555
1556     if (!empty($folder['folders']))
1557       $out .= rcmail_render_folder_tree_select($folder['folders'], $mbox_name, $maxlength, $select, $realnames, $nestLevel+1);
1558   }
1559
1560   return $out;
1561 }
1562
1563
1564 /**
1565  * Return internal name for the given folder if it matches the configured special folders
1566  * @access private
1567  * @return string
1568  */
1569 function rcmail_folder_classname($folder_id)
1570 {
1571   global $CONFIG;
1572
1573   if ($folder_id == 'INBOX')
1574     return 'inbox';
1575
1576   // for these mailboxes we have localized labels and css classes
1577   foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
1578   {
1579     if ($folder_id == $CONFIG[$smbx.'_mbox'])
1580       return $smbx;
1581   }
1582 }
1583
1584
1585 /**
1586  * Try to localize the given IMAP folder name.
1587  * UTF-7 decode it in case no localized text was found
1588  *
1589  * @param string Folder name
1590  * @return string Localized folder name in UTF-8 encoding
1591  */
1592 function rcmail_localize_foldername($name)
1593 {
1594   if ($folder_class = rcmail_folder_classname($name))
1595     return rcube_label($folder_class);
1596   else
1597     return rcube_charset_convert($name, 'UTF7-IMAP');
1598 }
1599
1600
1601 function rcmail_quota_display($attrib)
1602 {
1603   global $OUTPUT;
1604
1605   if (!$attrib['id'])
1606     $attrib['id'] = 'rcmquotadisplay';
1607
1608   if(isset($attrib['display']))
1609     $_SESSION['quota_display'] = $attrib['display'];
1610
1611   $OUTPUT->add_gui_object('quotadisplay', $attrib['id']);
1612
1613   $quota = rcmail_quota_content($attrib);
1614
1615   $OUTPUT->add_script('$(document).ready(function(){
1616         rcmail.set_quota('.json_serialize($quota).')});', 'foot');
1617
1618   return html::span($attrib, '');
1619 }
1620
1621
1622 function rcmail_quota_content($attrib=NULL)
1623 {
1624   global $RCMAIL;
1625
1626   $quota = $RCMAIL->imap->get_quota();
1627   $quota = $RCMAIL->plugins->exec_hook('quota', $quota);
1628
1629   $quota_result = (array) $quota;
1630   $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
1631
1632   if (!$quota['total'] && $RCMAIL->config->get('quota_zero_as_unlimited')) {
1633     $quota_result['title'] = rcube_label('unlimited');
1634     $quota_result['percent'] = 0;
1635   }
1636   else if ($quota['total']) {
1637     if (!isset($quota['percent']))
1638       $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
1639
1640     $title = sprintf('%s / %s (%.0f%%)',
1641         show_bytes($quota['used'] * 1024), show_bytes($quota['total'] * 1024),
1642         $quota_result['percent']);
1643
1644     $quota_result['title'] = $title;
1645
1646     if ($attrib['width'])
1647       $quota_result['width'] = $attrib['width'];
1648     if ($attrib['height'])
1649       $quota_result['height']   = $attrib['height'];
1650   }
1651   else {
1652     $quota_result['title'] = rcube_label('unknown');
1653     $quota_result['percent'] = 0;
1654   }
1655
1656   return $quota_result;
1657 }
1658
1659
1660 /**
1661  * Outputs error message according to server error/response codes
1662  *
1663  * @param string Fallback message label
1664  * @param string Fallback message label arguments
1665  *
1666  * @return void
1667  */
1668 function rcmail_display_server_error($fallback=null, $fallback_args=null)
1669 {
1670     global $RCMAIL;
1671
1672     $err_code = $RCMAIL->imap->get_error_code();
1673     $res_code = $RCMAIL->imap->get_response_code();
1674
1675     if ($res_code == rcube_imap::NOPERM) {
1676         $RCMAIL->output->show_message('errornoperm', 'error');
1677     }
1678     else if ($res_code == rcube_imap::READONLY) {
1679         $RCMAIL->output->show_message('errorreadonly', 'error');
1680     }
1681     else if ($err_code && ($err_str = $RCMAIL->imap->get_error_str())) {
1682         $RCMAIL->output->show_message('servererrormsg', 'error', array('msg' => $err_str));
1683     }
1684     else if ($fallback) {
1685         $RCMAIL->output->show_message($fallback, 'error', $fallback_args);
1686     }
1687
1688     return true;
1689 }
1690
1691
1692 /**
1693  * Output HTML editor scripts
1694  *
1695  * @param string Editor mode
1696  * @return void
1697  */
1698 function rcube_html_editor($mode='')
1699 {
1700   global $RCMAIL, $CONFIG;
1701
1702   $hook = $RCMAIL->plugins->exec_hook('html_editor', array('mode' => $mode));
1703
1704   if ($hook['abort'])
1705     return;  
1706
1707   $lang = strtolower($_SESSION['language']);
1708
1709   // TinyMCE uses 'tw' for zh_TW (which is wrong, because tw is a code of Twi language)
1710   $lang = ($lang == 'zh_tw') ? 'tw' : substr($lang, 0, 2);
1711
1712   if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js'))
1713     $lang = 'en';
1714
1715   $RCMAIL->output->include_script('tiny_mce/tiny_mce.js');
1716   $RCMAIL->output->include_script('editor.js');
1717   $RCMAIL->output->add_script(sprintf("rcmail_editor_init('\$__skin_path', '%s', %d, '%s');",
1718     JQ($lang), intval($CONFIG['enable_spellcheck']), $mode),
1719     'foot');
1720 }
1721
1722
1723 /**
1724  * Replaces TinyMCE's emoticon images with plain-text representation
1725  *
1726  * @param string HTML content
1727  * @return string HTML content
1728  */
1729 function rcmail_replace_emoticons($html)
1730 {
1731   $emoticons = array(
1732     '8-)' => 'smiley-cool',
1733     ':-#' => 'smiley-foot-in-mouth',
1734     ':-*' => 'smiley-kiss',
1735     ':-X' => 'smiley-sealed',
1736     ':-P' => 'smiley-tongue-out',
1737     ':-@' => 'smiley-yell',
1738     ":'(" => 'smiley-cry',
1739     ':-(' => 'smiley-frown',
1740     ':-D' => 'smiley-laughing',
1741     ':-)' => 'smiley-smile',
1742     ':-S' => 'smiley-undecided',
1743     ':-$' => 'smiley-embarassed',
1744     'O:-)' => 'smiley-innocent',
1745     ':-|' => 'smiley-money-mouth',
1746     ':-O' => 'smiley-surprised',
1747     ';-)' => 'smiley-wink',
1748   );
1749
1750   foreach ($emoticons as $idx => $file) {
1751     // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
1752     $search[]  = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
1753     $replace[] = $idx;
1754   }
1755
1756   return preg_replace($search, $replace, $html);
1757 }
1758
1759
1760 /**
1761  * Check if working in SSL mode
1762  *
1763  * @param integer HTTPS port number
1764  * @param boolean Enables 'use_https' option checking
1765  * @return boolean
1766  */
1767 function rcube_https_check($port=null, $use_https=true)
1768 {
1769   global $RCMAIL;
1770
1771   if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off')
1772     return true;
1773   if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https')
1774     return true;
1775   if ($port && $_SERVER['SERVER_PORT'] == $port)
1776     return true;
1777   if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https'))
1778     return true;
1779
1780   return false;
1781 }
1782
1783
1784 /**
1785  * For backward compatibility.
1786  *
1787  * @global rcmail $RCMAIL
1788  * @param string $var_name Variable name.
1789  * @return void
1790  */
1791 function rcube_sess_unset($var_name=null)
1792 {
1793   global $RCMAIL;
1794
1795   $RCMAIL->session->remove($var_name);
1796 }
1797
1798
1799
1800 /**
1801  * Replaces hostname variables
1802  *
1803  * @param string $name Hostname
1804  * @param string $host Optional IMAP hostname
1805  * @return string
1806  */
1807 function rcube_parse_host($name, $host='')
1808 {
1809   // %n - host
1810   $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
1811   // %d - domain name without first part, e.g. %d=mail.domain.tld, %m=domain.tld
1812   $d = preg_replace('/^[^\.]+\./', '', $n);
1813   // %h - IMAP host
1814   $h = $_SESSION['imap_host'] ? $_SESSION['imap_host'] : $host;
1815   // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
1816   $z = preg_replace('/^[^\.]+\./', '', $h);
1817
1818   $name = str_replace(array('%n', '%d', '%h', '%z'), array($n, $d, $h, $z), $name);
1819   return $name;
1820 }
1821
1822
1823 /**
1824  * E-mail address validation
1825  *
1826  * @param string $email Email address
1827  * @param boolean $dns_check True to check dns
1828  * @return boolean
1829  */
1830 function check_email($email, $dns_check=true)
1831 {
1832   // Check for invalid characters
1833   if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email))
1834     return false;
1835
1836   // Check for length limit specified by RFC 5321 (#1486453)
1837   if (strlen($email) > 254) 
1838     return false;
1839
1840   $email_array = explode('@', $email);
1841
1842   // Check that there's one @ symbol
1843   if (count($email_array) < 2)
1844     return false;
1845
1846   $domain_part = array_pop($email_array);
1847   $local_part = implode('@', $email_array);
1848
1849   // from PEAR::Validate
1850   $regexp = '&^(?:
1851         ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")|                             #1 quoted name
1852         ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*))  #2 OR dot-atom (RFC5322)
1853         $&xi';
1854
1855   if (!preg_match($regexp, $local_part))
1856     return false;
1857
1858   // Check domain part
1859   if (preg_match('/^\[*(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\]*$/', $domain_part))
1860     return true; // IP address
1861   else {
1862     // If not an IP address
1863     $domain_array = explode('.', $domain_part);
1864     if (sizeof($domain_array) < 2)
1865       return false; // Not enough parts to be a valid domain
1866
1867     foreach ($domain_array as $part)
1868       if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part))
1869         return false;
1870
1871     if (!$dns_check || !rcmail::get_instance()->config->get('email_dns_check'))
1872       return true;
1873
1874     if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
1875       $lookup = array();
1876       @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
1877       foreach ($lookup as $line) {
1878         if (strpos($line, 'MX preference'))
1879           return true;
1880       }
1881       return false;
1882     }
1883
1884     // find MX record(s)
1885     if (getmxrr($domain_part, $mx_records))
1886       return true;
1887
1888     // find any DNS record
1889     if (checkdnsrr($domain_part, 'ANY'))
1890       return true;
1891   }
1892
1893   return false;
1894 }
1895
1896 /*
1897  * Idn_to_ascii wrapper.
1898  * Intl/Idn modules version of this function doesn't work with e-mail address
1899  */
1900 function rcube_idn_to_ascii($str)
1901 {
1902   return rcube_idn_convert($str, true);
1903 }
1904
1905 /*
1906  * Idn_to_ascii wrapper.
1907  * Intl/Idn modules version of this function doesn't work with e-mail address
1908  */
1909 function rcube_idn_to_utf8($str)
1910 {
1911   return rcube_idn_convert($str, false);
1912 }
1913
1914 function rcube_idn_convert($input, $is_utf=false)
1915 {
1916   if ($at = strpos($input, '@')) {
1917     $user   = substr($input, 0, $at);
1918     $domain = substr($input, $at+1);
1919   }
1920   else {
1921     $domain = $input;
1922   }
1923
1924   $domain = $is_utf ? idn_to_ascii($domain) : idn_to_utf8($domain);
1925
1926   if ($domain === false) {
1927     return '';
1928   }
1929
1930   return $at ? $user . '@' . $domain : $domain;
1931 }
1932
1933
1934 /**
1935  * Helper class to turn relative urls into absolute ones
1936  * using a predefined base
1937  */
1938 class rcube_base_replacer
1939 {
1940   private $base_url;
1941
1942   public function __construct($base)
1943   {
1944     $this->base_url = $base;
1945   }
1946
1947   public function callback($matches)
1948   {
1949     return $matches[1] . '="' . make_absolute_url($matches[3], $this->base_url) . '"';
1950   }
1951 }
1952
1953
1954 /**
1955  * Throw system error and show error page
1956  *
1957  * @param array Named parameters
1958  *  - code: Error code (required)
1959  *  - type: Error type [php|db|imap|javascript] (required)
1960  *  - message: Error message
1961  *  - file: File where error occured
1962  *  - line: Line where error occured
1963  * @param boolean True to log the error
1964  * @param boolean Terminate script execution
1965  */
1966 // may be defined in Installer
1967 if (!function_exists('raise_error')) {
1968 function raise_error($arg=array(), $log=false, $terminate=false)
1969 {
1970     global $__page_content, $CONFIG, $OUTPUT, $ERROR_CODE, $ERROR_MESSAGE;
1971
1972     // report bug (if not incompatible browser)
1973     if ($log && $arg['type'] && $arg['message'])
1974         log_bug($arg);
1975
1976     // display error page and terminate script
1977     if ($terminate) {
1978         $ERROR_CODE = $arg['code'];
1979         $ERROR_MESSAGE = $arg['message'];
1980         include('program/steps/utils/error.inc');
1981         exit;
1982     }
1983 }
1984 }
1985
1986
1987 /**
1988  * Report error according to configured debug_level
1989  *
1990  * @param array Named parameters
1991  * @return void
1992  * @see raise_error()
1993  */
1994 function log_bug($arg_arr)
1995 {
1996     global $CONFIG;
1997     $program = strtoupper($arg_arr['type']);
1998
1999     // write error to local log file
2000     if ($CONFIG['debug_level'] & 1) {
2001         $post_query = ($_SERVER['REQUEST_METHOD'] == 'POST' ? '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']) : '');
2002         $log_entry = sprintf("%s Error: %s%s (%s %s)",
2003             $program,
2004             $arg_arr['message'],
2005             $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
2006             $_SERVER['REQUEST_METHOD'],
2007             $_SERVER['REQUEST_URI'] . $post_query);
2008
2009         if (!write_log('errors', $log_entry)) {
2010             // send error to PHPs error handler if write_log didn't succeed
2011             trigger_error($arg_arr['message']);
2012         }
2013     }
2014
2015     // resport the bug to the global bug reporting system
2016     if ($CONFIG['debug_level'] & 2) {
2017         // TODO: Send error via HTTP
2018     }
2019
2020     // show error if debug_mode is on
2021     if ($CONFIG['debug_level'] & 4) {
2022         print "<b>$program Error";
2023
2024         if (!empty($arg_arr['file']) && !empty($arg_arr['line']))
2025             print " in $arg_arr[file] ($arg_arr[line])";
2026
2027         print ':</b>&nbsp;';
2028         print nl2br($arg_arr['message']);
2029         print '<br />';
2030         flush();
2031     }
2032 }
2033