pickles/classes/Session.php
Josh Sherman 7f52efdbde Skip session start from CLI
Added check of the IS_CLI constant to determine if we should start the session or not.
2011-11-20 11:19:27 -05:00

357 lines
8.6 KiB
PHP

<?php
/**
* Session Handling for PICKLES
*
* PHP version 5
*
* Licensed under The MIT License
* Redistribution of these files must retain the above copyright notice.
*
* @author Josh Sherman <josh@gravityblvd.com>
* @copyright Copyright 2007-2011, Josh Sherman
* @license http://www.opensource.org/licenses/mit-license.html
* @package PICKLES
* @link http://p.ickl.es
*/
/**
* Session Class
*
* Provides session handling via database instead of the file based session
* handling built into PHP. Using this class requires an array to be defined
* in place of the boolean true/false (on/off). If simply array(), the
* datasource will default to the value in $config['pickles']['datasource'] and
* if the table will default to "sessions". The format is as follows:
*
* $config = array(
* 'pickles' => array(
* 'session' => array(
* 'datasource' => 'mysql',
* 'table' => 'sessions',
* )
* )
* );
*
* In addition to the configuration variables, a table in your database must
* be created. The [MySQL] table schema is as follows:
*
* CREATE TABLE sessions (
* id varchar(32) COLLATE utf8_unicode_ci NOT NULL,
* session text COLLATE utf8_unicode_ci NOT NULL,
* expires_at datetime NOT NULL,
* PRIMARY KEY (id)
* ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
*
* Note: The reason for not using a model class was to avoid a naming conflict
* between the Session model and the Session class itself. This will eventually
* be resolved when I abandon full 5.x support and migrate to 5.3+ (assuming
* that ever happens).
*/
class Session extends Object
{
/**
* Handler
*
* What the session is being handled by.
*
* @access private
* @var string
*/
private $handler = false;
/**
* Accessed At
*
* The UNIX timestamp of when the page was accessed.
*
* @access private
* @var integer
*/
private $accessed_at = null;
/**
* Time to Live
*
* The number of seconds the session should remain active. Corresponds to
* the INI variable session.gc_maxlifetime
*
* @access private
* @var integer
*/
private $time_to_live = null;
/**
* Datasource
*
* Name of the datasource, defaults to whatever the default datasource
* is defined to in config.php
*
* @access private
* @var string
*/
private $datasource = null;
/**
* Table
*
* Name of the database table in the aforementioned datasource that holds
* the session data. The expected schema is defined above.
*
* @access private
* @var string
*/
private $table = null;
/**
* Database
*
* Our database object to interact with the aforementioned datasource and
* table. This object is shared with other PICKLES internals.
*
* @access private
* @var object
*/
private $db = null;
/**
* Constructor
*
* All of our set up logic for the session in contained here. This object
* is initially instantiated from pickles.php and the session callbacks are
* established here. All variables are driven from php.ini and/or the site
* config. Once configured, the session is started automatically.
*/
public function __construct()
{
if (!IS_CLI && (isset($_REQUEST['request']) == false || preg_match('/^__pickles\/(css|js)\/.+$/', $_REQUEST['request']) == false))
{
parent::__construct();
// Sets up our configuration variables
$session = $this->config->pickles['session'];
$datasources = $this->config->datasources;
$datasource = false;
$table = 'sessions';
if (is_array($session))
{
if (isset($session['handler']) && in_array($session['handler'], array('files', 'memcache', 'mysql')))
{
$this->handler = $session['handler'];
if ($this->handler != 'files')
{
if (isset($session['datasource']))
{
$datasource = $session['datasource'];
}
if (isset($session['table']))
{
$table = $session['table'];
}
}
}
}
else
{
if ($session === true || $session == 'files')
{
$this->handler = 'files';
}
elseif ($session == 'memcache')
{
$this->handler = 'memcache';
$datasource = 'memcached';
}
elseif ($session == 'mysql')
{
$this->handler = 'mysql';
$datasource = 'mysql';
}
}
switch ($this->handler)
{
case 'files':
ini_set('session.save_handler', 'files');
session_start();
break;
case 'memcache':
$hostname = 'localhost';
$port = 11211;
if ($datasource !== false && isset($datasources[$datasource]))
{
$hostname = $datasources[$datasource]['hostname'];
$port = $datasources[$datasource]['port'];
}
ini_set('session.save_handler', 'memcache');
ini_set('session.save_path', 'tcp://' . $hostname . ':' . $port . '?persistent=1&amp;weight=1&amp;timeout=1&amp;retry_interval=15');
session_start();
break;
case 'mysql':
if ($datasource !== false && isset($datasources[$datasource]))
{
// Sets our access time and time to live
$this->accessed_at = time();
$this->time_to_live = ini_get('session.gc_maxlifetime');
$this->datasource = $datasource;
$this->table = $table;
// Gets a database instance
$this->db = Database::getInstance($this->datasource);
// Initializes the session
$this->initialize();
session_start();
}
else
{
throw new Exception('Unable to determine which datasource to use');
}
break;
}
}
}
/**
* Destructor
*
* Runs garbage collection and closes the session. I'm not sure if the
* garbage collection should stay as it could be accomplished via php.ini
* variables. The session_write_close() is present to combat a chicken
* and egg scenario in earlier versions of PHP 5.
*/
public function __destruct()
{
if ($this->handler == 'mysql')
{
$this->gc($this->time_to_live);
session_write_close();
}
}
/**
* Initializes the Session
*
* This method exists to combat the fact that calling session_destroy()
* also clears out the save handler. Upon destorying a session this method
* is called again so the save handler is all set.
*/
public function initialize()
{
// Sets up the session 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');
}
/**
* Opens the Session
*
* Since the session is in the database, opens the database connection.
* This step isn't really necessary as the Database object is smart enough
* to open itself up upon execute.
*/
public function open()
{
session_regenerate_id();
return $this->db->open();
}
/**
* Closes the Session
*
* Same as above, but in reverse.
*/
public function close()
{
return $this->db->close();
}
/**
* Reads the Session
*
* Checks the database for the session ID and returns the session data.
*
* @param string $id session ID
* @return string serialized session data
*/
public function read($id)
{
$sql = 'SELECT session FROM `' . $this->table . '` WHERE id = ?;';
$session = $this->db->fetch($sql, array($id));
return isset($session[0]['session']) ? $session[0]['session'] : '';
}
/**
* Writes the Session
*
* When there's changes to the session, writes the data to the database.
*
* @param string $id session ID
* @param string $session serialized session data
* @return boolean whether the query executed correctly
*/
public function write($id, $session)
{
$sql = 'REPLACE INTO `' . $this->table . '` VALUES (?, ? ,?);';
$parameters = array($id, $session, date('Y-m-d H:i:s', strtotime('+' . $this->time_to_live . ' seconds')));
return $this->db->execute($sql, $parameters);
}
/**
* Destroys the Session
*
* Deletes the session from the database.
*
* @param string $id session ID
* @return boolean whether the query executed correctly
*/
public function destroy($id)
{
$sql = 'DELETE FROM `' . $this->table . '` WHERE id = ?;';
return $this->db->execute($sql, array($id));
}
/**
* Garbage Collector
*
* This is who you call when you got trash to be taken out.
*
* @param integer $time_to_live number of seconds a session is active
* @return boolean whether the query executed correctly
*/
public function gc($time_to_live)
{
$sql = 'DELETE FROM `' . $this->table . '` WHERE expires_at < ?;';
$parameters = array(date('Y-m-d H:i:s', $this->accessed_at - $time_to_live));
return $this->db->execute($sql, $parameters);
}
}
?>