]> git.donarmstrong.com Git - roundcube.git/blob - program/include/main.inc
Imported Upstream version 0.5.2+dfsg
[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 4568 2011-02-24 12:12:09Z 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[^a]/', $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       $format = $CONFIG['date_today'] ? $CONFIG['date_today'] : 'H:i';
1025       $today  = true;
1026     }
1027     else if ($CONFIG['prettydate'] && $timestamp > $week_limit && $timestamp < $now)
1028       $format = $CONFIG['date_short'] ? $CONFIG['date_short'] : 'D H:i';
1029     else
1030       $format = $CONFIG['date_long'] ? $CONFIG['date_long'] : 'd.m.Y H:i';
1031   }
1032
1033   // strftime() format
1034   if (preg_match('/%[a-z]+/i', $format)) {
1035     $format = strftime($format, $timestamp);
1036     return $today ? (rcube_label('today') . ' ' . $format) : $format;
1037   }
1038
1039   // parse format string manually in order to provide localized weekday and month names
1040   // an alternative would be to convert the date() format string to fit with strftime()
1041   $out = '';
1042   for($i=0; $i<strlen($format); $i++) {
1043     if ($format{$i}=='\\')  // skip escape chars
1044       continue;
1045
1046     // write char "as-is"
1047     if ($format{$i}==' ' || $format{$i-1}=='\\')
1048       $out .= $format{$i};
1049     // weekday (short)
1050     else if ($format{$i}=='D')
1051       $out .= rcube_label(strtolower(date('D', $timestamp)));
1052     // weekday long
1053     else if ($format{$i}=='l')
1054       $out .= rcube_label(strtolower(date('l', $timestamp)));
1055     // month name (short)
1056     else if ($format{$i}=='M')
1057       $out .= rcube_label(strtolower(date('M', $timestamp)));
1058     // month name (long)
1059     else if ($format{$i}=='F')
1060       $out .= rcube_label('long'.strtolower(date('M', $timestamp)));
1061     else if ($format{$i}=='x')
1062       $out .= strftime('%x %X', $timestamp);
1063     else
1064       $out .= date($format{$i}, $timestamp);
1065   }
1066
1067   if ($today) {
1068     $label = rcube_label('today');
1069     // replcae $ character with "Today" label (#1486120)
1070     if (strpos($out, '$') !== false) {
1071       $out = preg_replace('/\$/', $label, $out, 1);
1072     }
1073     else {
1074       $out = $label . ' ' . $out;
1075     }
1076   }
1077
1078   return $out;
1079 }
1080
1081
1082 /**
1083  * Compose a valid representation of name and e-mail address
1084  *
1085  * @param string E-mail address
1086  * @param string Person name
1087  * @return string Formatted string
1088  */
1089 function format_email_recipient($email, $name='')
1090   {
1091   if ($name && $name != $email)
1092     {
1093     // Special chars as defined by RFC 822 need to in quoted string (or escaped).
1094     return sprintf('%s <%s>', preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name) ? '"'.addcslashes($name, '"').'"' : $name, trim($email));
1095     }
1096   else
1097     return trim($email);
1098   }
1099
1100
1101
1102 /****** debugging functions ********/
1103
1104
1105 /**
1106  * Print or write debug messages
1107  *
1108  * @param mixed Debug message or data
1109  * @return void
1110  */
1111 function console()
1112   {
1113   $args = func_get_args();
1114
1115   if (class_exists('rcmail', false)) {
1116     $rcmail = rcmail::get_instance();
1117     if (is_object($rcmail->plugins))
1118       $rcmail->plugins->exec_hook('console', $args);
1119   }
1120
1121   $msg = array();
1122   foreach ($args as $arg)
1123     $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
1124
1125   if (!($GLOBALS['CONFIG']['debug_level'] & 4))
1126     write_log('console', join(";\n", $msg));
1127   else if ($GLOBALS['OUTPUT']->ajax_call)
1128     print "/*\n " . join(";\n", $msg) . " \n*/\n";
1129   else
1130     {
1131     print '<div style="background:#eee; border:1px solid #ccc; margin-bottom:3px; padding:6px"><pre>';
1132     print join(";<br/>\n", $msg);
1133     print "</pre></div>\n";
1134     }
1135   }
1136
1137
1138 /**
1139  * Append a line to a logfile in the logs directory.
1140  * Date will be added automatically to the line.
1141  *
1142  * @param $name name of log file
1143  * @param line Line to append
1144  * @return void
1145  */
1146 function write_log($name, $line)
1147   {
1148   global $CONFIG, $RCMAIL;
1149
1150   if (!is_string($line))
1151     $line = var_export($line, true);
1152  
1153   if (empty($CONFIG['log_date_format']))
1154     $CONFIG['log_date_format'] = 'd-M-Y H:i:s O';
1155   
1156   $date = date($CONFIG['log_date_format']);
1157   
1158   // trigger logging hook
1159   if (is_object($RCMAIL) && is_object($RCMAIL->plugins)) {
1160     $log = $RCMAIL->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line));
1161     $name = $log['name'];
1162     $line = $log['line'];
1163     $date = $log['date'];
1164     if ($log['abort'])
1165       return true;
1166   }
1167  
1168   if ($CONFIG['log_driver'] == 'syslog') {
1169     $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
1170     syslog($prio, $line);
1171     return true;
1172   }
1173   else {
1174     $line = sprintf("[%s]: %s\n", $date, $line);
1175
1176     // log_driver == 'file' is assumed here
1177     if (empty($CONFIG['log_dir']))
1178       $CONFIG['log_dir'] = INSTALL_PATH.'logs';
1179
1180     // try to open specific log file for writing
1181     $logfile = $CONFIG['log_dir'].'/'.$name;
1182     if ($fp = @fopen($logfile, 'a')) {
1183       fwrite($fp, $line);
1184       fflush($fp);
1185       fclose($fp);
1186       return true;
1187     }
1188     else
1189       trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
1190   }
1191   return false;
1192 }
1193
1194
1195 /**
1196  * Write login data (name, ID, IP address) to the 'userlogins' log file.
1197  *
1198  * @return void
1199  */
1200 function rcmail_log_login()
1201 {
1202   global $RCMAIL;
1203
1204   if (!$RCMAIL->config->get('log_logins') || !$RCMAIL->user)
1205     return;
1206
1207   write_log('userlogins', sprintf('Successful login for %s (ID: %d) from %s',
1208     $RCMAIL->user->get_username(), $RCMAIL->user->ID, rcmail_remote_ip()));
1209 }
1210
1211
1212 /**
1213  * Returns remote IP address and forwarded addresses if found
1214  *
1215  * @return string Remote IP address(es)
1216  */
1217 function rcmail_remote_ip()
1218 {
1219     $address = $_SERVER['REMOTE_ADDR'];
1220
1221     // append the NGINX X-Real-IP header, if set
1222     if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
1223         $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
1224     }
1225     // append the X-Forwarded-For header, if set
1226     if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1227         $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
1228     }
1229
1230     if (!empty($remote_ip))
1231         $address .= '(' . implode(',', $remote_ip) . ')';
1232
1233     return $address;
1234 }
1235
1236
1237 /**
1238  * Check whether the HTTP referer matches the current request
1239  *
1240  * @return boolean True if referer is the same host+path, false if not
1241  */
1242 function rcube_check_referer()
1243 {
1244   $uri = parse_url($_SERVER['REQUEST_URI']);
1245   $referer = parse_url(rc_request_header('Referer'));
1246   return $referer['host'] == rc_request_header('Host') && $referer['path'] == $uri['path'];
1247 }
1248
1249
1250 /**
1251  * @access private
1252  * @return mixed
1253  */
1254 function rcube_timer()
1255 {
1256   return microtime(true);
1257 }
1258
1259
1260 /**
1261  * @access private
1262  * @return void
1263  */
1264 function rcube_print_time($timer, $label='Timer', $dest='console')
1265 {
1266   static $print_count = 0;
1267   
1268   $print_count++;
1269   $now = rcube_timer();
1270   $diff = $now-$timer;
1271   
1272   if (empty($label))
1273     $label = 'Timer '.$print_count;
1274   
1275   write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
1276 }
1277
1278
1279 /**
1280  * Return the mailboxlist in HTML
1281  *
1282  * @param array Named parameters
1283  * @return string HTML code for the gui object
1284  */
1285 function rcmail_mailbox_list($attrib)
1286 {
1287   global $RCMAIL;
1288   static $a_mailboxes;
1289   
1290   $attrib += array('maxlength' => 100, 'realnames' => false);
1291
1292   // add some labels to client
1293   $RCMAIL->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
1294   
1295   $type = $attrib['type'] ? $attrib['type'] : 'ul';
1296   unset($attrib['type']);
1297
1298   if ($type=='ul' && !$attrib['id'])
1299     $attrib['id'] = 'rcmboxlist';
1300
1301   // get mailbox list
1302   $mbox_name = $RCMAIL->imap->get_mailbox_name();
1303   
1304   // build the folders tree
1305   if (empty($a_mailboxes)) {
1306     // get mailbox list
1307     $a_folders = $RCMAIL->imap->list_mailboxes();
1308     $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1309     $a_mailboxes = array();
1310
1311     foreach ($a_folders as $folder)
1312       rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
1313   }
1314
1315   // allow plugins to alter the folder tree or to localize folder names
1316   $hook = $RCMAIL->plugins->exec_hook('render_mailboxlist', array('list' => $a_mailboxes, 'delimiter' => $delimiter));
1317
1318   if ($type=='select') {
1319     $select = new html_select($attrib);
1320     
1321     // add no-selection option
1322     if ($attrib['noselection'])
1323       $select->add(rcube_label($attrib['noselection']), '0');
1324     
1325     rcmail_render_folder_tree_select($hook['list'], $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
1326     $out = $select->show();
1327   }
1328   else {
1329     $js_mailboxlist = array();
1330     $out = html::tag('ul', $attrib, rcmail_render_folder_tree_html($hook['list'], $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
1331     
1332     $RCMAIL->output->add_gui_object('mailboxlist', $attrib['id']);
1333     $RCMAIL->output->set_env('mailboxes', $js_mailboxlist);
1334     $RCMAIL->output->set_env('collapsed_folders', $RCMAIL->config->get('collapsed_folders'));
1335   }
1336
1337   return $out;
1338 }
1339
1340
1341 /**
1342  * Return the mailboxlist as html_select object
1343  *
1344  * @param array Named parameters
1345  * @return html_select HTML drop-down object
1346  */
1347 function rcmail_mailbox_select($p = array())
1348 {
1349   global $RCMAIL;
1350   
1351   $p += array('maxlength' => 100, 'realnames' => false);
1352   $a_mailboxes = array();
1353
1354   if ($p['unsubscribed'])
1355     $list = $RCMAIL->imap->list_unsubscribed();
1356   else
1357     $list = $RCMAIL->imap->list_mailboxes();
1358
1359   foreach ($list as $folder)
1360     if (empty($p['exceptions']) || !in_array($folder, $p['exceptions']))
1361       rcmail_build_folder_tree($a_mailboxes, $folder, $RCMAIL->imap->get_hierarchy_delimiter());
1362
1363   $select = new html_select($p);
1364   
1365   if ($p['noselection'])
1366     $select->add($p['noselection'], '');
1367     
1368   rcmail_render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames']);
1369   
1370   return $select;
1371 }
1372
1373
1374 /**
1375  * Create a hierarchical array of the mailbox list
1376  * @access private
1377  * @return void
1378  */
1379 function rcmail_build_folder_tree(&$arrFolders, $folder, $delm='/', $path='')
1380 {
1381   global $RCMAIL;
1382
1383   $pos = strpos($folder, $delm);
1384
1385   if ($pos !== false) {
1386     $subFolders = substr($folder, $pos+1);
1387     $currentFolder = substr($folder, 0, $pos);
1388
1389     // sometimes folder has a delimiter as the last character
1390     if (!strlen($subFolders))
1391       $virtual = false;
1392     else if (!isset($arrFolders[$currentFolder]))
1393       $virtual = true;
1394     else
1395       $virtual = $arrFolders[$currentFolder]['virtual'];
1396   }
1397   else {
1398     $subFolders = false;
1399     $currentFolder = $folder;
1400     $virtual = false;
1401   }
1402
1403   $path .= $currentFolder;
1404
1405   // Check \Noselect option (if options are in cache)
1406   if (!$virtual && ($opts = $RCMAIL->imap->mailbox_options($path))) {
1407     $virtual = in_array('\\Noselect', $opts);
1408   }
1409
1410   if (!isset($arrFolders[$currentFolder])) {
1411     $arrFolders[$currentFolder] = array(
1412       'id' => $path,
1413       'name' => rcube_charset_convert($currentFolder, 'UTF7-IMAP'),
1414       'virtual' => $virtual,
1415       'folders' => array());
1416   }
1417   else
1418     $arrFolders[$currentFolder]['virtual'] = $virtual;
1419
1420   if (strlen($subFolders))
1421     rcmail_build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
1422 }
1423   
1424
1425 /**
1426  * Return html for a structured list &lt;ul&gt; for the mailbox tree
1427  * @access private
1428  * @return string
1429  */
1430 function rcmail_render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel=0)
1431 {
1432   global $RCMAIL, $CONFIG;
1433   
1434   $maxlength = intval($attrib['maxlength']);
1435   $realnames = (bool)$attrib['realnames'];
1436   $msgcounts = $RCMAIL->imap->get_cache('messagecount');
1437
1438   $idx = 0;
1439   $out = '';
1440   foreach ($arrFolders as $key => $folder) {
1441     $zebra_class = (($nestLevel+1)*$idx) % 2 == 0 ? 'even' : 'odd';
1442     $title = null;
1443
1444     if (($folder_class = rcmail_folder_classname($folder['id'])) && !$realnames) {
1445       $foldername = rcube_label($folder_class);
1446     }
1447     else {
1448       $foldername = $folder['name'];
1449
1450       // shorten the folder name to a given length
1451       if ($maxlength && $maxlength > 1) {
1452         $fname = abbreviate_string($foldername, $maxlength);
1453         if ($fname != $foldername)
1454           $title = $foldername;
1455         $foldername = $fname;
1456       }
1457     }
1458
1459     // make folder name safe for ids and class names
1460     $folder_id = asciiwords($folder['id'], true, '_');
1461     $classes = array('mailbox');
1462
1463     // set special class for Sent, Drafts, Trash and Junk
1464     if ($folder['id']==$CONFIG['sent_mbox'])
1465       $classes[] = 'sent';
1466     else if ($folder['id']==$CONFIG['drafts_mbox'])
1467       $classes[] = 'drafts';
1468     else if ($folder['id']==$CONFIG['trash_mbox'])
1469       $classes[] = 'trash';
1470     else if ($folder['id']==$CONFIG['junk_mbox'])
1471       $classes[] = 'junk';
1472     else if ($folder['id']=='INBOX')
1473       $classes[] = 'inbox';
1474     else
1475       $classes[] = '_'.asciiwords($folder_class ? $folder_class : strtolower($folder['id']), true);
1476       
1477     $classes[] = $zebra_class;
1478     
1479     if ($folder['id'] == $mbox_name)
1480       $classes[] = 'selected';
1481
1482     $collapsed = preg_match('/&'.rawurlencode($folder['id']).'&/', $RCMAIL->config->get('collapsed_folders'));
1483     $unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
1484     
1485     if ($folder['virtual'])
1486       $classes[] = 'virtual';
1487     else if ($unread)
1488       $classes[] = 'unread';
1489
1490     $js_name = JQ($folder['id']);
1491     $html_name = Q($foldername . ($unread ? " ($unread)" : ''));
1492     $link_attrib = $folder['virtual'] ? array() : array(
1493       'href' => rcmail_url('', array('_mbox' => $folder['id'])),
1494       'onclick' => sprintf("return %s.command('list','%s',this)", JS_OBJECT_NAME, $js_name),
1495       'title' => $title,
1496     );
1497
1498     $out .= html::tag('li', array(
1499         'id' => "rcmli".$folder_id,
1500         'class' => join(' ', $classes),
1501         'noclose' => true),
1502       html::a($link_attrib, $html_name) .
1503       (!empty($folder['folders']) ? html::div(array(
1504         'class' => ($collapsed ? 'collapsed' : 'expanded'),
1505         'style' => "position:absolute",
1506         'onclick' => sprintf("%s.command('collapse-folder', '%s')", JS_OBJECT_NAME, $js_name)
1507       ), '&nbsp;') : ''));
1508     
1509     $jslist[$folder_id] = array('id' => $folder['id'], 'name' => $foldername, 'virtual' => $folder['virtual']);
1510     
1511     if (!empty($folder['folders'])) {
1512       $out .= html::tag('ul', array('style' => ($collapsed ? "display:none;" : null)),
1513         rcmail_render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
1514     }
1515
1516     $out .= "</li>\n";
1517     $idx++;
1518   }
1519
1520   return $out;
1521 }
1522
1523
1524 /**
1525  * Return html for a flat list <select> for the mailbox tree
1526  * @access private
1527  * @return string
1528  */
1529 function rcmail_render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames=false, $nestLevel=0)
1530   {
1531   $idx = 0;
1532   $out = '';
1533   foreach ($arrFolders as $key=>$folder)
1534     {
1535     if (!$realnames && ($folder_class = rcmail_folder_classname($folder['id'])))
1536       $foldername = rcube_label($folder_class);
1537     else
1538       {
1539       $foldername = $folder['name'];
1540       
1541       // shorten the folder name to a given length
1542       if ($maxlength && $maxlength>1)
1543         $foldername = abbreviate_string($foldername, $maxlength);
1544       }
1545
1546     $select->add(str_repeat('&nbsp;', $nestLevel*4) . $foldername, $folder['id']);
1547
1548     if (!empty($folder['folders']))
1549       $out .= rcmail_render_folder_tree_select($folder['folders'], $mbox_name, $maxlength, $select, $realnames, $nestLevel+1);
1550
1551     $idx++;
1552     }
1553
1554   return $out;
1555   }
1556
1557
1558 /**
1559  * Return internal name for the given folder if it matches the configured special folders
1560  * @access private
1561  * @return string
1562  */
1563 function rcmail_folder_classname($folder_id)
1564 {
1565   global $CONFIG;
1566
1567   if ($folder_id == 'INBOX')
1568     return 'inbox';
1569
1570   // for these mailboxes we have localized labels and css classes
1571   foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
1572   {
1573     if ($folder_id == $CONFIG[$smbx.'_mbox'])
1574       return $smbx;
1575   }
1576 }
1577
1578
1579 /**
1580  * Try to localize the given IMAP folder name.
1581  * UTF-7 decode it in case no localized text was found
1582  *
1583  * @param string Folder name
1584  * @return string Localized folder name in UTF-8 encoding
1585  */
1586 function rcmail_localize_foldername($name)
1587 {
1588   if ($folder_class = rcmail_folder_classname($name))
1589     return rcube_label($folder_class);
1590   else
1591     return rcube_charset_convert($name, 'UTF7-IMAP');
1592 }
1593
1594
1595 function rcmail_quota_display($attrib)
1596 {
1597   global $OUTPUT;
1598
1599   if (!$attrib['id'])
1600     $attrib['id'] = 'rcmquotadisplay';
1601
1602   if(isset($attrib['display']))
1603     $_SESSION['quota_display'] = $attrib['display'];
1604
1605   $OUTPUT->add_gui_object('quotadisplay', $attrib['id']);
1606
1607   $quota = rcmail_quota_content($attrib);
1608
1609   $OUTPUT->add_script('$(document).ready(function(){
1610         rcmail.set_quota('.json_serialize($quota).')});', 'foot');
1611
1612   return html::span($attrib, '');
1613 }
1614
1615
1616 function rcmail_quota_content($attrib=NULL)
1617 {
1618   global $RCMAIL;
1619
1620   $quota = $RCMAIL->imap->get_quota();
1621   $quota = $RCMAIL->plugins->exec_hook('quota', $quota);
1622
1623   $quota_result = (array) $quota;
1624   $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
1625
1626   if (!$quota['total'] && $RCMAIL->config->get('quota_zero_as_unlimited')) {
1627     $quota_result['title'] = rcube_label('unlimited');
1628     $quota_result['percent'] = 0;
1629   }
1630   else if ($quota['total']) {
1631     if (!isset($quota['percent']))
1632       $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
1633
1634     $title = sprintf('%s / %s (%.0f%%)',
1635         show_bytes($quota['used'] * 1024), show_bytes($quota['total'] * 1024),
1636         $quota_result['percent']);
1637
1638     $quota_result['title'] = $title;
1639
1640     if ($attrib['width'])
1641       $quota_result['width'] = $attrib['width'];
1642     if ($attrib['height'])
1643       $quota_result['height']   = $attrib['height'];
1644   }
1645   else {
1646     $quota_result['title'] = rcube_label('unknown');
1647     $quota_result['percent'] = 0;
1648   }
1649
1650   return $quota_result;
1651 }
1652
1653
1654 /**
1655  * Outputs error message according to server error/response codes
1656  *
1657  * @param string Fallback message label
1658  * @param string Fallback message label arguments
1659  *
1660  * @return void
1661  */
1662 function rcmail_display_server_error($fallback=null, $fallback_args=null)
1663 {
1664     global $RCMAIL;
1665
1666     $err_code = $RCMAIL->imap->get_error_code();
1667     $res_code = $RCMAIL->imap->get_response_code();
1668
1669     if ($res_code == rcube_imap::NOPERM) {
1670         $RCMAIL->output->show_message('errornoperm', 'error');
1671     }
1672     else if ($res_code == rcube_imap::READONLY) {
1673         $RCMAIL->output->show_message('errorreadonly', 'error');
1674     }
1675     else if ($err_code && ($err_str = $RCMAIL->imap->get_error_str())) {
1676         $RCMAIL->output->show_message('servererrormsg', 'error', array('msg' => $err_str));
1677     }
1678     else if ($fallback) {
1679         $RCMAIL->output->show_message($fallback, 'error', $fallback_args);
1680     }
1681
1682     return true;
1683 }
1684
1685
1686 /**
1687  * Output HTML editor scripts
1688  *
1689  * @param string Editor mode
1690  * @return void
1691  */
1692 function rcube_html_editor($mode='')
1693 {
1694   global $RCMAIL, $CONFIG;
1695
1696   $hook = $RCMAIL->plugins->exec_hook('html_editor', array('mode' => $mode));
1697
1698   if ($hook['abort'])
1699     return;  
1700
1701   $lang = strtolower($_SESSION['language']);
1702
1703   // TinyMCE uses 'tw' for zh_TW (which is wrong, because tw is a code of Twi language)
1704   $lang = ($lang == 'zh_tw') ? 'tw' : substr($lang, 0, 2);
1705
1706   if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js'))
1707     $lang = 'en';
1708
1709   $RCMAIL->output->include_script('tiny_mce/tiny_mce.js');
1710   $RCMAIL->output->include_script('editor.js');
1711   $RCMAIL->output->add_script(sprintf("rcmail_editor_init('\$__skin_path', '%s', %d, '%s');",
1712     JQ($lang), intval($CONFIG['enable_spellcheck']), $mode),
1713     'foot');
1714 }
1715
1716
1717 /**
1718  * Replaces TinyMCE's emoticon images with plain-text representation
1719  *
1720  * @param string HTML content
1721  * @return string HTML content
1722  */
1723 function rcmail_replace_emoticons($html)
1724 {
1725   $emoticons = array(
1726     '8-)' => 'smiley-cool',
1727     ':-#' => 'smiley-foot-in-mouth',
1728     ':-*' => 'smiley-kiss',
1729     ':-X' => 'smiley-sealed',
1730     ':-P' => 'smiley-tongue-out',
1731     ':-@' => 'smiley-yell',
1732     ":'(" => 'smiley-cry',
1733     ':-(' => 'smiley-frown',
1734     ':-D' => 'smiley-laughing',
1735     ':-)' => 'smiley-smile',
1736     ':-S' => 'smiley-undecided',
1737     ':-$' => 'smiley-embarassed',
1738     'O:-)' => 'smiley-innocent',
1739     ':-|' => 'smiley-money-mouth',
1740     ':-O' => 'smiley-surprised',
1741     ';-)' => 'smiley-wink',
1742   );
1743
1744   foreach ($emoticons as $idx => $file) {
1745     // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
1746     $search[]  = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
1747     $replace[] = $idx;
1748   }
1749
1750   return preg_replace($search, $replace, $html);
1751 }
1752
1753
1754 /**
1755  * Check if working in SSL mode
1756  *
1757  * @param integer HTTPS port number
1758  * @param boolean Enables 'use_https' option checking
1759  * @return boolean
1760  */
1761 function rcube_https_check($port=null, $use_https=true)
1762 {
1763   global $RCMAIL;
1764
1765   if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off')
1766     return true;
1767   if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https')
1768     return true;
1769   if ($port && $_SERVER['SERVER_PORT'] == $port)
1770     return true;
1771   if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https'))
1772     return true;
1773
1774   return false;
1775 }
1776
1777
1778 /**
1779  * For backward compatibility.
1780  *
1781  * @global rcmail $RCMAIL
1782  * @param string $var_name Variable name.
1783  * @return void
1784  */
1785 function rcube_sess_unset($var_name=null)
1786 {
1787   global $RCMAIL;
1788
1789   $RCMAIL->session->remove($var_name);
1790 }
1791
1792
1793
1794 /**
1795  * Replaces hostname variables
1796  *
1797  * @param string $name Hostname
1798  * @param string $host Optional IMAP hostname
1799  * @return string
1800  */
1801 function rcube_parse_host($name, $host='')
1802 {
1803   // %n - host
1804   $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
1805   // %d - domain name without first part, e.g. %d=mail.domain.tld, %m=domain.tld
1806   $d = preg_replace('/^[^\.]+\./', '', $n);
1807   // %h - IMAP host
1808   $h = $_SESSION['imap_host'] ? $_SESSION['imap_host'] : $host;
1809   // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
1810   $z = preg_replace('/^[^\.]+\./', '', $h);
1811
1812   $name = str_replace(array('%n', '%d', '%h', '%z'), array($n, $d, $h, $z), $name);
1813   return $name;
1814 }
1815
1816
1817 /**
1818  * E-mail address validation
1819  *
1820  * @param string $email Email address
1821  * @param boolean $dns_check True to check dns
1822  * @return boolean
1823  */
1824 function check_email($email, $dns_check=true)
1825 {
1826   // Check for invalid characters
1827   if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email))
1828     return false;
1829
1830   // Check for length limit specified by RFC 5321 (#1486453)
1831   if (strlen($email) > 254) 
1832     return false;
1833
1834   $email_array = explode('@', $email);
1835
1836   // Check that there's one @ symbol
1837   if (count($email_array) < 2)
1838     return false;
1839
1840   $domain_part = array_pop($email_array);
1841   $local_part = implode('@', $email_array);
1842
1843   // from PEAR::Validate
1844   $regexp = '&^(?:
1845         ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")|                             #1 quoted name
1846         ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*))  #2 OR dot-atom (RFC5322)
1847         $&xi';
1848
1849   if (!preg_match($regexp, $local_part))
1850     return false;
1851
1852   // Check domain part
1853   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))
1854     return true; // IP address
1855   else {
1856     // If not an IP address
1857     $domain_array = explode('.', $domain_part);
1858     if (sizeof($domain_array) < 2)
1859       return false; // Not enough parts to be a valid domain
1860
1861     foreach ($domain_array as $part)
1862       if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part))
1863         return false;
1864
1865     if (!$dns_check || !rcmail::get_instance()->config->get('email_dns_check'))
1866       return true;
1867
1868     if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
1869       $lookup = array();
1870       @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
1871       foreach ($lookup as $line) {
1872         if (strpos($line, 'MX preference'))
1873           return true;
1874       }
1875       return false;
1876     }
1877
1878     // find MX record(s)
1879     if (getmxrr($domain_part, $mx_records))
1880       return true;
1881
1882     // find any DNS record
1883     if (checkdnsrr($domain_part, 'ANY'))
1884       return true;
1885   }
1886
1887   return false;
1888 }
1889
1890 /*
1891  * Idn_to_ascii wrapper.
1892  * Intl/Idn modules version of this function doesn't work with e-mail address
1893  */
1894 function rcube_idn_to_ascii($str)
1895 {
1896   return rcube_idn_convert($str, true);
1897 }
1898
1899 /*
1900  * Idn_to_ascii wrapper.
1901  * Intl/Idn modules version of this function doesn't work with e-mail address
1902  */
1903 function rcube_idn_to_utf8($str)
1904 {
1905   return rcube_idn_convert($str, false);
1906 }
1907
1908 function rcube_idn_convert($input, $is_utf=false)
1909 {
1910   if ($at = strpos($input, '@')) {
1911     $user   = substr($input, 0, $at);
1912     $domain = substr($input, $at+1);
1913   }
1914   else {
1915     $domain = $input;
1916   }
1917
1918   $domain = $is_utf ? idn_to_ascii($domain) : idn_to_utf8($domain);
1919
1920   return $at ? $user . '@' . $domain : $domain;
1921 }
1922
1923
1924 /**
1925  * Helper class to turn relative urls into absolute ones
1926  * using a predefined base
1927  */
1928 class rcube_base_replacer
1929 {
1930   private $base_url;
1931
1932   public function __construct($base)
1933   {
1934     $this->base_url = $base;
1935   }
1936
1937   public function callback($matches)
1938   {
1939     return $matches[1] . '="' . make_absolute_url($matches[3], $this->base_url) . '"';
1940   }
1941 }
1942
1943
1944 /**
1945  * Throw system error and show error page
1946  *
1947  * @param array Named parameters
1948  *  - code: Error code (required)
1949  *  - type: Error type [php|db|imap|javascript] (required)
1950  *  - message: Error message
1951  *  - file: File where error occured
1952  *  - line: Line where error occured
1953  * @param boolean True to log the error
1954  * @param boolean Terminate script execution
1955  */
1956 // may be defined in Installer
1957 if (!function_exists('raise_error')) {
1958 function raise_error($arg=array(), $log=false, $terminate=false)
1959 {
1960     global $__page_content, $CONFIG, $OUTPUT, $ERROR_CODE, $ERROR_MESSAGE;
1961
1962     // report bug (if not incompatible browser)
1963     if ($log && $arg['type'] && $arg['message'])
1964         log_bug($arg);
1965
1966     // display error page and terminate script
1967     if ($terminate) {
1968         $ERROR_CODE = $arg['code'];
1969         $ERROR_MESSAGE = $arg['message'];
1970         include('program/steps/utils/error.inc');
1971         exit;
1972     }
1973 }
1974 }
1975
1976
1977 /**
1978  * Report error according to configured debug_level
1979  *
1980  * @param array Named parameters
1981  * @return void
1982  * @see raise_error()
1983  */
1984 function log_bug($arg_arr)
1985 {
1986     global $CONFIG;
1987     $program = strtoupper($arg_arr['type']);
1988
1989     // write error to local log file
1990     if ($CONFIG['debug_level'] & 1) {
1991         $post_query = ($_SERVER['REQUEST_METHOD'] == 'POST' ? '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']) : '');
1992         $log_entry = sprintf("%s Error: %s%s (%s %s)",
1993             $program,
1994             $arg_arr['message'],
1995             $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
1996             $_SERVER['REQUEST_METHOD'],
1997             $_SERVER['REQUEST_URI'] . $post_query);
1998
1999         if (!write_log('errors', $log_entry)) {
2000             // send error to PHPs error handler if write_log didn't succeed
2001             trigger_error($arg_arr['message']);
2002         }
2003     }
2004
2005     // resport the bug to the global bug reporting system
2006     if ($CONFIG['debug_level'] & 2) {
2007         // TODO: Send error via HTTP
2008     }
2009
2010     // show error if debug_mode is on
2011     if ($CONFIG['debug_level'] & 4) {
2012         print "<b>$program Error";
2013
2014         if (!empty($arg_arr['file']) && !empty($arg_arr['line']))
2015             print " in $arg_arr[file] ($arg_arr[line])";
2016
2017         print ':</b>&nbsp;';
2018         print nl2br($arg_arr['message']);
2019         print '<br />';
2020         flush();
2021     }
2022 }
2023