Aleksander Machniak
2013-07-02 ed3e51f1b41d818ae757220fd41cdf0b093b20f6
commit | author | age
0c2596 1 <?php
A 2
3 /*
4  +-----------------------------------------------------------------------+
5  | This file is part of the Roundcube Webmail client                     |
6  | Copyright (C) 2008-2012, The Roundcube Dev Team                       |
7  | Copyright (C) 2011-2012, Kolab Systems AG                             |
8  |                                                                       |
9  | Licensed under the GNU General Public License version 3 or            |
10  | any later version with exceptions for skins & plugins.                |
11  | See the README file for a full license statement.                     |
12  |                                                                       |
13  | PURPOSE:                                                              |
14  |   Framework base class providing core functions and holding           |
15  |   instances of all 'global' objects like db- and storage-connections  |
16  +-----------------------------------------------------------------------+
17  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
18  +-----------------------------------------------------------------------+
19 */
20
21
22 /**
23  * Base class of the Roundcube Framework
24  * implemented as singleton
25  *
315418 26  * @package    Framework
AM 27  * @subpackage Core
0c2596 28  */
A 29 class rcube
30 {
315418 31     const INIT_WITH_DB = 1;
AM 32     const INIT_WITH_PLUGINS = 2;
0c2596 33
315418 34     /**
AM 35      * Singleton instace of rcube
36      *
996af3 37      * @var rcube
315418 38      */
AM 39     static protected $instance;
0c2596 40
315418 41     /**
AM 42      * Stores instance of rcube_config.
43      *
44      * @var rcube_config
45      */
46     public $config;
0c2596 47
315418 48     /**
AM 49      * Instace of database class.
50      *
51      * @var rcube_db
52      */
53     public $db;
0c2596 54
315418 55     /**
AM 56      * Instace of Memcache class.
57      *
58      * @var Memcache
59      */
60     public $memcache;
0c2596 61
315418 62    /**
AM 63      * Instace of rcube_session class.
64      *
65      * @var rcube_session
66      */
67     public $session;
0c2596 68
315418 69     /**
AM 70      * Instance of rcube_smtp class.
71      *
72      * @var rcube_smtp
73      */
74     public $smtp;
0c2596 75
315418 76     /**
AM 77      * Instance of rcube_storage class.
78      *
79      * @var rcube_storage
80      */
81     public $storage;
0c2596 82
315418 83     /**
AM 84      * Instance of rcube_output class.
85      *
86      * @var rcube_output
87      */
88     public $output;
0c2596 89
315418 90     /**
AM 91      * Instance of rcube_plugin_api.
92      *
93      * @var rcube_plugin_api
94      */
95     public $plugins;
0c2596 96
A 97
315418 98     /* private/protected vars */
AM 99     protected $texts;
100     protected $caches = array();
101     protected $shutdown_functions = array();
0c2596 102
A 103
315418 104     /**
AM 105      * This implements the 'singleton' design pattern
106      *
107      * @param integer Options to initialize with this instance. See rcube::INIT_WITH_* constants
108      *
109      * @return rcube The one and only instance
110      */
111     static function get_instance($mode = 0)
112     {
113         if (!self::$instance) {
114             self::$instance = new rcube();
115             self::$instance->init($mode);
71ee56 116         }
AM 117
315418 118         return self::$instance;
0c2596 119     }
A 120
121
315418 122     /**
AM 123      * Private constructor
124      */
125     protected function __construct()
126     {
127         // load configuration
128         $this->config  = new rcube_config;
129         $this->plugins = new rcube_dummy_plugin_api;
0c2596 130
315418 131         register_shutdown_function(array($this, 'shutdown'));
0c2596 132     }
A 133
134
315418 135     /**
AM 136      * Initial startup function
137      */
138     protected function init($mode = 0)
139     {
140         // initialize syslog
141         if ($this->config->get('log_driver') == 'syslog') {
142             $syslog_id       = $this->config->get('syslog_id', 'roundcube');
143             $syslog_facility = $this->config->get('syslog_facility', LOG_USER);
144             openlog($syslog_id, LOG_ODELAY, $syslog_facility);
145         }
0c2596 146
315418 147         // connect to database
AM 148         if ($mode & self::INIT_WITH_DB) {
149             $this->get_dbh();
150         }
0c2596 151
315418 152         // create plugin API and load plugins
AM 153         if ($mode & self::INIT_WITH_PLUGINS) {
154             $this->plugins = rcube_plugin_api::get_instance();
155         }
0c2596 156     }
A 157
158
315418 159     /**
AM 160      * Get the current database connection
161      *
162      * @return rcube_db Database object
163      */
164     public function get_dbh()
165     {
166         if (!$this->db) {
167             $config_all = $this->config->all();
168             $this->db = rcube_db::factory($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']);
169             $this->db->set_debug((bool)$config_all['sql_debug']);
170         }
0c2596 171
315418 172         return $this->db;
0c2596 173     }
A 174
175
315418 176     /**
AM 177      * Get global handle for memcache access
178      *
179      * @return object Memcache
180      */
181     public function get_memcache()
182     {
183         if (!isset($this->memcache)) {
184             // no memcache support in PHP
185             if (!class_exists('Memcache')) {
186                 $this->memcache = false;
187                 return false;
188             }
189
190             $this->memcache     = new Memcache;
191             $this->mc_available = 0;
192
193             // add all configured hosts to pool
194             $pconnect = $this->config->get('memcache_pconnect', true);
195             foreach ($this->config->get('memcache_hosts', array()) as $host) {
196                 if (substr($host, 0, 7) != 'unix://') {
197                     list($host, $port) = explode(':', $host);
198                     if (!$port) $port = 11211;
199                 }
200                 else {
201                     $port = 0;
202                 }
203
204                 $this->mc_available += intval($this->memcache->addServer(
205                     $host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure')));
206             }
207
208             // test connection and failover (will result in $this->mc_available == 0 on complete failure)
209             $this->memcache->increment('__CONNECTIONTEST__', 1);  // NOP if key doesn't exist
210
211             if (!$this->mc_available) {
212                 $this->memcache = false;
213             }
214         }
215
216         return $this->memcache;
0c2596 217     }
A 218
219
315418 220     /**
AM 221      * Callback for memcache failure
222      */
223     public function memcache_failure($host, $port)
224     {
225         static $seen = array();
0c2596 226
315418 227         // only report once
AM 228         if (!$seen["$host:$port"]++) {
229             $this->mc_available--;
230             self::raise_error(array(
231                 'code' => 604, 'type' => 'db',
232                 'line' => __LINE__, 'file' => __FILE__,
233                 'message' => "Memcache failure on host $host:$port"),
234                 true, false);
235         }
0c2596 236     }
A 237
238
315418 239     /**
AM 240      * Initialize and get cache object
241      *
242      * @param string $name   Cache identifier
243      * @param string $type   Cache type ('db', 'apc' or 'memcache')
244      * @param string $ttl    Expiration time for cache items
245      * @param bool   $packed Enables/disables data serialization
246      *
247      * @return rcube_cache Cache object
248      */
249     public function get_cache($name, $type='db', $ttl=0, $packed=true)
250     {
251         if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) {
252             $this->caches[$name] = new rcube_cache($type, $userid, $name, $ttl, $packed);
253         }
0c2596 254
315418 255         return $this->caches[$name];
0c2596 256     }
A 257
258
315418 259     /**
50abd5 260      * Initialize and get shared cache object
AM 261      *
262      * @param string $name   Cache identifier
263      * @param bool   $packed Enables/disables data serialization
264      *
265      * @return rcube_cache_shared Cache object
266      */
267     public function get_cache_shared($name, $packed=true)
268     {
269         $shared_name = "shared_$name";
270
22a41b 271         if (!array_key_exists($shared_name, $this->caches)) {
50abd5 272             $opt  = strtolower($name) . '_cache';
AM 273             $type = $this->config->get($opt);
274             $ttl  = $this->config->get($opt . '_ttl');
275
276             if (!$type) {
22a41b 277                 // cache is disabled
AM 278                 return $this->caches[$shared_name] = null;
50abd5 279             }
22a41b 280
50abd5 281             if ($ttl === null) {
22a41b 282                 $ttl = $this->config->get('shared_cache_ttl', '10d');
50abd5 283             }
AM 284
285             $this->caches[$shared_name] = new rcube_cache_shared($type, $name, $ttl, $packed);
286         }
287
288         return $this->caches[$shared_name];
289     }
290
291
292     /**
315418 293      * Create SMTP object and connect to server
AM 294      *
295      * @param boolean True if connection should be established
296      */
297     public function smtp_init($connect = false)
298     {
299         $this->smtp = new rcube_smtp();
0c2596 300
315418 301         if ($connect) {
AM 302             $this->smtp->connect();
303         }
0c2596 304     }
315418 305
AM 306
307     /**
308      * Initialize and get storage object
309      *
310      * @return rcube_storage Storage object
311      */
312     public function get_storage()
313     {
314         // already initialized
315         if (!is_object($this->storage)) {
316             $this->storage_init();
317         }
318
319         return $this->storage;
0c2596 320     }
315418 321
AM 322
323     /**
324      * Initialize storage object
325      */
326     public function storage_init()
327     {
328         // already initialized
329         if (is_object($this->storage)) {
330             return;
331         }
332
333         $driver       = $this->config->get('storage_driver', 'imap');
334         $driver_class = "rcube_{$driver}";
335
336         if (!class_exists($driver_class)) {
337             self::raise_error(array(
338                 'code' => 700, 'type' => 'php',
339                 'file' => __FILE__, 'line' => __LINE__,
340                 'message' => "Storage driver class ($driver) not found!"),
341                 true, true);
342         }
343
344         // Initialize storage object
345         $this->storage = new $driver_class;
346
347         // for backward compat. (deprecated, will be removed)
348         $this->imap = $this->storage;
349
350         // enable caching of mail data
351         $storage_cache  = $this->config->get("{$driver}_cache");
352         $messages_cache = $this->config->get('messages_cache');
353         // for backward compatybility
354         if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
355             $storage_cache  = 'db';
356             $messages_cache = true;
357         }
358
359         if ($storage_cache) {
360             $this->storage->set_caching($storage_cache);
361         }
362         if ($messages_cache) {
363             $this->storage->set_messages_caching(true);
364         }
365
366         // set pagesize from config
367         $pagesize = $this->config->get('mail_pagesize');
368         if (!$pagesize) {
369             $pagesize = $this->config->get('pagesize', 50);
370         }
371         $this->storage->set_pagesize($pagesize);
372
373         // set class options
374         $options = array(
375             'auth_type'   => $this->config->get("{$driver}_auth_type", 'check'),
376             'auth_cid'    => $this->config->get("{$driver}_auth_cid"),
377             'auth_pw'     => $this->config->get("{$driver}_auth_pw"),
378             'debug'       => (bool) $this->config->get("{$driver}_debug"),
379             'force_caps'  => (bool) $this->config->get("{$driver}_force_caps"),
ed3e51 380             'disabled_caps' => $this->config->get("{$driver}_disabled_caps"),
315418 381             'timeout'     => (int) $this->config->get("{$driver}_timeout"),
AM 382             'skip_deleted' => (bool) $this->config->get('skip_deleted'),
383             'driver'      => $driver,
384         );
385
386         if (!empty($_SESSION['storage_host'])) {
387             $options['host']     = $_SESSION['storage_host'];
388             $options['user']     = $_SESSION['username'];
389             $options['port']     = $_SESSION['storage_port'];
390             $options['ssl']      = $_SESSION['storage_ssl'];
391             $options['password'] = $this->decrypt($_SESSION['password']);
392             $_SESSION[$driver.'_host'] = $_SESSION['storage_host'];
393         }
394
395         $options = $this->plugins->exec_hook("storage_init", $options);
396
397         // for backward compat. (deprecated, to be removed)
398         $options = $this->plugins->exec_hook("imap_init", $options);
399
400         $this->storage->set_options($options);
401         $this->set_storage_prop();
0c2596 402     }
315418 403
AM 404
405     /**
406      * Set storage parameters.
407      * This must be done AFTER connecting to the server!
408      */
409     protected function set_storage_prop()
410     {
411         $storage = $this->get_storage();
412
a92beb 413         $storage->set_charset($this->config->get('default_charset', RCUBE_CHARSET));
315418 414
AM 415         if ($default_folders = $this->config->get('default_folders')) {
416             $storage->set_default_folders($default_folders);
417         }
418         if (isset($_SESSION['mbox'])) {
419             $storage->set_folder($_SESSION['mbox']);
420         }
421         if (isset($_SESSION['page'])) {
422             $storage->set_page($_SESSION['page']);
423         }
424     }
0c2596 425
A 426
963a10 427     /**
A 428      * Create session object and start the session.
429      */
430     public function session_init()
431     {
432         // session started (Installer?)
433         if (session_id()) {
434             return;
435         }
436
437         $sess_name   = $this->config->get('session_name');
438         $sess_domain = $this->config->get('session_domain');
ae7027 439         $sess_path   = $this->config->get('session_path');
963a10 440         $lifetime    = $this->config->get('session_lifetime', 0) * 60;
8e4b49 441         $is_secure   = $this->config->get('use_https') || rcube_utils::https_check();
963a10 442
A 443         // set session domain
444         if ($sess_domain) {
445             ini_set('session.cookie_domain', $sess_domain);
446         }
ae7027 447         // set session path
AM 448         if ($sess_path) {
449             ini_set('session.cookie_path', $sess_path);
450         }
963a10 451         // set session garbage collecting time according to session_lifetime
A 452         if ($lifetime) {
453             ini_set('session.gc_maxlifetime', $lifetime * 2);
454         }
455
8e4b49 456         ini_set('session.cookie_secure', $is_secure);
963a10 457         ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid');
A 458         ini_set('session.use_cookies', 1);
459         ini_set('session.use_only_cookies', 1);
0afe27 460         ini_set('session.cookie_httponly', 1);
963a10 461
A 462         // use database for storing session data
463         $this->session = new rcube_session($this->get_dbh(), $this->config);
464
3dbe4f 465         $this->session->register_gc_handler(array($this, 'gc'));
c442f8 466         $this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
AM 467         $this->session->set_ip_check($this->config->get('ip_check'));
468
963a10 469         // start PHP session (if not in CLI mode)
A 470         if ($_SERVER['REMOTE_ADDR']) {
42de33 471             $this->session->start();
963a10 472         }
A 473     }
474
475
476     /**
ee73a7 477      * Garbage collector - cache/temp cleaner
AM 478      */
479     public function gc()
480     {
60b6d7 481         rcube_cache::gc();
AM 482         rcube_cache_shared::gc();
483         $this->get_storage()->cache_gc();
ee73a7 484
AM 485         $this->gc_temp();
486     }
487
488
489     /**
963a10 490      * Garbage collector function for temp files.
A 491      * Remove temp files older than two days
492      */
ee73a7 493     public function gc_temp()
963a10 494     {
A 495         $tmp = unslashify($this->config->get('temp_dir'));
6a8b4c 496         $expire = time() - 172800;  // expire in 48 hours
963a10 497
A 498         if ($tmp && ($dir = opendir($tmp))) {
499             while (($fname = readdir($dir)) !== false) {
500                 if ($fname{0} == '.') {
501                     continue;
502                 }
503
504                 if (filemtime($tmp.'/'.$fname) < $expire) {
505                     @unlink($tmp.'/'.$fname);
506                 }
507             }
508
509             closedir($dir);
510         }
ee73a7 511     }
AM 512
513
514     /**
515      * Runs garbage collector with probability based on
516      * session settings. This is intended for environments
517      * without a session.
518      */
519     public function gc_run()
520     {
521         $probability = (int) ini_get('session.gc_probability');
522         $divisor     = (int) ini_get('session.gc_divisor');
523
524         if ($divisor > 0 && $probability > 0) {
525             $random = mt_rand(1, $divisor);
526             if ($random <= $probability) {
527                 $this->gc();
528             }
529         }
963a10 530     }
A 531
532
315418 533     /**
AM 534      * Get localized text in the desired language
535      *
536      * @param mixed   $attrib  Named parameters array or label name
537      * @param string  $domain  Label domain (plugin) name
538      *
539      * @return string Localized text
0c2596 540      */
315418 541     public function gettext($attrib, $domain=null)
AM 542     {
543         // load localization files if not done yet
544         if (empty($this->texts)) {
545             $this->load_language();
546         }
0c2596 547
315418 548         // extract attributes
AM 549         if (is_string($attrib)) {
550             $attrib = array('name' => $attrib);
551         }
0c2596 552
315418 553         $name = $attrib['name'] ? $attrib['name'] : '';
AM 554
555         // attrib contain text values: use them from now
556         if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us'])) {
557             $this->texts[$name] = $setval;
558         }
559
560         // check for text with domain
561         if ($domain && ($text = $this->texts[$domain.'.'.$name])) {
562         }
563         // text does not exist
564         else if (!($text = $this->texts[$name])) {
565             return "[$name]";
566         }
567
568         // replace vars in text
569         if (is_array($attrib['vars'])) {
570             foreach ($attrib['vars'] as $var_key => $var_value) {
571                 $text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text);
572             }
573         }
574
575         // format output
576         if (($attrib['uppercase'] && strtolower($attrib['uppercase'] == 'first')) || $attrib['ucfirst']) {
577             return ucfirst($text);
578         }
579         else if ($attrib['uppercase']) {
580             return mb_strtoupper($text);
581         }
582         else if ($attrib['lowercase']) {
583             return mb_strtolower($text);
584         }
585
586         return strtr($text, array('\n' => "\n"));
0c2596 587     }
A 588
589
315418 590     /**
AM 591      * Check if the given text label exists
592      *
593      * @param string  $name       Label name
594      * @param string  $domain     Label domain (plugin) name or '*' for all domains
595      * @param string  $ref_domain Sets domain name if label is found
596      *
597      * @return boolean True if text exists (either in the current language or in en_US)
598      */
599     public function text_exists($name, $domain = null, &$ref_domain = null)
600     {
601         // load localization files if not done yet
602         if (empty($this->texts)) {
603             $this->load_language();
604         }
0c2596 605
315418 606         if (isset($this->texts[$name])) {
AM 607             $ref_domain = '';
608             return true;
609         }
0c2596 610
315418 611         // any of loaded domains (plugins)
AM 612         if ($domain == '*') {
613             foreach ($this->plugins->loaded_plugins() as $domain) {
614                 if (isset($this->texts[$domain.'.'.$name])) {
615                     $ref_domain = $domain;
616                     return true;
617                 }
618             }
619         }
620         // specified domain
621         else if ($domain) {
622             $ref_domain = $domain;
623             return isset($this->texts[$domain.'.'.$name]);
624         }
0c2596 625
315418 626         return false;
AM 627     }
628
629
630     /**
631      * Load a localization package
632      *
633      * @param string Language ID
634      * @param array  Additional text labels/messages
635      */
636     public function load_language($lang = null, $add = array())
637     {
638         $lang = $this->language_prop(($lang ? $lang : $_SESSION['language']));
639
640         // load localized texts
641         if (empty($this->texts) || $lang != $_SESSION['language']) {
642             $this->texts = array();
643
644             // handle empty lines after closing PHP tag in localization files
645             ob_start();
646
647             // get english labels (these should be complete)
9be2f4 648             @include(RCUBE_LOCALIZATION_DIR . 'en_US/labels.inc');
TB 649             @include(RCUBE_LOCALIZATION_DIR . 'en_US/messages.inc');
315418 650
AM 651             if (is_array($labels))
652                 $this->texts = $labels;
653             if (is_array($messages))
654                 $this->texts = array_merge($this->texts, $messages);
655
656             // include user language files
9be2f4 657             if ($lang != 'en' && $lang != 'en_US' && is_dir(RCUBE_LOCALIZATION_DIR . $lang)) {
TB 658                 include_once(RCUBE_LOCALIZATION_DIR . $lang . '/labels.inc');
659                 include_once(RCUBE_LOCALIZATION_DIR . $lang . '/messages.inc');
315418 660
AM 661                 if (is_array($labels))
662                     $this->texts = array_merge($this->texts, $labels);
663                 if (is_array($messages))
664                     $this->texts = array_merge($this->texts, $messages);
665             }
666
667             ob_end_clean();
668
669             $_SESSION['language'] = $lang;
670         }
671
672         // append additional texts (from plugin)
673         if (is_array($add) && !empty($add)) {
674             $this->texts += $add;
675         }
676     }
677
678
679     /**
680      * Check the given string and return a valid language code
681      *
682      * @param string Language code
683      *
684      * @return string Valid language code
685      */
686     protected function language_prop($lang)
687     {
688         static $rcube_languages, $rcube_language_aliases;
689
690         // user HTTP_ACCEPT_LANGUAGE if no language is specified
691         if (empty($lang) || $lang == 'auto') {
692             $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
693             $lang         = str_replace('-', '_', $accept_langs[0]);
694         }
695
696         if (empty($rcube_languages)) {
9be2f4 697             @include(RCUBE_LOCALIZATION_DIR . 'index.inc');
315418 698         }
AM 699
700         // check if we have an alias for that language
701         if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) {
702             $lang = $rcube_language_aliases[$lang];
703         }
704         // try the first two chars
705         else if (!isset($rcube_languages[$lang])) {
706             $short = substr($lang, 0, 2);
707
708             // check if we have an alias for the short language code
709             if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) {
710                 $lang = $rcube_language_aliases[$short];
711             }
712             // expand 'nn' to 'nn_NN'
713             else if (!isset($rcube_languages[$short])) {
714                 $lang = $short.'_'.strtoupper($short);
715             }
716         }
717
9be2f4 718         if (!isset($rcube_languages[$lang]) || !is_dir(RCUBE_LOCALIZATION_DIR . $lang)) {
315418 719             $lang = 'en_US';
AM 720         }
721
722         return $lang;
723     }
724
725
726     /**
727      * Read directory program/localization and return a list of available languages
728      *
729      * @return array List of available localizations
730      */
731     public function list_languages()
732     {
733         static $sa_languages = array();
734
735         if (!sizeof($sa_languages)) {
9be2f4 736             @include(RCUBE_LOCALIZATION_DIR . 'index.inc');
315418 737
9be2f4 738             if ($dh = @opendir(RCUBE_LOCALIZATION_DIR)) {
315418 739                 while (($name = readdir($dh)) !== false) {
9be2f4 740                     if ($name[0] == '.' || !is_dir(RCUBE_LOCALIZATION_DIR . $name)) {
315418 741                         continue;
AM 742                     }
743
744                     if ($label = $rcube_languages[$name]) {
745                         $sa_languages[$name] = $label;
746                     }
747                 }
748                 closedir($dh);
749             }
750         }
751
752         return $sa_languages;
753     }
754
755
756     /**
757      * Encrypt using 3DES
758      *
759      * @param string $clear clear text input
760      * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
761      * @param boolean $base64 whether or not to base64_encode() the result before returning
762      *
763      * @return string encrypted text
764      */
765     public function encrypt($clear, $key = 'des_key', $base64 = true)
766     {
767         if (!$clear) {
768             return '';
769         }
770
771         /*-
772          * Add a single canary byte to the end of the clear text, which
773          * will help find out how much of padding will need to be removed
774          * upon decryption; see http://php.net/mcrypt_generic#68082
775          */
776         $clear = pack("a*H2", $clear, "80");
777
778         if (function_exists('mcrypt_module_open') &&
779             ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, ""))
780         ) {
781             $iv = $this->create_iv(mcrypt_enc_get_iv_size($td));
782             mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
783             $cipher = $iv . mcrypt_generic($td, $clear);
784             mcrypt_generic_deinit($td);
785             mcrypt_module_close($td);
786         }
787         else {
788             @include_once 'des.inc';
789
790             if (function_exists('des')) {
791                 $des_iv_size = 8;
792                 $iv = $this->create_iv($des_iv_size);
793                 $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv);
794             }
795             else {
796                 self::raise_error(array(
797                     'code' => 500, 'type' => 'php',
798                     'file' => __FILE__, 'line' => __LINE__,
799                     'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available"
800                     ), true, true);
801             }
802         }
803
804         return $base64 ? base64_encode($cipher) : $cipher;
805     }
806
807
808     /**
809      * Decrypt 3DES-encrypted string
810      *
811      * @param string $cipher encrypted text
812      * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
813      * @param boolean $base64 whether or not input is base64-encoded
814      *
815      * @return string decrypted text
816      */
817     public function decrypt($cipher, $key = 'des_key', $base64 = true)
818     {
819         if (!$cipher) {
820             return '';
821         }
822
823         $cipher = $base64 ? base64_decode($cipher) : $cipher;
824
825         if (function_exists('mcrypt_module_open') &&
826             ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, ""))
827         ) {
828             $iv_size = mcrypt_enc_get_iv_size($td);
829             $iv = substr($cipher, 0, $iv_size);
830
831             // session corruption? (#1485970)
832             if (strlen($iv) < $iv_size) {
833                 return '';
834             }
835
836             $cipher = substr($cipher, $iv_size);
837             mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
838             $clear = mdecrypt_generic($td, $cipher);
839             mcrypt_generic_deinit($td);
840             mcrypt_module_close($td);
841         }
842         else {
843             @include_once 'des.inc';
844
845             if (function_exists('des')) {
846                 $des_iv_size = 8;
847                 $iv = substr($cipher, 0, $des_iv_size);
848                 $cipher = substr($cipher, $des_iv_size);
849                 $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv);
850             }
851             else {
852                 self::raise_error(array(
853                     'code' => 500, 'type' => 'php',
854                     'file' => __FILE__, 'line' => __LINE__,
855                     'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available"
856                     ), true, true);
857             }
858         }
859
860         /*-
861          * Trim PHP's padding and the canary byte; see note in
862          * rcube::encrypt() and http://php.net/mcrypt_generic#68082
863          */
864         $clear = substr(rtrim($clear, "\0"), 0, -1);
865
866         return $clear;
867     }
868
869
870     /**
871      * Generates encryption initialization vector (IV)
872      *
873      * @param int Vector size
874      *
875      * @return string Vector string
876      */
877     private function create_iv($size)
878     {
879         // mcrypt_create_iv() can be slow when system lacks entrophy
880         // we'll generate IV vector manually
881         $iv = '';
882         for ($i = 0; $i < $size; $i++) {
883             $iv .= chr(mt_rand(0, 255));
884         }
885
886         return $iv;
887     }
888
889
890     /**
891      * Build a valid URL to this instance of Roundcube
892      *
893      * @param mixed Either a string with the action or url parameters as key-value pairs
894      * @return string Valid application URL
895      */
896     public function url($p)
897     {
898         // STUB: should be overloaded by the application
0c2596 899         return '';
A 900     }
901
315418 902
AM 903     /**
904      * Function to be executed in script shutdown
905      * Registered with register_shutdown_function()
0c2596 906      */
315418 907     public function shutdown()
AM 908     {
909         foreach ($this->shutdown_functions as $function) {
910             call_user_func($function);
0c2596 911         }
A 912
3dbe4f 913         // write session data as soon as possible and before
AM 914         // closing database connection, don't do this before
915         // registered shutdown functions, they may need the session
916         // Note: this will run registered gc handlers (ie. cache gc)
917         if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
918             $this->session->write_close();
315418 919         }
AM 920
3dbe4f 921         if (is_object($this->smtp)) {
AM 922             $this->smtp->disconnect();
ee73a7 923         }
AM 924
315418 925         foreach ($this->caches as $cache) {
AM 926             if (is_object($cache)) {
927                 $cache->close();
928             }
929         }
930
931         if (is_object($this->storage)) {
932             $this->storage->close();
933         }
0c2596 934     }
A 935
936
315418 937     /**
AM 938      * Registers shutdown function to be executed on shutdown.
939      * The functions will be executed before destroying any
940      * objects like smtp, imap, session, etc.
941      *
942      * @param callback Function callback
943      */
944     public function add_shutdown_function($function)
945     {
946         $this->shutdown_functions[] = $function;
947     }
948
949
950     /**
10da75 951      * Quote a given string.
TB 952      * Shortcut function for rcube_utils::rep_specialchars_output()
953      *
954      * @return string HTML-quoted string
955      */
956     public static function Q($str, $mode = 'strict', $newlines = true)
957     {
958         return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines);
959     }
960
961
962     /**
963      * Quote a given string for javascript output.
964      * Shortcut function for rcube_utils::rep_specialchars_output()
965      *
966      * @return string JS-quoted string
967      */
968     public static function JQ($str)
969     {
970         return rcube_utils::rep_specialchars_output($str, 'js');
971     }
972
973
974     /**
315418 975      * Construct shell command, execute it and return output as string.
AM 976      * Keywords {keyword} are replaced with arguments
977      *
978      * @param $cmd Format string with {keywords} to be replaced
979      * @param $values (zero, one or more arrays can be passed)
980      *
981      * @return output of command. shell errors not detectable
982      */
983     public static function exec(/* $cmd, $values1 = array(), ... */)
984     {
985         $args   = func_get_args();
986         $cmd    = array_shift($args);
987         $values = $replacements = array();
988
989         // merge values into one array
990         foreach ($args as $arg) {
991             $values += (array)$arg;
992         }
993
994         preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER);
995         foreach ($matches as $tags) {
996             list(, $tag, $option, $key) = $tags;
997             $parts = array();
998
999             if ($option) {
1000                 foreach ((array)$values["-$key"] as $key => $value) {
1001                     if ($value === true || $value === false || $value === null) {
1002                         $parts[] = $value ? $key : "";
1003                     }
1004                     else {
1005                         foreach ((array)$value as $val) {
1006                             $parts[] = "$key " . escapeshellarg($val);
1007                         }
1008                     }
1009                 }
1010             }
1011             else {
1012                 foreach ((array)$values[$key] as $value) {
1013                     $parts[] = escapeshellarg($value);
1014                 }
1015             }
1016
1017             $replacements[$tag] = join(" ", $parts);
1018         }
1019
1020         // use strtr behaviour of going through source string once
1021         $cmd = strtr($cmd, $replacements);
1022
1023         return (string)shell_exec($cmd);
1024     }
0c2596 1025
A 1026
1027     /**
1028      * Print or write debug messages
1029      *
1030      * @param mixed Debug message or data
1031      */
1032     public static function console()
1033     {
1034         $args = func_get_args();
1035
be98df 1036         if (class_exists('rcube', false)) {
0c2596 1037             $rcube = self::get_instance();
be98df 1038             $plugin = $rcube->plugins->exec_hook('console', array('args' => $args));
A 1039             if ($plugin['abort']) {
1040                 return;
0c2596 1041             }
be98df 1042            $args = $plugin['args'];
0c2596 1043         }
A 1044
1045         $msg = array();
1046         foreach ($args as $arg) {
1047             $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
1048         }
1049
1050         self::write_log('console', join(";\n", $msg));
1051     }
1052
1053
1054     /**
1055      * Append a line to a logfile in the logs directory.
1056      * Date will be added automatically to the line.
1057      *
1058      * @param $name name of log file
1059      * @param line Line to append
1060      */
1061     public static function write_log($name, $line)
1062     {
1063         if (!is_string($line)) {
1064             $line = var_export($line, true);
1065         }
1066
1067         $date_format = self::$instance ? self::$instance->config->get('log_date_format') : null;
1068         $log_driver  = self::$instance ? self::$instance->config->get('log_driver') : null;
1069
1070         if (empty($date_format)) {
1071             $date_format = 'd-M-Y H:i:s O';
1072         }
1073
1074         $date = date($date_format);
1075
1076         // trigger logging hook
1077         if (is_object(self::$instance) && is_object(self::$instance->plugins)) {
1078             $log  = self::$instance->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line));
1079             $name = $log['name'];
1080             $line = $log['line'];
1081             $date = $log['date'];
1082             if ($log['abort'])
1083                 return true;
1084         }
1085
1086         if ($log_driver == 'syslog') {
1087             $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
1088             syslog($prio, $line);
1089             return true;
1090         }
1091
1092         // log_driver == 'file' is assumed here
1093
1094         $line = sprintf("[%s]: %s\n", $date, $line);
1095         $log_dir  = self::$instance ? self::$instance->config->get('log_dir') : null;
1096
1097         if (empty($log_dir)) {
9be2f4 1098             $log_dir = RCUBE_INSTALL_PATH . 'logs';
0c2596 1099         }
A 1100
1101         // try to open specific log file for writing
1102         $logfile = $log_dir.'/'.$name;
1103
1104         if ($fp = @fopen($logfile, 'a')) {
1105             fwrite($fp, $line);
1106             fflush($fp);
1107             fclose($fp);
1108             return true;
1109         }
1110
1111         trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
1112         return false;
1113     }
1114
1115
1116     /**
1117      * Throw system error (and show error page).
1118      *
1119      * @param array Named parameters
1120      *      - code:    Error code (required)
1121      *      - type:    Error type [php|db|imap|javascript] (required)
1122      *      - message: Error message
1123      *      - file:    File where error occured
1124      *      - line:    Line where error occured
1125      * @param boolean True to log the error
1126      * @param boolean Terminate script execution
1127      */
1128     public static function raise_error($arg = array(), $log = false, $terminate = false)
1129     {
76e499 1130         // handle PHP exceptions
TB 1131         if (is_object($arg) && is_a($arg, 'Exception')) {
a39fd4 1132             $arg = array(
76e499 1133                 'type' => 'php',
TB 1134                 'code' => $arg->getCode(),
1135                 'line' => $arg->getLine(),
1136                 'file' => $arg->getFile(),
1137                 'message' => $arg->getMessage(),
1138             );
a39fd4 1139         }
f23ef1 1140         else if (is_string($arg)) {
AM 1141             $arg = array('message' => $arg, 'type' => 'php');
1142         }
a39fd4 1143
AM 1144         if (empty($arg['code'])) {
1145             $arg['code'] = 500;
76e499 1146         }
TB 1147
0c2596 1148         // installer
A 1149         if (class_exists('rcube_install', false)) {
1150             $rci = rcube_install::get_instance();
1151             $rci->raise_error($arg);
1152             return;
1153         }
1154
f23ef1 1155         $cli = php_sapi_name() == 'cli';
AM 1156
1157         if (($log || $terminate) && !$cli && $arg['type'] && $arg['message']) {
819315 1158             $arg['fatal'] = $terminate;
0c2596 1159             self::log_bug($arg);
A 1160         }
1161
f23ef1 1162         // terminate script
AM 1163         if ($terminate) {
1164             // display error page
1165             if (is_object(self::$instance->output)) {
1166                 self::$instance->output->raise_error($arg['code'], $arg['message']);
1167             }
1168             else if ($cli) {
1169                 fwrite(STDERR, 'ERROR: ' . $arg['message']);
1170             }
1171
1172             exit(1);
0c2596 1173         }
A 1174     }
1175
1176
1177     /**
1178      * Report error according to configured debug_level
1179      *
1180      * @param array Named parameters
1181      * @see self::raise_error()
1182      */
1183     public static function log_bug($arg_arr)
1184     {
1185         $program = strtoupper($arg_arr['type']);
1186         $level   = self::get_instance()->config->get('debug_level');
1187
1188         // disable errors for ajax requests, write to log instead (#1487831)
1189         if (($level & 4) && !empty($_REQUEST['_remote'])) {
1190             $level = ($level ^ 4) | 1;
1191         }
1192
1193         // write error to local log file
819315 1194         if (($level & 1) || !empty($arg_arr['fatal'])) {
0c2596 1195             if ($_SERVER['REQUEST_METHOD'] == 'POST') {
A 1196                 $post_query = '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']);
1197             }
1198             else {
1199                 $post_query = '';
1200             }
1201
1202             $log_entry = sprintf("%s Error: %s%s (%s %s)",
1203                 $program,
1204                 $arg_arr['message'],
1205                 $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
1206                 $_SERVER['REQUEST_METHOD'],
1207                 $_SERVER['REQUEST_URI'] . $post_query);
1208
1209             if (!self::write_log('errors', $log_entry)) {
1210                 // send error to PHPs error handler if write_log didn't succeed
f23ef1 1211                 trigger_error($arg_arr['message'], E_USER_WARNING);
0c2596 1212             }
A 1213         }
1214
1215         // report the bug to the global bug reporting system
1216         if ($level & 2) {
1217             // TODO: Send error via HTTP
1218         }
1219
1220         // show error if debug_mode is on
1221         if ($level & 4) {
1222             print "<b>$program Error";
1223
1224             if (!empty($arg_arr['file']) && !empty($arg_arr['line'])) {
1225                 print " in $arg_arr[file] ($arg_arr[line])";
1226             }
1227
1228             print ':</b>&nbsp;';
1229             print nl2br($arg_arr['message']);
1230             print '<br />';
1231             flush();
1232         }
1233     }
1234
1235
1236     /**
1237      * Returns current time (with microseconds).
1238      *
1239      * @return float Current time in seconds since the Unix
1240      */
1241     public static function timer()
1242     {
1243         return microtime(true);
1244     }
1245
1246
1247     /**
1248      * Logs time difference according to provided timer
1249      *
1250      * @param float  $timer  Timer (self::timer() result)
1251      * @param string $label  Log line prefix
1252      * @param string $dest   Log file name
1253      *
1254      * @see self::timer()
1255      */
1256     public static function print_timer($timer, $label = 'Timer', $dest = 'console')
1257     {
1258         static $print_count = 0;
1259
1260         $print_count++;
1261         $now  = self::timer();
1262         $diff = $now - $timer;
1263
1264         if (empty($label)) {
1265             $label = 'Timer '.$print_count;
1266         }
1267
1268         self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
1269     }
1270
1271
1272     /**
1273      * Getter for logged user ID.
1274      *
1275      * @return mixed User identifier
1276      */
1277     public function get_user_id()
1278     {
1279         if (is_object($this->user)) {
1280             return $this->user->ID;
1281         }
a2f896 1282         else if (isset($_SESSION['user_id'])) {
A 1283             return $_SESSION['user_id'];
1284         }
0c2596 1285
A 1286         return null;
1287     }
1288
1289
1290     /**
1291      * Getter for logged user name.
1292      *
1293      * @return string User name
1294      */
1295     public function get_user_name()
1296     {
1297         if (is_object($this->user)) {
1298             return $this->user->get_username();
1299         }
789e59 1300         else if (isset($_SESSION['username'])) {
AM 1301             return $_SESSION['username'];
1302         }
1303     }
0c2596 1304
789e59 1305
AM 1306     /**
1307      * Getter for logged user email (derived from user name not identity).
1308      *
1309      * @return string User email address
1310      */
1311     public function get_user_email()
1312     {
1313         if (is_object($this->user)) {
1314             return $this->user->get_username('mail');
1315         }
0c2596 1316     }
5b06e2 1317
AM 1318
1319     /**
1320      * Getter for logged user password.
1321      *
1322      * @return string User password
1323      */
1324     public function get_user_password()
1325     {
1326         if ($this->password) {
1327             return $this->password;
1328         }
1329         else if ($_SESSION['password']) {
1330             return $this->decrypt($_SESSION['password']);
1331         }
1332     }
a5b8ef 1333
AM 1334
1335     /**
1336      * Getter for logged user language code.
1337      *
1338      * @return string User language code
1339      */
1340     public function get_user_language()
1341     {
1342         if (is_object($this->user)) {
1343             return $this->user->language;
1344         }
1345         else if (isset($_SESSION['language'])) {
1346             return $_SESSION['language'];
1347         }
1348     }
0b9a7b 1349
TB 1350     /**
1351      * Unique Message-ID generator.
1352      *
1353      * @return string Message-ID
1354      */
1355     public function gen_message_id()
1356     {
1357         $local_part  = md5(uniqid('rcube'.mt_rand(), true));
1358         $domain_part = $this->user->get_username('domain');
1359
1360         // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
1361         if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
1362             foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
1363                 $host = preg_replace('/:[0-9]+$/', '', $host);
1364                 if ($host && preg_match('/\.[a-z]+$/i', $host)) {
1365                     $domain_part = $host;
1366                 }
1367             }
1368         }
1369
1370         return sprintf('<%s@%s>', $local_part, $domain_part);
1371     }
1372
1373     /**
1374      * Send the given message using the configured method.
1375      *
1376      * @param object $message    Reference to Mail_MIME object
1377      * @param string $from       Sender address string
1378      * @param array  $mailto     Array of recipient address strings
1379      * @param array  $error      SMTP error array (reference)
1380      * @param string $body_file  Location of file with saved message body (reference),
1381      *                           used when delay_file_io is enabled
1382      * @param array  $options    SMTP options (e.g. DSN request)
1383      *
1384      * @return boolean Send status.
1385      */
1386     public function deliver_message(&$message, $from, $mailto, &$error, &$body_file = null, $options = null)
1387     {
1388         $plugin = $this->plugins->exec_hook('message_before_send', array(
1389             'message' => $message,
1390             'from'    => $from,
1391             'mailto'  => $mailto,
1392             'options' => $options,
1393         ));
1394
1395         $from    = $plugin['from'];
1396         $mailto  = $plugin['mailto'];
1397         $options = $plugin['options'];
1398         $message = $plugin['message'];
1399         $headers = $message->headers();
1400
1401         // send thru SMTP server using custom SMTP library
1402         if ($this->config->get('smtp_server')) {
1403             // generate list of recipients
1404             $a_recipients = array($mailto);
1405
1406             if (strlen($headers['Cc']))
1407                 $a_recipients[] = $headers['Cc'];
1408             if (strlen($headers['Bcc']))
1409                 $a_recipients[] = $headers['Bcc'];
1410
1411             // clean Bcc from header for recipients
1412             $send_headers = $headers;
1413             unset($send_headers['Bcc']);
1414             // here too, it because txtHeaders() below use $message->_headers not only $send_headers
1415             unset($message->_headers['Bcc']);
1416
1417             $smtp_headers = $message->txtHeaders($send_headers, true);
1418
1419             if ($message->getParam('delay_file_io')) {
1420                 // use common temp dir
1421                 $temp_dir = $this->config->get('temp_dir');
1422                 $body_file = tempnam($temp_dir, 'rcmMsg');
1423                 if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) {
1424                     self::raise_error(array('code' => 650, 'type' => 'php',
1425                         'file' => __FILE__, 'line' => __LINE__,
1426                         'message' => "Could not create message: ".$mime_result->getMessage()),
1427                         TRUE, FALSE);
1428                     return false;
1429                 }
1430                 $msg_body = fopen($body_file, 'r');
1431             }
1432             else {
1433                 $msg_body = $message->get();
1434             }
1435
1436             // send message
1437             if (!is_object($this->smtp)) {
1438                 $this->smtp_init(true);
1439             }
1440
1441             $sent     = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $options);
1442             $response = $this->smtp->get_response();
1443             $error    = $this->smtp->get_error();
1444
1445             // log error
1446             if (!$sent) {
1447                 self::raise_error(array('code' => 800, 'type' => 'smtp',
1448                     'line' => __LINE__, 'file' => __FILE__,
1449                     'message' => "SMTP error: ".join("\n", $response)), TRUE, FALSE);
1450             }
1451         }
1452         // send mail using PHP's mail() function
1453         else {
1454             // unset some headers because they will be added by the mail() function
1455             $headers_enc = $message->headers($headers);
1456             $headers_php = $message->_headers;
1457             unset($headers_php['To'], $headers_php['Subject']);
1458
1459             // reset stored headers and overwrite
1460             $message->_headers = array();
1461             $header_str = $message->txtHeaders($headers_php);
1462
1463             // #1485779
1464             if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1465                 if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
1466                     $headers_enc['To'] = implode(', ', $m[1]);
1467                 }
1468             }
1469
1470             $msg_body = $message->get();
1471
1472             if (PEAR::isError($msg_body)) {
1473                 self::raise_error(array('code' => 650, 'type' => 'php',
1474                     'file' => __FILE__, 'line' => __LINE__,
1475                     'message' => "Could not create message: ".$msg_body->getMessage()),
1476                     TRUE, FALSE);
1477             }
1478             else {
1479                 $delim   = $this->config->header_delimiter();
1480                 $to      = $headers_enc['To'];
1481                 $subject = $headers_enc['Subject'];
1482                 $header_str = rtrim($header_str);
1483
1484                 if ($delim != "\r\n") {
1485                     $header_str = str_replace("\r\n", $delim, $header_str);
1486                     $msg_body   = str_replace("\r\n", $delim, $msg_body);
1487                     $to         = str_replace("\r\n", $delim, $to);
1488                     $subject    = str_replace("\r\n", $delim, $subject);
1489                 }
1490
39b905 1491                 if (filter_var(ini_get('safe_mode'), FILTER_VALIDATE_BOOLEAN))
0b9a7b 1492                     $sent = mail($to, $subject, $msg_body, $header_str);
TB 1493                 else
1494                     $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
1495             }
1496         }
1497
1498         if ($sent) {
1499             $this->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
1500
1501             // remove MDN headers after sending
1502             unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
1503
1504             // get all recipients
1505             if ($headers['Cc'])
1506                 $mailto .= $headers['Cc'];
1507             if ($headers['Bcc'])
1508                 $mailto .= $headers['Bcc'];
1509             if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
1510                 $mailto = implode(', ', array_unique($m[1]));
1511
1512             if ($this->config->get('smtp_log')) {
1513                 self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
1514                     $this->user->get_username(),
1515                     $_SERVER['REMOTE_ADDR'],
1516                     $mailto,
1517                     !empty($response) ? join('; ', $response) : ''));
1518             }
1519         }
1520
1521         if (is_resource($msg_body)) {
1522             fclose($msg_body);
1523         }
1524
1525         $message->_headers = array();
1526         $message->headers($headers);
1527
1528         return $sent;
1529     }
1530
0c2596 1531 }
A 1532
1533
1534 /**
1535  * Lightweight plugin API class serving as a dummy if plugins are not enabled
1536  *
a07224 1537  * @package Framework
TB 1538  * @subpackage Core
0c2596 1539  */
A 1540 class rcube_dummy_plugin_api
1541 {
1542     /**
1543      * Triggers a plugin hook.
1544      * @see rcube_plugin_api::exec_hook()
1545      */
1546     public function exec_hook($hook, $args = array())
1547     {
1548         return $args;
1549     }
1550 }