ExpressionEngine CMS
Open, Free, Amazing

Thread

This is an archived forum and the content is probably no longer relevant, but is provided here for posterity.

The active forums are here.

NGSession: a combination of CI’s Session in 1.6 and DBSession

February 02, 2008 12:14pm

Subscribe [14]
  • #16 / Feb 14, 2008 3:05pm

    doors

    39 posts

    I started going back through the code and fixing up the class objects and it’s working fine now.

    Not sure what actually caused that.

    I’m not sure if it’s a bug or if it was just my code.

  • #17 / Feb 18, 2008 2:42pm

    doors

    39 posts

    This is the code I put together:


    It works by just loading the session library and using the $_SESSION array as usal.

    Test it and if any bugs in the code please let me know:

    <?php
    
    /*******************************************************
     *
     * Copyright (c) 2008 FiwiMarket.com
     * 
     * Portions Copyright (c) 2005 - 2006 SystemsManager.Net
     * 
     ******************************************************/
    
    class CI_Session {
        
        // Objects
        var $CI;
        
        // Variables
        private $encryption        = TRUE; //
        private $flashdata_key     = 'flash';
        private $now; //
        private $sess_cookie    = 'ci_session'; //
        private $sess_length    = 7200; //
        private $session_table    = FALSE; //
        private $use_database    = FALSE; //
        private $time_to_update    = 300; //
        
        private $ip_address;
        private $last_activity;
        private $session_id    = '';
        private $user_agent;
        
        function __construct(){
            $this->CI =& get_instance();
            
            log_message('debug', "Session Class Initialized");
            
            $this->sess_run();
            
            // Set this value to true session.use_only_cookies
            // For the encryption feature set session.hash_function to 0 for MD5 and 1 for SHA-1
            
            $this->session_name();
            //$this->session_save_path(SESSION_WRITE_DIRECTORY); // Needs to be called before session_start(). Well I am using MySQL to store my sessions
            $this->session_set_cookie_params();
            
            $this->session_start();
        }
        
        /**
         * Run the session routines
         *
         * @access    public
         * @return    void
         */
        function sess_run(){
            /*
             *  Set the "now" time
             *
             * It can either set to GMT or time(). The pref
             * is set in the config file.  If the developer
             * is doing any sort of time localization they
             * might want to set the session time to GMT so
             * they can offset the "last_activity" time
             * time based on each user's locale.
             *
             */
            if (is_numeric($this->CI->config->item('sess_time_to_update'))){
                $this->time_to_update = $this->CI->config->item('sess_time_to_update');
            }
    
            if (strtolower($this->CI->config->item('time_reference')) == 'gmt'){
                $now = time();
                $this->now = mktime(gmdate("H", $now), gmdate("i", $now), gmdate("s", $now), gmdate("m", $now), gmdate("d", $now), gmdate("Y", $now));
    
                if (strlen($this->now) < 10){
                    $this->now = time();
                    log_message('error', 'The session class could not set a proper GMT timestamp so the local time() value was used.');
                }
            }
            else{
                $this->now = time();
            }
    
            /*
             * Set the session length
             *
             * If the session expiration is set to zero in
             * the config file we'll set the expiration
             * two years from now.
             *
             */
            $expiration = $this->CI->config->item('sess_expiration');
    
            if (is_numeric($expiration)){
                if ($expiration > 0){
                    $this->sess_length = $this->CI->config->item('sess_expiration');
                }
                else{
                    $this->sess_length = (60*60*24*365*2);
                }
            }
    
            // Do we need encryption?
            $this->encryption = $this->CI->config->item('sess_encrypt_cookie');
    
            if ($this->encryption == TRUE){
                //$this->CI->load->library('encrypt');
            }
    
            // Are we using a database?
            if ($this->CI->config->item('sess_use_database') === TRUE AND $this->CI->config->item('sess_table_name') != ''){
                $this->use_database = TRUE;
                $this->session_table = $this->CI->config->item('sess_table_name');
                $this->CI->load->database();
                
                // Adrian: I am using this to use the old style $_SESSION array
                session_set_save_handler(    array(&$this, '_sess_open'),
                                             array(&$this, '_sess_close'),
                                            array(&$this, '_sess_read'),
                                             array(&$this, '_sess_write'),
                                             array(&$this, '_sess_destroy'),
                                             array(&$this, '_sess_gc'));
                                             
                register_shutdown_function('session_write_close');
            }
    
            // Set the cookie name
            if ($this->CI->config->item('sess_cookie_name') != FALSE){
                $this->sess_cookie = $this->CI->config->item('cookie_prefix') . $this->CI->config->item('sess_cookie_name');
            }
            
            // Delete expired sessions if necessary
            if ($this->use_database == TRUE){
                $this->_sess_gc(0);
            }
    
        }
        
        /////////////////////////////////////////////////////////////////////////////////////////////////////////////
        
        function _sess_open($save_path, $session_name) {
            return true;
        }
    
        function _sess_close() {
            return true;
        }
  • #18 / Feb 18, 2008 2:43pm

    doors

    39 posts

    function _sess_read($key) {
            $session_query = $this->CI->db->query("select session_id, session_data, ip_address, user_agent, last_activity from " . $this->session_table . " where session_id = " . $this->CI->db->escape($key));
            
            if ($session_query->num_rows() > 0){
                $session = $session_query->row_array();
                
                // Is the session current or timed out?
                if (($session['last_activity'] + $this->sess_length) < $this->now){
                    return FALSE;
                }
                
                // Does the IP Match?
                if ($this->CI->config->item('sess_match_ip') == TRUE AND $session['ip_address'] != $this->CI->input->ip_address()){
                    return FALSE;
                }
        
                // Does the User Agent Match?
                if ($this->CI->config->item('sess_match_useragent') == TRUE AND trim($session['user_agent']) != trim(substr($this->CI->input->user_agent(), 0, 50))){
                    return FALSE;
                }
                
                $this->ip_address = $session['ip_address'];
                $this->user_agent = $session['user_agent'];
                $this->last_activity = $session['last_activity'];
                $this->session_id = $session['session_id']; // If it passes the two if statements above it is now safe to set this class variable
                
                return $session['session_data'];
            }
    
            return FALSE;
        }
    
        function _sess_write($key, $val) {
    
            $value = $val;
    
            $check_query = $this->CI->db->query("select count(*) as total from " . $this->session_table . " where session_id = " . $this->CI->db->escape($key));
            $check = $check_query->row_array();
    
            if ($check['total'] > 0) {
                // Format data to match database structure
                $userdata = array(    'ip_address'     => $this->ip_address,
                                    'user_agent'     => $this->user_agent,
                                    'last_activity'    => $this->last_activity,
                                    'session_data' => $value);
                
                // Update database
                return $this->CI->db->query($this->CI->db->update_string($this->session_table, $userdata, array('session_id' => $key)));
            
            }
            else {
                $userdata = array(    'session_id'     => $key,
                                    'ip_address'     => $this->CI->input->ip_address(),
                                    'user_agent'     => substr($this->CI->input->user_agent(), 0, 50),
                                    'last_activity'    => $this->now,
                                    'session_data' => $value);
                
                return $this->CI->db->query($this->CI->db->insert_string($this->session_table, $userdata));
            }
        }
    
        function _sess_destroy($key) {
            return $this->CI->db->query("delete from " . $this->session_table . " where session_id = " . $this->CI->db->escape($key));
        }
        
        /**
         * Try to call this function everytime sess_run() is called.
         *
         * @param unknown_type $maxlifetime
         * @return unknown
         */
        function _sess_gc($maxlifetime) {
            $expire = $this->now - $this->sess_length;
            $this->CI->db->query("delete from " . $this->session_table . " where last_activity < '" . $expire . "'");
    
            return true;
        }
    
        /////////////////////////////////////////////////////////////////////////////////////////////////////////////
        
        function delete_session_cookie(){
            $domain = $this->CI->config->item('cookie_domain');
            $path = $this->CI->config->item('cookie_path');
            $prefix = $this->CI->config->item('cookie_prefix');
            
            delete_cookie($this->sess_cookie, $domain, $path, $prefix);
        }
        
        function session_name() {
            if (!empty($this->sess_cookie)) {
                session_name($this->sess_cookie);
            } 
        }
        
        function session_set_cookie_params(){
            $domain = $this->CI->config->item('cookie_domain');
            $path = $this->CI->config->item('cookie_path');
            session_set_cookie_params($this->sess_length, $path, $domain);
        }
        
        function session_start() {
            session_start();
        }
        
    
        /**
         * I don't need this function. Just use the session_write_close() function as it is.
         *
         * @return unknown
         */
        function session_close() {
            return session_write_close();
        }
    
        function session_destroy() {
            return session_destroy();
        }
    
        function session_save_path($path = '') {
            if (!empty($path)) {
                return session_save_path($path);
            } 
            else {
                return session_save_path();
            }
        }
        
    
        function session_recreate() {
    
            $session_backup = $_SESSION;
            
            $this->sess_run();
    
            //$this->delete_session_cookie();
            unset($_COOKIE[$this->sess_cookie]);
    
            $this->session_destroy();
    
            $this->session_start();
    
            $_SESSION = $session_backup;
            unset($session_backup);
    
        }
        
    }
    
    ?>
  • #19 / Feb 18, 2008 2:44pm

    doors

    39 posts

    Hopefully this will deal with objects in sessions excellently.

    This class is more than a regular sessions class, it stores the session data in a database.

    I have not set it to store sessions in a cookie. The cookie that is created by a session only stores the session id.

    It can be set to store sessions on the server as well. Uncomment session_save_path() for that.

  • #20 / Feb 18, 2008 4:42pm

    doors

    39 posts

    People I finally figured out what’s happening.

    Because all the objects I am trying to store in a class have the CI instance as as variable:

    $this->CI =& get_instance();

    When a session is being stored it is storing every single variable that’s available to Code Igniter through it’s singleton. All those variables are being stored as apart of the object which in reality it’s not.

    This is causing large amount of data to be added to the serialized session data and after 4 to 7 session objects the text field gets full.

    This is a big problem. Please help me to get around this.

  • #21 / Feb 18, 2008 4:44pm

    doors

    39 posts

    This is evident in the custom code I posted above and the NG Session code.

    I believe it will be the same for the cookies as well, which would be even more burdensome because of the 4kb limit. There must be a way around this.

    Come on developers.

  • #22 / Feb 19, 2008 6:37am

    WolfgangA

    46 posts

    A lot of code 😊 - is there a fix for your issue build in somewhere? (haven’t made a diff yet)

    I would like to help, but i need a bit more info. Lets try to summarize:
    - You want to store an object.
    - That object contains a property which references the CI instance
    - When you try to store that object, the session lib iterates throughh the object and therefore through the CI instance.
    - The CI instance creates a lot of data and therfore the process generates massive data which leads to your issues (exceeding fieldsize for serialized data)

    Is this correct?

    Well if this is correct, then i do not see a failure in the NGSession / nor CI Session lib.
    Why: You want to store an object, so the class attepmts to store that object. The fact that the object keeps a ref to the CI instance is your decision.

    I see two option to get arround this.
    a.) Before saveing the object, set the property that holds the CI ref. in your object to null. (untested but should work)

    b.) Have a check in NGSession wether a property has a ref to the CI instance. Note that this would requires a recursive check over any nested properties though.

    My opinion - I prefer a.) because it is more clean. I could imagine other cases where an object at runtime references any other big data structure that should not be stored in a session.
    So the ref to the CI instance is just one example of these scenarios.

    Any comments on this are welcome.

    Can you try a.) and let us know wether it works for you?

  • #23 / Feb 19, 2008 1:22pm

    doors

    39 posts

    What I did was to create a local variable $CI =& get_instance() in all functions that need to access the CI instance.

    It’s cumbersome but it works.

    Atleast I found out what the problem was after trying to fix it for some days now.

    NG Sessions and CI werent the problem really, it was just the logic.

    Setting the CI reference to null could be error proned as the session variable must be able to call functions within the class and setting and unsetting the CI reference would cause alot of problem.

  • #24 / Mar 07, 2008 5:31am

    webthink

    170 posts

    Hi I searched the forums high and low for a ‘Remember me’ solution and have to say was quite surprised that there were none that integrated nicely with any of the current session handler libraries.
    Most solutions out there involve setting a separate cookie which I find to be both clunky and in a lot of cases incredibly insecure.

    Clunky - I’ve already got a cookie to track the session so it stands to reason that the lifetime of that cookie should be the one that dictates session expirey. Also I don’t want to have to write seperate code to look for that cookie when I’ve already got a session handler doing essentially that.

    Insecure - this is important because I saw lots of so called solutions suggesting this, including the one suggested by the creator of OB_session. What they suggest is to set a cookie with the username and if that cookie exists log the user in (!!!!). I’m sure I’m not the only one who can see that basing your entire security system off such easily obtained data is inadvisable to say the least. Even encrypting the data stored in that cookie when not implemented properly (ie 1 way encryption in the cookie and storing the corresponding data in your database) can be dangerous for reasons discussed in lots of different threads about passwords, encryption, and cookies.

    So I set about integrating the functionality directly in the session handler using only the already existing session cookie. I decided to base my changes on NG_Session but they could probably be ported to any of the db based session handlers.

    The challenges to doing this were as follows:

    a)Most people like to autoload their sessions including me. Doing this prevents you from setting the configuration value for the session on the fly because the library gets loaded (and run) before any of your controller code executes.

    b)You could set the config value in your controller and then call $this->session->sess_update() which would then recreate the cookie with the new expiry. This isn’t really a usable solution though because lets say you do that in the login controller if they click remember me - on the very next page-load the session handler rewrites the expiry based on the config value so you’re back to square one.

    What I did to overcome these challenges was to to add a field to the session table that could be read by the session handler and used to determine expiry. It’s called expiration_override and it defaults to zero indicating no extended session expiry is necessary.

    Solution in next post…

  • #25 / Mar 07, 2008 5:35am

    webthink

    170 posts

    Here is the new table:

    CREATE TABLE `ci_sessions` (
      `session_id` varchar(40) NOT NULL default '0',
      `ip_address` varchar(16) NOT NULL default '0',
      `user_agent` varchar(50) NOT NULL,
      `last_activity` int(10) unsigned NOT NULL default '0',
      `expiration_override` int(10) unsigned NOT NULL default '0',
      `session_data` text,
      PRIMARY KEY  (`session_id`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8

    or if you just want to update your existing table:

    ALTER TABLE ci_sessions ADD expiration_override int(10) unsigned NOT NULL default 0;

    with that done you make the following changes to your session handler:

    set class attribute for expiration override defaulted to 0:

    class CI_Session {
    
        var $CI;
        var $now;
        var $encryption        = TRUE;
        var $use_database    = FALSE;
        var $session_table    = FALSE;
        var $sess_length    = 7200;
        var $expiration_override = 0;
    [...]

    In sess_read() we read the expiration_override field in and then if it’s greater then zero we use it in favour of the config var:

    $session['session_id'] = $this->session_id;
    $session['ip_address'] = $row->ip_address;
    $session['user_agent'] = $row->user_agent;
    $session['last_activity'] = $row->last_activity;
    $session['expiration_override'] = $row->expiration_override;
    if ($session['expiration_override'] > 0)
    {
        $this->sess_length = $session['expiration_override'];
    }
    [...]

    in sess_write_database() make sure we’re writing the new field

    function sess_write_database()
        {
            // format data to match database structure
            $db_data = array
            (
                'ip_address'            => $this->userdata['ip_address'],
                'user_agent'            => $this->userdata['user_agent'],
                'last_activity'            => $this->userdata['last_activity'],
                'expiration_override'    => $this->userdata['expiration_override']
            );
    
            // now serialize only the userdata part
            $db_userdata = $this->userdata;
            unset($db_userdata['session_id']);
            unset($db_userdata['ip_address']);
            unset($db_userdata['user_agent']);
            unset($db_userdata['last_activity']);
            unset($db_userdata['expiration_override']);
            $db_data['session_data'] = serialize($db_userdata);
    
            // update database
            $this->CI->db->query($this->CI->db->update_string($this->session_table, $db_data, array('session_id' => $this->session_id)));
        }

    make sure sess_write writes the new value as well:

    $this->userdata = array(
                                'session_id'     => $this->session_id,
                                'ip_address'     => $this->CI->input->ip_address(),
                                'user_agent'     => substr($this->CI->input->user_agent(), 0, 50),
                                'last_activity'    => $this->now,
                                'expiration_override'    => $this->expiration_override
                                );

    and sess_update

    $this->userdata['session_id'] = $new_sessid;
            $this->userdata['last_activity'] = $this->now;
            $this->userdata['expiration_override'] = $this->expiration_override;

    add this public function which can be called from your controller:

    /*
        * This function overrides the session expiry and therefore can be used as a "remember me" or auto login.
        * Pass number of seconds to override.
        * 
        * @access    public
        * @param   integer
        * @return    void
        */    
        function override_expiration ($override)
        {
            $this->expiration_override = $override;
            $this->sess_length = $override;
            $this->sess_update($override);
            return TRUE;
        }

    Now in order to use it you can do something like this in your login controller:

    if ($_POST['remember_me'] == 1)
    {
        //extend session by two weeks
        $this->session->override_expiration(60 * 60 * 24 * 7 * 2);
    }

    That’s about it. I’ve only tested this with database sessions. It could probably be adapted to use cookie based sessions too.

    Hope someone finds it useful

  • #26 / Mar 10, 2008 6:28am

    WolfgangA

    46 posts

    Thank you for having choosen NGSession and thank you for your patch.
    After carefully reading here some thoughts on it:

    - NGSession claims to be a drop in replacement for the native CI Session and DBSession. This means the database table should be compatible or the code should take care of a different database structure.
    So how to implement without breaking compatibility?
    An idea could be to move the “expiration_override” into the userdata part, bc that is why userdata is serialized - to add fields without having to extend the db structure. (Like for example storing a user_id and the like…)

    - NGSession claims to work in both database and cookie mode.
    Well, taking the userdata approach it would work in both modi so this is not an issue then.

    - Should a remember_me have a global setting for the session lifetime? Example:
    In session config:

    //two weeks as default. 
    //to maintain compatibiltity, the seesion lib should have an internal default as well
    //if the config does not provide that value.
    remember_me_lifetime = 60 * 60 * 24 * 7 * 2;

    AND
    In application:

    remember_me = true;

    OR (no default in config, only via application)
    Meaning of “remember_me_lifetime” matches your “expiration_override” variable.

    remember_me_lifetime = 60 * 60 * 24 * 7 * 2; //example for two weeks, 0 = no remember me


    Pro - Having a default for the lifetime in the config: gives consistency

    Pro - Using the pure application setting: gives more flexibility to allow different user to have different lifetimes.

  • #27 / Mar 10, 2008 7:38am

    webthink

    170 posts

    OK That all sounds good. I did this mod first and foremost so that we could use it (the reason any of us bother to modify anything really 😉 ) in our applications at webthink… It was only after I’d finished that I thought about sharing it here, so the claims and mission statements of NG itself weren’t really my top priority but I do see your point. If you’re interested in integrating the functionality into NGSession then I don’t mind taking a shot at implementing your suggestions so that it doesn’t break from NG’s original intent.
    If however you want to keep it as is and let people implement their own solutions then that’s fine too.

    re: the remember_me_lifetime. I can see the value in both approaches however I think it’s probably best to allow it to be defined on the fly. The issues I had with session lifetime being defined as a config option are what lead me to do all this in the first place. This is in keeping with maintaining a drop in replacement for CI and DB_session (no extra config params). Also, as you say, it has the added benefit of allowing a per-user remember me value.

  • #28 / Mar 10, 2008 7:57am

    WolfgangA

    46 posts

    Ok i’ll implement this in NGSession and take the approach to allow it to be defined on the fly.
    Not sure wether I’ll provide a setter and mostlikely a getter (so the application does not need to keep track in cases where you want to know wether the user has activated this setting, for example in forms), or ask the developer to simply set/get a user var with an appropirate name like “remember_me_lifetime”.

    This way, NGSession keeps compatibilty and anyone in need for a remember me does not need to patch the lib.

    Regards

    Wolfgang

  • #29 / Mar 10, 2008 8:20am

    webthink

    170 posts

    well as for the setter you’ll need to force a new cookie using sess_update() I chose to put that in a setter (you can think of my function override_expiration ($override) as a setter). If you didn’t do this you’d need to detect the remember_me user var on the subsequent request and then call sess_update but the problem with that is that you’ll end up calling sess_update on *every* subsequent request.

  • #30 / Mar 11, 2008 6:04am

    WolfgangA

    46 posts

    Thank you for the hint.

.(JavaScript must be enabled to view this email address)

ExpressionEngine News!

#eecms, #events, #releases