source: subversion/trunk/roundcubemail/program/include/main.inc @ 5769

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