From 929a508d801d9434c8d98dccd0311e3a707303ba Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Mon, 01 Mar 2010 14:04:34 -0500
Subject: [PATCH] - Improve performance by avoiding unnecessary updates to the session table (#1486325)

---
 CHANGELOG                                   |    1 
 index.php                                   |    4 
 program/include/rcube_plugin_api.php        |    2 
 program/include/main.inc                    |   10 +
 program/include/rcmail.php                  |  110 ++++++++----
 program/steps/mail/func.inc                 |    6 
 program/include/rcube_session.php           |  330 ++++++++++++++++++++++++++++++++++++
 program/include/rcube_user.php              |    5 
 program/include/rcube_template.php          |    2 
 plugins/managesieve/managesieve.php         |    4 
 plugins/new_user_dialog/new_user_dialog.php |    2 
 program/include/iniset.php                  |    9 -
 12 files changed, 426 insertions(+), 59 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index c7627e6..63bd103 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 8d3e10a..fe7f6b5 100644
--- a/index.php
+++ b/index.php
@@ -96,8 +96,8 @@
         !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 219c334..e88fcff 100644
--- a/plugins/managesieve/managesieve.php
+++ b/plugins/managesieve/managesieve.php
@@ -221,7 +221,7 @@
 	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 @@
       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 11154ce..006cb27 100644
--- a/plugins/new_user_dialog/new_user_dialog.php
+++ b/plugins/new_user_dialog/new_user_dialog.php
@@ -98,7 +98,7 @@
     // 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 94d49d8..82ec49f 100755
--- a/program/include/iniset.php
+++ b/program/include/iniset.php
@@ -52,15 +52,6 @@
 }
 
 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 2076148..9cf6dd0 100644
--- a/program/include/main.inc
+++ b/program/include/main.inc
@@ -1487,12 +1487,20 @@
     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 b0d130e..04c7151 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -35,6 +35,7 @@
   public $config;
   public $user;
   public $db;
+  public $session;
   public $smtp;
   public $imap;
   public $output;
@@ -82,8 +83,6 @@
    */
   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,30 +93,14 @@
     // 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));
@@ -125,14 +108,14 @@
 
     // 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 @@
       $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 @@
    *
    * @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);
@@ -440,6 +420,65 @@
     }
 
     return $conn;
+  }
+
+
+  /**
+   * 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);
+    }
   }
 
 
@@ -794,8 +833,6 @@
    */
   function authenticate_session()
   {
-    global $SESS_CLIENT_IP, $SESS_CHANGED;
-
     // advanced session authentication
     if ($this->config->get('double_auth')) {
       $now = time();
@@ -810,12 +847,13 @@
       }
     }
     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 @@
   {
     $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 c895578..6b6a0a2 100644
--- a/program/include/rcube_plugin_api.php
+++ b/program/include/rcube_plugin_api.php
@@ -69,7 +69,7 @@
   /**
    * 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 0000000..da799be
--- /dev/null
+++ b/program/include/rcube_session.php
@@ -0,0 +1,330 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_session.php                                     |
+ |                                                                       |
+ | This file is part of the RoundCube Webmail client                     |
+ | Copyright (C) 2005-2009, RoundCube Dev. - Switzerland                 |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Provide database supported session management                       |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ | Author: Aleksander Machniak <alec@alec.pl>                            |
+ +-----------------------------------------------------------------------+
+
+ $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 5bd9136..5272ace 100755
--- a/program/include/rcube_template.php
+++ b/program/include/rcube_template.php
@@ -986,7 +986,7 @@
             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 127efa7..ab44cdc 100644
--- a/program/include/rcube_user.php
+++ b/program/include/rcube_user.php
@@ -47,7 +47,7 @@
     
     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 @@
     // 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 da21220..a8ef651 100644
--- a/program/steps/mail/func.inc
+++ b/program/steps/mail/func.inc
@@ -1325,9 +1325,9 @@
   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');
   }
   
 

--
Gitblit v1.9.1