From 929a508d801d9434c8d98dccd0311e3a707303ba Mon Sep 17 00:00:00 2001 From: alecpl Date: Mon, 1 Mar 2010 19:04:34 +0000 Subject: [PATCH] - Improve performance by avoiding unnecessary updates to the session table (#1486325) --- CHANGELOG | 1 + index.php | 4 +- plugins/managesieve/managesieve.php | 4 +- plugins/new_user_dialog/new_user_dialog.php | 2 +- program/include/iniset.php | 9 - program/include/main.inc | 10 +- program/include/rcmail.php | 110 ++++--- program/include/rcube_plugin_api.php | 2 +- program/include/rcube_session.php | 330 ++++++++++++++++++++ program/include/rcube_template.php | 2 +- program/include/rcube_user.php | 5 +- program/steps/mail/func.inc | 6 +- 12 files changed, 426 insertions(+), 59 deletions(-) create mode 100644 program/include/rcube_session.php diff --git a/CHANGELOG b/CHANGELOG index c7627e6a5..63bd103ec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG RoundCube Webmail =========================== +- Improve performance by avoiding unnecessary updates to the session table (#1486325) - Fix invalid font tags which cause HTML message rendering problems (#1486521) - Fix CVE-2010-0464: Disable DNS prefetching (#1486449) - Fix Received headers to behave better with SpamAssassin (#1486513) diff --git a/index.php b/index.php index 8d3e10acd..fe7f6b5fa 100644 --- a/index.php +++ b/index.php @@ -96,8 +96,8 @@ if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') { !empty($auth['host']) && !empty($auth['user']) && $RCMAIL->login($auth['user'], $auth['pass'], $auth['host'])) { // create new session ID - rcube_sess_unset('temp'); - rcube_sess_regenerate_id(); + $RCMAIL->session->remove('temp'); + $RCMAIL->session->regenerate_id(); // send auth cookie if necessary $RCMAIL->authenticate_session(); diff --git a/plugins/managesieve/managesieve.php b/plugins/managesieve/managesieve.php index 219c33489..e88fcff45 100644 --- a/plugins/managesieve/managesieve.php +++ b/plugins/managesieve/managesieve.php @@ -221,7 +221,7 @@ class managesieve extends rcube_plugin if ($result === true) { $this->rc->output->show_message('managesieve.setdeleted', 'confirmation'); $this->rc->output->command('managesieve_reload'); - rcube_sess_unset('managesieve_current'); + $this->rc->session->remove('managesieve_current'); } else { $this->rc->output->show_message('managesieve.setdeleteerror', 'error'); } @@ -270,7 +270,7 @@ class managesieve extends rcube_plugin if (!$error) { $this->rc->output->show_message('managesieve.setcreated', 'confirmation'); $this->rc->output->command('parent.managesieve_reload', $name); -// rcube_sess_unset('managesieve_current'); +// $this->rc->session->remove('managesieve_current'); } else { $this->rc->output->show_message($error, 'error'); } diff --git a/plugins/new_user_dialog/new_user_dialog.php b/plugins/new_user_dialog/new_user_dialog.php index 11154ce29..006cb2711 100644 --- a/plugins/new_user_dialog/new_user_dialog.php +++ b/plugins/new_user_dialog/new_user_dialog.php @@ -98,7 +98,7 @@ class new_user_dialog extends rcube_plugin // save data if not empty if (!empty($save_data['name']) && !empty($save_data['email'])) { $rcmail->user->update_identity($identity['identity_id'], $save_data); - rcube_sess_unset('plugin.newuserdialog'); + $rcmail->session->remove('plugin.newuserdialog'); } $rcmail->output->redirect(''); diff --git a/program/include/iniset.php b/program/include/iniset.php index 94d49d8aa..82ec49f19 100755 --- a/program/include/iniset.php +++ b/program/include/iniset.php @@ -52,15 +52,6 @@ if (set_include_path($include_path) === false) { } ini_set('error_reporting', E_ALL&~E_NOTICE); -if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') { - ini_set('session.cookie_secure', 1); -} else { - ini_set('session.cookie_secure', 0); -} -ini_set('session.name', 'roundcube_sessid'); -ini_set('session.use_cookies', 1); -ini_set('session.use_only_cookies', 1); -ini_set('session.serialize_handler', 'php'); // increase maximum execution time for php scripts // (does not work in safe mode) diff --git a/program/include/main.inc b/program/include/main.inc index 207614885..9cf6dd00c 100644 --- a/program/include/main.inc +++ b/program/include/main.inc @@ -1487,12 +1487,20 @@ function rcube_https_check($port=null, $use_https=true) return true; if ($port && $_SERVER['SERVER_PORT'] == $port) return true; - if ($use_https && $RCMAIL->config->get('use_https')) + if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https')) return true; return false; } +// for backward compatibility +function rcube_sess_unset($var_name=null) +{ + global $RCMAIL; + + $RCMAIL->session->remove($var_name); +} + /** * E-mail address validation diff --git a/program/include/rcmail.php b/program/include/rcmail.php index b0d130e75..04c7151dc 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -35,6 +35,7 @@ class rcmail public $config; public $user; public $db; + public $session; public $smtp; public $imap; public $output; @@ -82,8 +83,6 @@ class rcmail */ private function startup() { - $config_all = $this->config->all(); - // initialize syslog if ($this->config->get('log_driver') == 'syslog') { $syslog_id = $this->config->get('syslog_id', 'roundcube'); @@ -94,45 +93,29 @@ class rcmail // connect to database $GLOBALS['DB'] = $this->get_dbh(); - // use database for storing session data - include_once('include/session.inc'); - - // set session domain - if (!empty($config_all['session_domain'])) { - ini_set('session.cookie_domain', $config_all['session_domain']); - } - // set session garbage collecting time according to session_lifetime - if (!empty($config_all['session_lifetime'])) { - ini_set('session.gc_maxlifetime', ($config_all['session_lifetime']) * 120); - } - - // start PHP session (if not in CLI mode) - if ($_SERVER['REMOTE_ADDR']) - session_start(); - - // set initial session vars - if (!isset($_SESSION['auth_time'])) { - $_SESSION['auth_time'] = time(); - $_SESSION['temp'] = true; - } + // start session + $this->session_init(); // create user object $this->set_user(new rcube_user($_SESSION['user_id'])); + // configure session (after user config merge!) + $this->session_configure(); + // set task and action properties $this->set_task(get_input_value('_task', RCUBE_INPUT_GPC)); $this->action = asciiwords(get_input_value('_action', RCUBE_INPUT_GPC)); // reset some session parameters when changing task if ($_SESSION['task'] != $this->task) - rcube_sess_unset('page'); + $this->session->remove('page'); // set current task to session $_SESSION['task'] = $this->task; // init output class if (!empty($_REQUEST['_remote'])) - $GLOBALS['OUTPUT'] = $this->init_json(); + $GLOBALS['OUTPUT'] = $this->json_init(); else $GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed'])); @@ -314,11 +297,8 @@ class rcmail $this->output = new rcube_template($this->task, $framed); // set keep-alive/check-recent interval - if ($keep_alive = $this->config->get('keep_alive')) { - // be sure that it's less than session lifetime - if ($session_lifetime = $this->config->get('session_lifetime')) - $keep_alive = min($keep_alive, $session_lifetime * 60 - 30); - $this->output->set_env('keep_alive', max(60, $keep_alive)); + if ($keep_alive = $this->session->get_keep_alive()) { + $this->output->set_env('keep_alive', $keep_alive); } if ($framed) { @@ -343,7 +323,7 @@ class rcmail * * @return object rcube_json_output Reference to JSON output object */ - public function init_json() + public function json_init() { if (!($this->output instanceof rcube_json_output)) $this->output = new rcube_json_output($this->task); @@ -443,6 +423,65 @@ class rcmail } + /** + * Create session object and start the session. + */ + public function session_init() + { + $lifetime = $this->config->get('session_lifetime', 0) * 60; + + // set session domain + if ($domain = $this->config->get('session_domain')) { + ini_set('session.cookie_domain', $domain); + } + // set session garbage collecting time according to session_lifetime + if ($lifetime) { + ini_set('session.gc_maxlifetime', $lifetime * 2); + } + + ini_set('session.cookie_secure', rcube_https_check()); + ini_set('session.name', 'roundcube_sessid'); + ini_set('session.use_cookies', 1); + ini_set('session.use_only_cookies', 1); + ini_set('session.serialize_handler', 'php'); + + // use database for storing session data + $this->session = new rcube_session($this->get_dbh(), $lifetime); + + $this->session->register_gc_handler('rcmail_temp_gc'); + if ($this->config->get('enable_caching')) + $this->session->register_gc_handler('rcmail_cache_gc'); + + // start PHP session (if not in CLI mode) + if ($_SERVER['REMOTE_ADDR']) + session_start(); + + // set initial session vars + if (!isset($_SESSION['auth_time'])) { + $_SESSION['auth_time'] = time(); + $_SESSION['temp'] = true; + } + } + + + /** + * Configure session object internals + */ + public function session_configure() + { + $lifetime = $this->config->get('session_lifetime', 0) * 60; + + // set keep-alive/check-recent interval + if ($keep_alive = $this->config->get('keep_alive')) { + // be sure that it's less than session lifetime + if ($lifetime) + $keep_alive = min($keep_alive, $lifetime - 30); + $keep_alive = max(60, $keep_alive); + $this->session->set_keep_alive($keep_alive); + } + } + + /** * Perfom login to the IMAP server and to the webmail service. * This will also create a new user entry if auto_create_user is configured. @@ -794,8 +833,6 @@ class rcmail */ function authenticate_session() { - global $SESS_CLIENT_IP, $SESS_CHANGED; - // advanced session authentication if ($this->config->get('double_auth')) { $now = time(); @@ -810,12 +847,13 @@ class rcmail } } else { - $valid = $this->config->get('ip_check') ? $_SERVER['REMOTE_ADDR'] == $SESS_CLIENT_IP : true; + $valid = $this->config->get('ip_check') ? $_SERVER['REMOTE_ADDR'] == $this->session->get_ip() : true; } // check session filetime $lifetime = $this->config->get('session_lifetime'); - if (!empty($lifetime) && isset($SESS_CHANGED) && $SESS_CHANGED + $lifetime*60 < time()) { + $sess_ts = $this->session->get_ts(); + if (!empty($lifetime) && !empty($sess_ts) && $sess_ts + $lifetime*60 < time()) { $valid = false; } @@ -830,7 +868,7 @@ class rcmail { $this->plugins->exec_hook('kill_session'); - rcube_sess_unset(); + $this->session->remove(); $_SESSION = array('language' => $this->user->language, 'auth_time' => time(), 'temp' => true); rcmail::setcookie('sessauth', '-del-', time() - 60); $this->user->reset(); diff --git a/program/include/rcube_plugin_api.php b/program/include/rcube_plugin_api.php index c895578b9..6b6a0a263 100644 --- a/program/include/rcube_plugin_api.php +++ b/program/include/rcube_plugin_api.php @@ -69,7 +69,7 @@ class rcube_plugin_api /** * Load and init all enabled plugins * - * This has to be done after rcmail::load_gui() or rcmail::init_json() + * This has to be done after rcmail::load_gui() or rcmail::json_init() * was called because plugins need to have access to rcmail->output */ public function init() diff --git a/program/include/rcube_session.php b/program/include/rcube_session.php new file mode 100644 index 000000000..da799be19 --- /dev/null +++ b/program/include/rcube_session.php @@ -0,0 +1,330 @@ + | + | Author: Aleksander Machniak | + +-----------------------------------------------------------------------+ + + $Id: session.inc 2932 2009-09-07 12:51:21Z alec $ + +*/ + +class rcube_session +{ + private $db; + private $ip; + private $changed; + private $unsets = array(); + private $gc_handlers = array(); + private $start; + private $vars = false; + private $key; + private $keep_alive = 0; + + /** + * Default constructor + */ + public function __construct($db, $lifetime=60) + { + $this->db = $db; + $this->lifetime = $lifetime; + $this->start = microtime(true); + + // set custom functions for PHP session management + session_set_save_handler( + array($this, 'open'), + array($this, 'close'), + array($this, 'read'), + array($this, 'write'), + array($this, 'destroy'), + array($this, 'gc')); + } + + + public function open($save_path, $session_name) + { + return true; + } + + + public function close() + { + return true; + } + + + // read session data + public function read($key) + { + $sql_result = $this->db->query( + sprintf("SELECT vars, ip, %s AS changed FROM %s WHERE sess_id = ?", + $this->db->unixtimestamp('changed'), get_table_name('session')), + $key); + + if ($sql_arr = $this->db->fetch_assoc($sql_result)) { + $this->changed = $sql_arr['changed']; + $this->vars = $sql_arr['vars']; + $this->ip = $sql_arr['ip']; + $this->key = $key; + + if (!empty($sql_arr['vars'])) + return $sql_arr['vars']; + } + + return false; + } + + + // save session data + public function write($key, $vars) + { + $ts = microtime(true); + $now = $this->db->fromunixtime((int)$ts); + + // use internal data from read() for fast requests (up to 0.5 sec.) + if ($key == $this->key && $ts - $this->start < 0.5) { + $oldvars = $this->vars; + } else { // else read data again from DB + $oldvars = $this->read($key); + } + + if ($oldvars !== false) { + $a_oldvars = $this->unserialize($oldvars); + foreach ((array)$this->unsets as $k) + unset($a_oldvars[$k]); + + $newvars = $this->serialize(array_merge( + (array)$a_oldvars, (array)$this->unserialize($vars))); + + if ($this->keep_alive>0) { + $timeout = min($this->lifetime * 0.5, + $this->lifetime - $this->keep_alive); + } else { + $timeout = 0; + } + + if (!($newvars === $oldvars) || ($ts - $this->changed > $timeout)) { + $this->db->query( + sprintf("UPDATE %s SET vars = ?, changed = %s WHERE sess_id = ?", + get_table_name('session'), $now), + $newvars, $key); + } + } + else { + $this->db->query( + sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ". + "VALUES (?, ?, ?, %s, %s)", + get_table_name('session'), $now, $now), + $key, $vars, (string)$_SERVER['REMOTE_ADDR']); + } + + $this->unsets = array(); + return true; + } + + + // handler for session_destroy() + public function destroy($key) + { + $this->db->query( + sprintf("DELETE FROM %s WHERE sess_id = ?", get_table_name('session')), + $key); + + return true; + } + + + // garbage collecting function + public function gc($maxlifetime) + { + // just delete all expired sessions + $this->db->query( + sprintf("DELETE FROM %s WHERE changed < %s", + get_table_name('session'), $this->db->fromunixtime(time() - $maxlifetime))); + + foreach ($this->gc_handlers as $fct) + $fct(); + + return true; + } + + + // registering additional garbage collector functions + public function register_gc_handler($func_name) + { + if ($func_name && !in_array($func_name, $this->gc_handlers)) + $this->gc_handlers[] = $func_name; + } + + + public function regenerate_id() + { + $randval = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + for ($random = '', $i=1; $i <= 32; $i++) { + $random .= substr($randval, mt_rand(0,(strlen($randval) - 1)), 1); + } + + // use md5 value for id or remove capitals from string $randval + $random = md5($random); + + // delete old session record + $this->destroy(session_id()); + + session_id($random); + + $cookie = session_get_cookie_params(); + $lifetime = $cookie['lifetime'] ? time() + $cookie['lifetime'] : 0; + + rcmail::setcookie(session_name(), $random, $lifetime); + + return true; + } + + + // unset session variable + public function remove($var=NULL) + { + if (empty($var)) + return $this->destroy(session_id()); + + $this->unsets[] = $var; + unset($_SESSION[$var]); + + return true; + } + + + // serialize session data + private function serialize($vars) + { + $data = ''; + if (is_array($vars)) + foreach ($vars as $var=>$value) + $data .= $var.'|'.serialize($value); + else + $data = 'b:0;'; + return $data; + } + + + // unserialize session data + // http://www.php.net/manual/en/function.session-decode.php#56106 + private function unserialize($str) + { + $str = (string)$str; + $endptr = strlen($str); + $p = 0; + + $serialized = ''; + $items = 0; + $level = 0; + + while ($p < $endptr) { + $q = $p; + while ($str[$q] != '|') + if (++$q >= $endptr) break 2; + + if ($str[$p] == '!') { + $p++; + $has_value = false; + } else { + $has_value = true; + } + + $name = substr($str, $p, $q - $p); + $q++; + + $serialized .= 's:' . strlen($name) . ':"' . $name . '";'; + + if ($has_value) { + for (;;) { + $p = $q; + switch (strtolower($str[$q])) { + case 'n': /* null */ + case 'b': /* boolean */ + case 'i': /* integer */ + case 'd': /* decimal */ + do $q++; + while ( ($q < $endptr) && ($str[$q] != ';') ); + $q++; + $serialized .= substr($str, $p, $q - $p); + if ($level == 0) break 2; + break; + case 'r': /* reference */ + $q+= 2; + for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) $id .= $str[$q]; + $q++; + $serialized .= 'R:' . ($id + 1) . ';'; /* increment pointer because of outer array */ + if ($level == 0) break 2; + break; + case 's': /* string */ + $q+=2; + for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) $length .= $str[$q]; + $q+=2; + $q+= (int)$length + 2; + $serialized .= substr($str, $p, $q - $p); + if ($level == 0) break 2; + break; + case 'a': /* array */ + case 'o': /* object */ + do $q++; + while ( ($q < $endptr) && ($str[$q] != '{') ); + $q++; + $level++; + $serialized .= substr($str, $p, $q - $p); + break; + case '}': /* end of array|object */ + $q++; + $serialized .= substr($str, $p, $q - $p); + if (--$level == 0) break 2; + break; + default: + return false; + } + } + } else { + $serialized .= 'N;'; + $q += 2; + } + $items++; + $p = $q; + } + + return unserialize( 'a:' . $items . ':{' . $serialized . '}' ); + } + + public function set_keep_alive($keep_alive) + { + $this->keep_alive = $keep_alive; + } + + public function get_keep_alive() + { + return $this->keep_alive; + } + + // getter for private variables + public function get_ts() + { + return $this->changed; + } + + // getter for private variables + public function get_ip() + { + return $this->ip; + } + +} diff --git a/program/include/rcube_template.php b/program/include/rcube_template.php index 5bd9136a6..5272ace20 100755 --- a/program/include/rcube_template.php +++ b/program/include/rcube_template.php @@ -986,7 +986,7 @@ class rcube_template extends rcube_html_page return $username; } - // get e-mail address form default identity + // get e-mail address from default identity if ($sql_arr = $this->app->user->get_identity()) { $username = $sql_arr['email']; } diff --git a/program/include/rcube_user.php b/program/include/rcube_user.php index 127efa746..ab44cdca2 100644 --- a/program/include/rcube_user.php +++ b/program/include/rcube_user.php @@ -47,7 +47,7 @@ class rcube_user if ($id && !$sql_arr) { - $sql_result = $this->db->query("SELECT * FROM ".get_table_name('users')." WHERE user_id=?", $id); + $sql_result = $this->db->query("SELECT * FROM ".get_table_name('users')." WHERE user_id=?", $id); $sql_arr = $this->db->fetch_assoc($sql_result); } @@ -154,8 +154,7 @@ class rcube_user // get contacts from DB $sql_result = $this->db->query( "SELECT * FROM ".get_table_name('identities')." - WHERE del<>1 - AND user_id=? + WHERE del<>1 AND user_id=? $sql_add ORDER BY ".$this->db->quoteIdentifier('standard')." DESC, name ASC, identity_id ASC", $this->ID); diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index da21220ff..a8ef65139 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -1325,9 +1325,9 @@ function rcmail_compose_cleanup() if (!isset($_SESSION['compose'])) return; - rcmail::get_instance()->plugins->exec_hook('cleanup_attachments',array()); - - rcube_sess_unset('compose'); + $rcmail = rcmail::get_instance(); + $rcmail->plugins->exec_hook('cleanup_attachments',array()); + $rcmail->session->remove('compose'); }