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