]> git.donarmstrong.com Git - roundcube.git/blob - program/include/main.inc.orig
Imported Upstream version 0.5.4+dfsg
[roundcube.git] / program / include / main.inc.orig
1 <?php
2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/main.inc                                              |
6  |                                                                       |
7  | This file is part of the Roundcube Webmail client                     |
8  | Copyright (C) 2005-2009, Roundcube Dev, - Switzerland                 |
9  | Licensed under the GNU GPL                                            |
10  |                                                                       |
11  | PURPOSE:                                                              |
12  |   Provide basic functions for the webmail package                     |
13  |                                                                       |
14  +-----------------------------------------------------------------------+
15  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16  +-----------------------------------------------------------------------+
17
18  $Id: main.inc 4830 2011-06-02 12:36:32Z alec $
19
20 */
21
22 /**
23  * Roundcube Webmail common functions
24  *
25  * @package Core
26  * @author Thomas Bruederli <roundcube@gmail.com>
27  */
28
29 require_once('lib/utf7.inc');
30 require_once('include/rcube_shared.inc');
31
32 // define constannts for input reading
33 define('RCUBE_INPUT_GET', 0x0101);
34 define('RCUBE_INPUT_POST', 0x0102);
35 define('RCUBE_INPUT_GPC', 0x0103);
36
37
38
39 /**
40  * Return correct name for a specific database table
41  *
42  * @param string Table name
43  * @return string Translated table name
44  */
45 function get_table_name($table)
46   {
47   global $CONFIG;
48
49   // return table name if configured
50   $config_key = 'db_table_'.$table;
51
52   if (strlen($CONFIG[$config_key]))
53     return $CONFIG[$config_key];
54
55   return $table;
56   }
57
58
59 /**
60  * Return correct name for a specific database sequence
61  * (used for Postgres only)
62  *
63  * @param string Secuence name
64  * @return string Translated sequence name
65  */
66 function get_sequence_name($sequence)
67   {
68   // return sequence name if configured
69   $config_key = 'db_sequence_'.$sequence;
70   $opt = rcmail::get_instance()->config->get($config_key);
71
72   if (!empty($opt))
73     return $opt;
74     
75   return $sequence;
76   }
77
78
79 /**
80  * Get localized text in the desired language
81  * It's a global wrapper for rcmail::gettext()
82  *
83  * @param mixed Named parameters array or label name
84  * @return string Localized text
85  * @see rcmail::gettext()
86  */
87 function rcube_label($p, $domain=null)
88 {
89   return rcmail::get_instance()->gettext($p, $domain);
90 }
91
92
93 /**
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']), '');
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   $out = '';
1532
1533   foreach ($arrFolders as $key=>$folder) {
1534     if (!$realnames && ($folder_class = rcmail_folder_classname($folder['id'])))
1535       $foldername = rcube_label($folder_class);
1536     else {
1537       $foldername = $folder['name'];
1538
1539       // shorten the folder name to a given length
1540       if ($maxlength && $maxlength>1)
1541         $foldername = abbreviate_string($foldername, $maxlength);
1542     }
1543
1544     $select->add(str_repeat('&nbsp;', $nestLevel*4) . $foldername, $folder['id']);
1545
1546     if (!empty($folder['folders']))
1547       $out .= rcmail_render_folder_tree_select($folder['folders'], $mbox_name, $maxlength, $select, $realnames, $nestLevel+1);
1548   }
1549
1550   return $out;
1551 }
1552
1553
1554 /**
1555  * Return internal name for the given folder if it matches the configured special folders
1556  * @access private
1557  * @return string
1558  */
1559 function rcmail_folder_classname($folder_id)
1560 {
1561   global $CONFIG;
1562
1563   if ($folder_id == 'INBOX')
1564     return 'inbox';
1565
1566   // for these mailboxes we have localized labels and css classes
1567   foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
1568   {
1569     if ($folder_id == $CONFIG[$smbx.'_mbox'])
1570       return $smbx;
1571   }
1572 }
1573
1574
1575 /**
1576  * Try to localize the given IMAP folder name.
1577  * UTF-7 decode it in case no localized text was found
1578  *
1579  * @param string Folder name
1580  * @return string Localized folder name in UTF-8 encoding
1581  */
1582 function rcmail_localize_foldername($name)
1583 {
1584   if ($folder_class = rcmail_folder_classname($name))
1585     return rcube_label($folder_class);
1586   else
1587     return rcube_charset_convert($name, 'UTF7-IMAP');
1588 }
1589
1590
1591 function rcmail_quota_display($attrib)
1592 {
1593   global $OUTPUT;
1594
1595   if (!$attrib['id'])
1596     $attrib['id'] = 'rcmquotadisplay';
1597
1598   if(isset($attrib['display']))
1599     $_SESSION['quota_display'] = $attrib['display'];
1600
1601   $OUTPUT->add_gui_object('quotadisplay', $attrib['id']);
1602
1603   $quota = rcmail_quota_content($attrib);
1604
1605   $OUTPUT->add_script('$(document).ready(function(){
1606         rcmail.set_quota('.json_serialize($quota).')});', 'foot');
1607
1608   return html::span($attrib, '');
1609 }
1610
1611
1612 function rcmail_quota_content($attrib=NULL)
1613 {
1614   global $RCMAIL;
1615
1616   $quota = $RCMAIL->imap->get_quota();
1617   $quota = $RCMAIL->plugins->exec_hook('quota', $quota);
1618
1619   $quota_result = (array) $quota;
1620   $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
1621
1622   if (!$quota['total'] && $RCMAIL->config->get('quota_zero_as_unlimited')) {
1623     $quota_result['title'] = rcube_label('unlimited');
1624     $quota_result['percent'] = 0;
1625   }
1626   else if ($quota['total']) {
1627     if (!isset($quota['percent']))
1628       $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
1629
1630     $title = sprintf('%s / %s (%.0f%%)',
1631         show_bytes($quota['used'] * 1024), show_bytes($quota['total'] * 1024),
1632         $quota_result['percent']);
1633
1634     $quota_result['title'] = $title;
1635
1636     if ($attrib['width'])
1637       $quota_result['width'] = $attrib['width'];
1638     if ($attrib['height'])
1639       $quota_result['height']   = $attrib['height'];
1640   }
1641   else {
1642     $quota_result['title'] = rcube_label('unknown');
1643     $quota_result['percent'] = 0;
1644   }
1645
1646   return $quota_result;
1647 }
1648
1649
1650 /**
1651  * Outputs error message according to server error/response codes
1652  *
1653  * @param string Fallback message label
1654  * @param string Fallback message label arguments
1655  *
1656  * @return void
1657  */
1658 function rcmail_display_server_error($fallback=null, $fallback_args=null)
1659 {
1660     global $RCMAIL;
1661
1662     $err_code = $RCMAIL->imap->get_error_code();
1663     $res_code = $RCMAIL->imap->get_response_code();
1664
1665     if ($res_code == rcube_imap::NOPERM) {
1666         $RCMAIL->output->show_message('errornoperm', 'error');
1667     }
1668     else if ($res_code == rcube_imap::READONLY) {
1669         $RCMAIL->output->show_message('errorreadonly', 'error');
1670     }
1671     else if ($err_code && ($err_str = $RCMAIL->imap->get_error_str())) {
1672         $RCMAIL->output->show_message('servererrormsg', 'error', array('msg' => $err_str));
1673     }
1674     else if ($fallback) {
1675         $RCMAIL->output->show_message($fallback, 'error', $fallback_args);
1676     }
1677
1678     return true;
1679 }
1680
1681
1682 /**
1683  * Output HTML editor scripts
1684  *
1685  * @param string Editor mode
1686  * @return void
1687  */
1688 function rcube_html_editor($mode='')
1689 {
1690   global $RCMAIL, $CONFIG;
1691
1692   $hook = $RCMAIL->plugins->exec_hook('html_editor', array('mode' => $mode));
1693
1694   if ($hook['abort'])
1695     return;  
1696
1697   $lang = strtolower($_SESSION['language']);
1698
1699   // TinyMCE uses 'tw' for zh_TW (which is wrong, because tw is a code of Twi language)
1700   $lang = ($lang == 'zh_tw') ? 'tw' : substr($lang, 0, 2);
1701
1702   if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js'))
1703     $lang = 'en';
1704
1705   $RCMAIL->output->include_script('tiny_mce/tiny_mce.js');
1706   $RCMAIL->output->include_script('editor.js');
1707   $RCMAIL->output->add_script(sprintf("rcmail_editor_init('\$__skin_path', '%s', %d, '%s');",
1708     JQ($lang), intval($CONFIG['enable_spellcheck']), $mode),
1709     'foot');
1710 }
1711
1712
1713 /**
1714  * Replaces TinyMCE's emoticon images with plain-text representation
1715  *
1716  * @param string HTML content
1717  * @return string HTML content
1718  */
1719 function rcmail_replace_emoticons($html)
1720 {
1721   $emoticons = array(
1722     '8-)' => 'smiley-cool',
1723     ':-#' => 'smiley-foot-in-mouth',
1724     ':-*' => 'smiley-kiss',
1725     ':-X' => 'smiley-sealed',
1726     ':-P' => 'smiley-tongue-out',
1727     ':-@' => 'smiley-yell',
1728     ":'(" => 'smiley-cry',
1729     ':-(' => 'smiley-frown',
1730     ':-D' => 'smiley-laughing',
1731     ':-)' => 'smiley-smile',
1732     ':-S' => 'smiley-undecided',
1733     ':-$' => 'smiley-embarassed',
1734     'O:-)' => 'smiley-innocent',
1735     ':-|' => 'smiley-money-mouth',
1736     ':-O' => 'smiley-surprised',
1737     ';-)' => 'smiley-wink',
1738   );
1739
1740   foreach ($emoticons as $idx => $file) {
1741     // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
1742     $search[]  = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
1743     $replace[] = $idx;
1744   }
1745
1746   return preg_replace($search, $replace, $html);
1747 }
1748
1749
1750 /**
1751  * Check if working in SSL mode
1752  *
1753  * @param integer HTTPS port number
1754  * @param boolean Enables 'use_https' option checking
1755  * @return boolean
1756  */
1757 function rcube_https_check($port=null, $use_https=true)
1758 {
1759   global $RCMAIL;
1760
1761   if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off')
1762     return true;
1763   if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https')
1764     return true;
1765   if ($port && $_SERVER['SERVER_PORT'] == $port)
1766     return true;
1767   if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https'))
1768     return true;
1769
1770   return false;
1771 }
1772
1773
1774 /**
1775  * For backward compatibility.
1776  *
1777  * @global rcmail $RCMAIL
1778  * @param string $var_name Variable name.
1779  * @return void
1780  */
1781 function rcube_sess_unset($var_name=null)
1782 {
1783   global $RCMAIL;
1784
1785   $RCMAIL->session->remove($var_name);
1786 }
1787
1788
1789
1790 /**
1791  * Replaces hostname variables
1792  *
1793  * @param string $name Hostname
1794  * @param string $host Optional IMAP hostname
1795  * @return string
1796  */
1797 function rcube_parse_host($name, $host='')
1798 {
1799   // %n - host
1800   $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
1801   // %d - domain name without first part, e.g. %d=mail.domain.tld, %m=domain.tld
1802   $d = preg_replace('/^[^\.]+\./', '', $n);
1803   // %h - IMAP host
1804   $h = $_SESSION['imap_host'] ? $_SESSION['imap_host'] : $host;
1805   // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
1806   $z = preg_replace('/^[^\.]+\./', '', $h);
1807
1808   $name = str_replace(array('%n', '%d', '%h', '%z'), array($n, $d, $h, $z), $name);
1809   return $name;
1810 }
1811
1812
1813 /**
1814  * E-mail address validation
1815  *
1816  * @param string $email Email address
1817  * @param boolean $dns_check True to check dns
1818  * @return boolean
1819  */
1820 function check_email($email, $dns_check=true)
1821 {
1822   // Check for invalid characters
1823   if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email))
1824     return false;
1825
1826   // Check for length limit specified by RFC 5321 (#1486453)
1827   if (strlen($email) > 254) 
1828     return false;
1829
1830   $email_array = explode('@', $email);
1831
1832   // Check that there's one @ symbol
1833   if (count($email_array) < 2)
1834     return false;
1835
1836   $domain_part = array_pop($email_array);
1837   $local_part = implode('@', $email_array);
1838
1839   // from PEAR::Validate
1840   $regexp = '&^(?:
1841         ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")|                             #1 quoted name
1842         ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*))  #2 OR dot-atom (RFC5322)
1843         $&xi';
1844
1845   if (!preg_match($regexp, $local_part))
1846     return false;
1847
1848   // Check domain part
1849   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))
1850     return true; // IP address
1851   else {
1852     // If not an IP address
1853     $domain_array = explode('.', $domain_part);
1854     if (sizeof($domain_array) < 2)
1855       return false; // Not enough parts to be a valid domain
1856
1857     foreach ($domain_array as $part)
1858       if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part))
1859         return false;
1860
1861     if (!$dns_check || !rcmail::get_instance()->config->get('email_dns_check'))
1862       return true;
1863
1864     if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
1865       $lookup = array();
1866       @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
1867       foreach ($lookup as $line) {
1868         if (strpos($line, 'MX preference'))
1869           return true;
1870       }
1871       return false;
1872     }
1873
1874     // find MX record(s)
1875     if (getmxrr($domain_part, $mx_records))
1876       return true;
1877
1878     // find any DNS record
1879     if (checkdnsrr($domain_part, 'ANY'))
1880       return true;
1881   }
1882
1883   return false;
1884 }
1885
1886 /*
1887  * Idn_to_ascii wrapper.
1888  * Intl/Idn modules version of this function doesn't work with e-mail address
1889  */
1890 function rcube_idn_to_ascii($str)
1891 {
1892   return rcube_idn_convert($str, true);
1893 }
1894
1895 /*
1896  * Idn_to_ascii wrapper.
1897  * Intl/Idn modules version of this function doesn't work with e-mail address
1898  */
1899 function rcube_idn_to_utf8($str)
1900 {
1901   return rcube_idn_convert($str, false);
1902 }
1903
1904 function rcube_idn_convert($input, $is_utf=false)
1905 {
1906   if ($at = strpos($input, '@')) {
1907     $user   = substr($input, 0, $at);
1908     $domain = substr($input, $at+1);
1909   }
1910   else {
1911     $domain = $input;
1912   }
1913
1914   $domain = $is_utf ? idn_to_ascii($domain) : idn_to_utf8($domain);
1915
1916   if ($domain === false) {
1917     return '';
1918   }
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