a little more on session/class interaction

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

Post Reply
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

a little more on session/class interaction

Post by Luke »

So, I'm working with sessions (obviously) and I am just wondering... is it common to use a class to set/destroy/work with all session variables. If so, does anybody have any examples for me to look at? I was thinking of doing something like this...

My problem is that I still don't know a lot of the technical workings of php, so I'm not sure of the benefits of doing something like this

Code: Select all

class sessionHandle{
	function sessionHandle(){
		if (isset($session_save_path)) session_save_path($session_save_path);
		session_start();
	}
	function set_var($var_name, $var_val){
		$_SESSION[$var_name] = $var_val;
	}
	function kill_var($var_name){
		unset($_SESSION[$var_name]);
	}
	function kill_session(){
		$_SESSION = array();
		session_destroy();
		setcookie(session_name(),"",0,"/");
	}
}
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

You can do some neat (ish) things with a session class in a modular system.

If you break your session down into at least two dimensions you can store vars in their own part of the namespace. Then use setvar()/getvar() type methods to add and retreive data that persists as you move around the system, always just looking at the module in question.

Obviously things like flush() and unsetvar() would be needed too.

So basically yes, there are advantages to writing your own session class. Double it with your own session handling mechanism (DB?) and you've got a whole session solution :)
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

I'd be interested to see other peoples session classes look like. Here is the one I am using currently. It's PHP4 code, so no nice property handling.

Code: Select all

class Session {
    var $namespace;
    var $regenerate;
    
    function Session($namespace='Session', $regenerate=false) {
        $this->namespace = $namespace;
        $this->regenerate = $regenerate;
    }

    function start() {
        if (session_id() == '') {
            if (strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
                session_cache_limiter('must-revalidate');
            }
            session_start();
            if ($this->regenerate) {
                session_regenerate_id();
            }
        }
    }

    function get($name) {
        $this->start();
        return (isset($_SESSION[$this->namespace][$name]) ? $_SESSION[$this->namespace][$name] : null);
    }

    function set($name, $value) {
        $this->start();
    	$_SESSION[$this->namespace][$name] = $value;
    }

    function has($name) {
        $this->start();
    	return isset($_SESSION[$this->namespace][$name]);
    }

