Aleksander Machniak
2015-08-18 eb318205fcebe671f20a62a6e5e1d81622e0586f
program/lib/Roundcube/rcube.php
@@ -99,20 +99,20 @@
    protected $texts;
    protected $caches = array();
    protected $shutdown_functions = array();
    protected $expunge_cache = false;
    /**
     * This implements the 'singleton' design pattern
     *
     * @param integer Options to initialize with this instance. See rcube::INIT_WITH_* constants
     * @param string Environment name to run (e.g. live, dev, test)
     *
     * @return rcube The one and only instance
     */
    static function get_instance($mode = 0)
    static function get_instance($mode = 0, $env = '')
    {
        if (!self::$instance) {
            self::$instance = new rcube();
            self::$instance = new rcube($env);
            self::$instance->init($mode);
        }
@@ -123,10 +123,10 @@
    /**
     * Private constructor
     */
    protected function __construct()
    protected function __construct($env = '')
    {
        // load configuration
        $this->config  = new rcube_config;
        $this->config  = new rcube_config($env);
        $this->plugins = new rcube_dummy_plugin_api;
        register_shutdown_function(array($this, 'shutdown'));
@@ -269,16 +269,18 @@
    {
        $shared_name = "shared_$name";
        if (!isset($this->caches[$shared_name])) {
        if (!array_key_exists($shared_name, $this->caches)) {
            $opt  = strtolower($name) . '_cache';
            $type = $this->config->get($opt);
            $ttl  = $this->config->get($opt . '_ttl');
            if (!$type) {
                $type = $this->config->get('shared_cache');
                // cache is disabled
                return $this->caches[$shared_name] = null;
            }
            if ($ttl === null) {
                $ttl = $this->config->get('shared_cache_ttl');
                $ttl = $this->config->get('shared_cache_ttl', '10d');
            }
            $this->caches[$shared_name] = new rcube_cache_shared($type, $name, $ttl, $packed);
@@ -371,14 +373,16 @@
        // set class options
        $options = array(
            'auth_type'   => $this->config->get("{$driver}_auth_type", 'check'),
            'auth_cid'    => $this->config->get("{$driver}_auth_cid"),
            'auth_pw'     => $this->config->get("{$driver}_auth_pw"),
            'debug'       => (bool) $this->config->get("{$driver}_debug"),
            'force_caps'  => (bool) $this->config->get("{$driver}_force_caps"),
            'timeout'     => (int) $this->config->get("{$driver}_timeout"),
            'skip_deleted' => (bool) $this->config->get('skip_deleted'),
            'driver'      => $driver,
            'auth_type'      => $this->config->get("{$driver}_auth_type", 'check'),
            'auth_cid'       => $this->config->get("{$driver}_auth_cid"),
            'auth_pw'        => $this->config->get("{$driver}_auth_pw"),
            'debug'          => (bool) $this->config->get("{$driver}_debug"),
            'force_caps'     => (bool) $this->config->get("{$driver}_force_caps"),
            'disabled_caps'  => $this->config->get("{$driver}_disabled_caps"),
            'socket_options' => $this->config->get("{$driver}_conn_options"),
            'timeout'        => (int) $this->config->get("{$driver}_timeout"),
            'skip_deleted'   => (bool) $this->config->get('skip_deleted'),
            'driver'         => $driver,
        );
        if (!empty($_SESSION['storage_host'])) {
@@ -455,22 +459,36 @@
        ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid');
        ini_set('session.use_cookies', 1);
        ini_set('session.use_only_cookies', 1);
        ini_set('session.serialize_handler', 'php');
        ini_set('session.cookie_httponly', 1);
        // use database for storing session data
        $this->session = new rcube_session($this->get_dbh(), $this->config);
        $this->session->register_gc_handler(array($this, 'temp_gc'));
        $this->session->register_gc_handler(array($this, 'cache_gc'));
        $this->session->register_gc_handler(array($this, 'gc'));
        $this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
        $this->session->set_ip_check($this->config->get('ip_check'));
        if ($this->config->get('session_auth_name')) {
            $this->session->set_cookiename($this->config->get('session_auth_name'));
        }
        // start PHP session (if not in CLI mode)
        if ($_SERVER['REMOTE_ADDR']) {
            session_start();
            $this->session->start();
        }
    }
    /**
     * Garbage collector - cache/temp cleaner
     */
    public function gc()
    {
        rcube_cache::gc();
        rcube_cache_shared::gc();
        $this->get_storage()->cache_gc();
        $this->gc_temp();
    }
@@ -478,18 +496,25 @@
     * Garbage collector function for temp files.
     * Remove temp files older than two days
     */
    public function temp_gc()
    public function gc_temp()
    {
        $tmp = unslashify($this->config->get('temp_dir'));
        $expire = time() - 172800;  // expire in 48 hours
        // expire in 48 hours by default
        $temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h');
        $temp_dir_ttl = get_offset_sec($temp_dir_ttl);
        if ($temp_dir_ttl < 6*3600)
            $temp_dir_ttl = 6*3600;   // 6 hours sensible lower bound.
        $expire = time() - $temp_dir_ttl;
        if ($tmp && ($dir = opendir($tmp))) {
            while (($fname = readdir($dir)) !== false) {
                if ($fname{0} == '.') {
                if ($fname[0] == '.') {
                    continue;
                }
                if (filemtime($tmp.'/'.$fname) < $expire) {
                if (@filemtime($tmp.'/'.$fname) < $expire) {
                    @unlink($tmp.'/'.$fname);
                }
            }
@@ -500,14 +525,21 @@
    /**
     * Garbage collector for cache entries.
     * Set flag to expunge caches on shutdown
     * Runs garbage collector with probability based on
     * session settings. This is intended for environments
     * without a session.
     */
    public function cache_gc()
    public function gc_run()
    {
        // because this gc function is called before storage is initialized,
        // we just set a flag to expunge storage cache on shutdown.
        $this->expunge_cache = true;
        $probability = (int) ini_get('session.gc_probability');
        $divisor     = (int) ini_get('session.gc_divisor');
        if ($divisor > 0 && $probability > 0) {
            $random = mt_rand(1, $divisor);
            if ($random <= $probability) {
                $this->gc();
            }
        }
    }
@@ -611,10 +643,11 @@
    /**
     * Load a localization package
     *
     * @param string Language ID
     * @param array  Additional text labels/messages
     * @param string $lang  Language ID
     * @param array  $add   Additional text labels/messages
     * @param array  $merge Additional text labels/messages to merge
     */
    public function load_language($lang = null, $add = array())
    public function load_language($lang = null, $add = array(), $merge = array())
    {
        $lang = $this->language_prop(($lang ? $lang : $_SESSION['language']));
@@ -654,6 +687,11 @@
        if (is_array($add) && !empty($add)) {
            $this->texts += $add;
        }
        // merge additional texts (from plugin)
        if (is_array($merge) && !empty($merge)) {
            $this->texts = array_merge($this->texts, $merge);
        }
    }
@@ -671,7 +709,11 @@
        // user HTTP_ACCEPT_LANGUAGE if no language is specified
        if (empty($lang) || $lang == 'auto') {
            $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
            $lang         = str_replace('-', '_', $accept_langs[0]);
            $lang         = $accept_langs[0];
            if (preg_match('/^([a-z]+)[_-]([a-z]+)$/i', $lang, $m)) {
                $lang = $m[1] . '_' . strtoupper($m[2]);
            }
        }
        if (empty($rcube_languages)) {
@@ -891,23 +933,25 @@
            call_user_func($function);
        }
        // write session data as soon as possible and before
        // closing database connection, don't do this before
        // registered shutdown functions, they may need the session
        // Note: this will run registered gc handlers (ie. cache gc)
        if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
            $this->session->write_close();
        }
        if (is_object($this->smtp)) {
            $this->smtp->disconnect();
        }
        foreach ($this->caches as $cache) {
            if (is_object($cache)) {
                if ($this->expunge_cache) {
                    $cache->expunge();
                }
                $cache->close();
            }
        }
        if (is_object($this->storage)) {
            if ($this->expunge_cache) {
                $this->storage->expunge_cache();
            }
            $this->storage->close();
        }
    }
@@ -1071,7 +1115,20 @@
        // log_driver == 'file' is assumed here
        $line = sprintf("[%s]: %s\n", $date, $line);
        $log_dir  = self::$instance ? self::$instance->config->get('log_dir') : null;
        $log_dir = null;
        // per-user logging is activated
        if (self::$instance && self::$instance->config->get('per_user_logging', false) && self::$instance->get_user_id()) {
            $log_dir = self::$instance->get_user_log_dir();
            if (empty($log_dir))
                return false;
        }
        else if (!empty($log['dir'])) {
            $log_dir = $log['dir'];
        }
        else if (self::$instance) {
            $log_dir = self::$instance->config->get('log_dir');
        }
        if (empty($log_dir)) {
            $log_dir = RCUBE_INSTALL_PATH . 'logs';
@@ -1099,8 +1156,8 @@
     *      - code:    Error code (required)
     *      - type:    Error type [php|db|imap|javascript] (required)
     *      - message: Error message
     *      - file:    File where error occured
     *      - line:    Line where error occured
     *      - file:    File where error occurred
     *      - line:    Line where error occurred
     * @param boolean True to log the error
     * @param boolean Terminate script execution
     */
@@ -1109,7 +1166,6 @@
        // handle PHP exceptions
        if (is_object($arg) && is_a($arg, 'Exception')) {
            $arg = array(
                'type' => 'php',
                'code' => $arg->getCode(),
                'line' => $arg->getLine(),
                'file' => $arg->getFile(),
@@ -1117,7 +1173,7 @@
            );
        }
        else if (is_string($arg)) {
            $arg = array('message' => $arg, 'type' => 'php');
            $arg = array('message' => $arg);
        }
        if (empty($arg['code'])) {
@@ -1133,7 +1189,7 @@
        $cli = php_sapi_name() == 'cli';
        if (($log || $terminate) && !$cli && $arg['type'] && $arg['message']) {
        if (($log || $terminate) && !$cli && $arg['message']) {
            $arg['fatal'] = $terminate;
            self::log_bug($arg);
        }
@@ -1161,7 +1217,7 @@
     */
    public static function log_bug($arg_arr)
    {
        $program = strtoupper($arg_arr['type']);
        $program = strtoupper(!empty($arg_arr['type']) ? $arg_arr['type'] : 'php');
        $level   = self::get_instance()->config->get('debug_level');
        // disable errors for ajax requests, write to log instead (#1487831)
@@ -1310,6 +1366,17 @@
        }
    }
    /**
     * Get the per-user log directory
     */
    protected function get_user_log_dir()
    {
        $log_dir = $this->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
        $user_name = $this->get_user_name();
        $user_log_dir = $log_dir . '/' . $user_name;
        return !empty($user_name) && is_writable($user_log_dir) ? $user_log_dir : false;
    }
    /**
     * Getter for logged user language code.
@@ -1371,6 +1438,17 @@
            'options' => $options,
        ));
        if ($plugin['abort']) {
            if (!empty($plugin['error'])) {
                $error = $plugin['error'];
            }
            if (!empty($plugin['body_file'])) {
                $body_file = $plugin['body_file'];
            }
            return isset($plugin['result']) ? $plugin['result'] : false;
        }
        $from    = $plugin['from'];
        $mailto  = $plugin['mailto'];
        $options = $plugin['options'];
@@ -1380,20 +1458,15 @@
        // send thru SMTP server using custom SMTP library
        if ($this->config->get('smtp_server')) {
            // generate list of recipients
            $a_recipients = array($mailto);
            $a_recipients = (array) $mailto;
            if (strlen($headers['Cc']))
                $a_recipients[] = $headers['Cc'];
            if (strlen($headers['Bcc']))
                $a_recipients[] = $headers['Bcc'];
            // clean Bcc from header for recipients
            $send_headers = $headers;
            unset($send_headers['Bcc']);
            // here too, it because txtHeaders() below use $message->_headers not only $send_headers
            unset($message->_headers['Bcc']);
            $smtp_headers = $message->txtHeaders($send_headers, true);
            // remove Bcc header and get the whole head of the message as string
            $smtp_headers = $this->message_head($message, array('Bcc'));
            if ($message->getParam('delay_file_io')) {
                // use common temp dir
@@ -1425,24 +1498,18 @@
            if (!$sent) {
                self::raise_error(array('code' => 800, 'type' => 'smtp',
                    'line' => __LINE__, 'file' => __FILE__,
                    'message' => "SMTP error: ".join("\n", $response)), TRUE, FALSE);
                    'message' => join("\n", $response)), true, false);
            }
        }
        // send mail using PHP's mail() function
        else {
            // unset some headers because they will be added by the mail() function
            $headers_enc = $message->headers($headers);
            $headers_php = $message->_headers;
            unset($headers_php['To'], $headers_php['Subject']);
            // reset stored headers and overwrite
            $message->_headers = array();
            $header_str = $message->txtHeaders($headers_php);
            // unset To,Subject headers because they will be added by the mail() function
            $header_str = $this->message_head($message, array('To', 'Subject'));
            // #1485779
            if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
                if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
                    $headers_enc['To'] = implode(', ', $m[1]);
                if (preg_match_all('/<([^@]+@[^>]+)>/', $headers['To'], $m)) {
                    $headers['To'] = implode(', ', $m[1]);
                }
            }
@@ -1456,8 +1523,8 @@
            }
            else {
                $delim   = $this->config->header_delimiter();
                $to      = $headers_enc['To'];
                $subject = $headers_enc['Subject'];
                $to      = $headers['To'];
                $subject = $headers['Subject'];
                $header_str = rtrim($header_str);
                if ($delim != "\r\n") {
@@ -1467,7 +1534,7 @@
                    $subject    = str_replace("\r\n", $delim, $subject);
                }
                if (ini_get('safe_mode'))
                if (filter_var(ini_get('safe_mode'), FILTER_VALIDATE_BOOLEAN))
                    $sent = mail($to, $subject, $msg_body, $header_str);
                else
                    $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
@@ -1480,33 +1547,62 @@
            // remove MDN headers after sending
            unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
            // get all recipients
            if ($headers['Cc'])
                $mailto .= $headers['Cc'];
            if ($headers['Bcc'])
                $mailto .= $headers['Bcc'];
            if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
                $mailto = implode(', ', array_unique($m[1]));
            if ($this->config->get('smtp_log')) {
                // get all recipient addresses
                if (is_array($mailto)) {
                    $mailto = implode(',', $mailto);
                }
                if ($headers['Cc']) {
                    $mailto .= ',' . $headers['Cc'];
                }
                if ($headers['Bcc']) {
                    $mailto .= ',' . $headers['Bcc'];
                }
                $mailto = rcube_mime::decode_address_list($mailto, null, false, null, true);
                self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
                    $this->user->get_username(),
                    $_SERVER['REMOTE_ADDR'],
                    $mailto,
                    rcube_utils::remote_addr(),
                    implode(', ', $mailto),
                    !empty($response) ? join('; ', $response) : ''));
            }
        }
        else {
            // allow plugins to catch sending errors with the same parameters as in 'message_before_send'
            $this->plugins->exec_hook('message_send_error', $plugin + array('error' => $error));
        }
        if (is_resource($msg_body)) {
            fclose($msg_body);
        }
        $message->_headers = array();
        $message->headers($headers);
        $message->headers($headers, true);
        return $sent;
    }
    /**
     * Return message headers as a string
     */
    protected function message_head($message, $unset = array())
    {
        // Mail_mime >= 1.9.0
        if (method_exists($message, 'isMultipart')) {
            foreach ($unset as $header) {
                $headers[$header] = null;
            }
        }
        else {
            $headers = $message->headers();
            foreach ($unset as $header) {
                unset($headers[$header]);
            }
            $message->_headers = array();
        }
        return $message->txtHeaders($headers, true);
    }
}