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