Skip to content
sidh edited this page Oct 8, 2012 · 16 revisions

Code Igniter bundles a session class, working with cookies. Unfortunately, this class stores session data directly inside the cookie, thus allowing the client to see and edit those data. Here is a replacement class that stores data in the database. (note: the original Code Igniter Session class can use a database, but only for validation purposes. The actual data is stored in the cookie itself)

1.) Using the class

This class works with CI >= 1.71 (maby also with lower versions)

The usage is the same as the bundled Code Igniter session class. So you use it like described in the user guide:

http://codeigniter.com/user_guide/libraries/sessions.html

Features:

  • Supports native PHP-Session-Handling
  • Supports CI-flashdata-Handling
  • Using a "fingerprint" instead of Browser or IP to identify the user

2.) Configuration

This class uses the same configuration directives than the original session class. So don't forget to set inside your 'config.php' :

$config['sess_cookie_name'] = 'mysite';
$config['sess_expiration'] = 7200;
$config['sess_use_database'] = TRUE;
$config['sess_table_name'] = 'ci_sessions';
$config['sess_match_ip'] = TRUE;
$config['sess_match_useragent'] = FALSE;
$config['cookie_prefix'] = "";
$config['cookie_domain'] = "";
$config['cookie_path'] = "/";

Additionally there are the folowing directives:

$config['sess_gc_probability']    = 0;
$config['sess_gc_divisor'] = 0;

Set to 0 the defaults (php.ini) are used for Garbage Collection.

3.) Database

Here is the table schema needed by the new session class :

    CREATE TABLE `ci_sessions` (
      `session_id` varchar(32) NOT NULL default '',
      `fingerprint` varchar(32) NOT NULL default '',
      `session_data` blob NOT NULL,
      `session_expire` int(11) NOT NULL default '0',
      PRIMARY KEY  (`session_id`)
    ) ENGINE=MyISAM DEFAULT CHARSET=latin1; 

4.) Installing the package

Just get the zip File:EckoSession_1.0.2.zip.

Move the file Session.php in your application/libraries directory. Then use the autoload feature of Code Igniter : open your "autoload.php" configuration file and add "session" in the core autoload array :

$autoload['libraries'] = array('database', 'session');

5.) Inside working

Using this class, the cookie only stores a unique session identifier. Everything else is matched from the database.

6.) Contents of /application/libraries/Session.php:

<?php
/**
 The EckoTools Session Library

 @package The EckoTools Session Library
 @category Libraries
 @author Hartmut König (h.koenig@eckotools.com)
 @link http://www.okidoe.de
 @version 1.0.2
 @copyright Hartmut König 2009

 A class to handle sessions by using a mySQL database for session related
 data storage providing better security then the default session handler 
 used by PHP with added protection against Session Hijacking & Fixation 
 including the flashdata-Feature of CI. It don't use Browser or IP to identify 
 the user. Instead I generate a fingerprint of different seldom changing data 
 (@link _generate_fingerprint) 

 To prevent session hijacking, don't forget to use the {@link regenerate_id} 
 method whenever you do a privilege change in your application

  -- 
  -- MYSQL: Table structure for table `ci_sessions`
  -- 
  
CREATE TABLE `ci_sessions` (
  `session_id` varchar(32) NOT NULL default '',
  `fingerprint` varchar(32) NOT NULL default '',
  `session_data` blob NOT NULL,
  `session_expire` int(11) NOT NULL default '0',
  PRIMARY KEY  (`session_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1; 


  This class is an adaptation between the original CI Sessions, Native Sessions 
  and my own coding

*/

error_reporting(E_ALL);

class CI_Session
{
    
