PageRenderTime 61ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/program/lib/Roundcube/rcube.php

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