PDA

View Full Version : Session class.



marek_mar
12-29-2005, 11:38 PM
This is a session class that has nothing to do with PHP sessions. It uses PDO so it should be usable with any DB PDO supports. The PHP requirements are quite high. You need PHP5.1.1 for it to run.
Ok but what can this thing do?

It does everything what the PHP sessions would do (Ionclusing the usage of the $_SESSION superglobal)
It checks wether the user wa inactive for too long.
Obviously it checks if the session didn't expire.
It checks if the users IP/User Agent are the same.
It regenerates the sid every n seconds (which makes it harder to steal the session)
Everything listed above can be configured.



<?php
/**
* Session Class
*
* @author Marek_mar
*
*/

class session
{
/*
This is the table you need.
CREATE TABLE `TABLE_SESSIONS`
(
`sid` varchar(32) NOT NULL default '0',
`last_page` varchar(200) NOT NULL,
`session_start` int(11) NOT NULL,
`last_active` int(11) NOT NULL,
`last_sid_change` int(11) NOT NULL,
`session_ip` varchar(40) NOT NULL,
`session_agent` varchar(150) NOT NULL,
`data` text NOT NULL,
PRIMARY KEY (`sid`)
);

*/

public $sid = false,
$session_data = array();

private $db = false,
$use_cookies = false,
$cookie_data,
$new = false,
$time = 0,
$db_sid = '';

private $fields = array('sid', 'last_page', 'session_start', 'last_active', 'last_sid_change', 'session_ip', 'session_agent');
// These fileds are in the DB and should not be stored with the other data.

private $config = array(
'session_max_time' => 3600, // 1 hour
'session_max_inactive' => 600, // 10 minutes
'sid_regeneration_interval' => 60, // Regenerate the sid every n seconds
'cookie_name' => 'my_session', // The cookie name
'check_ip' => true, // Should the IP be checked
'check_agent' => true, // Should the user agent be checked
);

public function __construct(&$db)
{
$this->db = $db;
$this->time = time();
$_SESSION = array();
$this->session_data =& $_SESSION;


// Ok one way or another we are ready to check for the sid.
if($this->getCookieData())
{ // Cookies enabled.
return true;
}
else if(isset($_REQUEST['sid']) && (strlen($_REQUEST['sid']) > 0))
{ // We do not have cookies enabled.
$this->sid = $_REQUEST['sid'];
}
else
{ // No session.
$this->create();
return 0;
}
return true;
// Ok we we can start the session any time now.
}

public function start()
{
if($this->new)
{
return true;
}
// Let's check if a session exists.
$sql = 'SELECT * FROM `' . TABLE_SESSIONS . '`
WHERE `sid` = ' . $this->db->quote($this->sid) . ' LIMIT 1;';
try
{
$result = $this->db->query($sql);
}
catch (PDOException $e)
{
print "Error!: " . $e->getMessage() . "<br/>";
die();
}
$data = $result->fetchAll(PDO::FETCH_ASSOC);
if(isset($data[0]))
{ // Session exists.
$data = $data[0];
if($data['last_active'] + $this->cfg('session_max_inactive', 3600) < $this->time)
{ // Either the user was inactive for too long.
$this->create();
return 1;
}
if($data['session_start'] + $this->cfg('session_max_time', 3600) < $this->time)
{ // The session expired.
$this->create();
return 2;
}
if($this->cfg('check_ip', false) && $data['session_ip'] != $_SERVER['REMOTE_ADDR'])
{ // INvalid IP.
$this->create();
return 3;
}
if($this->cfg('check_agent', true) && $data['session_agent'] != $_SERVER['HTTP_USER_AGENT'])
{ // INvalid useragent.
$this->create();
return 4;
}
// Ok the session should be valid by this point.

// Updating the needed variables.
$data['last_active'] = $this->time;
$data['last_page'] = substr($_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING'], 0, 200);

// Merging with the other values.
$other_data = unserialize($data['data']);
unset($data['data']); // We need to get rid of this becouse someone might want to add a $_SESSION['data'] index.
$data = array_merge($data, $other_data);

$this->db_sid = $data['sid'];

// Does the sid need regenerating?
if($data['last_sid_change'] + $this->cfg('sid_regeneration_interval', 60) < $this->time)
{ // Yes it does.
$data['last_sid_change'] = $this->time;
$data['sid'] = $this->generate_sid();
// The sid will be simply updated so no unused sessions will be there.
}

$this->session_data = $data;

if($this->use_cookies)
{
$this->setSessionCookie('_sid', array('sid' => $this->session_data['sid']));
}
// For the users use.
$GLOBALS['SID'] = '?sid=' . (($this->use_cookies) ? '' : $this->session_data['sid']);
define('SID', $GLOBALS['SID']);

return true;
}
// Session does not exist.
$this->create();
return 5;
}

private function create()
{ // Default settings.
$this->sid = $this->generate_sid();
$this->session_data = array(
'sid' => $this->sid,
'session_start' => $this->time,
'last_active' => $this->time,
'last_sid_change' => $this->time,
'last_page' => substr($_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING'], 0, 200),
'session_ip' => $_SERVER['REMOTE_ADDR'],
'session_agent' => substr($_SERVER['HTTP_USER_AGENT'], 0, 150)
);

$this->new = true;
// All data set.

// We assume cookies are enabled.
$this->setSessionCookie('_sid', array('sid' => $this->session_data['sid']));

// For the users use.
if(!defined('SID'))
{
$GLOBALS['SID'] = '?sid=' . (($this->use_cookies) ? '' : $this->session_data['sid']);
define('SID', $GLOBALS['SID']);
}
return true;
}

private function generate_sid()
{
return md5($this->time . uniqid() . $_SERVER['REMOTE_ADDR'] . 'salt'); // Overkill?
}

function __destruct()
{

foreach($this->fields as $key)
{
if(!in_array($key, $this->fields))
{
continue;
}
$sql_array[$key] = $this->session_data[$key];
unset($this->session_data[$key]);
}
$sql_array['data'] = serialize($this->session_data);
if($this->new)
{
$sql = $this->build_query('insert', TABLE_SESSIONS, $sql_array);
}
else
{
$sql = $this->build_query('update', TABLE_SESSIONS, $sql_array, 'sid', $this->db_sid);
}

try
{
$this->db->query($sql);
}
catch (PDOException $e)
{
$data = "Error!: " . $e->getMessage() . "<br/>";
die();
}
if(!rand(0, 9))
{ // 10% chance of gc (Garbage Collection - Deletion of old sessions)
$this->gc();
}
return true;
}

function gc()
{
$sql = 'DELETE FROM `' . TABLE_SESSIONS . '`
WHERE `session_start` < ' . ($this->time - $this->cfg('session_max_time', 3600)) . '
OR `last_active` < ' . ($this->time - $this->cfg('session_max_inactive', 600)) . ';';
try
{
$this->db->query($sql);
}
catch (PDOException $e)
{
print "Error!: " . $e->getMessage() . "<br/>";
die();
}
return true;
}

// This is something that proves that this class was written to work with other classes / functions.
function cfg($cfg, $default = false)
{
if(!isset($this->config[$cfg]))
{
if($default)
{
return $default;
}
else
{
return -1;
}
}
return $this->config[$cfg];
}

private function setSessionCookie($suffix, $data, $expires = false)
{
if(!is_array($data))
{
$data = array($data);
}
return setcookie($this->cfg('cookie_name') . $suffix, serialize($data), $this->time + (($expires) ? $expires : $this->cfg('session_max_time', 3600)));
}

private function getCookieData()
{
if(isset($_COOKIE[$this->cfg('cookie_name') . '_sid']))
{
$this->use_cookies = true;
$this->cookie_data = unserialize($_COOKIE[$this->cfg('cookie_name') . '_sid']);
$this->sid = $this->cookie_data['sid'];
return true;
}
return false;
}
private function build_query($type, $table, $array, $where = false, $value = false)
{ // Remember this?
$type = strtoupper($type);
switch($type)
{
case 'UPDATE':
$ret = array();
foreach($array as $k => $v)
{
if(is_array($v))
{
$ret[] = '`' . $k . '` = `' . $k . '`' . $v[0];
}
else
{
$ret[] = '`' . $k . '` = ' . $this->db->quote($v);
}
}
$ret = 'SET ' . implode(', ', $ret);
if($where && $value)
{
$ret .= ' WHERE `' . $where . '` = ' . $this->db->quote($value);
}
break;
case 'INSERT':
$type = 'INSERT INTO';
foreach($array as $k => $v)
{
$array[$k] = $this->db->quote($v);
}
$ret = '(`' . implode('`, `', array_keys($array)) . '`) VALUES (' . implode(', ', $array) . ')';
break;
}
return $type . ' `' . $table . '` ' . $ret;
}
}
?>

The constructor expects a PDO class.


$db = new PDO('mysql:host=localhost;dbname=session', 'root', ''); // These are the default mysql DB settings. The table name is "session"
// You should define the session table name:
define('TABLE_SESSIONS', 'sessions');
$session = new session($db);

After that all you need to do is staret the session


$session->start();

session::start() doesn't expect any arguments.
It returns an error code (integer) or bool(true); See example.

Another method you can use it the session::cfg() method. It returns sessions configuration values.


print $session->cfg('session_max_time');


After the session was started you will have acces to the $_SESSION superglobal. It will have a vew variables predefined* (they may come in handy). If you change/delete them the class might stop working. You can set any other variables just like with normal PHP sessions.

Example:


<?php

$db = new PDO('mysql:host=localhost;dbname=session', 'root', ''); // These are the default mysql DB settings. The table name is "session"
define('TABLE_SESSIONS', 'sessions');
$error = array(
'New session',
'You were inactive for too long.',
'Your session expired.',
'You IP doesn\'t match the IP with which the session was started.',
'Your User Agent doesn\'t match the User Agent with which the session was started.',
'Session does not exist.'
);

$session = new session($db);
$error_code = $session->start();
if($error_code !== true)
{
print '<span style="color:red">' . $error[$error_code] . '</span>' . "<br />\n";
}
print '<a href="' . $_SERVER['PHP_SELF'] . SID . '">Test </a>' . "<br />\n";
if(!isset($_SESSION['clicks']))
{
$_SESSION['clicks'] = 0;
}
else
{
$_SESSION['clicks']++;
}
if(!isset($_SESSION['testing']))
{
$_SESSION['testing'] = time();
}
print 'You were inactive for ' . (time() - $_SESSION['testing']) . ' seconds!' . ((time() - $_SESSION['testing'] == 0) ? ' You are so imptient!' : '' ) . "<br />\n";
print 'This session will last only for another ' . (($session->cfg('session_max_time') + $_SESSION['session_start']) - time()) . ' seconds!' . "<br />\n";
$_SESSION['testing'] = time();
print 'This is the ' . $_SESSION['clicks'] . ' page you visited this session.' . "<br />\n";
print 'Your sid should have regenerated ' . floor((time() - $_SESSION['session_start']) / $session->cfg('sid_regeneration_interval')) . ' times by now.' . "<br />\n";
?>

Have fun!
* These are those predefined values: 'sid', 'last_page', 'session_start', 'last_active', 'last_sid_change', 'session_ip' and 'session_agent'.
I've wanted to post this for a while now... I just waited for a suitable date...

dumpfi
12-30-2005, 02:32 AM
Few notes on your script:

Why does the constructor take the db-argument by reference? I mean your code is for PHP 5. And objects are passed by reference by default.

I see that the start() method returns error codes. Wouldn't it be more safe to define those error codes as class constants (maybe along with corresponding error messages):

// instead of
$errorCode = $sessObj->start();
if($errorCode == 3)
{
echo 'Invalid user agent.'; // ops, errorCode 3 means "invalid IP" !
}
// use this:
$errorCode = $sessObj->start();
if($errorCode == session::ECODE_INVALID_UAGENT)
{
echo session::EMSG_INVALID_UAGENT;
}

Both in start() and create you use exactly the same code:

// For the users use.
$GLOBALS['SID'] = '?sid=' . (($this->use_cookies) ? '' : $this->session_data['sid']);
define('SID', $GLOBALS['SID']);Maybe, make a new method for this for easier maintainance?

setSessionCookie() expects a suffix argument for the cookie name. However, this suffix is hard-coded in the calling functions (and thus can't be changed without changing the code). Furthermore, getCookieData() does not expect a suffix argument and assumes that the suffix, which pass the calling functions to setSessionCookie(), will not change. Thus, the suffix argument from setSessinCookie() is completely useless. Either drop it, or make the suffix a public property, which is used both by setSessionCookie() and getCookieData().

Imho, the build_query() method is misplaced. The few lines of sql could be easily hard-coded, because 1) they don't change and 2) there is still not the least database abstraction (maybe use adodb or something similar or let the user optionally implement some sort of storing / retrieving the data for better abstraction?).

Your code relies heavily on a populated $_SERVER global. However, there might php configurations which don't register $_SERVER for performance (such as mine, although I have auto_globals_jit on, so the $_SERVER variables are still created, when needed). Using getenv('WHATEVER') instead of $_SERVER['WHATEVER'] your code will run on such configurations.

There is no apparent option to disable the session id regeneration other than setting the sid_regeneration_interval configuration variable to a high value, which seems unintuitive (for the lack of a better word). This is especially bad with tabbed browsing (or new windows), if you create a new tab of the same site, do something there, go back to the previous site and click a link: BANG! you're not logged in anymore, because the session id has changed in the new tab, but not in the other.

dumpfi

marek_mar
06-10-2006, 05:40 PM
I updated this class.


Added: Error constants were added.
Added method: session::set_config(array $config). Accepts an array which may replace the values of the session::config array.
Added method: sesion::sid($url). This mewthod will automaticly add a sid to the url if it is needed.
Added config option: regenerate_sid. If set to false the sid will not be regenerated.
Added config option: gc_probability. The probability of executing garbage collection (in percent, 0 disables).
Added/Changed config option: table. This options replaces the TABLE_SESSION constant. It stores the table name.
Added/Changed property: session::use_cookies. If session::use_cookies is true you can be sure that the user accepts cookies.
Added: Lots of comments to the methods and properties.

Changed: The constructor doesn't take the arguments as references... not that it changes anything but it was not needed.
Changed: Same code is now a method session::set_user_vars()
Changed: The cookie methods are now sorted and renamed to match the naming convention of other methods.
Changed: getenv() is used for the environment variables.

Fixed: Queries should be compatible with MySQL, MSSQL, SQLite, Oracle and Firebird (Though only a MySQL schema is given in the comments). As before PDO is the DBAL.
Fixed: If a query fails an exception is thrown and cought. Before the code was there but no exceptions were thrown.

Removed: session::build_query() was removed as it was not needed.

Example usage:


<?php
require_once 'session.class.php';
$db = new PDO('mysql:host=localhost;dbname=session', 'root', '');

$error = array(
session::NEW_SESSION => 'New session',
session::ERROR_USER_INACTIVE => 'You were inactive for too long.',
session::ERROR_SESSION_EXPIRED => 'Your session expired.',
session::ERROR_IP_CHANGED => 'You IP doesn\'t match the IP with which the session was started.',
session::ERROR_UA_CHANGED => 'Your User Agent doesn\'t match the User Agent with which the session was started.',
session::ERROR_NO_SESSION => 'Session does not exist.'
);

$session = new session($db);

$session->set_config(array('table' => 'sessions'));

$error_code = $session->start();
if($error_code !== true)
{
print '<span style="color:red">' . $error[$error_code] . '</span>' . "<br />\n";
}
print '<a href="' . $_SERVER['PHP_SELF'] . SID . '">Test </a>' . "<br />\n";
if(!isset($_SESSION['clicks']))
{
$_SESSION['clicks'] = 0;
}
else
{
$_SESSION['clicks']++;
}
if(!isset($_SESSION['testing']))
{
$_SESSION['testing'] = time();
}
print 'You were inactive for ' . (time() - $_SESSION['testing']) . ' seconds!' . ((time() - $_SESSION['testing'] == 0) ? ' You are so imptient!' : '' ) . "<br />\n";
print 'This session will last only for another ' . (($session->cfg('session_max_time') + $_SESSION['session_start']) - time()) . ' seconds!' . "<br />\n";
$_SESSION['testing'] = time();
print 'This is the ' . $_SESSION['clicks'] . ' page you visited this session.' . "<br />\n";
print 'Your sid should have regenerated ' . floor((time() - $_SESSION['session_start']) / $session->cfg('sid_regeneration_interval')) . ' times by now.' . "<br />\n";
?>

Class is attached.
Do you agree that this update took too long?

I just fixed a few things that got broken when I fixed the previous set of things :)

d11wtq
06-11-2006, 01:24 PM
I'm pretty enthusiastic about design but I'm curious. What advantages does your session class have over using PHP sessions with your own session save handler?

marek_mar
06-11-2006, 02:22 PM
Apart form the fact that you would have to write a PHP session handler yourself?
The class behaves like a real class you can extend as a class for the PHP handler is just a set of methods put together quite loosely as the PHP handler expects functions not a class.

Oh and re-read my previous post as I edited it.

marek_mar
06-27-2006, 12:25 AM
I've fixed some small bugs and made a few cahnges

Added method: session::is_new(). The method returns true if the session was just created. This is usefull when you wan't the session not to be new on some pages (form handling pages and such).
Added config option: force_input. This setting forces a input method for the sid either get, post, cookie or request (default and allows all methods).
Added session::sid() argument $force_append (defaults to false). If set to true the sid will be appended no matter if it is needed or not.
Updated: the sample code to use the session::is_new() method.



<?php
ob_start();
require_once 'session.class.php';

$db = new PDO('mysql:host=localhost;dbname=session', 'root', 'root');

$error = array(
session::NEW_SESSION => 'New session',
session::ERROR_USER_INACTIVE => 'You were inactive for too long.',
session::ERROR_SESSION_EXPIRED => 'Your session expired.',
session::ERROR_IP_CHANGED => 'You IP doesn\'t match the IP with which the session was started.',
session::ERROR_UA_CHANGED => 'Your User Agent doesn\'t match the User Agent with which the session was started.',
session::ERROR_NO_SESSION => 'Session does not exist.'
);

$session = new session($db);

$session->set_config(array('table' => 'sessions'));

$error_code = $session->start();
if($error_code !== true)
{
print '<span style="color:red">' . $error[$error_code] . '</span>' . "<br />\n";
}
if($session->is_new())
{
print '<span style="color:green">New Session Created</span>' . "<br />\n";
}
print '<a href="' . $session->sid($_SERVER['PHP_SELF']) . '">Test </a>' . "<br />\n";
if(!isset($_SESSION['clicks']))
{
$_SESSION['clicks'] = 0;
}
else
{
$_SESSION['clicks']++;
}
if(!isset($_SESSION['testing']))
{
$_SESSION['testing'] = time();
}
print 'You were inactive for ' . (time() - $_SESSION['testing']) . ' seconds!' . ((time() - $_SESSION['testing'] == 0) ? ' You are so imptient!' : '' ) . "<br />\n";
print 'This session will last only for another ' . (($session->cfg('session_max_time') + $_SESSION['session_start']) - time()) . ' seconds!' . "<br />\n";
$_SESSION['testing'] = time();
print 'This is the ' . $_SESSION['clicks'] . ' page you visited this session.' . "<br />\n";
print 'Your sid should have regenerated ' . floor((time() - $_SESSION['session_start']) / $session->cfg('sid_regeneration_interval')) . ' times by now.' . "<br />\n";
$_SESSION['type_test'] = (float) 12.656;
var_dump($_SESSION);
ob_end_flush();
?>

Is anyone using this class?

marek_mar
09-10-2006, 02:47 AM
This time it is a bigger update.

Added: An internal database abstraction layer (or rather a php database extension abstraction) which will enable you to use this class without PDO but also with the MySQL, MySQLi extensions as well as SQLite, PostgreSQL and Oracle (note anything non-MySQL or non PDO works only in theory).
Changed: The last_page and last_active were updated when the session started making the values useless for the coder. Now they are updated in the destructor.
Changed: The session will now only start when session::start() is called.
Fixed: Getting the sid from a cookie failed when magic quotes was enabled.

A few words about the DBAL.
The session class will only accept an instance of the internal_dbal class.
You use internal_dbal::start() to create an instance of the class.
The first parameter is either a resource (MySQL, SQLite, PG, Oracle) or a class instance (PDO or MySQLi). The method will detect what you are using and return the appropriate object. One of the nicer features of the dbal is how it handles SQL errors (see screen shot) especially when used with GeSHi (http://qbnz.com/highlighter/). This error reporting is disabled by default. You can enable it by setting the constants DEBUG (and SPECIAL_BACKTRACE) to true in the internal_dbal class.

I'd be thankful if some would test these classes.