symphony/lib/core/class.session.php
<?php
/**
* @package core
*/
/**
* The Session class is a handler for all Session related logic in PHP. The functions
* map directly to all handler functions as defined by session_set_save_handler in
* PHP. In Symphony, this function is used in conjunction with the `Cookie` class.
* Based on: http://php.net/manual/en/function.session-set-save-handler.php#81761
* by klose at openriverbed dot de which was based on
* http://php.net/manual/en/function.session-set-save-handler.php#79706 by
* maria at junkies dot jp
*
* @link http://php.net/manual/en/function.session-set-save-handler.php
*/
class Session
{
/**
* If a Session has been created, this will be true, otherwise false
*
* @var boolean
*/
private static $_initialized = false;
/**
* Disallow public construction
*/
private function __construct()
{
}
/**
* Starts a Session object, only if one doesn't already exist. This function maps
* the Session Handler functions to this classes methods by reading the default
* information from the PHP ini file.
*
* @link http://php.net/manual/en/function.session-set-save-handler.php
* @link http://php.net/manual/en/function.session-set-cookie-params.php
* @param integer $lifetime
* How long a Session is valid for, by default this is 0, which means it
* never expires
* @param string $path
* The path the cookie is valid for on the domain
* @param string $domain
* The domain this cookie is valid for
* @param boolean $httpOnly
* Whether this cookie can be read by Javascript. By default the cookie
* cannot be read by Javascript
* @throws Exception
* @return string|boolean
* Returns the Session ID on success, or false on error.
*/
public static function start($lifetime = 0, $path = '/', $domain = null, $httpOnly = true)
{
if (!self::$_initialized) {
if (!is_object(Symphony::Database()) || !Symphony::Database()->isConnected()) {
throw new Exception('Failed to start session, no Database found.');
}
// Get config
$gcDivisor = Symphony::Configuration()->get('session_gc_divisor', 'symphony');
$strictDomain = Symphony::Configuration()->get('session_strict_domain', 'symphony') === 'yes';
// Set php parameters
if (session_id() == '' && !headers_sent()) {
ini_set('session.use_trans_sid', '0');
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.gc_maxlifetime', $lifetime);
ini_set('session.gc_probability', '1');
ini_set('session.gc_divisor', $gcDivisor);
}
// Register handler
$handler = new Session;
session_set_save_handler(
[$handler ,'open'],
[$handler ,'close'],
[$handler ,'read'],
[$handler ,'write'],
[$handler ,'destroy'],
[$handler ,'gc']
);
// Set cookie parameters
if ($strictDomain) {
// setting the domain to null makes the cookie valid for the current host only
$domain = null;
} else {
$domain = $domain ? : $handler->getDomain();
}
session_set_cookie_params(
$lifetime,
$handler->createCookieSafePath($path),
$domain,
defined('__SECURE__') && __SECURE__,
$httpOnly
);
session_cache_limiter('');
if (!session_id()) {
if (headers_sent()) {
throw new Exception('Headers already sent. Cannot start session.');
}
register_shutdown_function('session_write_close');
session_start();
}
self::$_initialized = true;
}
return session_id();
}
/**
* Returns a properly formatted ascii string for the cookie path.
* Browsers are notoriously bad at parsing the cookie path. They do not
* respect the content-encoding header. So we must be careful when dealing
* with setups with special characters in their paths.
*
* @since Symphony 2.7.0
**/
protected function createCookieSafePath($path)
{
$path = array_filter(explode('/', $path));
if (empty($path)) {
return '/';
}
$path = array_map('rawurlencode', $path);
return '/' . implode('/', $path);
}
/**
* Returns the current domain for the Session to be saved to, if the installation
* is on localhost, this returns null and just allows PHP to take care of setting
* the valid domain for the Session, otherwise it will return the non-www version
* of the domain host.
*
* @return string|null
* Null if on localhost, or HTTP_HOST is not set, a string of the domain name sans
* www otherwise
*/
public function getDomain()
{
if (HTTP_HOST) {
if (preg_match('/(localhost|127\.0\.0\.1)/', HTTP_HOST)) {
return null; // prevent problems on local setups
}
// Remove leading www and ending :port
return preg_replace('/(^www\.|:\d+$)/i', null, HTTP_HOST);
}
return null;
}
/**
* Allows the Session to open without any further logic.
*
* @return boolean
* Always returns true
*/
public function open()
{
return true;
}
/**
* Allows the Session to close without any further logic. Acts as a
* destructor function for the Session.
*
* @return boolean
* Always returns true
*/
public function close()
{
return true;
}
/**
* Given an ID, and some data, save it into `tbl_sessions`. This uses
* the ID as a unique key, and will override any existing data. If the
* `$data` is deemed to be empty, no row will be saved in the database
* unless there is an existing row.
*
* @param string $id
* The ID of the Session, usually a hash
* @param string $data
* The Session information, usually a serialized object of
* `$_SESSION[Cookie->_index]`
* @throws DatabaseException
* @return boolean
* true if the Session information was saved successfully, false otherwise
*/
public function write($id, $data)
{
// Only prevent this record from saving if there isn't already a record
// in the database. This prevents empty Sessions from being created, but
// allows them to be nulled.
$session_data = $this->read($id);
if (!$session_data) {
$empty = true;
if (function_exists('session_status') && session_status() === PHP_SESSION_ACTIVE) {
$unserialized_data = $this->unserialize($data);
foreach ($unserialized_data as $d) {
if (!empty($d)) {
$empty = false;
}
}
if ($empty) {
return true;
}
// PHP 7.0 makes the session inactive in write callback,
// so we try to detect empty sessions without decoding them
} elseif ($data === Symphony::Configuration()->get('cookie_prefix', 'symphony') . '|a:0:{}') {
return true;
}
}
$fields = array(
'session' => $id,
'session_expires' => time(),
'session_data' => $data
);
return Symphony::Database()
->insert('tbl_sessions')
->values($fields)
->updateOnDuplicateKey()
->execute()
->success();
}
/**
* Given raw session data return the unserialized array.
* Used to check if the session is really empty before writing.
*
* @since Symphony 2.3.3
* @param string $data
* The serialized session data
* @return array
* The unserialised session data
*/
private function unserialize($data)
{
$hasBuffer = isset($_SESSION);
$buffer = $_SESSION;
session_decode($data);
$session = $_SESSION;
if ($hasBuffer) {
$_SESSION = $buffer;
} else {
unset($_SESSION);
}
return $session;
}
/**
* Given a session's ID, return it's row from `tbl_sessions`
*
* @param string $id
* The identifier for the Session to fetch
* @return string
* The serialised session data
*/
public function read($id)
{
if (!$id) {
return null;
}
return Symphony::Database()
->select(['session_data'])
->from('tbl_sessions')
->where(['session' => $id])
->limit(1)
->execute()
->string('session_data');
}
/**
* Given a session's ID, remove it's row from `tbl_sessions`
*
* @param string $id
* The identifier for the Session to destroy
* @throws DatabaseException
* @return boolean
* true if the Session was deleted successfully, false otherwise
*/
public function destroy($id)
{
if (!$id) {
return true;
}
return Symphony::Database()
->delete('tbl_sessions')
->where(['session' => $id])
->execute()
->success();
}
/**
* The garbage collector, which removes all empty Sessions, or any
* Sessions that have expired. This has a 10% chance of firing based
* off the `gc_probability`/`gc_divisor`.
*
* @param integer $max
* The max session lifetime.
* @throws DatabaseException
* @return boolean
* true on Session deletion, false if an error occurs
*/
public function gc($max)
{
return Symphony::Database()
->delete('tbl_sessions')
->where(['session_expires' => ['<=' => time() - $max]])
->execute()
->success();
}
}