]> git.donarmstrong.com Git - roundcube.git/blob - program/include/main.inc
Imported Upstream version 0.3
[roundcube.git] / program / include / main.inc
1 <?php
2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/main.inc                                              |
6  |                                                                       |
7  | This file is part of the RoundCube Webmail client                     |
8  | Copyright (C) 2005-2009, RoundCube Dev, - Switzerland                 |
9  | Licensed under the GNU GPL                                            |
10  |                                                                       |
11  | PURPOSE:                                                              |
12  |   Provide basic functions for the webmail package                     |
13  |                                                                       |
14  +-----------------------------------------------------------------------+
15  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16  +-----------------------------------------------------------------------+
17
18  $Id: main.inc 2852 2009-08-10 21:32:44Z thomasb $
19
20 */
21
22 /**
23  * RoundCube Webmail common functions
24  *
25  * @package Core
26  * @author Thomas Bruederli <roundcube@gmail.com>
27  */
28
29 require_once('lib/utf7.inc');
30 require_once('include/rcube_shared.inc');
31
32 // define constannts for input reading
33 define('RCUBE_INPUT_GET', 0x0101);
34 define('RCUBE_INPUT_POST', 0x0102);
35 define('RCUBE_INPUT_GPC', 0x0103);
36
37
38
39 /**
40  * Return correct name for a specific database table
41  *
42  * @param string Table name
43  * @return string Translated table name
44  */
45 function get_table_name($table)
46   {
47   global $CONFIG;
48
49   // return table name if configured
50   $config_key = 'db_table_'.$table;
51
52   if (strlen($CONFIG[$config_key]))
53     return $CONFIG[$config_key];
54
55   return $table;
56   }
57
58
59 /**
60  * Return correct name for a specific database sequence
61  * (used for Postgres only)
62  *
63  * @param string Secuence name
64  * @return string Translated sequence name
65  */
66 function get_sequence_name($sequence)
67   {
68   // return table 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  * @return string Localized text
85  * @see rcmail::gettext()
86  */
87 function rcube_label($p, $domain=null)
88 {
89   return rcmail::get_instance()->gettext($p, $domain);
90 }
91
92
93 /**
94  * Overwrite action variable
95  *
96  * @param string New action value
97  */
98 function rcmail_overwrite_action($action)
99   {
100   $app = rcmail::get_instance();
101   $app->action = $action;
102   $app->output->set_env('action', $action);
103   }
104
105
106 /**
107  * Compose an URL for a specific action
108  *
109  * @param string  Request action
110  * @param array   More URL parameters
111  * @param string  Request task (omit if the same)
112  * @return The application URL
113  */
114 function rcmail_url($action, $p=array(), $task=null)
115 {
116   $app = rcmail::get_instance();
117   return $app->url((array)$p + array('_action' => $action, 'task' => $task));
118 }
119
120
121 /**
122  * Garbage collector function for temp files.
123  * Remove temp files older than two days
124  */
125 function rcmail_temp_gc()
126   {
127   $rcmail = rcmail::get_instance();
128
129   $tmp = unslashify($rcmail->config->get('temp_dir'));
130   $expire = mktime() - 172800;  // expire in 48 hours
131
132   if ($dir = opendir($tmp))
133     {
134     while (($fname = readdir($dir)) !== false)
135       {
136       if ($fname{0} == '.')
137         continue;
138
139       if (filemtime($tmp.'/'.$fname) < $expire)
140         @unlink($tmp.'/'.$fname);
141       }
142
143     closedir($dir);
144     }
145   }
146
147
148 /**
149  * Garbage collector for cache entries.
150  * Remove all expired message cache records
151  */
152 function rcmail_cache_gc()
153   {
154   $rcmail = rcmail::get_instance();
155   $db = $rcmail->get_dbh();
156   
157   // get target timestamp
158   $ts = get_offset_time($rcmail->config->get('message_cache_lifetime', '30d'), -1);
159   
160   $db->query("DELETE FROM ".get_table_name('messages')."
161              WHERE  created < " . $db->fromunixtime($ts));
162
163   $db->query("DELETE FROM ".get_table_name('cache')."
164               WHERE  created < " . $db->fromunixtime($ts));
165   }
166
167
168 /**
169  * Convert a string from one charset to another.
170  * Uses mbstring and iconv functions if possible
171  *
172  * @param  string Input string
173  * @param  string Suspected charset of the input string
174  * @param  string Target charset to convert to; defaults to RCMAIL_CHARSET
175  * @return Converted string
176  */
177 function rcube_charset_convert($str, $from, $to=NULL)
178   {
179   static $mbstring_loaded = null;
180   static $mbstring_list = null;
181   static $convert_warning = false;
182   static $conv = null;
183   
184   $error = false;
185
186   $to = empty($to) ? $to = strtoupper(RCMAIL_CHARSET) : rcube_parse_charset($to);
187   $from = rcube_parse_charset($from);
188
189   if ($from == $to || empty($str) || empty($from))
190     return $str;
191
192   // convert charset using iconv module  
193   if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') {
194     $_iconv = iconv($from, $to . '//IGNORE', $str);
195     if ($_iconv !== false) {
196         return $_iconv;
197     }
198   }
199
200   if (is_null($mbstring_loaded))
201     $mbstring_loaded = extension_loaded('mbstring');
202     
203   // convert charset using mbstring module
204   if ($mbstring_loaded) {
205     $aliases['WINDOWS-1257'] = 'ISO-8859-13';
206     
207     if (is_null($mbstring_list)) {
208       $mbstring_list = mb_list_encodings();
209       $mbstring_list = array_map('strtoupper', $mbstring_list);
210     }
211
212     $mb_from = $aliases[$from] ? $aliases[$from] : $from;
213     $mb_to = $aliases[$to] ? $aliases[$to] : $to;
214     
215     // return if encoding found, string matches encoding and convert succeeded
216     if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) {
217       if (mb_check_encoding($str, $mb_from) && ($out = mb_convert_encoding($str, $mb_to, $mb_from)))
218         return $out;
219     }
220   }
221
222   // convert charset using bundled classes/functions
223   if ($to == 'UTF-8') {
224     if ($from == 'UTF7-IMAP') {
225       if ($_str = utf7_to_utf8($str))
226         return $_str;
227     }
228     else if ($from == 'UTF-7') {
229       if ($_str = rcube_utf7_to_utf8($str))
230         return $_str;
231     }
232     else if (($from == 'ISO-8859-1') && function_exists('utf8_encode')) {
233       return utf8_encode($str);
234     }
235     else if (class_exists('utf8')) {
236       if (!$conv)
237         $conv = new utf8($from);
238       else
239         $conv->loadCharset($from);
240
241       if($_str = $conv->strToUtf8($str))
242         return $_str;
243     }
244     $error = true;
245   }
246   
247   // encode string for output
248   if ($from == 'UTF-8') {
249     // @TODO: we need a function for UTF-7 (RFC2152) conversion
250     if ($to == 'UTF7-IMAP' || $to == 'UTF-7') {
251       if ($_str = utf8_to_utf7($str))
252         return $_str;
253     }
254     else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) {
255       return utf8_decode($str);
256     }
257     else if (class_exists('utf8')) {
258       if (!$conv)
259         $conv = new utf8($to);
260       else
261         $conv->loadCharset($from);
262
263       if ($_str = $conv->strToUtf8($str))
264         return $_str;
265     }
266     $error = true;
267   }
268   
269   // report error
270   if ($error && !$convert_warning) {
271     raise_error(array(
272       'code' => 500,
273       'type' => 'php',
274       'file' => __FILE__,
275       'line' => __LINE__,
276       'message' => "Could not convert string from $from to $to. Make sure iconv/mbstring is installed or lib/utf8.class is available."
277       ), true, false);
278     
279     $convert_warning = true;
280   }
281   
282   // return UTF-8 or original string
283   return $str;
284   }
285
286
287 /**
288  * Parse and validate charset name string (see #1485758).
289  * Sometimes charset string is malformed, there are also charset aliases 
290  * but we need strict names for charset conversion (specially utf8 class)
291  *
292  * @param  string  Input charset name
293  * @return The validated charset name
294  */
295 function rcube_parse_charset($charset)
296   {
297   $charset = strtoupper($charset);
298
299   # RFC1642
300   $charset = str_replace('UNICODE-1-1-', '', $charset);
301
302   # Aliases: some of them from HTML5 spec.
303   $aliases = array(
304     'USASCII'       => 'WINDOWS-1252',
305     'ANSIX31101983' => 'WINDOWS-1252',
306     'ANSIX341968'   => 'WINDOWS-1252',
307     'UNKNOWN8BIT'   => 'ISO-8859-15',
308     'UNKNOWN'       => 'ISO-8859-15',
309     'USERDEFINED'   => 'ISO-8859-15',
310     'KSC56011987'   => 'EUC-KR',
311     'GB2312'        => 'GBK',
312     'GB231280'      => 'GBK',
313     'UNICODE'       => 'UTF-8',
314     'UTF7IMAP'      => 'UTF7-IMAP',
315     'TIS620'        => 'WINDOWS-874',
316     'ISO88599'      => 'WINDOWS-1254',
317     'ISO885911'     => 'WINDOWS-874',
318   );
319
320   // allow a-z and 0-9 only and remove X- prefix (e.g. X-ROMAN8 => ROMAN8)
321   $str = preg_replace(array('/[^a-z0-9]/i', '/^x+/i'), '', $charset);
322
323   if (isset($aliases[$str]))
324     return $aliases[$str];
325
326   if (preg_match('/UTF(7|8|16|32)(BE|LE)*/', $str, $m))
327     return 'UTF-' . $m[1] . $m[2];
328
329   if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) {
330     $iso = 'ISO-8859-' . ($m[1] ? $m[1] : 1);
331     # some clients sends windows-1252 text as latin1,
332     # it is safe to use windows-1252 for all latin1
333     return $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso;
334     }
335
336   return $charset;
337   }
338
339
340 /**
341  * Converts string from standard UTF-7 (RFC 2152) to UTF-8.
342  *
343  * @param  string  Input string
344  * @return The converted string
345  */
346 function rcube_utf7_to_utf8($str)
347 {
348   $Index_64 = array(
349     0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
350     0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
351     0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0,
352     1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0,
353     0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
354     1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
355     0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
356     1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
357   );
358
359   $u7len = strlen($str);
360   $str = strval($str);
361   $res = '';
362
363   for ($i=0; $u7len > 0; $i++, $u7len--)
364   {
365     $u7 = $str[$i];
366     if ($u7 == '+')
367     {
368       $i++;
369       $u7len--;
370       $ch = '';
371
372       for (; $u7len > 0; $i++, $u7len--)
373       {
374         $u7 = $str[$i];
375
376         if (!$Index_64[ord($u7)])
377           break;
378
379         $ch .= $u7;
380       }
381
382       if ($ch == '') {
383         if ($u7 == '-')
384           $res .= '+';
385         continue;
386       }
387
388       $res .= rcube_utf16_to_utf8(base64_decode($ch));
389     }
390     else
391     {
392       $res .= $u7;
393     }
394   }
395
396   return $res;
397 }
398
399 /**
400  * Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion)
401  *
402  * @param  string  Input string
403  * @return The converted string
404  */
405 function rcube_utf16_to_utf8($str)
406 {
407   $len = strlen($str);
408   $dec = '';
409
410   for ($i = 0; $i < $len; $i += 2) {
411     $c = ord($str[$i]) << 8 | ord($str[$i + 1]);
412     if ($c >= 0x0001 && $c <= 0x007F) {
413       $dec .= chr($c);
414     } else if ($c > 0x07FF) {
415       $dec .= chr(0xE0 | (($c >> 12) & 0x0F));
416       $dec .= chr(0x80 | (($c >>  6) & 0x3F));
417       $dec .= chr(0x80 | (($c >>  0) & 0x3F));
418     } else {
419       $dec .= chr(0xC0 | (($c >>  6) & 0x1F));
420       $dec .= chr(0x80 | (($c >>  0) & 0x3F));
421     }
422   }
423   return $dec;
424 }
425
426
427 /**
428  * Replacing specials characters to a specific encoding type
429  *
430  * @param  string  Input string
431  * @param  string  Encoding type: text|html|xml|js|url
432  * @param  string  Replace mode for tags: show|replace|remove
433  * @param  boolean Convert newlines
434  * @return The quoted string
435  */
436 function rep_specialchars_output($str, $enctype='', $mode='', $newlines=TRUE)
437   {
438   static $html_encode_arr = false;
439   static $js_rep_table = false;
440   static $xml_rep_table = false;
441
442   if (!$enctype)
443     $enctype = $OUTPUT->type;
444
445   // encode for HTML output
446   if ($enctype=='html')
447     {
448     if (!$html_encode_arr)
449       {
450       $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);        
451       unset($html_encode_arr['?']);
452       }
453
454     $ltpos = strpos($str, '<');
455     $encode_arr = $html_encode_arr;
456
457     // don't replace quotes and html tags
458     if (($mode=='show' || $mode=='') && $ltpos!==false && strpos($str, '>', $ltpos)!==false)
459       {
460       unset($encode_arr['"']);
461       unset($encode_arr['<']);
462       unset($encode_arr['>']);
463       unset($encode_arr['&']);
464       }
465     else if ($mode=='remove')
466       $str = strip_tags($str);
467     
468     // avoid douple quotation of &
469     $out = preg_replace('/&amp;([A-Za-z]{2,6}|#[0-9]{2,4});/', '&\\1;', strtr($str, $encode_arr));
470       
471     return $newlines ? nl2br($out) : $out;
472     }
473
474   // if the replace tables for XML and JS are not yet defined
475   if ($js_rep_table===false)
476     {
477     $js_rep_table = $xml_rep_table = array();
478     $xml_rep_table['&'] = '&amp;';
479
480     for ($c=160; $c<256; $c++)  // can be increased to support more charsets
481       $xml_rep_table[Chr($c)] = "&#$c;";
482
483     $xml_rep_table['"'] = '&quot;';
484     $js_rep_table['"'] = '\\"';
485     $js_rep_table["'"] = "\\'";
486     $js_rep_table["\\"] = "\\\\";
487     }
488
489   // encode for javascript use
490   if ($enctype=='js')
491     return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));
492
493   // encode for plaintext
494   if ($enctype=='text')
495     return str_replace("\r\n", "\n", $mode=='remove' ? strip_tags($str) : $str);
496
497   if ($enctype=='url')
498     return rawurlencode($str);
499
500   // encode for XML
501   if ($enctype=='xml')
502     return strtr($str, $xml_rep_table);
503
504   // no encoding given -> return original string
505   return $str;
506   }
507   
508 /**
509  * Quote a given string.
510  * Shortcut function for rep_specialchars_output
511  *
512  * @return string HTML-quoted string
513  * @see rep_specialchars_output()
514  */
515 function Q($str, $mode='strict', $newlines=TRUE)
516   {
517   return rep_specialchars_output($str, 'html', $mode, $newlines);
518   }
519
520 /**
521  * Quote a given string for javascript output.
522  * Shortcut function for rep_specialchars_output
523  * 
524  * @return string JS-quoted string
525  * @see rep_specialchars_output()
526  */
527 function JQ($str)
528   {
529   return rep_specialchars_output($str, 'js');
530   }
531
532
533 /**
534  * Read input value and convert it for internal use
535  * Performs stripslashes() and charset conversion if necessary
536  * 
537  * @param  string   Field name to read
538  * @param  int      Source to get value from (GPC)
539  * @param  boolean  Allow HTML tags in field value
540  * @param  string   Charset to convert into
541  * @return string   Field value or NULL if not available
542  */
543 function get_input_value($fname, $source, $allow_html=FALSE, $charset=NULL)
544 {
545   global $OUTPUT;
546   $value = NULL;
547   
548   if ($source==RCUBE_INPUT_GET && isset($_GET[$fname]))
549     $value = $_GET[$fname];
550   else if ($source==RCUBE_INPUT_POST && isset($_POST[$fname]))
551     $value = $_POST[$fname];
552   else if ($source==RCUBE_INPUT_GPC)
553     {
554     if (isset($_POST[$fname]))
555       $value = $_POST[$fname];
556     else if (isset($_GET[$fname]))
557       $value = $_GET[$fname];
558     else if (isset($_COOKIE[$fname]))
559       $value = $_COOKIE[$fname];
560     }
561
562   if (empty($value))
563     return $value;
564
565   // strip single quotes if magic_quotes_sybase is enabled
566   if (ini_get('magic_quotes_sybase'))
567     $value = str_replace("''", "'", $value);
568   // strip slashes if magic_quotes enabled
569   else if (get_magic_quotes_gpc() || get_magic_quotes_runtime())
570     $value = stripslashes($value);
571
572   // remove HTML tags if not allowed    
573   if (!$allow_html)
574     $value = strip_tags($value);
575   
576   // convert to internal charset
577   if (is_object($OUTPUT))
578     return rcube_charset_convert($value, $OUTPUT->get_charset(), $charset);
579   else
580     return $value;
581 }
582
583 /**
584  * Convert array of request parameters (prefixed with _)
585  * to a regular array with non-prefixed keys.
586  *
587  * @param  int   Source to get value from (GPC)
588  * @return array Hash array with all request parameters
589  */
590 function request2param($mode = RCUBE_INPUT_GPC)
591 {
592   $out = array();
593   $src = $mode == RCUBE_INPUT_GET ? $_GET : ($mode == RCUBE_INPUT_POST ? $_POST : $_REQUEST);
594   foreach ($src as $key => $value) {
595     $fname = $key[0] == '_' ? substr($key, 1) : $key;
596     $out[$fname] = get_input_value($key, $mode);
597   }
598   
599   return $out;
600 }
601
602 /**
603  * Remove all non-ascii and non-word chars
604  * except ., -, _
605  */
606 function asciiwords($str, $css_id = false, $replace_with = '')
607 {
608   $allowed = 'a-z0-9\_\-' . (!$css_id ? '\.' : '');
609   return preg_replace("/[^$allowed]/i", $replace_with, $str);
610 }
611
612 /**
613  * Remove single and double quotes from given string
614  *
615  * @param string Input value
616  * @return string Dequoted string
617  */
618 function strip_quotes($str)
619 {
620   return preg_replace('/[\'"]/', '', $str);
621 }
622
623
624 /**
625  * Remove new lines characters from given string
626  *
627  * @param string Input value
628  * @return string Stripped string
629  */
630 function strip_newlines($str)
631 {
632   return preg_replace('/[\r\n]/', '', $str);
633 }
634
635
636 /**
637  * Create a HTML table based on the given data
638  *
639  * @param  array  Named table attributes
640  * @param  mixed  Table row data. Either a two-dimensional array or a valid SQL result set
641  * @param  array  List of cols to show
642  * @param  string Name of the identifier col
643  * @return string HTML table code
644  */
645 function rcube_table_output($attrib, $table_data, $a_show_cols, $id_col)
646   {
647   global $RCMAIL;
648   
649   $table = new html_table(/*array('cols' => count($a_show_cols))*/);
650     
651   // add table header
652   foreach ($a_show_cols as $col)
653     $table->add_header($col, Q(rcube_label($col)));
654   
655   $c = 0;
656   if (!is_array($table_data)) 
657   {
658     $db = $RCMAIL->get_dbh();
659     while ($table_data && ($sql_arr = $db->fetch_assoc($table_data)))
660     {
661       $zebra_class = $c % 2 ? 'even' : 'odd';
662       $table->add_row(array('id' => 'rcmrow' . $sql_arr[$id_col], 'class' => $zebra_class));
663
664       // format each col
665       foreach ($a_show_cols as $col)
666         $table->add($col, Q($sql_arr[$col]));
667       
668       $c++;
669     }
670   }
671   else 
672   {
673     foreach ($table_data as $row_data)
674     {
675       $zebra_class = $c % 2 ? 'even' : 'odd';
676       $table->add_row(array('id' => 'rcmrow' . $row_data[$id_col], 'class' => $zebra_class));
677
678       // format each col
679       foreach ($a_show_cols as $col)
680         $table->add($col, Q($row_data[$col]));
681         
682       $c++;
683     }
684   }
685
686   return $table->show($attrib);
687   }
688
689
690 /**
691  * Create an edit field for inclusion on a form
692  * 
693  * @param string col field name
694  * @param string value field value
695  * @param array attrib HTML element attributes for field
696  * @param string type HTML element type (default 'text')
697  * @return string HTML field definition
698  */
699 function rcmail_get_edit_field($col, $value, $attrib, $type='text')
700   {
701   $fname = '_'.$col;
702   $attrib['name'] = $fname;
703   
704   if ($type=='checkbox')
705     {
706     $attrib['value'] = '1';
707     $input = new html_checkbox($attrib);
708     }
709   else if ($type=='textarea')
710     {
711     $attrib['cols'] = $attrib['size'];
712     $input = new html_textarea($attrib);
713     }
714   else
715     $input = new html_inputfield($attrib);
716
717   // use value from post
718   if (!empty($_POST[$fname]))
719     $value = get_input_value($fname, RCUBE_INPUT_POST,
720             $type == 'textarea' && strpos($attrib['class'], 'mce_editor')!==false ? true : false);
721
722   $out = $input->show($value);
723          
724   return $out;
725   }
726
727
728 /**
729  * Replace all css definitions with #container [def]
730  * and remove css-inlined scripting
731  *
732  * @param string CSS source code
733  * @param string Container ID to use as prefix
734  * @return string Modified CSS source
735  */
736 function rcmail_mod_css_styles($source, $container_id)
737   {
738   $last_pos = 0;
739   $replacements = new rcube_string_replacer;
740   
741   // ignore the whole block if evil styles are detected
742   $stripped = preg_replace('/[^a-z\(:]/', '', rcmail_xss_entity_decode($source));
743   if (preg_match('/expression|behavior|url\(|import/', $stripped))
744     return '/* evil! */';
745
746   // cut out all contents between { and }
747   while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos)))
748   {
749     $key = $replacements->add(substr($source, $pos+1, $pos2-($pos+1)));
750     $source = substr($source, 0, $pos+1) . $replacements->get_replacement($key) . substr($source, $pos2, strlen($source)-$pos2);
751     $last_pos = $pos+2;
752   }
753   
754   // remove html comments and add #container to each tag selector.
755   // also replace body definition because we also stripped off the <body> tag
756   $styles = preg_replace(
757     array(
758       '/(^\s*<!--)|(-->\s*$)/',
759       '/(^\s*|,\s*|\}\s*)([a-z0-9\._#][a-z0-9\.\-_]*)/im',
760       "/$container_id\s+body/i",
761     ),
762     array(
763       '',
764       "\\1#$container_id \\2",
765       "$container_id div.rcmBody",
766     ),
767     $source);
768   
769   // put block contents back in
770   $styles = $replacements->resolve($styles);
771
772   return $styles;
773   }
774
775
776 /**
777  * Decode escaped entities used by known XSS exploits.
778  * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
779  *
780  * @param string CSS content to decode
781  * @return string Decoded string
782  */
783 function rcmail_xss_entity_decode($content)
784 {
785   $out = html_entity_decode(html_entity_decode($content));
786   $out = preg_replace_callback('/\\\([0-9a-f]{4})/i', 'rcmail_xss_entity_decode_callback', $out);
787   $out = preg_replace('#/\*.*\*/#Um', '', $out);
788   return $out;
789 }
790
791
792 /**
793  * preg_replace_callback callback for rcmail_xss_entity_decode_callback
794  *
795  * @param array matches result from preg_replace_callback
796  * @return string decoded entity
797  */ 
798 function rcmail_xss_entity_decode_callback($matches)
799
800   return chr(hexdec($matches[1]));
801 }
802
803 /**
804  * Compose a valid attribute string for HTML tags
805  *
806  * @param array Named tag attributes
807  * @param array List of allowed attributes
808  * @return string HTML formatted attribute string
809  */
810 function create_attrib_string($attrib, $allowed_attribs=array('id', 'class', 'style'))
811   {
812   // allow the following attributes to be added to the <iframe> tag
813   $attrib_str = '';
814   foreach ($allowed_attribs as $a)
815     if (isset($attrib[$a]))
816       $attrib_str .= sprintf(' %s="%s"', $a, str_replace('"', '&quot;', $attrib[$a]));
817
818   return $attrib_str;
819   }
820
821
822 /**
823  * Convert a HTML attribute string attributes to an associative array (name => value)
824  *
825  * @param string Input string
826  * @return array Key-value pairs of parsed attributes
827  */
828 function parse_attrib_string($str)
829   {
830   $attrib = array();
831   preg_match_all('/\s*([-_a-z]+)=(["\'])??(?(2)([^\2]*)\2|(\S+?))/Ui', stripslashes($str), $regs, PREG_SET_ORDER);
832
833   // convert attributes to an associative array (name => value)
834   if ($regs) {
835     foreach ($regs as $attr) {
836       $attrib[strtolower($attr[1])] = html_entity_decode($attr[3] . $attr[4]);
837     }
838   }
839
840   return $attrib;
841   }
842
843
844 /**
845  * Convert the given date to a human readable form
846  * This uses the date formatting properties from config
847  *
848  * @param mixed Date representation (string or timestamp)
849  * @param string Date format to use
850  * @return string Formatted date string
851  */
852 function format_date($date, $format=NULL)
853   {
854   global $CONFIG;
855   
856   $ts = NULL;
857
858   if (is_numeric($date))
859     $ts = $date;
860   else if (!empty($date))
861     {
862     // support non-standard "GMTXXXX" literal
863     $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
864     // if date parsing fails, we have a date in non-rfc format.
865     // remove token from the end and try again
866     while ((($ts = @strtotime($date))===false) || ($ts < 0))
867       {
868         $d = explode(' ', $date);
869         array_pop($d);
870         if (!$d) break;
871         $date = implode(' ', $d);
872       }
873     }
874
875   if (empty($ts))
876     return '';
877    
878   // get user's timezone
879   if ($CONFIG['timezone'] === 'auto')
880     $tz = isset($_SESSION['timezone']) ? $_SESSION['timezone'] : date('Z')/3600;
881   else {
882     $tz = $CONFIG['timezone'];
883     if ($CONFIG['dst_active'])
884       $tz++;
885   }
886
887   // convert time to user's timezone
888   $timestamp = $ts - date('Z', $ts) + ($tz * 3600);
889   
890   // get current timestamp in user's timezone
891   $now = time();  // local time
892   $now -= (int)date('Z'); // make GMT time
893   $now += ($tz * 3600); // user's time
894   $now_date = getdate($now);
895
896   $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
897   $week_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
898
899   // define date format depending on current time  
900   if ($CONFIG['prettydate'] && !$format && $timestamp > $today_limit && $timestamp < $now)
901     return sprintf('%s %s', rcube_label('today'), date($CONFIG['date_today'] ? $CONFIG['date_today'] : 'H:i', $timestamp));
902   else if ($CONFIG['prettydate'] && !$format && $timestamp > $week_limit && $timestamp < $now)
903     $format = $CONFIG['date_short'] ? $CONFIG['date_short'] : 'D H:i';
904   else if (!$format)
905     $format = $CONFIG['date_long'] ? $CONFIG['date_long'] : 'd.m.Y H:i';
906
907   // strftime() format
908   if (preg_match('/%[a-z]+/i', $format))
909     return strftime($format, $timestamp);
910
911   // parse format string manually in order to provide localized weekday and month names
912   // an alternative would be to convert the date() format string to fit with strftime()
913   $out = '';
914   for($i=0; $i<strlen($format); $i++)
915     {
916     if ($format{$i}=='\\')  // skip escape chars
917       continue;
918     
919     // write char "as-is"
920     if ($format{$i}==' ' || $format{$i-1}=='\\')
921       $out .= $format{$i};
922     // weekday (short)
923     else if ($format{$i}=='D')
924       $out .= rcube_label(strtolower(date('D', $timestamp)));
925     // weekday long
926     else if ($format{$i}=='l')
927       $out .= rcube_label(strtolower(date('l', $timestamp)));
928     // month name (short)
929     else if ($format{$i}=='M')
930       $out .= rcube_label(strtolower(date('M', $timestamp)));
931     // month name (long)
932     else if ($format{$i}=='F')
933       $out .= rcube_label('long'.strtolower(date('M', $timestamp)));
934     else if ($format{$i}=='x')
935       $out .= strftime('%x %X', $timestamp);
936     else
937       $out .= date($format{$i}, $timestamp);
938     }
939   
940   return $out;
941   }
942
943
944 /**
945  * Compose a valid representaion of name and e-mail address
946  *
947  * @param string E-mail address
948  * @param string Person name
949  * @return string Formatted string
950  */
951 function format_email_recipient($email, $name='')
952   {
953   if ($name && $name != $email)
954     {
955     // Special chars as defined by RFC 822 need to in quoted string (or escaped).
956     return sprintf('%s <%s>', preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name) ? '"'.addcslashes($name, '"').'"' : $name, $email);
957     }
958   else
959     return $email;
960   }
961
962
963
964 /****** debugging functions ********/
965
966
967 /**
968  * Print or write debug messages
969  *
970  * @param mixed Debug message or data
971  */
972 function console()
973   {
974   $args = func_get_args();
975
976   if (class_exists('rcmail', false)) {
977     $rcmail = rcmail::get_instance();
978     if (is_object($rcmail->plugins))
979       $rcmail->plugins->exec_hook('console', $args);
980   }
981
982   $msg = array();
983   foreach ($args as $arg)
984     $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
985
986   if (!($GLOBALS['CONFIG']['debug_level'] & 4))
987     write_log('console', join(";\n", $msg));
988   else if ($GLOBALS['OUTPUT']->ajax_call)
989     print "/*\n " . join(";\n", $msg) . " \n*/\n";
990   else
991     {
992     print '<div style="background:#eee; border:1px solid #ccc; margin-bottom:3px; padding:6px"><pre>';
993     print join(";<br/>\n", $msg);
994     print "</pre></div>\n";
995     }
996   }
997
998
999 /**
1000  * Append a line to a logfile in the logs directory.
1001  * Date will be added automatically to the line.
1002  *
1003  * @param $name name of log file
1004  * @param line Line to append
1005  */
1006 function write_log($name, $line)
1007   {
1008   global $CONFIG, $RCMAIL;
1009
1010   if (!is_string($line))
1011     $line = var_export($line, true);
1012  
1013   if (empty($CONFIG['log_date_format']))
1014     $CONFIG['log_date_format'] = 'd-M-Y H:i:s O';
1015   
1016   $date = date($CONFIG['log_date_format']);
1017   
1018   // trigger logging hook
1019   if (is_object($RCMAIL) && is_object($RCMAIL->plugins)) {
1020     $log = $RCMAIL->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line));
1021     $name = $log['name'];
1022     $line = $log['line'];
1023     $date = $log['date'];
1024     if ($log['abort'])
1025       return;
1026   }
1027  
1028   $log_entry = sprintf("[%s]: %s\n", $date, $line);
1029
1030   if ($CONFIG['log_driver'] == 'syslog') {
1031     $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
1032     syslog($prio, $log_entry);
1033     return true;
1034   }
1035   else {
1036     // log_driver == 'file' is assumed here
1037     if (empty($CONFIG['log_dir']))
1038       $CONFIG['log_dir'] = INSTALL_PATH.'logs';
1039
1040     // try to open specific log file for writing
1041     if ($fp = @fopen($CONFIG['log_dir'].'/'.$name, 'a')) {
1042       fwrite($fp, $log_entry);
1043       fflush($fp);
1044       fclose($fp);
1045       return true;
1046     }
1047   }
1048   return false;
1049 }
1050
1051
1052 /**
1053  * @access private
1054  */
1055 function rcube_timer()
1056 {
1057   return microtime(true);
1058 }
1059   
1060
1061 /**
1062  * @access private
1063  */
1064 function rcube_print_time($timer, $label='Timer', $dest='console')
1065 {
1066   static $print_count = 0;
1067   
1068   $print_count++;
1069   $now = rcube_timer();
1070   $diff = $now-$timer;
1071   
1072   if (empty($label))
1073     $label = 'Timer '.$print_count;
1074   
1075   write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
1076 }
1077
1078
1079 /**
1080  * Return the mailboxlist in HTML
1081  *
1082  * @param array Named parameters
1083  * @return string HTML code for the gui object
1084  */
1085 function rcmail_mailbox_list($attrib)
1086 {
1087   global $RCMAIL;
1088   static $a_mailboxes;
1089   
1090   $attrib += array('maxlength' => 100, 'relanames' => false);
1091
1092   // add some labels to client
1093   $RCMAIL->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
1094   
1095   $type = $attrib['type'] ? $attrib['type'] : 'ul';
1096   unset($attrib['type']);
1097
1098   if ($type=='ul' && !$attrib['id'])
1099     $attrib['id'] = 'rcmboxlist';
1100
1101   // get mailbox list
1102   $mbox_name = $RCMAIL->imap->get_mailbox_name();
1103   
1104   // build the folders tree
1105   if (empty($a_mailboxes)) {
1106     // get mailbox list
1107     $a_folders = $RCMAIL->imap->list_mailboxes();
1108     $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1109     $a_mailboxes = array();
1110
1111     foreach ($a_folders as $folder)
1112       rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
1113   }
1114   
1115   // allow plugins to alter the folder tree or to localize folder names
1116   $hook = $RCMAIL->plugins->exec_hook('render_mailboxlist', array('list' => $a_mailboxes, 'delimiter' => $delimiter));
1117
1118   if ($type=='select') {
1119     $select = new html_select($attrib);
1120     
1121     // add no-selection option
1122     if ($attrib['noselection'])
1123       $select->add(rcube_label($attrib['noselection']), '0');
1124     
1125     rcmail_render_folder_tree_select($hook['list'], $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
1126     $out = $select->show();
1127   }
1128   else {
1129     $js_mailboxlist = array();
1130     $out = html::tag('ul', $attrib, rcmail_render_folder_tree_html($hook['list'], $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
1131     
1132     $RCMAIL->output->add_gui_object('mailboxlist', $attrib['id']);
1133     $RCMAIL->output->set_env('mailboxes', $js_mailboxlist);
1134     $RCMAIL->output->set_env('collapsed_folders', $RCMAIL->config->get('collapsed_folders'));
1135   }
1136
1137   return $out;
1138 }
1139
1140
1141 /**
1142  * Return the mailboxlist as html_select object
1143  *
1144  * @param array Named parameters
1145  * @return object html_select HTML drop-down object
1146  */
1147 function rcmail_mailbox_select($p = array())
1148 {
1149   global $RCMAIL;
1150   
1151   $p += array('maxlength' => 100, 'relanames' => false);
1152   $a_mailboxes = array();
1153   
1154   foreach ($RCMAIL->imap->list_mailboxes() as $folder)
1155     rcmail_build_folder_tree($a_mailboxes, $folder, $RCMAIL->imap->get_hierarchy_delimiter());
1156
1157   $select = new html_select($p);
1158   
1159   if ($p['noselection'])
1160     $select->add($p['noselection'], '');
1161     
1162   rcmail_render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames']);
1163   
1164   return $select;
1165 }
1166
1167
1168 /**
1169  * Create a hierarchical array of the mailbox list
1170  * @access private
1171  */
1172 function rcmail_build_folder_tree(&$arrFolders, $folder, $delm='/', $path='')
1173 {
1174   $pos = strpos($folder, $delm);
1175   if ($pos !== false) {
1176     $subFolders = substr($folder, $pos+1);
1177     $currentFolder = substr($folder, 0, $pos);
1178     $virtual = !isset($arrFolders[$currentFolder]);
1179   }
1180   else {
1181     $subFolders = false;
1182     $currentFolder = $folder;
1183     $virtual = false;
1184   }
1185
1186   $path .= $currentFolder;
1187
1188   if (!isset($arrFolders[$currentFolder])) {
1189     $arrFolders[$currentFolder] = array(
1190       'id' => $path,
1191       'name' => rcube_charset_convert($currentFolder, 'UTF7-IMAP'),
1192       'virtual' => $virtual,
1193       'folders' => array());
1194   }
1195   else
1196     $arrFolders[$currentFolder]['virtual'] = $virtual;
1197
1198   if (!empty($subFolders))
1199     rcmail_build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
1200 }
1201   
1202
1203 /**
1204  * Return html for a structured list &lt;ul&gt; for the mailbox tree
1205  * @access private
1206  */
1207 function rcmail_render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel=0)
1208 {
1209   global $RCMAIL, $CONFIG;
1210   
1211   $maxlength = intval($attrib['maxlength']);
1212   $realnames = (bool)$attrib['realnames'];
1213   $msgcounts = $RCMAIL->imap->get_cache('messagecount');
1214
1215   $idx = 0;
1216   $out = '';
1217   foreach ($arrFolders as $key => $folder) {
1218     $zebra_class = (($nestLevel+1)*$idx) % 2 == 0 ? 'even' : 'odd';
1219     $title = null;
1220
1221     if (($folder_class = rcmail_folder_classname($folder['id'])) && !$realnames) {
1222       $foldername = rcube_label($folder_class);
1223     }
1224     else {
1225       $foldername = $folder['name'];
1226
1227       // shorten the folder name to a given length
1228       if ($maxlength && $maxlength > 1) {
1229         $fname = abbreviate_string($foldername, $maxlength);
1230         if ($fname != $foldername)
1231           $title = $foldername;
1232         $foldername = $fname;
1233       }
1234     }
1235
1236     // make folder name safe for ids and class names
1237     $folder_id = asciiwords($folder['id'], true, '_');
1238     $classes = array('mailbox');
1239
1240     // set special class for Sent, Drafts, Trash and Junk
1241     if ($folder['id']==$CONFIG['sent_mbox'])
1242       $classes[] = 'sent';
1243     else if ($folder['id']==$CONFIG['drafts_mbox'])
1244       $classes[] = 'drafts';
1245     else if ($folder['id']==$CONFIG['trash_mbox'])
1246       $classes[] = 'trash';
1247     else if ($folder['id']==$CONFIG['junk_mbox'])
1248       $classes[] = 'junk';
1249     else if ($folder['id']=='INBOX')
1250       $classes[] = 'inbox';
1251     else
1252       $classes[] = '_'.asciiwords($folder_class ? $folder_class : strtolower($folder['id']), true);
1253       
1254     $classes[] = $zebra_class;
1255     
1256     if ($folder['id'] == $mbox_name)
1257       $classes[] = 'selected';
1258
1259     $collapsed = preg_match('/&'.rawurlencode($folder['id']).'&/', $RCMAIL->config->get('collapsed_folders'));
1260     $unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
1261     
1262     if ($folder['virtual'])
1263       $classes[] = 'virtual';
1264     else if ($unread)
1265       $classes[] = 'unread';
1266
1267     $js_name = JQ($folder['id']);
1268     $html_name = Q($foldername . ($unread ? " ($unread)" : ''));
1269     $link_attrib = $folder['virtual'] ? array() : array(
1270       'href' => rcmail_url('', array('_mbox' => $folder['id'])),
1271       'onclick' => sprintf("return %s.command('list','%s',this)", JS_OBJECT_NAME, $js_name),
1272       'title' => $title,
1273     );
1274
1275     $out .= html::tag('li', array(
1276         'id' => "rcmli".$folder_id,
1277         'class' => join(' ', $classes),
1278         'noclose' => true),
1279       html::a($link_attrib, $html_name) .
1280       (!empty($folder['folders']) ? html::div(array(
1281         'class' => ($collapsed ? 'collapsed' : 'expanded'),
1282         'style' => "position:absolute",
1283         'onclick' => sprintf("%s.command('collapse-folder', '%s')", JS_OBJECT_NAME, $js_name)
1284       ), '&nbsp;') : ''));
1285     
1286     $jslist[$folder_id] = array('id' => $folder['id'], 'name' => $foldername, 'virtual' => $folder['virtual']);
1287     
1288     if (!empty($folder['folders'])) {
1289       $out .= html::tag('ul', array('style' => ($collapsed ? "display:none;" : null)),
1290         rcmail_render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
1291     }
1292
1293     $out .= "</li>\n";
1294     $idx++;
1295   }
1296
1297   return $out;
1298 }
1299
1300
1301 /**
1302  * Return html for a flat list <select> for the mailbox tree
1303  * @access private
1304  */
1305 function rcmail_render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames=false, $nestLevel=0)
1306   {
1307   $idx = 0;
1308   $out = '';
1309   foreach ($arrFolders as $key=>$folder)
1310     {
1311     if (!$realnames && ($folder_class = rcmail_folder_classname($folder['id'])))
1312       $foldername = rcube_label($folder_class);
1313     else
1314       {
1315       $foldername = $folder['name'];
1316       
1317       // shorten the folder name to a given length
1318       if ($maxlength && $maxlength>1)
1319         $foldername = abbreviate_string($foldername, $maxlength);
1320       }
1321
1322     $select->add(str_repeat('&nbsp;', $nestLevel*4) . $foldername, $folder['id']);
1323
1324     if (!empty($folder['folders']))
1325       $out .= rcmail_render_folder_tree_select($folder['folders'], $mbox_name, $maxlength, $select, $realnames, $nestLevel+1);
1326
1327     $idx++;
1328     }
1329
1330   return $out;
1331   }
1332
1333
1334 /**
1335  * Return internal name for the given folder if it matches the configured special folders
1336  * @access private
1337  */
1338 function rcmail_folder_classname($folder_id)
1339 {
1340   global $CONFIG;
1341
1342   // for these mailboxes we have localized labels and css classes
1343   foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
1344   {
1345     if ($folder_id == $CONFIG[$smbx.'_mbox'])
1346       return $smbx;
1347   }
1348
1349   if ($folder_id == 'INBOX')
1350     return 'inbox';
1351 }
1352
1353
1354 /**
1355  * Try to localize the given IMAP folder name.
1356  * UTF-7 decode it in case no localized text was found
1357  *
1358  * @param string Folder name
1359  * @return string Localized folder name in UTF-8 encoding
1360  */
1361 function rcmail_localize_foldername($name)
1362 {
1363   if ($folder_class = rcmail_folder_classname($name))
1364     return rcube_label($folder_class);
1365   else
1366     return rcube_charset_convert($name, 'UTF7-IMAP');
1367 }
1368
1369
1370 /**
1371  * Output HTML editor scripts
1372  *
1373  * @param string Editor mode
1374  */
1375 function rcube_html_editor($mode='')
1376 {
1377   global $OUTPUT, $CONFIG;
1378
1379   $lang = $tinylang = strtolower(substr($_SESSION['language'], 0, 2));
1380   if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$tinylang.'.js'))
1381     $tinylang = 'en';
1382
1383   $OUTPUT->include_script('tiny_mce/tiny_mce.js');
1384   $OUTPUT->include_script('editor.js');
1385   $OUTPUT->add_script('rcmail_editor_init("$__skin_path", "'.JQ($tinylang).'", '.intval($CONFIG['enable_spellcheck']).', "'.$mode.'");');
1386 }
1387
1388
1389
1390 /**
1391  * Helper class to turn relative urls into absolute ones
1392  * using a predefined base
1393  */
1394 class rcube_base_replacer
1395 {
1396   private $base_url;
1397   
1398   public function __construct($base)
1399   {
1400     $this->base_url = $base;
1401   }
1402   
1403   public function callback($matches)
1404   {
1405     return $matches[1] . '="' . make_absolute_url($matches[3], $this->base_url) . '"';
1406   }
1407 }
1408
1409 ?>