  /**
   *  Constructor of class
   *
   *  Initializes the class and starts a new session
   *
   *  There is no need to call start_session() after instantiating this class
   *
   *  $gc_maxlifetime    (optional) the number of seconds after which data will be seen as 'garbage' and
   *                     cleaned up on the next run of the gc (garbage collection) routine
   *                     
   *                     Default is specified in php.ini file
   *                     
   *  $gc_probability    (optional) used in conjunction with gc_divisor, is used to manage probability that
   *                     the gc routine is started. the probability is expressed by the formula
   *                     
   *                     probability = $gc_probability / $gc_divisor
   *                     
   *                     So if $gc_probability is 1 and $gc_divisor is 100 means that there is
   *                     a 1% chance the the gc routine will be called on each request
   *                     
   *                     Default is specified in php.ini file
   *                     
   *  $gc_divisor        (optional) used in conjunction with gc_probability, is used to manage probability
   *                     that the gc routine is started. the probability is expressed by the formula
   *                     
   *                     probability = $gc_probability / $gc_divisor
   *                     
   *                     So if $gc_probability is 1 and $gc_divisor is 100 means that there is
   *                     a 1% chance the the gc routine will be called on each request
   *                     
   *                     Default is specified in php.ini file
   *                     
   *  $security_code     (optional) the value of this argument is appended to the fingerprint before
   *                     creating the md5 hash out of it. this way we'll try to prevent fingerprint
   *                     spoofing
   *                     
   *                     Default is 'LeouOeEkKpvSnD-YCHd5ogt3y'
   *                     
   *  $table_name        (optional) You can change the name of that table by setting this property
   *
   *                  Default is 'ci_sessions'
   *
   *  @return void
   */
  function CI_Session(    $security_code="LeouOeEkKpvSnD-YCHd5ogt3y",$table_name="ci_sessions" )
  {
           //-- CI Config
           $this->CI                         = & get_instance();
        $this->flashdata_key     = 'flash'; // prefix for "flash" variables (eg. flash:new:message)           
           $table_name                     = $this->CI->config->item('sess_table_name');
            $gc_maxlifetime                = $this->CI->config->item('sess_expiration');
            $gc_probability             = $this->CI->config->item('sess_gc_probability');
            $gc_divisor                        = $this->CI->config->item('sess_gc_divisor');
            $sess_name                        = $this->CI->config->item('sess_cookie_name');
            
      // if $gc_maxlifetime is specified and is an integer number
      (!empty($gc_maxlifetime) && is_integer($gc_maxlifetime))
                ? @ini_set('session.gc_maxlifetime', $gc_maxlifetime)
                : false;

      // if $gc_probability is specified and is an integer number
      (!empty($gc_probability) && is_integer($gc_probability))      
                ? @ini_set('session.gc_probability', $gc_probability)
                : false;

      // if $gc_divisor is specified and is an integer number
      (!empty($gc_divisor) && is_integer($gc_divisor))            
                ? @ini_set('session.gc_divisor', $gc_divisor)
                : false;

      (!empty($sess_name))            
                ? @ini_set('session.name', $sess_name)
                : false;
        
      // get session lifetime
      $this->sessionLifetime = ini_get("session.gc_maxlifetime");
      
      // we'll use this later in order to prevent fingerprint spoofing
      $this->securityCode = $security_code;
      $this->tableName         = $table_name;

      // register the new handler
      session_set_save_handler(
          array(&$this, '_open'),
          array(&$this, '_close'),
          array(&$this, '_read'),
          array(&$this, '_write'),
          array(&$this, '_destroy'),
          array(&$this, '_gc')
            );
      register_shutdown_function('session_write_close');

      // start the session
      session_start();
      
            // Delete 'old' flashdata (from last request)
           $this->_flashdata_sweep();

            // Mark all new flashdata as old (data will be deleted before next request)
           $this->_flashdata_mark();
  }
    /**
    * Reads given session attribute value
    * 
    * @return integer sessionvalue
    */    
    function userdata($item)
    {
            //added for backward-compatibility
        if($item == 'session_id')
            { 
          return session_id();
            }

            if(isset($_SESSION[$item]))
                {
                return($_SESSION[$item]);
            }

            return(false);
    }  
    /**
     * Fetch all session data
     *
     * @access    public
     * @return    mixed
     */
    function all_userdata()
    {
        return ( ! isset($_SESSION)) ? FALSE : $_SESSION;
    }

