1<?php 2 3/** 4 +-----------------------------------------------------------------------+ 5 | This file is part of the Roundcube Webmail client | 6 | | 7 | Copyright (C) The Roundcube Dev Team | 8 | Copyright (C) Kolab Systems AG | 9 | | 10 | Licensed under the GNU General Public License version 3 or | 11 | any later version with exceptions for skins & plugins. | 12 | See the README file for a full license statement. | 13 | | 14 | PURPOSE: | 15 | Framework base class providing core functions and holding | 16 | instances of all 'global' objects like db- and storage-connections | 17 +-----------------------------------------------------------------------+ 18 | Author: Thomas Bruederli <roundcube@gmail.com> | 19 +-----------------------------------------------------------------------+ 20*/ 21 22/** 23 * Base class of the Roundcube Framework 24 * implemented as singleton 25 * 26 * @package Framework 27 * @subpackage Core 28 */ 29class rcube 30{ 31 // Init options 32 const INIT_WITH_DB = 1; 33 const INIT_WITH_PLUGINS = 2; 34 35 // Request status 36 const REQUEST_VALID = 0; 37 const REQUEST_ERROR_URL = 1; 38 const REQUEST_ERROR_TOKEN = 2; 39 40 const DEBUG_LINE_LENGTH = 4096; 41 42 /** @var rcube_config Stores instance of rcube_config */ 43 public $config; 44 45 /** @var rcube_db Instance of database class */ 46 public $db; 47 48 /** @var Memcache Instance of Memcache class */ 49 public $memcache; 50 51 /** @var Memcached Instance of Memcached class */ 52 public $memcached; 53 54 /** @var Redis Instance of Redis class */ 55 public $redis; 56 57 /** @var rcube_session Instance of rcube_session class */ 58 public $session; 59 60 /** @var rcube_smtp Instance of rcube_smtp class */ 61 public $smtp; 62 63 /** @var rcube_storage Instance of rcube_storage class */ 64 public $storage; 65 66 /** @var rcube_output Instance of rcube_output class */ 67 public $output; 68 69 /** @var rcube_plugin_api Instance of rcube_plugin_api */ 70 public $plugins; 71 72 /** @var rcube_user Instance of rcube_user class */ 73 public $user; 74 75 /** @var int Request status */ 76 public $request_status = 0; 77 78 /** @var array Localization */ 79 protected $texts; 80 81 /** @var rcube_cache[] Initialized cache objects */ 82 protected $caches = []; 83 84 /** @var array Registered shutdown functions */ 85 protected $shutdown_functions = []; 86 87 /** @var rcube Singleton instance of rcube */ 88 static protected $instance; 89 90 91 /** 92 * This implements the 'singleton' design pattern 93 * 94 * @param int $mode Options to initialize with this instance. See rcube::INIT_WITH_* constants 95 * @param string $env Environment name to run (e.g. live, dev, test) 96 * 97 * @return rcube The one and only instance 98 */ 99 static function get_instance($mode = 0, $env = '') 100 { 101 if (!self::$instance) { 102 self::$instance = new rcube($env); 103 self::$instance->init($mode); 104 } 105 106 return self::$instance; 107 } 108 109 /** 110 * Private constructor 111 * 112 * @param string $env Environment name to run (e.g. live, dev, test) 113 */ 114 protected function __construct($env = '') 115 { 116 // load configuration 117 $this->config = new rcube_config($env); 118 $this->plugins = new rcube_dummy_plugin_api; 119 120 register_shutdown_function([$this, 'shutdown']); 121 } 122 123 /** 124 * Initial startup function 125 * 126 * @param int $mode Options to initialize with this instance. See rcube::INIT_WITH_* constants 127 */ 128 protected function init($mode = 0) 129 { 130 // initialize syslog 131 if ($this->config->get('log_driver') == 'syslog') { 132 $syslog_id = $this->config->get('syslog_id', 'roundcube'); 133 $syslog_facility = $this->config->get('syslog_facility', LOG_USER); 134 openlog($syslog_id, LOG_ODELAY, $syslog_facility); 135 } 136 137 // connect to database 138 if ($mode & self::INIT_WITH_DB) { 139 $this->get_dbh(); 140 } 141 142 // create plugin API and load plugins 143 if ($mode & self::INIT_WITH_PLUGINS) { 144 $this->plugins = rcube_plugin_api::get_instance(); 145 } 146 } 147 148 /** 149 * Get the current database connection 150 * 151 * @return rcube_db Database object 152 */ 153 public function get_dbh() 154 { 155 if (!$this->db) { 156 $this->db = rcube_db::factory( 157 $this->config->get('db_dsnw'), 158 $this->config->get('db_dsnr'), 159 $this->config->get('db_persistent') 160 ); 161 162 $this->db->set_debug((bool)$this->config->get('sql_debug')); 163 } 164 165 return $this->db; 166 } 167 168 /** 169 * Get global handle for memcache access 170 * 171 * @return Memcache The memcache engine 172 */ 173 public function get_memcache() 174 { 175 if (!isset($this->memcache)) { 176 $this->memcache = rcube_cache_memcache::engine(); 177 } 178 179 return $this->memcache; 180 } 181 182 /** 183 * Get global handle for memcached access 184 * 185 * @return Memcached The memcached engine 186 */ 187 public function get_memcached() 188 { 189 if (!isset($this->memcached)) { 190 $this->memcached = rcube_cache_memcached::engine(); 191 } 192 193 return $this->memcached; 194 } 195 196 /** 197 * Get global handle for redis access 198 * 199 * @return Redis The redis engine 200 */ 201 public function get_redis() 202 { 203 if (!isset($this->redis)) { 204 $this->redis = rcube_cache_redis::engine(); 205 } 206 207 return $this->redis; 208 } 209 210 /** 211 * Initialize and get user cache object 212 * 213 * @param string $name Cache identifier 214 * @param string $type Cache type ('db', 'apc', 'memcache', 'redis') 215 * @param string $ttl Expiration time for cache items 216 * @param bool $packed Enables/disables data serialization 217 * @param bool $indexed Use indexed cache 218 * 219 * @return rcube_cache|null User cache object 220 */ 221 public function get_cache($name, $type = 'db', $ttl = 0, $packed = true, $indexed = false) 222 { 223 if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) { 224 $this->caches[$name] = rcube_cache::factory($type, $userid, $name, $ttl, $packed, $indexed); 225 } 226 227 return $this->caches[$name]; 228 } 229 230 /** 231 * Initialize and get shared cache object 232 * 233 * @param string $name Cache identifier 234 * @param bool $packed Enables/disables data serialization 235 * 236 * @return rcube_cache Shared cache object 237 */ 238 public function get_cache_shared($name, $packed = true) 239 { 240 $shared_name = "shared_$name"; 241 242 if (!array_key_exists($shared_name, $this->caches)) { 243 $opt = strtolower($name) . '_cache'; 244 $type = $this->config->get($opt); 245 $ttl = $this->config->get($opt . '_ttl'); 246 247 if (!$type) { 248 // cache is disabled 249 return $this->caches[$shared_name] = null; 250 } 251 252 if ($ttl === null) { 253 $ttl = $this->config->get('shared_cache_ttl', '10d'); 254 } 255 256 $this->caches[$shared_name] = rcube_cache::factory($type, null, $name, $ttl, $packed); 257 } 258 259 return $this->caches[$shared_name]; 260 } 261 262 /** 263 * Initialize HTTP client 264 * 265 * @param array $options Configuration options 266 * 267 * @return \GuzzleHttp\Client HTTP client 268 */ 269 public function get_http_client($options = []) 270 { 271 return new \GuzzleHttp\Client($options + $this->config->get('http_client')); 272 } 273 274 /** 275 * Create SMTP object and connect to server 276 * 277 * @param boolean $connect True if connection should be established 278 */ 279 public function smtp_init($connect = false) 280 { 281 $this->smtp = new rcube_smtp(); 282 283 if ($connect) { 284 $this->smtp->connect(); 285 } 286 } 287 288 /** 289 * Initialize and get storage object 290 * 291 * @return rcube_storage Storage object 292 */ 293 public function get_storage() 294 { 295 // already initialized 296 if (!is_object($this->storage)) { 297 $this->storage_init(); 298 } 299 300 return $this->storage; 301 } 302 303 /** 304 * Initialize storage object 305 */ 306 public function storage_init() 307 { 308 // already initialized 309 if (is_object($this->storage)) { 310 return; 311 } 312 313 $driver = $this->config->get('storage_driver', 'imap'); 314 $driver_class = "rcube_{$driver}"; 315 316 if (!class_exists($driver_class)) { 317 self::raise_error([ 318 'code' => 700, 'file' => __FILE__, 'line' => __LINE__, 319 'message' => "Storage driver class ($driver) not found!" 320 ], 321 true, true 322 ); 323 } 324 325 // Initialize storage object 326 $this->storage = new $driver_class; 327 328 // for backward compat. (deprecated, will be removed) 329 $this->imap = $this->storage; 330 331 // set class options 332 $options = [ 333 'auth_type' => $this->config->get("{$driver}_auth_type", 'check'), 334 'auth_cid' => $this->config->get("{$driver}_auth_cid"), 335 'auth_pw' => $this->config->get("{$driver}_auth_pw"), 336 'debug' => (bool) $this->config->get("{$driver}_debug"), 337 'force_caps' => (bool) $this->config->get("{$driver}_force_caps"), 338 'disabled_caps' => $this->config->get("{$driver}_disabled_caps"), 339 'socket_options' => $this->config->get("{$driver}_conn_options"), 340 'timeout' => (int) $this->config->get("{$driver}_timeout"), 341 'skip_deleted' => (bool) $this->config->get('skip_deleted'), 342 'driver' => $driver, 343 ]; 344 345 if (!empty($_SESSION['storage_host'])) { 346 $options['language'] = $_SESSION['language']; 347 $options['host'] = $_SESSION['storage_host']; 348 $options['user'] = $_SESSION['username']; 349 $options['port'] = $_SESSION['storage_port']; 350 $options['ssl'] = $_SESSION['storage_ssl']; 351 $options['password'] = $this->decrypt($_SESSION['password']); 352 $_SESSION[$driver.'_host'] = $_SESSION['storage_host']; 353 } 354 355 $options = $this->plugins->exec_hook("storage_init", $options); 356 357 // for backward compat. (deprecated, to be removed) 358 $options = $this->plugins->exec_hook("imap_init", $options); 359 360 $this->storage->set_options($options); 361 $this->set_storage_prop(); 362 363 // subscribe to 'storage_connected' hook for session logging 364 if ($this->config->get('imap_log_session', false)) { 365 $this->plugins->register_hook('storage_connected', [$this, 'storage_log_session']); 366 } 367 } 368 369 /** 370 * Set storage parameters. 371 */ 372 protected function set_storage_prop() 373 { 374 $storage = $this->get_storage(); 375 376 // set pagesize from config 377 $pagesize = $this->config->get('mail_pagesize'); 378 if (!$pagesize) { 379 $pagesize = $this->config->get('pagesize', 50); 380 } 381 382 $storage->set_pagesize($pagesize); 383 $storage->set_charset($this->config->get('default_charset', RCUBE_CHARSET)); 384 385 // enable caching of mail data 386 $driver = $this->config->get('storage_driver', 'imap'); 387 $storage_cache = $this->config->get("{$driver}_cache"); 388 $messages_cache = $this->config->get('messages_cache'); 389 390 // for backward compatibility 391 if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) { 392 $storage_cache = 'db'; 393 $messages_cache = true; 394 } 395 396 if ($storage_cache) { 397 $storage->set_caching($storage_cache); 398 } 399 400 if ($messages_cache) { 401 $storage->set_messages_caching(true); 402 } 403 } 404 405 /** 406 * Set special folders type association. 407 * This must be done AFTER connecting to the server! 408 */ 409 protected function set_special_folders() 410 { 411 $storage = $this->get_storage(); 412 $folders = $storage->get_special_folders(true); 413 $prefs = []; 414 415 // check SPECIAL-USE flags on IMAP folders 416 foreach ($folders as $type => $folder) { 417 $idx = $type . '_mbox'; 418 if ($folder !== $this->config->get($idx)) { 419 $prefs[$idx] = $folder; 420 } 421 } 422 423 // Some special folders differ, update user preferences 424 if (!empty($prefs) && $this->user) { 425 $this->user->save_prefs($prefs); 426 } 427 428 // create default folders (on login) 429 if ($this->config->get('create_default_folders')) { 430 $storage->create_default_folders(); 431 } 432 } 433 434 /** 435 * Callback for IMAP connection events to log session identifiers 436 * 437 * @param array $args Callback arguments 438 */ 439 public function storage_log_session($args) 440 { 441 if (!empty($args['session']) && session_id()) { 442 $this->write_log('imap_session', $args['session']); 443 } 444 } 445 446 /** 447 * Create session object and start the session. 448 */ 449 public function session_init() 450 { 451 // Ignore in CLI mode or when session started (Installer?) 452 if (empty($_SERVER['REMOTE_ADDR']) || session_id()) { 453 return; 454 } 455 456 $storage = $this->config->get('session_storage', 'db'); 457 $sess_name = $this->config->get('session_name'); 458 $sess_domain = $this->config->get('session_domain'); 459 $sess_path = $this->config->get('session_path'); 460 $sess_samesite = $this->config->get('session_samesite'); 461 $lifetime = $this->config->get('session_lifetime', 0) * 60; 462 $is_secure = $this->config->get('use_https') || rcube_utils::https_check(); 463 464 // set session domain 465 if ($sess_domain) { 466 ini_set('session.cookie_domain', $sess_domain); 467 } 468 // set session path 469 if ($sess_path) { 470 ini_set('session.cookie_path', $sess_path); 471 } 472 // set session samesite attribute 473 // requires PHP >= 7.3.0, see https://wiki.php.net/rfc/same-site-cookie for more info 474 if (version_compare(PHP_VERSION, '7.3.0', '>=') && $sess_samesite) { 475 ini_set('session.cookie_samesite', $sess_samesite); 476 } 477 // set session garbage collecting time according to session_lifetime 478 if ($lifetime) { 479 ini_set('session.gc_maxlifetime', $lifetime * 2); 480 } 481 482 // set session cookie lifetime so it never expires (#5961) 483 ini_set('session.cookie_lifetime', 0); 484 ini_set('session.cookie_secure', $is_secure); 485 ini_set('session.name', $sess_name ?: 'roundcube_sessid'); 486 ini_set('session.use_cookies', 1); 487 ini_set('session.use_only_cookies', 1); 488 ini_set('session.cookie_httponly', 1); 489 490 // Make sure session garbage collector is enabled when using custom handlers (#6560) 491 // Note: Use session.gc_divisor to control accuracy 492 if ($storage != 'php' && !ini_get('session.gc_probability')) { 493 ini_set('session.gc_probability', 1); 494 } 495 496 // Start the session 497 $this->session = rcube_session::factory($this->config); 498 $this->session->register_gc_handler([$this, 'gc']); 499 $this->session->start(); 500 } 501 502 /** 503 * Garbage collector - cache/temp cleaner 504 */ 505 public function gc() 506 { 507 rcube_cache::gc(); 508 $this->get_storage()->cache_gc(); 509 $this->gc_temp(); 510 } 511 512 /** 513 * Garbage collector function for temp files. 514 * Removes temporary files older than temp_dir_ttl. 515 */ 516 public function gc_temp() 517 { 518 $tmp = unslashify($this->config->get('temp_dir')); 519 520 // expire in 48 hours by default 521 $temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h'); 522 $temp_dir_ttl = get_offset_sec($temp_dir_ttl); 523 if ($temp_dir_ttl < 6*3600) { 524 $temp_dir_ttl = 6*3600; // 6 hours sensible lower bound. 525 } 526 527 $expire = time() - $temp_dir_ttl; 528 529 if ($tmp && ($dir = opendir($tmp))) { 530 while (($fname = readdir($dir)) !== false) { 531 if (strpos($fname, RCUBE_TEMP_FILE_PREFIX) !== 0) { 532 continue; 533 } 534 535 if (@filemtime("$tmp/$fname") < $expire) { 536 @unlink("$tmp/$fname"); 537 } 538 } 539 540 closedir($dir); 541 } 542 } 543 544 /** 545 * Runs garbage collector with probability based on 546 * session settings. This is intended for environments 547 * without a session. 548 */ 549 public function gc_run() 550 { 551 $probability = (int) ini_get('session.gc_probability'); 552 $divisor = (int) ini_get('session.gc_divisor'); 553 554 if ($divisor > 0 && $probability > 0) { 555 $random = mt_rand(1, $divisor); 556 if ($random <= $probability) { 557 $this->gc(); 558 } 559 } 560 } 561 562 /** 563 * Get localized text in the desired language 564 * 565 * @param mixed $attrib Named parameters array or label name 566 * @param string $domain Label domain (plugin) name 567 * 568 * @return string Localized text 569 */ 570 public function gettext($attrib, $domain = null) 571 { 572 // load localization files if not done yet 573 if (empty($this->texts)) { 574 $this->load_language(); 575 } 576 577 // extract attributes 578 if (is_string($attrib)) { 579 $attrib = ['name' => $attrib]; 580 } 581 582 $name = (string) $attrib['name']; 583 584 // attrib contain text values: use them from now 585 $slang = !empty($_SESSION['language']) ? strtolower($_SESSION['language']) : 'en_us'; 586 if (isset($attrib[$slang])) { 587 $this->texts[$name] = $attrib[$slang]; 588 } 589 else if ($slang != 'en_us' && isset($attrib['en_us'])) { 590 $this->texts[$name] = $attrib['en_us']; 591 } 592 593 // check for text with domain 594 if ($domain && isset($this->texts["$domain.$name"])) { 595 $text = $this->texts["$domain.$name"]; 596 } 597 else if (isset($this->texts[$name])) { 598 $text = $this->texts[$name]; 599 } 600 601 // text does not exist 602 if (!isset($text)) { 603 return "[$name]"; 604 } 605 606 // replace vars in text 607 if (!empty($attrib['vars']) && is_array($attrib['vars'])) { 608 foreach ($attrib['vars'] as $var_key => $var_value) { 609 $text = str_replace($var_key[0] != '$' ? '$'.$var_key : $var_key, $var_value, $text); 610 } 611 } 612 613 // replace \n with real line break 614 $text = strtr($text, ['\n' => "\n"]); 615 616 // case folding 617 if ((!empty($attrib['uppercase']) && strtolower($attrib['uppercase']) == 'first') || !empty($attrib['ucfirst'])) { 618 $case_mode = MB_CASE_TITLE; 619 } 620 else if (!empty($attrib['uppercase'])) { 621 $case_mode = MB_CASE_UPPER; 622 } 623 else if (!empty($attrib['lowercase'])) { 624 $case_mode = MB_CASE_LOWER; 625 } 626 627 if (isset($case_mode)) { 628 $text = mb_convert_case($text, $case_mode); 629 } 630 631 return $text; 632 } 633 634 /** 635 * Check if the given text label exists 636 * 637 * @param string $name Label name 638 * @param string $domain Label domain (plugin) name or '*' for all domains 639 * @param string &$ref_domain Sets domain name if label is found 640 * 641 * @return bool True if text exists (either in the current language or in en_US) 642 */ 643 public function text_exists($name, $domain = null, &$ref_domain = null) 644 { 645 // load localization files if not done yet 646 if (empty($this->texts)) { 647 $this->load_language(); 648 } 649 650 if (isset($this->texts[$name])) { 651 $ref_domain = ''; 652 return true; 653 } 654 655 // any of loaded domains (plugins) 656 if ($domain == '*') { 657 foreach ($this->plugins->loaded_plugins() as $domain) { 658 if (isset($this->texts[$domain.'.'.$name])) { 659 $ref_domain = $domain; 660 return true; 661 } 662 } 663 } 664 // specified domain 665 else if ($domain && isset($this->texts[$domain.'.'.$name])) { 666 $ref_domain = $domain; 667 return true; 668 } 669 670 return false; 671 } 672 673 /** 674 * Load a localization package 675 * 676 * @param string $lang Language ID 677 * @param array $add Additional text labels/messages 678 * @param array $merge Additional text labels/messages to merge 679 */ 680 public function load_language($lang = null, $add = [], $merge = []) 681 { 682 $sess_lang = !empty($_SESSION['language']) ? $_SESSION['language'] : 'en_US'; 683 $lang = $this->language_prop($lang ?: $sess_lang); 684 685 // load localized texts 686 if (empty($this->texts) || $lang != $sess_lang) { 687 // get english labels (these should be complete) 688 $files = [ 689 RCUBE_LOCALIZATION_DIR . 'en_US/labels.inc', 690 RCUBE_LOCALIZATION_DIR . 'en_US/messages.inc', 691 ]; 692 693 // include user language files 694 if ($lang != 'en' && $lang != 'en_US' && is_dir(RCUBE_LOCALIZATION_DIR . $lang)) { 695 $files[] = RCUBE_LOCALIZATION_DIR . $lang . '/labels.inc'; 696 $files[] = RCUBE_LOCALIZATION_DIR . $lang . '/messages.inc'; 697 } 698 699 $this->texts = []; 700 701 foreach ($files as $file) { 702 $this->texts = self::read_localization_file($file, $this->texts); 703 } 704 705 $_SESSION['language'] = $lang; 706 } 707 708 // append additional texts (from plugin) 709 if (is_array($add) && !empty($add)) { 710 $this->texts += $add; 711 } 712 713 // merge additional texts (from plugin) 714 if (is_array($merge) && !empty($merge)) { 715 $this->texts = array_merge($this->texts, $merge); 716 } 717 } 718 719 /** 720 * Read localized texts from an additional location (plugins, skins). 721 * Then you can use the result as 2nd arg to load_language(). 722 * 723 * @param string $dir Directory to search in 724 * @param string|null $lang Language code to read 725 * 726 * @return array Localization labels/messages 727 */ 728 public function read_localization($dir, $lang = null) 729 { 730 if ($lang == null) { 731 $lang = $_SESSION['language']; 732 } 733 $langs = array_unique(['en_US', $lang]); 734 $locdir = slashify($dir); 735 $texts = []; 736 737 // Language aliases used to find localization in similar lang, see below 738 $aliases = [ 739 'de_CH' => 'de_DE', 740 'es_AR' => 'es_ES', 741 'fa_AF' => 'fa_IR', 742 'nl_BE' => 'nl_NL', 743 'pt_BR' => 'pt_PT', 744 'zh_CN' => 'zh_TW', 745 ]; 746 747 foreach ($langs as $lng) { 748 $fpath = $locdir . $lng . '.inc'; 749 $_texts = self::read_localization_file($fpath); 750 751 if (!empty($_texts)) { 752 $texts = array_merge($texts, $_texts); 753 } 754 // Fallback to a localization in similar language (#1488401) 755 else if ($lng != 'en_US') { 756 $alias = null; 757 if (!empty($aliases[$lng])) { 758 $alias = $aliases[$lng]; 759 } 760 else if ($key = array_search($lng, $aliases)) { 761 $alias = $key; 762 } 763 764 if (!empty($alias)) { 765 $fpath = $locdir . $alias . '.inc'; 766 $texts = self::read_localization_file($fpath, $texts); 767 } 768 } 769 } 770 771 return $texts; 772 } 773 774 775 /** 776 * Load localization file 777 * 778 * @param string $file File location 779 * @param array $texts Additional texts to merge with 780 * 781 * @return array Localization labels/messages 782 */ 783 public static function read_localization_file($file, $texts = []) 784 { 785 if (is_file($file) && is_readable($file)) { 786 $labels = []; 787 $messages = []; 788 789 // use buffering to handle empty lines/spaces after closing PHP tag 790 ob_start(); 791 include $file; 792 ob_end_clean(); 793 794 if (!empty($labels)) { 795 $texts = array_merge($texts, $labels); 796 } 797 798 if (!empty($messages)) { 799 $texts = array_merge($texts, $messages); 800 } 801 } 802 803 return $texts; 804 } 805 806 /** 807 * Check the given string and return a valid language code 808 * 809 * @param string $lang Language code 810 * 811 * @return string Valid language code 812 */ 813 protected function language_prop($lang) 814 { 815 static $rcube_languages, $rcube_language_aliases; 816 817 // user HTTP_ACCEPT_LANGUAGE if no language is specified 818 if ((empty($lang) || $lang == 'auto') && !empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { 819 $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); 820 $lang = $accept_langs[0]; 821 822 if (preg_match('/^([a-z]+)[_-]([a-z]+)$/i', $lang, $m)) { 823 $lang = $m[1] . '_' . strtoupper($m[2]); 824 } 825 } 826 827 if (empty($rcube_languages)) { 828 @include(RCUBE_LOCALIZATION_DIR . 'index.inc'); 829 } 830 831 // check if we have an alias for that language 832 if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) { 833 $lang = $rcube_language_aliases[$lang]; 834 } 835 // try the first two chars 836 else if (!isset($rcube_languages[$lang])) { 837 $short = substr($lang, 0, 2); 838 839 // check if we have an alias for the short language code 840 if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) { 841 $lang = $rcube_language_aliases[$short]; 842 } 843 // expand 'nn' to 'nn_NN' 844 else if (!isset($rcube_languages[$short])) { 845 $lang = $short.'_'.strtoupper($short); 846 } 847 } 848 849 if (!isset($rcube_languages[$lang]) || !is_dir(RCUBE_LOCALIZATION_DIR . $lang)) { 850 $lang = 'en_US'; 851 } 852 853 return $lang; 854 } 855 856 /** 857 * Read directory program/localization and return a list of available languages 858 * 859 * @return array List of available localizations 860 */ 861 public function list_languages() 862 { 863 static $sa_languages = []; 864 865 if (!count($sa_languages)) { 866 @include(RCUBE_LOCALIZATION_DIR . 'index.inc'); 867 868 if ($dh = @opendir(RCUBE_LOCALIZATION_DIR)) { 869 while (($name = readdir($dh)) !== false) { 870 if ($name[0] == '.' || !is_dir(RCUBE_LOCALIZATION_DIR . $name)) { 871 continue; 872 } 873 874 if (isset($rcube_languages[$name])) { 875 $sa_languages[$name] = $rcube_languages[$name]; 876 } 877 } 878 879 closedir($dh); 880 } 881 } 882 883 return $sa_languages; 884 } 885 886 /** 887 * Encrypt a string 888 * 889 * @param string $clear Clear text input 890 * @param string $key Encryption key to retrieve from the configuration, defaults to 'des_key' 891 * @param bool $base64 Whether or not to base64_encode() the result before returning 892 * 893 * @return string|false Encrypted text, false on error 894 */ 895 public function encrypt($clear, $key = 'des_key', $base64 = true) 896 { 897 if (!is_string($clear) || !strlen($clear)) { 898 return ''; 899 } 900 901 $ckey = $this->config->get_crypto_key($key); 902 $method = $this->config->get_crypto_method(); 903 $opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true; 904 $iv = rcube_utils::random_bytes(openssl_cipher_iv_length($method), true); 905 $cipher = openssl_encrypt($clear, $method, $ckey, $opts, $iv); 906 907 if ($cipher === false) { 908 self::raise_error([ 909 'file' => __FILE__, 910 'line' => __LINE__, 911 'message' => "Failed to encrypt data with configured cipher method: $method!" 912 ], true, false); 913 914 return false; 915 } 916 917 $cipher = $iv . $cipher; 918 919 return $base64 ? base64_encode($cipher) : $cipher; 920 } 921 922 /** 923 * Decrypt a string 924 * 925 * @param string $cipher Encrypted text 926 * @param string $key Encryption key to retrieve from the configuration, defaults to 'des_key' 927 * @param bool $base64 Whether or not input is base64-encoded 928 * 929 * @return string|false Decrypted text, false on error 930 */ 931 public function decrypt($cipher, $key = 'des_key', $base64 = true) 932 { 933 if (strlen($cipher) == 0) { 934 return false; 935 } 936 937 if ($base64) { 938 $cipher = base64_decode($cipher); 939 if ($cipher === false) { 940 return false; 941 } 942 } 943 944 $ckey = $this->config->get_crypto_key($key); 945 $method = $this->config->get_crypto_method(); 946 $opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true; 947 $iv_size = openssl_cipher_iv_length($method); 948 $iv = substr($cipher, 0, $iv_size); 949 950 // session corruption? (#1485970) 951 if (strlen($iv) < $iv_size) { 952 return false; 953 } 954 955 $cipher = substr($cipher, $iv_size); 956 $clear = openssl_decrypt($cipher, $method, $ckey, $opts, $iv); 957 958 return $clear; 959 } 960 961 /** 962 * Returns session token for secure URLs 963 * 964 * @param bool $generate Generate token if not exists in session yet 965 * 966 * @return string|bool Token string, False when disabled 967 */ 968 public function get_secure_url_token($generate = false) 969 { 970 if ($len = $this->config->get('use_secure_urls')) { 971 if (empty($_SESSION['secure_token']) && $generate) { 972 // generate x characters long token 973 $length = $len > 1 ? $len : 16; 974 $token = rcube_utils::random_bytes($length); 975 976 $plugin = $this->plugins->exec_hook('secure_token', ['value' => $token, 'length' => $length]); 977 978 $_SESSION['secure_token'] = $plugin['value']; 979 } 980 981 return $_SESSION['secure_token']; 982 } 983 984 return false; 985 } 986 987 /** 988 * Generate a unique token to be used in a form request 989 * 990 * @return string The request token 991 */ 992 public function get_request_token() 993 { 994 if (empty($_SESSION['request_token'])) { 995 $plugin = $this->plugins->exec_hook('request_token', ['value' => rcube_utils::random_bytes(32)]); 996 997 $_SESSION['request_token'] = $plugin['value']; 998 } 999 1000 return $_SESSION['request_token']; 1001 } 1002 1003 /** 1004 * Check if the current request contains a valid token. 1005 * Empty requests aren't checked until use_secure_urls is set. 1006 * 1007 * @param int $mode Request method 1008 * 1009 * @return bool True if request token is valid false if not 1010 */ 1011 public function check_request($mode = rcube_utils::INPUT_POST) 1012 { 1013 // check secure token in URL if enabled 1014 if ($token = $this->get_secure_url_token()) { 1015 foreach (explode('/', preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI'])) as $tok) { 1016 if ($tok == $token) { 1017 return true; 1018 } 1019 } 1020 1021 $this->request_status = self::REQUEST_ERROR_URL; 1022 1023 return false; 1024 } 1025 1026 $sess_tok = $this->get_request_token(); 1027 1028 // ajax requests 1029 if (rcube_utils::request_header('X-Roundcube-Request') === $sess_tok) { 1030 return true; 1031 } 1032 1033 // skip empty requests 1034 if (($mode == rcube_utils::INPUT_POST && empty($_POST)) 1035 || ($mode == rcube_utils::INPUT_GET && empty($_GET)) 1036 ) { 1037 return true; 1038 } 1039 1040 // default method of securing requests 1041 $token = rcube_utils::get_input_value('_token', $mode); 1042 1043 if (empty($_COOKIE[ini_get('session.name')]) || $token !== $sess_tok) { 1044 $this->request_status = self::REQUEST_ERROR_TOKEN; 1045 return false; 1046 } 1047 1048 return true; 1049 } 1050 1051 /** 1052 * Build a valid URL to this instance of Roundcube 1053 * 1054 * @param mixed $p Either a string with the action or url parameters as key-value pairs 1055 * 1056 * @return string Valid application URL 1057 */ 1058 public function url($p) 1059 { 1060 // STUB: should be overloaded by the application 1061 return ''; 1062 } 1063 1064 /** 1065 * Function to be executed in script shutdown 1066 * Registered with register_shutdown_function() 1067 */ 1068 public function shutdown() 1069 { 1070 foreach ($this->shutdown_functions as $function) { 1071 call_user_func($function); 1072 } 1073 1074 // write session data as soon as possible and before 1075 // closing database connection, don't do this before 1076 // registered shutdown functions, they may need the session 1077 // Note: this will run registered gc handlers (ie. cache gc) 1078 if (!empty($_SERVER['REMOTE_ADDR']) && is_object($this->session)) { 1079 $this->session->write_close(); 1080 } 1081 1082 if (is_object($this->smtp)) { 1083 $this->smtp->disconnect(); 1084 } 1085 1086 foreach ($this->caches as $cache) { 1087 if (is_object($cache)) { 1088 $cache->close(); 1089 } 1090 } 1091 1092 if (is_object($this->storage)) { 1093 $this->storage->close(); 1094 } 1095 1096 if ($this->config->get('log_driver') == 'syslog') { 1097 closelog(); 1098 } 1099 } 1100 1101 /** 1102 * Registers shutdown function to be executed on shutdown. 1103 * The functions will be executed before destroying any 1104 * objects like smtp, imap, session, etc. 1105 * 1106 * @param callback $function Function callback 1107 */ 1108 public function add_shutdown_function($function) 1109 { 1110 $this->shutdown_functions[] = $function; 1111 } 1112 1113 /** 1114 * When you're going to sleep the script execution for a longer time 1115 * it is good to close all external connections (sql, memcache, redis, SMTP, IMAP). 1116 * 1117 * No action is required on wake up, all connections will be 1118 * re-established automatically. 1119 */ 1120 public function sleep() 1121 { 1122 foreach ($this->caches as $cache) { 1123 if (is_object($cache)) { 1124 $cache->close(); 1125 } 1126 } 1127 1128 if ($this->storage) { 1129 $this->storage->close(); 1130 } 1131 1132 if ($this->db) { 1133 $this->db->closeConnection(); 1134 } 1135 1136 if ($this->memcache) { 1137 $this->memcache->close(); 1138 } 1139 1140 if ($this->memcached) { 1141 $this->memcached->quit(); 1142 } 1143 1144 if ($this->smtp) { 1145 $this->smtp->disconnect(); 1146 } 1147 1148 if ($this->redis) { 1149 $this->redis->close(); 1150 } 1151 } 1152 1153 /** 1154 * Quote a given string. 1155 * Shortcut function for rcube_utils::rep_specialchars_output() 1156 * 1157 * @param string $str A string to quote 1158 * @param string $mode Replace mode for tags: show|remove|strict 1159 * @param bool $newlines Convert newlines 1160 * 1161 * @return string HTML-quoted string 1162 */ 1163 public static function Q($str, $mode = 'strict', $newlines = true) 1164 { 1165 return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines); 1166 } 1167 1168 /** 1169 * Quote a given string for javascript output. 1170 * Shortcut function for rcube_utils::rep_specialchars_output() 1171 * 1172 * @param string $str A string to quote 1173 * 1174 * @return string JS-quoted string 1175 */ 1176 public static function JQ($str) 1177 { 1178 return rcube_utils::rep_specialchars_output($str, 'js'); 1179 } 1180 1181 /** 1182 * Quote a given string, remove new-line characters, use strict mode. 1183 * Shortcut function for rcube_utils::rep_specialchars_output() 1184 * 1185 * @param string $str A string to quote 1186 * 1187 * @return string HTML-quoted string 1188 */ 1189 public static function SQ($str) 1190 { 1191 return rcube_utils::rep_specialchars_output($str, 'html', 'strict', false); 1192 } 1193 1194 /** 1195 * Construct shell command, execute it and return output as string. 1196 * Keywords {keyword} are replaced with arguments 1197 * 1198 * @param string $cmd Format string with {keywords} to be replaced 1199 * @param mixed $values,... (zero, one or more arrays can be passed) 1200 * 1201 * @return string Output of command. Shell errors not detectable 1202 */ 1203 public static function exec(/* $cmd, $values1 = [], ... */) 1204 { 1205 $args = func_get_args(); 1206 $cmd = array_shift($args); 1207 $values = $replacements = []; 1208 1209 // merge values into one array 1210 foreach ($args as $arg) { 1211 $values += (array) $arg; 1212 } 1213 1214 preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER); 1215 foreach ($matches as $tags) { 1216 list(, $tag, $option, $key) = $tags; 1217 $parts = []; 1218 1219 if ($option) { 1220 foreach ((array) $values["-$key"] as $key => $value) { 1221 if ($value === true || $value === false || $value === null) { 1222 $parts[] = $value ? $key : ""; 1223 } 1224 else { 1225 foreach ((array)$value as $val) { 1226 $parts[] = "$key " . escapeshellarg($val); 1227 } 1228 } 1229 } 1230 } 1231 else { 1232 foreach ((array) $values[$key] as $value) { 1233 $parts[] = escapeshellarg($value); 1234 } 1235 } 1236 1237 $replacements[$tag] = implode(' ', $parts); 1238 } 1239 1240 // use strtr behaviour of going through source string once 1241 $cmd = strtr($cmd, $replacements); 1242 1243 return (string) shell_exec($cmd); 1244 } 1245 1246 /** 1247 * Print or write debug messages 1248 * 1249 * @param mixed Debug message or data 1250 */ 1251 public static function console() 1252 { 1253 $args = func_get_args(); 1254 1255 if (class_exists('rcube', false)) { 1256 $rcube = self::get_instance(); 1257 $plugin = $rcube->plugins->exec_hook('console', ['args' => $args]); 1258 if ($plugin['abort']) { 1259 return; 1260 } 1261 1262 $args = $plugin['args']; 1263 } 1264 1265 $msg = []; 1266 foreach ($args as $arg) { 1267 $msg[] = !is_string($arg) ? var_export($arg, true) : $arg; 1268 } 1269 1270 self::write_log('console', implode(";\n", $msg)); 1271 } 1272 1273 /** 1274 * Append a line to a logfile in the logs directory. 1275 * Date will be added automatically to the line. 1276 * 1277 * @param string $name Name of the log file 1278 * @param mixed $line Line to append 1279 * 1280 * @return bool True on success, False on failure 1281 */ 1282 public static function write_log($name, $line) 1283 { 1284 if (!is_string($line)) { 1285 $line = var_export($line, true); 1286 } 1287 1288 $date_format = $log_driver = $session_key = null; 1289 if (self::$instance) { 1290 $date_format = self::$instance->config->get('log_date_format'); 1291 $log_driver = self::$instance->config->get('log_driver'); 1292 $session_key = intval(self::$instance->config->get('log_session_id', 8)); 1293 } 1294 1295 $date = rcube_utils::date_format($date_format); 1296 1297 // trigger logging hook 1298 if (is_object(self::$instance) && is_object(self::$instance->plugins)) { 1299 $log = self::$instance->plugins->exec_hook('write_log', 1300 ['name' => $name, 'date' => $date, 'line' => $line] 1301 ); 1302 1303 $name = $log['name']; 1304 $line = $log['line']; 1305 $date = $log['date']; 1306 1307 if (!empty($log['abort'])) { 1308 return true; 1309 } 1310 } 1311 1312 // add session ID to the log 1313 if ($session_key > 0 && ($sess = session_id())) { 1314 $line = '<' . substr($sess, 0, $session_key) . '> ' . $line; 1315 } 1316 1317 if ($log_driver == 'syslog') { 1318 $prio = $name == 'errors' ? LOG_ERR : LOG_INFO; 1319 return syslog($prio, $line); 1320 } 1321 1322 // write message with file name when configured to log to STDOUT 1323 if ($log_driver == 'stdout') { 1324 $stdout = "php://stdout"; 1325 $line = "$name: $line\n"; 1326 return file_put_contents($stdout, $line, FILE_APPEND) !== false; 1327 } 1328 1329 // log_driver == 'file' is assumed here 1330 1331 $line = sprintf("[%s]: %s\n", $date, $line); 1332 1333 // per-user logging is activated 1334 if (self::$instance && self::$instance->config->get('per_user_logging') 1335 && self::$instance->get_user_id() 1336 && !in_array($name, ['userlogins', 'sendmail']) 1337 ) { 1338 $log_dir = self::$instance->get_user_log_dir(); 1339 if (empty($log_dir) && $name !== 'errors') { 1340 return false; 1341 } 1342 } 1343 1344 if (empty($log_dir)) { 1345 if (!empty($log['dir'])) { 1346 $log_dir = $log['dir']; 1347 } 1348 else if (self::$instance) { 1349 $log_dir = self::$instance->config->get('log_dir'); 1350 } 1351 } 1352 1353 if (empty($log_dir)) { 1354 $log_dir = RCUBE_INSTALL_PATH . 'logs'; 1355 } 1356 1357 if (self::$instance) { 1358 $name .= self::$instance->config->get('log_file_ext', '.log'); 1359 } 1360 else { 1361 $name .= '.log'; 1362 } 1363 1364 return file_put_contents("$log_dir/$name", $line, FILE_APPEND) !== false; 1365 } 1366 1367 /** 1368 * Throw system error (and show error page). 1369 * 1370 * @param array $arg Named parameters 1371 * - code: Error code (required) 1372 * - type: Error type [php|db|imap|javascript] 1373 * - message: Error message 1374 * - file: File where error occurred 1375 * - line: Line where error occurred 1376 * @param bool $log True to log the error 1377 * @param bool $terminate Terminate script execution 1378 */ 1379 public static function raise_error($arg = [], $log = false, $terminate = false) 1380 { 1381 // handle PHP exceptions 1382 if ($arg instanceof Exception) { 1383 $arg = [ 1384 'code' => $arg->getCode(), 1385 'line' => $arg->getLine(), 1386 'file' => $arg->getFile(), 1387 'message' => $arg->getMessage(), 1388 ]; 1389 } 1390 else if ($arg instanceof PEAR_Error) { 1391 $info = $arg->getUserInfo(); 1392 $arg = [ 1393 'code' => $arg->getCode(), 1394 'message' => $arg->getMessage() . ($info ? ': ' . $info : ''), 1395 ]; 1396 } 1397 else if (is_string($arg)) { 1398 $arg = ['message' => $arg]; 1399 } 1400 1401 if (empty($arg['code'])) { 1402 $arg['code'] = 500; 1403 } 1404 1405 $cli = php_sapi_name() == 'cli'; 1406 1407 $arg['cli'] = $cli; 1408 $arg['log'] = $log; 1409 $arg['terminate'] = $terminate; 1410 1411 // send error to external error tracking tool 1412 if (self::$instance) { 1413 $arg = self::$instance->plugins->exec_hook('raise_error', $arg); 1414 } 1415 1416 // installer 1417 if (!$cli && class_exists('rcmail_install', false)) { 1418 $rci = rcmail_install::get_instance(); 1419 $rci->raise_error($arg); 1420 return; 1421 } 1422 1423 if (($log || $terminate) && !$cli && $arg['message']) { 1424 $arg['fatal'] = $terminate; 1425 self::log_bug($arg); 1426 } 1427 1428 if ($cli) { 1429 fwrite(STDERR, 'ERROR: ' . trim($arg['message']) . "\n"); 1430 } 1431 else if ($terminate && is_object(self::$instance->output)) { 1432 self::$instance->output->raise_error($arg['code'], $arg['message']); 1433 } 1434 else if ($terminate) { 1435 header("HTTP/1.0 500 Internal Error"); 1436 } 1437 1438 // terminate script 1439 if ($terminate) { 1440 if (defined('ROUNDCUBE_TEST_MODE') && ROUNDCUBE_TEST_MODE) { 1441 throw new Exception('Error raised'); 1442 } 1443 exit(1); 1444 } 1445 } 1446 1447 /** 1448 * Log an error 1449 * 1450 * @param array $arg_arr Named parameters 1451 * @see self::raise_error() 1452 */ 1453 public static function log_bug($arg_arr) 1454 { 1455 $program = !empty($arg_arr['type']) ? strtoupper($arg_arr['type']) : 'PHP'; 1456 $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; 1457 1458 // write error to local log file 1459 if ($_SERVER['REQUEST_METHOD'] == 'POST') { 1460 $post_query = []; 1461 foreach (['_task', '_action'] as $arg) { 1462 if (isset($_POST[$arg]) && !isset($_GET[$arg])) { 1463 $post_query[$arg] = $_POST[$arg]; 1464 } 1465 } 1466 1467 if (!empty($post_query)) { 1468 $uri .= (strpos($uri, '?') != false ? '&' : '?') 1469 . http_build_query($post_query, '', '&'); 1470 } 1471 } 1472 1473 $log_entry = sprintf("%s Error: %s%s (%s %s)", 1474 $program, 1475 $arg_arr['message'], 1476 !empty($arg_arr['file']) ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '', 1477 $_SERVER['REQUEST_METHOD'], 1478 $uri 1479 ); 1480 1481 if (!self::write_log('errors', $log_entry)) { 1482 // send error to PHPs error handler if write_log didn't succeed 1483 trigger_error($arg_arr['message'], E_USER_WARNING); 1484 } 1485 } 1486 1487 /** 1488 * Write debug info to the log 1489 * 1490 * @param string $engine Engine type - file name (memcache, apc, redis) 1491 * @param string $data Data string to log 1492 * @param bool $result Operation result 1493 */ 1494 public static function debug($engine, $data, $result = null) 1495 { 1496 static $debug_counter; 1497 1498 $line = '[' . (++$debug_counter[$engine]) . '] ' . $data; 1499 1500 if (($len = strlen($line)) > self::DEBUG_LINE_LENGTH) { 1501 $diff = $len - self::DEBUG_LINE_LENGTH; 1502 $line = substr($line, 0, self::DEBUG_LINE_LENGTH) . "... [truncated $diff bytes]"; 1503 } 1504 1505 if ($result !== null) { 1506 $line .= ' [' . ($result ? 'TRUE' : 'FALSE') . ']'; 1507 } 1508 1509 self::write_log($engine, $line); 1510 } 1511 1512 /** 1513 * Returns current time (with microseconds). 1514 * 1515 * @return float Current time in seconds since the Unix 1516 */ 1517 public static function timer() 1518 { 1519 return microtime(true); 1520 } 1521 1522 /** 1523 * Logs time difference according to provided timer 1524 * 1525 * @param float $timer Timer (self::timer() result) 1526 * @param string $label Log line prefix 1527 * @param string $dest Log file name 1528 * 1529 * @see self::timer() 1530 */ 1531 public static function print_timer($timer, $label = 'Timer', $dest = 'console') 1532 { 1533 static $print_count = 0; 1534 1535 $print_count++; 1536 $now = self::timer(); 1537 $diff = $now - $timer; 1538 1539 if (empty($label)) { 1540 $label = 'Timer '.$print_count; 1541 } 1542 1543 self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff)); 1544 } 1545 1546 /** 1547 * Setter for system user object 1548 * 1549 * @param rcube_user Current user instance 1550 */ 1551 public function set_user($user) 1552 { 1553 if (is_object($user)) { 1554 $this->user = $user; 1555 1556 // overwrite config with user preferences 1557 $this->config->set_user_prefs((array)$this->user->get_prefs()); 1558 } 1559 } 1560 1561 /** 1562 * Getter for logged user ID. 1563 * 1564 * @return mixed User identifier 1565 */ 1566 public function get_user_id() 1567 { 1568 if (is_object($this->user)) { 1569 return $this->user->ID; 1570 } 1571 else if (isset($_SESSION['user_id'])) { 1572 return $_SESSION['user_id']; 1573 } 1574 } 1575 1576 /** 1577 * Getter for logged user name. 1578 * 1579 * @return string User name 1580 */ 1581 public function get_user_name() 1582 { 1583 if (is_object($this->user)) { 1584 return $this->user->get_username(); 1585 } 1586 else if (isset($_SESSION['username'])) { 1587 return $_SESSION['username']; 1588 } 1589 } 1590 1591 /** 1592 * Getter for logged user email (derived from user name not identity). 1593 * 1594 * @return string User email address 1595 */ 1596 public function get_user_email() 1597 { 1598 if (!empty($this->user_email)) { 1599 return $this->user_email; 1600 } 1601 1602 if (is_object($this->user)) { 1603 return $this->user->get_username('mail'); 1604 } 1605 } 1606 1607 /** 1608 * Getter for logged user password. 1609 * 1610 * @return string User password 1611 */ 1612 public function get_user_password() 1613 { 1614 if (!empty($this->password)) { 1615 return $this->password; 1616 } 1617 1618 if (isset($_SESSION['password'])) { 1619 return $this->decrypt($_SESSION['password']); 1620 } 1621 } 1622 1623 /** 1624 * Get the per-user log directory 1625 * 1626 * @return string|false Per-user log directory if it exists and is writable, False otherwise 1627 */ 1628 protected function get_user_log_dir() 1629 { 1630 $log_dir = $this->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs'); 1631 $user_name = $this->get_user_name(); 1632 $user_log_dir = $log_dir . '/' . $user_name; 1633 1634 return !empty($user_name) && is_writable($user_log_dir) ? $user_log_dir : false; 1635 } 1636 1637 /** 1638 * Getter for logged user language code. 1639 * 1640 * @return string User language code 1641 */ 1642 public function get_user_language() 1643 { 1644 if (is_object($this->user)) { 1645 return $this->user->language; 1646 } 1647 else if (isset($_SESSION['language'])) { 1648 return $_SESSION['language']; 1649 } 1650 } 1651 1652 /** 1653 * Unique Message-ID generator. 1654 * 1655 * @param string $sender Optional sender e-mail address 1656 * 1657 * @return string Message-ID 1658 */ 1659 public function gen_message_id($sender = null) 1660 { 1661 $local_part = md5(uniqid('rcube'.mt_rand(), true)); 1662 $domain_part = ''; 1663 1664 if ($sender && preg_match('/@([^\s]+\.[a-z0-9-]+)/', $sender, $m)) { 1665 $domain_part = $m[1]; 1666 } 1667 else { 1668 $domain_part = $this->user->get_username('domain'); 1669 } 1670 1671 // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924) 1672 if (!preg_match('/\.[a-z0-9-]+$/i', $domain_part)) { 1673 foreach ([$_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']] as $host) { 1674 $host = preg_replace('/:[0-9]+$/', '', $host); 1675 if ($host && preg_match('/\.[a-z]+$/i', $host)) { 1676 $domain_part = $host; 1677 break; 1678 } 1679 } 1680 } 1681 1682 return sprintf('<%s@%s>', $local_part, $domain_part); 1683 } 1684 1685 /** 1686 * Send the given message using the configured method. 1687 * 1688 * @param Mail_Mime &$message Reference to Mail_MIME object 1689 * @param string $from Sender address string 1690 * @param array|string $mailto Either a comma-separated list of recipients (RFC822 compliant), 1691 * or an array of recipients, each RFC822 valid 1692 * @param array|string &$error SMTP error array or (deprecated) string 1693 * @param string &$body_file Location of file with saved message body, 1694 * used when delay_file_io is enabled 1695 * @param array $options SMTP options (e.g. DSN request) 1696 * @param bool $disconnect Close SMTP connection ASAP 1697 * 1698 * @return bool Send status. 1699 */ 1700 public function deliver_message(&$message, $from, $mailto, &$error, 1701 &$body_file = null, $options = null, $disconnect = false) 1702 { 1703 $plugin = $this->plugins->exec_hook('message_before_send', [ 1704 'message' => $message, 1705 'from' => $from, 1706 'mailto' => $mailto, 1707 'options' => $options, 1708 ]); 1709 1710 if ($plugin['abort']) { 1711 if (!empty($plugin['error'])) { 1712 $error = $plugin['error']; 1713 } 1714 if (!empty($plugin['body_file'])) { 1715 $body_file = $plugin['body_file']; 1716 } 1717 1718 return isset($plugin['result']) ? $plugin['result'] : false; 1719 } 1720 1721 $from = $plugin['from']; 1722 $mailto = $plugin['mailto']; 1723 $options = $plugin['options']; 1724 $message = $plugin['message']; 1725 $headers = $message->headers(); 1726 1727 // generate list of recipients 1728 $a_recipients = (array) $mailto; 1729 1730 if (!empty($headers['Cc'])) { 1731 $a_recipients[] = $headers['Cc']; 1732 } 1733 if (!empty($headers['Bcc'])) { 1734 $a_recipients[] = $headers['Bcc']; 1735 } 1736 1737 // remove Bcc header and get the whole head of the message as string 1738 $smtp_headers = $message->txtHeaders(['Bcc' => null], true); 1739 1740 if ($message->getParam('delay_file_io')) { 1741 // use common temp dir 1742 $body_file = rcube_utils::temp_filename('msg'); 1743 $mime_result = $message->saveMessageBody($body_file); 1744 1745 if (is_a($mime_result, 'PEAR_Error')) { 1746 self::raise_error([ 1747 'code' => 650, 'file' => __FILE__, 'line' => __LINE__, 1748 'message' => "Could not create message: ".$mime_result->getMessage() 1749 ], 1750 true, false 1751 ); 1752 return false; 1753 } 1754 1755 $msg_body = fopen($body_file, 'r'); 1756 } 1757 else { 1758 $msg_body = $message->get(); 1759 } 1760 1761 // initialize SMTP connection 1762 if (!is_object($this->smtp)) { 1763 $this->smtp_init(true); 1764 } 1765 1766 // send message 1767 $sent = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $options); 1768 $response = $this->smtp->get_response(); 1769 $error = $this->smtp->get_error(); 1770 1771 if (!$sent) { 1772 self::raise_error([ 1773 'code' => 800, 'type' => 'smtp', 1774 'line' => __LINE__, 'file' => __FILE__, 1775 'message' => implode("\n", $response) 1776 ], true, false); 1777 1778 // allow plugins to catch sending errors with the same parameters as in 'message_before_send' 1779 $plugin = $this->plugins->exec_hook('message_send_error', $plugin + ['error' => $error]); 1780 $error = $plugin['error']; 1781 } 1782 else { 1783 $this->plugins->exec_hook('message_sent', ['headers' => $headers, 'body' => $msg_body, 'message' => $message]); 1784 1785 // remove MDN/DSN headers after sending 1786 unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']); 1787 1788 if ($this->config->get('smtp_log')) { 1789 // get all recipient addresses 1790 $mailto = implode(',', $a_recipients); 1791 $mailto = rcube_mime::decode_address_list($mailto, null, false, null, true); 1792 1793 self::write_log('sendmail', sprintf("User %s [%s]; Message %s for %s; %s", 1794 $this->user->get_username(), 1795 rcube_utils::remote_addr(), 1796 $headers['Message-ID'], 1797 implode(', ', $mailto), 1798 !empty($response) ? implode('; ', $response) : '') 1799 ); 1800 } 1801 } 1802 1803 if (is_resource($msg_body)) { 1804 fclose($msg_body); 1805 } 1806 1807 if ($disconnect) { 1808 $this->smtp->disconnect(); 1809 } 1810 1811 // Add Bcc header back 1812 if (!empty($headers['Bcc'])) { 1813 $message->headers(['Bcc' => $headers['Bcc']], true); 1814 } 1815 1816 return $sent; 1817 } 1818} 1819 1820 1821/** 1822 * Lightweight plugin API class serving as a dummy if plugins are not enabled 1823 * 1824 * @package Framework 1825 * @subpackage Core 1826 */ 1827class rcube_dummy_plugin_api 1828{ 1829 /** 1830 * Triggers a plugin hook. 1831 * 1832 * @param string $hook Hook name 1833 * @param array $args Hook arguments 1834 * 1835 * @return array Hook arguments 1836 * @see rcube_plugin_api::exec_hook() 1837 */ 1838 public function exec_hook($hook, $args = []) 1839 { 1840 return $args; 1841 } 1842} 1843