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

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