    /**
    * Sets session attributes to the given values
    * 
    * @return void
    */
    function set_userdata($newdata = array(), $newval = '')
    {
        (is_string($newdata))
            ? $newdata = array($newdata => $newval)
            : false;
    
        if(count($newdata) > 0)
            {
          foreach($newdata as $key => $val)
            {
            $_SESSION[$key] = $val;
            }
            }
    }
    /**
    * Erases given session attributes
    *
    * @return void    
    */
    function unset_userdata($newdata = array())
    {
        (is_string($newdata))
            ? $newdata = array($newdata => '')
            : false;
    
        if(count($newdata) > 0)
            {
          foreach ($newdata as $key => $val)
            {
            unset($_SESSION[$key]);
            }
            }        
    }    
  /**
   *  Deletes all data related to the session
   *  @return void
   */
  function sess_destroy()
  {
      $this->regenerate_id();
      session_unset();
      session_destroy();
  }
    
  /**
   *  Regenerates the session id.
   *
   *  <b>Call this method whenever you do a privilege change!</b>
   *
   *  @return void
   */
  function regenerate_id()
  {
      // saves the old session's id
      $oldSessionID = session_id();

      // regenerates the id
      // this function will create a new session, with a new id and containing the data from the old session
      // but will not delete the old session
      session_regenerate_id();

      // because the session_regenerate_id() function does not delete the old session,
      // we have to delete it manually
      $this->_destroy($oldSessionID);
  }
    /**
     * Add or change flashdata, only available
     * until the next request
     *
     * @access    public
     * @param    mixed
     * @param    string
     * @return    void
     */
    function set_flashdata($newdata = array(), $newval = '')
    {
        if (is_string($newdata))
        {
            $newdata = array($newdata => $newval);
        }

        if (count($newdata) > 0)
        {
            foreach ($newdata as $key => $val)
            {
                $flashdata_key = $this->flashdata_key.':new:'.$key;
                $this->set_userdata($flashdata_key, $val);
            }
        }
    }

    // ------------------------------------------------------------------------

    /**
     * Keeps existing flashdata available to next request.
     *
     * @access    public
     * @param    string
     * @return    void
     */
    function keep_flashdata($key)
    {
        // 'old' flashdata gets removed.  Here we mark all
        // flashdata as 'new' to preserve it from _flashdata_sweep()
        // Note the function will return FALSE if the $key
        // provided cannot be found
        $old_flashdata_key = $this->flashdata_key.':old:'.$key;
        $value = $this->userdata($old_flashdata_key);

        $new_flashdata_key = $this->flashdata_key.':new:'.$key;
        $this->set_userdata($new_flashdata_key, $value);
    }

    // ------------------------------------------------------------------------

    /**
     * Fetch a specific flashdata item from the session array
     *
     * @access    public
     * @param    string
     * @return    string
     */
    function flashdata($key)
    {
        $flashdata_key = $this->flashdata_key.':old:'.$key;
        return $this->userdata($flashdata_key);
    }

    // ------------------------------------------------------------------------

    /**
     * Identifies flashdata as 'old' for removal
     * when _flashdata_sweep() runs.
     *
     * @access    private
     * @return    void
     */
    function _flashdata_mark()
    {
        $userdata = $this->all_userdata();
        foreach ($userdata as $name => $value)
        {
            $parts = explode(':new:', $name);
            if (is_array($parts) && count($parts) === 2)
            {
                $new_name = $this->flashdata_key.':old:'.$parts[1];
                $this->set_userdata($new_name, $value);
                $this->unset_userdata($name);
            }
        }
    }

    // ------------------------------------------------------------------------

    /**
     * Removes all flashdata marked as 'old'
     *
     * @access    private
     * @return    void
     */

    function _flashdata_sweep()
    {
        $userdata = $this->all_userdata();
        foreach($userdata as $key => $value)
            {
            if (strpos($key, ':old:'))
                {
                $this->unset_userdata($key);
                }
            }
    }
  /**
   *  Get the number of online users
   *
   *  @return integer     number of users currently online
   */
  function get_users_online()
  {
      // counts the rows from the database
      $query     = $this->CI->db->query("SELECT COUNT(session_id) as count FROM ".$this->tableName);
      $result = $query->row(); 
      
      // return the number of found rows
      return $result->count;
  }

