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