    function close() {
        session_write_close();
    }

}
My basic needs were namespacing, lazy start, plus support for regenerating ID and doing a write/close before redirecting.
(#10850)
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

I don't know what a name space is... see this is exactly the kind of answer I was looking for... can somebody please explain why this namespace idea is necessary/important?
Any other explanations of flush() and unsetvar() would be nice as well... of course I am going to go look up all of these terms, but when I come back, it would be nice to have some human explanation of this whole thing... thanks in advance!
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

I use the namespace value when I configure a session object for different parts of the application, or different applications combined. Then I can be a little more sure that I am not trampling one some other part's session vars that might have the same name.

To unset, I set with NULL. You could easily add another method if you want.

I am not sure what flush() is, but it may be the same as my close() which calls session_write_close() to write the current values in the session back to the datastore. The only place I use it is before redirects so the next page can get any session vars set in the redirecting page.
(#10850)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

arborint wrote:I use the namespace value when I configure a session object for different parts of the application, or different applications combined. Then I can be a little more sure that I am not trampling one some other part's session vars that might have the same name.

To unset, I set with NULL. You could easily add another method if you want.

I am not sure what flush() is, but it may be the same as my close() which calls session_write_close() to write the current values in the session back to the datastore. The only place I use it is before redirects so the next page can get any session vars set in the redirecting page.
What you've done looks incredibly similar to our stuff. I can't post the actual code we're using at work but I can show you similar concepts in my own personal code. The only really difference with what we have at work is that the modular system we use is DB backed where each module has an ID from the database, so these ID's define our namespaces and we don't actually need to specify them because the application already knows which module to look at (the session class is part of a larger collection of objects that make up the core of the app).

Take out the app specific stuff from it and you're essentially left with what you've done though.

flush() empties out all vars in the current namespace. Not really session_write_close() - it's more of a multiple unset().
User avatar
neophyte
DevNet Resident
Posts: 1537
Joined: Tue Jan 20, 2004 4:58 pm
Location: Minnesota

Post by neophyte »

Code: Select all

function start() {
        if (session_id() == '') {
            if (strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
                session_cache_limiter('must-revalidate');
            }
            session_start();
            if ($this->regenerate) {
                session_regenerate_id();
            }
        }
    }
Why are you doing this with IE?
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

I'm guessing it's because IE has standardisation issues...;). Far as I know it's recommended for SSL under IE. I also think I needed to use it one for a download in IE of a text file created by PHP, and directed to browser.
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

wouldn't the start() method be more appropriate as the constructor? If not, how come?

Also, is there some sort of built in session time-out? I thought I remembered my sessions timing out automatically, but I am using sessions right now and i left a connection open for a whole day... I refreshed, and it was fine. I realize I can build my own time-out, but I just don't want to re-invent the wheel.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

neophyte wrote:Why are you doing this with IE?
I found I was sticking that snippet into code here and there to get various things to work in IE (uploads, downloads, back from redirects, etc.) and so I just centralized it. I would love it if someone who actually understood compatablity told be how it really should be.
(#10850)
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

The Ninja Space Goat wrote:wouldn't the start() method be more appropriate as the constructor? If not, how come?
I have lazy start because I have found that it simplifies having to deal with headers sent on the pages that are not HTML. Again this may be a artifact of my using a Front Controller. It just makes it so session_start() is not called until and unless the session is actually used.
The Ninja Space Goat wrote:Also, is there some sort of built in session time-out? I thought I remembered my sessions timing out automatically, but I am using sessions right now and i left a connection open for a whole day... I refreshed, and it was fine. I realize I can build my own time-out, but I just don't want to re-invent the wheel.
I would be interested in adding the ability to control the time-out.
(#10850)
User avatar
John Cartwright
Site Admin
Posts: 11470
Joined: Tue Dec 23, 2003 2:10 am
Location: Toronto
Contact:

Post by John Cartwright »

My last contract I used CakePHP framework, and their session class is perhaps a bit more extensive than it needs to be.. nonetheless it is very usefull. Since you wanted to see what we use Aborint,

Code: Select all

<?php
/* SVN FILE: $Id: session.php 2528 2006-04-24 16:24:57Z phpnut $ */

/**
 * Session class for Cake.
 *
 * Cake abstracts the handling of sessions. There are several convenient methods to access session information. This class is the implementation of those methods. They are mostly used by the Session Component.
 *
 * PHP versions 4 and 5
 *
 * CakePHP :  Rapid Development Framework <http://www.cakephp.org/>
 * Copyright (c) 2006, Cake Software Foundation, Inc.
 *                     1785 E. Sahara Avenue, Suite 490-204
 *                     Las Vegas, Nevada 89104
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @filesource
 * @copyright    Copyright (c) 2006, Cake Software Foundation, Inc.
 * @link         http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
 * @package      cake
 * @subpackage   cake.cake.libs
 * @since        CakePHP v .0.10.0.1222
 * @version      $Revision: 2528 $
 * @modifiedby   $LastChangedBy: phpnut $
 * @lastmodified $Date: 2006-04-24 11:24:57 -0500 (Mon, 24 Apr 2006) $
 * @license      http://www.opensource.org/licenses/mit-license.php The MIT License
 */

/**
 * Session class for Cake.
 *
 * Cake abstracts the handling of sessions. There are several convenient methods to access session information.
 * This class is the implementation of those methods. They are mostly used by the Session Component.
 *
 * @package    cake
 * @subpackage cake.cake.libs
 * @since      CakePHP v .0.10.0.1222
 */
class CakeSession extends Object
{
/**
 * True if the Session is still valid
 *
 * @var boolean
 */
     var $valid = false;
/**
 * Error messages for this session
 *
 * @var array
 */
    var $error = false;
/**
 * User agent string
 *
 * @var string
 */
    var $_userAgent = false;
/**
 * Path to where the session is active.
 *
 * @var string
 */
    var $path = false;
/**
 * Error number of last occurred error
 *
 * @var integer
 */
    var $lastError = null;
/**
 * CAKE_SECURITY setting, "high", "medium", or "low".
 *
 * @var string
 */
    var $security = null;
/**
 * Start time for this session.
 *
 * @var integer
 */
    var $time = false;
/**
 * Time when this session becomes invalid.
 *
 * @var integer
 */
    var $sessionTime = false;
/**
 * Constructor.
 *
 * @param string $base The base path for the Session
 */
    function __construct($base = null)
    {
        $this->host = env('HTTP_HOST');

        if (empty($base))
        {
            $this->path = '/';
        }
        else
        {
            $this->path = $base;
        }

        if (strpos($this->host, ':') !== false)
        {
            $this->host = substr($this->host,0, strpos($this->host, ':'));
        }

        if(env('HTTP_USER_AGENT') != null)
        {
            $this->_userAgent = md5(env('HTTP_USER_AGENT').CAKE_SESSION_STRING);
        }
        else
        {
            $this->_userAgent = "";
        }

        $this->time = time();
        $this->sessionTime = $this->time + (Security::inactiveMins() * CAKE_SESSION_TIMEOUT);
        $this->security = CAKE_SECURITY;
        if (function_exists('session_write_close'))
        {
            session_write_close();
        }

        $this->__initSession();

        session_cache_limiter("must-revalidate");
        session_start();

        if (!isset($_SESSION))
        {
        	$this->__begin();
        }

        $this->__checkValid();
        parent::__construct();
    }

/**
 * Returns true if given variable is set in session.
 *
 * @param string $name Variable name to check for
 * @return boolean True if variable is there
 */
    function checkSessionVar($name)
    {
        $expression = "return isset(".$this->__sessionVarNames($name).");";
        return eval($expression);
    }

/**
 * Removes a variable from session.
 *
 * @param string $name Session variable to remove
 * @return boolean Success
 */
    function delSessionVar($name)
    {
        if($this->checkSessionVar($name))
        {
            $var = $this->__sessionVarNames($name);
            eval("unset($var);");
            return true;
        }
        $this->__setError(2, "$name doesn't exist");
        return false;
    }

/**
 * Return error description for given error number.
 *
 * @param int $errorNumber
 * @return string Error as string
 */
    function getError($errorNumber)
    {
        if(!is_array($this->error) || !array_key_exists($errorNumber, $this->error))
        {
            return false;
        }
        else
        {
        return $this->error[$errorNumber];
        }
    }

/**
 * Returns last occurred error as a string, if any.
 *
 * @return mixed Error description as a string, or false.
 */
    function getLastError()
    {
        if($this->lastError)
        {
            return $this->getError($this->lastError);
        }
        else
        {
            return false;
        }
    }

/**
 * Returns true if session is valid.
 *
 * @return boolean
 */
    function isValid()
    {
        return $this->valid;
    }

/**
 * Returns given session variable, or all of them, if no parameters given.
 *
 * @param mixed $name
 * @return unknown
 */
    function readSessionVar($name = null)
    {
        if(is_null($name))
        {
            return $this->returnSessionVars();
        }

        if($this->checkSessionVar($name))
        {
            $result = eval("return ".$this->__sessionVarNames($name).";");
            return $result;
        }
        $this->__setError(2, "$name doesn't exist");
        $return = null;
        return $return;
    }

/**
 * Returns all session variables.
 *
 * @return mixed Full $_SESSION array, or false on error.
 */
    function returnSessionVars()
    {
        if(!empty($_SESSION))
        {
            $result = eval("return \$_SESSION;");
            return $result;
        }
        $this->__setError(2, "No Session vars set");
        return false;
    }

/**
 * Writes value to given session variable name.
 *
 * @param mixed $name
 * @param string $value
 */
    function writeSessionVar($name, $value)
    {
        $expression = $this->__sessionVarNames($name);
        $expression .= " = \$value;";
        eval($expression);
    }

/**
 * Begins a session.
 *
 * @access private
 */
    function __begin()
    {
        header('P3P: CP="NOI ADM DEV PSAi COM NAV OUR OTRo STP IND DEM"');
    }

/**
 * Enter description here...
 *
 * @access private
 */
    function __close()
    {
        return true;
    }

/**
 * Enter description here...
 *
 * @param unknown_type $key
 * @return unknown
 * @access private
 */
    function __destroy($key)
    {
    	$db =& ConnectionManager::getDataSource('default');
    	$db->execute("DELETE FROM ".$db->name('cake_sessions')." WHERE ".$db->name('cake_sessions.id')." = ".$db->value($key, 'integer'));
    	return true;
    }

/**
 * Private helper method to destroy invalid sessions.
 *
 * @access private
 */
    function __destroyInvalid()
    {
        $sessionpath = session_save_path();
        if (empty($sessionpath))
        {
            $sessionpath = "/tmp";
        }
        if (isset($_COOKIE[session_name()]))
        {
            setcookie(CAKE_SESSION_COOKIE, '', time()-42000, $this->path);
        }
        $file = $sessionpath.DS."sess_".session_id();
        @session_destroy();
        @unlink($file);
        $this->__construct($this->path);
    }

/**
 * Enter description here...
 *
 * @param unknown_type $expires
 * @return unknown
 * @access private
 */
    function __gc($expires)
    {
		$db =& ConnectionManager::getDataSource('default');
    	$db->execute("DELETE FROM ".$db->name('cake_sessions')." WHERE ".$db->name('cake_sessions.expires')." < " . $db->value(time()));
    	return true;
    }

/**
 * Private helper method to initialize a session, based on Cake core settings.
 *
 * @access private
 */
    function __initSession()
    {
        switch ($this->security)
        {
            case 'high':
                $this->cookieLifeTime = 0;
                if(function_exists('ini_set'))
                {
                    ini_set('session.referer_check', $this->host);
                }
            break;
            case 'medium':
                $this->cookieLifeTime = 7 * 86400;
            break;
            case 'low':
            default :
                $this->cookieLifeTime = 788940000;
            break;
        }

        switch (CAKE_SESSION_SAVE)
        {
            case 'cake':
                if(!isset($_SESSION))
                {
                    if(function_exists('ini_set'))
                    {
                        ini_set('session.use_trans_sid', 0);
                        ini_set('url_rewriter.tags', '');
                        ini_set('session.serialize_handler', 'php');
                        ini_set('session.use_cookies', 1);
                        ini_set('session.name', CAKE_SESSION_COOKIE);
                        ini_set('session.cookie_lifetime', $this->cookieLifeTime);
                        ini_set('session.cookie_path', $this->path);
                        ini_set('session.gc_probability', 1);
                        ini_set('session.auto_start', 0);
                        ini_set('session.save_path', TMP.'sessions');
                    }
                }
            break;
            case 'database':
                if(!isset($_SESSION))
                {
                    if(function_exists('ini_set'))
                    {
                        ini_set('session.use_trans_sid', 0);
                        ini_set('url_rewriter.tags', '');
                        ini_set('session.save_handler', 'user');
                        ini_set('session.serialize_handler', 'php');
                        ini_set('session.use_cookies', 1);
                        ini_set('session.name', CAKE_SESSION_COOKIE);
                        ini_set('session.cookie_lifetime', $this->cookieLifeTime);
                        ini_set('session.cookie_path', $this->path);
                        ini_set('session.gc_probability', 1);
                        ini_set('session.auto_start', 0);
                    }
                }
                session_set_save_handler(array('CakeSession', '__open'),
                                         array('CakeSession', '__close'),
                                         array('CakeSession', '__read'),
                                         array('CakeSession', '__write'),
                                         array('CakeSession', '__destroy'),
                                         array('CakeSession', '__gc'));
            break;
            case 'php':
                if(!isset($_SESSION))
                {
                    if(function_exists('ini_set'))
                    {
                        ini_set('session.use_trans_sid', 0);
                        ini_set('session.name', CAKE_SESSION_COOKIE);
                        ini_set('session.cookie_lifetime', $this->cookieLifeTime);
                        ini_set('session.cookie_path', $this->path);
                        ini_set('session.gc_probability', 1);
                    }
                }
            break;
            default:
                if(!isset($_SESSION))
                {
                    $config = CONFIGS.CAKE_SESSION_SAVE.'.php';
                    if(is_file($config))
                    {
                        require_once($config);
                    }
                }
                else
                {
                    if(!isset($_SESSION))
                    {
                        if(function_exists('ini_set'))
                        {
                            ini_set('session.use_trans_sid', 0);
                            ini_set('session.name', CAKE_SESSION_COOKIE);
                            ini_set('session.cookie_lifetime', $this->cookieLifeTime);
                            ini_set('session.cookie_path', $this->path);
                            ini_set('session.gc_probability', 1);
                        }
                    }
                }
            break;
        }
    }

/**
 * Private helper method to create a new session.
 *
 * @access private
 *
 */
    function __checkValid()
    {
        if($this->readSessionVar("Config"))
        {
            if($this->_userAgent == $this->readSessionVar("Config.userAgent")
                && $this->time <= $this->readSessionVar("Config.time"))
            {
                $this->writeSessionVar("Config.time", $this->sessionTime);
                $this->valid = true;
            }
            else
            {
                $this->valid = false;
                $this->__setError(1, "Session Highjacking Attempted !!!");
                $this->__destroyInvalid();
            }
        }
        else
        {
            srand((double)microtime() * 1000000);
            $this->writeSessionVar('Config.rand', rand());
            $this->writeSessionVar("Config.time", $this->sessionTime);
            $this->writeSessionVar("Config.userAgent", $this->_userAgent);
            $this->valid = true;
            $this->__setError(1, "Session is valid");
        }
    }

/**
 * Enter description here... To be implemented.
 *
 * @access private
 *
 */
    function __open()
    {
        return true;
    }

/**
 * Enter description here...
 *
 * @param unknown_type $key
 * @return unknown
 * @access private
 */
    function __read($key)
    {
        $db =& ConnectionManager::getDataSource('default');

        $row = $db->query("SELECT ".$db->name('cake_sessions.data')." FROM ".$db->name('cake_sessions')." WHERE ".$db->name('cake_sessions.id')." =  ".$db->value($key), false);

		if ($row && $row[0]['cake_sessions']['data'])
		{
			return $row[0]['cake_sessions']['data'];
		}
		else
		{
			return false;
		}
    }

/**
 * Private helper method to restart a session.
 *
 *
 * @access private
 *
 */
    function __regenerateId()
    {
        $oldSessionId = session_id();
        $sessionpath = session_save_path();
        if (empty($sessionpath))
        {
            $sessionpath = "/tmp";
        }
        if (isset($_COOKIE[session_name()]))
        {
            setcookie(CAKE_SESSION_COOKIE, '', time()-42000, $this->path);
        }
        session_regenerate_id();
        $newSessid = session_id();
        $file = $sessionpath.DS."sess_$oldSessionId";
        @unlink($file);
        @session_destroy($oldSessionId);
        if (function_exists('session_write_close'))
        {
            session_write_close();
        }
        $this->__initSession();
        session_id($newSessid);
        session_start();
    }

/**
 * Restarts this session.
 *
 * @access public
 *
 */
    function renew()
    {
        $this->__regenerateId();
    }

/**
 * Private helper method to extract variable names.
 *
 * @param mixed $name Variable names as array or string.
 * @return string
 * @access private
 */
    function __sessionVarNames($name)
    {
        if(is_string($name))
        {
            if(strpos($name, "."))
            {
                $names = explode(".", $name);
            }
            else
            {
                $names = array($name);
            }
            $expression = "\$_SESSION";

            foreach($names as $item)
            {
                $expression .= is_numeric($item) ? "[$item]" : "['$item']";
            }
            return $expression;
        }
        $this->__setError(3, "$name is not a string");
        return false;
    }

/**
 * Private helper method to set an internal error message.
 *
 * @param int $errorNumber Number of the error
 * @param string $errorMessage Description of the error
 * @access private
 */
    function __setError($errorNumber, $errorMessage)
    {
        if($this->error === false)
        {
            $this->error = array();
        }

        $this->error[$errorNumber] = $errorMessage;
        $this->lastError = $errorNumber;
    }


/**
 * Enter description here...
 *
 * @param unknown_type $key
 * @param unknown_type $value
 * @return unknown
 * @access private
 */
    function __write($key, $value)
    {
        $db =& ConnectionManager::getDataSource('default');

        switch (CAKE_SECURITY)
        {
            case 'high':
        		$factor = 10;
        		break;
        	case 'medium':
        		$factor = 100;
        		break;
        	case 'low':
        		$factor = 300;
        		break;

        	default:
        		$factor = 10;
        		break;
        }

        $expires = time() + CAKE_SESSION_TIMEOUT * $factor;

		$row = $db->query("SELECT COUNT(id) AS count FROM ".$db->name('cake_sessions')." WHERE ".$db->name('cake_sessions.id')." = ".$db->value($key), false);

		if($row[0][0]['count'] > 0)
		{
			$db->execute("UPDATE ".$db->name('cake_sessions')." SET ".$db->name('cake_sessions.data')." = ".$db->value($value).", ".$db->name('cake_sessions.expires')." = ".$db->value($expires)." WHERE ".$db->name('cake_sessions.id')." = ".$db->value($key));
		}
		else
		{
			$db->execute("INSERT INTO ".$db->name('cake_sessions')." (".$db->name('cake_sessions.data').",".$db->name('cake_sessions.expires').",".$db->name('cake_sessions.id').") VALUES (".$db->value($value).", ".$db->value($expires).", ".$db->value($key).")");
		}
		return true;
    }
}
?>
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

Jcart wrote:My last contract I used CakePHP framework, and their session class is perhaps a bit more extensive than it needs to be.. nonetheless it is very usefull. Since you wanted to see what we use Aborint,
Really interesting code. I just spent a few minutes looking around in it. I am not sure I like the 3-in-1 design (including DB functions) as multiple classes would probably be cleaner, but I see why they did what they did.

I notice that they always do session_cache_limiter("must-revalidate"); as opposed to only for IE. Anyone know if one is better than another or if it matters?

There are a couple of features that I have questions about. First is the security features. Do you think that a Session class should do the User Agent / Timestamp security check that Cake does? And are they actually effective or can they be easily spoofed?

Second is if you found their path access to session vars useful or necessary? They allow dot separated paths to sub-arrays and eval() everything. I have done that in the past but rarely used it, so I wondered if this is a feature that actually gets used? Especially because of all the code to support it, plus the performance hit and potential security problems of using eval().

Also, the P3P line is interesting. Anyone know if that is universal or have more info about P3P?
(#10850)
Post Reply