    /**
     * Generates key as protection against Session Hijacking & Fixation.
     * @access private
     * @return string
     */
    function _generate_fingerprint()
    {
        //-- We don't use the ip-adress, because this is a subject to change in most cases (proxies etc.)
        $list = array('HTTP_ACCEPT_CHARSET',
                                    'HTTP_ACCEPT_ENCODING',
                                    'HTTP_ACCEPT_LANGUAGE', 
                                    'HTTP_USER_AGENT');
        $key = array($this->securityCode);
        foreach($list as $item) 
            {
            $key[] = $this->CI->input->server($item);
            }
        return md5(implode("\0", $key));
    }
  /**
   *  Custom open() function
   *
   *  @access private
   */
  function _open($save_path, $session_name)
  {
      return(true);
  }

  /**
   *  Custom close() function
   *
   *  @access private
   */
  function _close()
  {
      return(true);
  }

  /**
   *  Custom read() function
   *
   *  @access private
   */
  function _read($session_id)
  {
      // reads session data associated with the session id
      // but only
      // - if the fingerprint is the same as the one who had previously written to this session AND
      // - if session has not expired
      $result = $this->CI->db->query("SELECT session_data ".
                                                                      "FROM ".$this->tableName." ".
                                                                      "WHERE session_id = ".$this->CI->db->escape($session_id)." ".
                                                                      "AND fingerprint = ".$this->CI->db->escape($this->_generate_fingerprint())." ".
                                                                      "AND session_expire > '".time()."' LIMIT 1");

      // if anything was found
      if($result->num_rows() > 0) 
          {
        // return found data
        $fields = $result->row();

        // Unserialization - PHP handles this automatically
        return $fields->session_data;
          }

      // if there was an error return an empty string - this HAS to be an empty string
      return("");

  }

  /**
   *  Custom write() function
   *
   *  @access private
   */
  function _write($session_id, $session_data)
  {
      // insert OR update session's data - this is how it works:
      // first it tries to insert a new row in the database BUT if session_id is already in the database then just
      // update session_data and session_expire for that specific session_id
      // read more here http://dev.mysql.com/doc/refman/4.1/en/insert-on-duplicate.html
      $result = $this->CI->db->query(
                                                              "INSERT INTO ".$this->tableName." (".
                                                        "session_id,".
                                                        "fingerprint,".
                                                        "session_data,".
                                                        "session_expire".
                                                    ") VALUES (".
                                                              $this->CI->db->escape($session_id).",".
                                                              $this->CI->db->escape($this->_generate_fingerprint()).",".
                                                              $this->CI->db->escape($session_data).",".
                                                              $this->CI->db->escape(time() + $this->sessionLifetime).
                                                                  ")".
                                                          "ON DUPLICATE KEY UPDATE ".
                                                                  "session_data = ".$this->CI->db->escape($session_data).",".
                                                              "session_expire = ".$this->CI->db->escape(time() + $this->sessionLifetime));
                                                        
            // note that after this type of queries, mysql_affected_rows() returns
            // - 1 if the row was inserted
            // - 2 if the row was updated
            switch($this->CI->db->affected_rows())
                {
          // if the row was inserted
          case 1:
                    return("");
        break;
                // if the row was updated
                case 2:
              return(true);
                break;
                // if something went wrong, return false
                default:
              return(false);
          break;
          }
  }

  /**
   *  Custom destroy() function
   *
   *  @access private
   */
  function _destroy($session_id)
  {
      // deletes the current session id from the database
      $result = $this->CI->db->query("DELETE FROM ".$this->tableName." ".
                                                                  "WHERE session_id = ".$this->CI->db->escape($session_id));

      // if anything happened
      if($this->CI->db->affected_rows())
          {
        return(true);
                }
            
      // if something went wrong, return false
      return(false);
  }

  /**
   *  Custom gc() function (garbage collector)
   *
   *  @access private
   */
function _gc($maxlifetime)
  {
        // it deletes expired sessions from database
    $result = $this->CI->db->query("DELETE FROM ".$this->tableName." ".
                                                                    "WHERE session_expire < ".$this->CI->db->escape(time() - $maxlifetime));
    }

}
?>

Category:SessionCategory:Libraries::Session

Category:Contributions::Libraries::PHP

Clone this wiki locally