398 lines
11 KiB
PHP
398 lines
11 KiB
PHP
<?php
|
|
/*
|
|
* This MongoDB session handler is intended to store any data you see fit.
|
|
* One interesting optimization to note is the setting of the active flag
|
|
* to 0 when a session has expired. The intended purpose of this garbage
|
|
* collection is to allow you to create a batch process for removal of
|
|
* all expired sessions. This should most likely be implemented as a cronjob
|
|
* script.
|
|
*
|
|
* @author Corey Ballou
|
|
* @copyright Corey Ballou (2010)
|
|
*
|
|
*/
|
|
class MongoSession {
|
|
|
|
// default config with support for multiple servers
|
|
// (helpful for sharding and replication setups)
|
|
protected $_config = array(
|
|
// cookie related vars
|
|
'cookie_path' => '/',
|
|
'cookie_domain' => '.mydomain.com', // .mydomain.com
|
|
|
|
// session related vars
|
|
'lifetime' => 3600, // session lifetime in seconds
|
|
'database' => 'session', // name of MongoDB database
|
|
'collection' => 'session', // name of MongoDB collection
|
|
|
|
// persistent related vars
|
|
'persistent' => false, // persistent connection to DB?
|
|
'persistentId' => 'MongoSession', // name of persistent connection
|
|
|
|
// whether we're supporting replicaSet
|
|
'replicaSet' => false,
|
|
|
|
// array of mongo db servers
|
|
'servers' => array(
|
|
array(
|
|
'host' => Mongo::DEFAULT_HOST,
|
|
'port' => Mongo::DEFAULT_PORT,
|
|
'username' => null,
|
|
'password' => null
|
|
)
|
|
)
|
|
);
|
|
|
|
// stores the connection
|
|
protected $_connection;
|
|
|
|
// stores the mongo db
|
|
protected $_mongo;
|
|
|
|
// stores session data results
|
|
protected $_session;
|
|
|
|
/**
|
|
* Default constructor.
|
|
*
|
|
* @access public
|
|
* @param array $config
|
|
*/
|
|
public function __construct($config = array())
|
|
{
|
|
// initialize the database
|
|
$this->_init(empty($config) ? $this->_config : $config);
|
|
|
|
// set object as the save handler
|
|
session_set_save_handler(
|
|
array(&$this, 'open'),
|
|
array(&$this, 'close'),
|
|
array(&$this, 'read'),
|
|
array(&$this, 'write'),
|
|
array(&$this, 'destroy'),
|
|
array(&$this, 'gc')
|
|
);
|
|
|
|
// set some important session vars
|
|
ini_set('session.auto_start', 0);
|
|
ini_set('session.gc_probability', 1);
|
|
ini_set('session.gc_divisor', 100);
|
|
ini_set('session.gc_maxlifetime', $this->_config['lifetime']);
|
|
ini_set('session.referer_check', '');
|
|
ini_set('session.entropy_file', '/dev/urandom');
|
|
ini_set('session.entropy_length', 16);
|
|
ini_set('session.use_cookies', 1);
|
|
ini_set('session.use_only_cookies', 1);
|
|
ini_set('session.use_trans_sid', 0);
|
|
ini_set('session.hash_function', 1);
|
|
ini_set('session.hash_bits_per_character', 5);
|
|
|
|
// disable client/proxy caching
|
|
session_cache_limiter('nocache');
|
|
|
|
// set the cookie parameters
|
|
session_set_cookie_params(
|
|
$this->_config['lifetime'],
|
|
$this->_config['cookie_path'],
|
|
$this->_config['cookie_domain']
|
|
);
|
|
|
|
// name the session
|
|
session_name('mongo_sess');
|
|
|
|
// start it up
|
|
session_start();
|
|
}
|
|
|
|
/**
|
|
* Initialize MongoDB. There is currently no support for persistent
|
|
* connections. It would be very easy to implement, I just didn't need it.
|
|
*
|
|
* @access private
|
|
* @param array $config
|
|
*/
|
|
private function _init($config)
|
|
{
|
|
// ensure they supplied a database
|
|
if (empty($config['database'])) {
|
|
throw new Exception('You must specify a MongoDB database to use for session storage.');
|
|
}
|
|
|
|
if (empty($config['collection'])) {
|
|
throw new Exception('You must specify a MongoDB collection to use for session storage.');
|
|
}
|
|
|
|
// update config
|
|
$this->_config = $config;
|
|
|
|
// generate server connection strings
|
|
$connections = array();
|
|
if (!empty($this->_config['servers'])) {
|
|
foreach ($this->_config['servers'] as $server) {
|
|
$str = '';
|
|
if (!empty($server['username']) && !empty($server['password'])) {
|
|
$str .= $server['username'] . ':' . $server['password'] . '@';
|
|
}
|
|
$str .= !empty($server['host']) ? $server['host'] : Mongo::DEFAULT_HOST;
|
|
$str .= ':' . (!empty($server['port']) ? (int) $server['port'] : Mongo::DEFAULT_PORT);
|
|
array_push($connections, $str);
|
|
}
|
|
} else {
|
|
// use default connection settings
|
|
array_push($connections, Mongo::DEFAULT_HOST . ':' . Mongo::DEFAULT_PORT);
|
|
}
|
|
|
|
// add immediate connection
|
|
$opts = array('connect' => true);
|
|
|
|
// support persistent connections
|
|
if ($this->_config['persistent'] && !empty($this->_config['persistentId'])) {
|
|
$opts['persist'] = $this->_config['persistentId'];
|
|
}
|
|
|
|
// support replica sets
|
|
if ($this->_config['replicaSet']) {
|
|
$opts['replicaSet'] = true;
|
|
}
|
|
|
|
// load mongo server connection
|
|
try {
|
|
$this->_connection = new Mongo('mongodb://' . implode(',', $connections), $opts);
|
|
} catch (Exception $e) {
|
|
throw new Exception('Can\'t connect to the MongoDB server.');
|
|
}
|
|
|
|
// load the db
|
|
try {
|
|
$mongo = $this->_connection->selectDB($this->_config['database']);
|
|
} catch (InvalidArgumentException $e) {
|
|
throw new Exception('The MongoDB database specified in the config does not exist.');
|
|
}
|
|
|
|
// load collection
|
|
try {
|
|
$this->_mongo = $mongo->selectCollection($this->_config['collection']);
|
|
} catch(Exception $e) {
|
|
throw new Exception('The MongoDB collection specified in the config does not exist.');
|
|
}
|
|
|
|
// proper indexing on the expiration
|
|
$this->_mongo->ensureIndex(
|
|
array('expiry' => 1),
|
|
array('name' => 'expiry',
|
|
'unique' => true,
|
|
'dropDups' => true,
|
|
'safe' => true
|
|
)
|
|
);
|
|
|
|
// proper indexing of session id and lock
|
|
$this->_mongo->ensureIndex(
|
|
array('session_id' => 1, 'lock' => 1),
|
|
array('name' => 'session_id',
|
|
'unique' => true,
|
|
'dropDups' => true,
|
|
'safe' => true
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Open does absolutely nothing as we already have an open connection.
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
public function open($save_path, $session_name)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Close does absolutely nothing as we can assume __destruct handles
|
|
* things just fine.
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
public function close()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Read the session data.
|
|
*
|
|
* @access public
|
|
* @param string $id
|
|
* @return string
|
|
*/
|
|
public function read($id)
|
|
{
|
|
// obtain a read lock on the data, or subsequently wait for
|
|
// the lock to be released
|
|
$this->_lock($id);
|
|
|
|
// exclude results that are inactive or expired
|
|
$result = $this->_mongo->findOne(
|
|
array(
|
|
'session_id' => $id,
|
|
'expiry' => array('$gte' => time()),
|
|
'active' => 1
|
|
)
|
|
);
|
|
|
|
if (isset($result['data'])) {
|
|
$this->_session = $result;
|
|
return $result['data'];
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Atomically write data to the session, ensuring we remove any
|
|
* read locks.
|
|
*
|
|
* @access public
|
|
* @param string $id
|
|
* @param mixed $data
|
|
* @return bool
|
|
*/
|
|
public function write($id, $data)
|
|
{
|
|
// create expires
|
|
$expiry = time() + $this->_config['lifetime'];
|
|
|
|
// create new session data
|
|
$new_obj = array(
|
|
'data' => $data,
|
|
'lock' => 0,
|
|
'active' => 1,
|
|
'expiry' => $expiry
|
|
);
|
|
|
|
// check for existing session for merge
|
|
if (!empty($this->_session)) {
|
|
$obj = (array) $this->_session;
|
|
$new_obj = array_merge($obj, $new_obj);
|
|
}
|
|
|
|
// atomic update
|
|
$query = array('session_id' => $id);
|
|
|
|
// update options
|
|
$options = array(
|
|
'upsert' => TRUE,
|
|
'safe' => TRUE,
|
|
'fsync' => TRUE
|
|
);
|
|
|
|
// perform the update or insert
|
|
try {
|
|
$result = $this->_mongo->update($query, array('$set' => $new_obj), $options);
|
|
return $result['ok'] == 1;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Destroys the session by removing the document with
|
|
* matching session_id.
|
|
*
|
|
* @access public
|
|
* @param string $id
|
|
* @return bool
|
|
*/
|
|
public function destroy($id)
|
|
{
|
|
$this->_mongo->remove(array('session_id' => $id), true);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Garbage collection. Remove all expired entries atomically.
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
public function gc()
|
|
{
|
|
// define the query
|
|
$query = array('expiry' => array('$lt' => time()));
|
|
|
|
// specify the update vars
|
|
$update = array('$set' => array('active' => 0));
|
|
|
|
// update options
|
|
$options = array(
|
|
'multiple' => TRUE,
|
|
'safe' => TRUE,
|
|
'fsync' => TRUE
|
|
);
|
|
|
|
// update expired elements and set to inactive
|
|
$this->_mongo->update($query, $update, $options);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Solves issues with write() and close() throwing exceptions.
|
|
*
|
|
* @access public
|
|
* @return void
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
session_write_close();
|
|
}
|
|
|
|
/**
|
|
* Create a global lock for the specified document.
|
|
*
|
|
* @author Benson Wong (mostlygeek@gmail.com)
|
|
* @access private
|
|
* @param string $id
|
|
*/
|
|
private function _lock($id)
|
|
{
|
|
$remaining = 30000000;
|
|
$timeout = 5000;
|
|
|
|
do {
|
|
|
|
try {
|
|
|
|
$query = array('session_id' => $id, 'lock' => 0);
|
|
$update = array('$set' => array('lock' => 1));
|
|
$options = array('safe' => true, 'upsert' => true);
|
|
$result = $this->_mongo->update($query, $update, $options);
|
|
if ($result['ok'] == 1) {
|
|
return true;
|
|
}
|
|
|
|
} catch (MongoCursorException $e) {
|
|
if (substr($e->getMessage(), 0, 26) != 'E11000 duplicate key error') {
|
|
throw $e; // not a dup key?
|
|
}
|
|
}
|
|
|
|
// force delay in microseconds
|
|
usleep($timeout);
|
|
$remaining -= $timeout;
|
|
|
|
// backoff on timeout, save a tree. max wait 1 second
|
|
$timeout = ($timeout < 1000000) ? $timeout * 2 : 1000000;
|
|
|
|
} while ($remaining > 0);
|
|
|
|
// aww shit.
|
|
throw new Exception('Could not obtain a session lock.');
|
|
}
|
|
|
|
}
|