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