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