blob: c0e62affa7b8e0a8b5d59dd0b15d0f4376253d83 [file] [log] [blame]
<?php
/**
* CodeIgniter
*
* An open source application development framework for PHP
*
* This content is released under the MIT License (MIT)
*
* Copyright (c) 2014 - 2015, British Columbia Institute of Technology
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @package CodeIgniter
* @author EllisLab Dev Team
* @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
* @copyright Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
* @license http://opensource.org/licenses/MIT MIT License
* @link http://codeigniter.com
* @since Version 1.0.0
* @filesource
*/
defined('BASEPATH') OR exit('No direct script access allowed');
/**
* Cookie-based session management driver
*
* This is the classic CI_Session functionality, as written by EllisLab, abstracted out to a driver.
*
* @package CodeIgniter
* @subpackage Libraries
* @category Sessions
* @author EllisLab Dev Team
* @link http://codeigniter.com/user_guide/libraries/sessions.html
*/
class CI_Session_cookie extends CI_Session_driver {
/**
* Whether to encrypt the session cookie
*
* @var bool
*/
public $sess_encrypt_cookie = FALSE;
/**
* Whether to use to the database for session storage
*
* @var bool
*/
public $sess_use_database = FALSE;
/**
* Name of the database table in which to store sessions
*
* @var string
*/
public $sess_table_name = '';
/**
* Length of time (in seconds) for sessions to expire
*
* @var int
*/
public $sess_expiration = 7200;
/**
* Whether to kill session on close of browser window
*
* @var bool
*/
public $sess_expire_on_close = FALSE;
/**
* Whether to match session on ip address
*
* @var bool
*/
public $sess_match_ip = FALSE;
/**
* Whether to match session on user-agent
*
* @var bool
*/
public $sess_match_useragent = TRUE;
/**
* Name of session cookie
*
* @var string
*/
public $sess_cookie_name = 'ci_session';
/**
* Session cookie prefix
*
* @var string
*/
public $cookie_prefix = '';
/**
* Session cookie path
*
* @var string
*/
public $cookie_path = '';
/**
* Session cookie domain
*
* @var string
*/
public $cookie_domain = '';
/**
* Whether to set the cookie only on HTTPS connections
*
* @var bool
*/
public $cookie_secure = FALSE;
/**
* Whether cookie should be allowed only to be sent by the server
*
* @var bool
*/
public $cookie_httponly = FALSE;
/**
* Interval at which to update session
*
* @var int
*/
public $sess_time_to_update = 300;
/**
* Key with which to encrypt the session cookie
*
* @var string
*/
public $encryption_key = '';
/**
* Timezone to use for the current time
*
* @var string
*/
public $time_reference = 'local';
/**
* Session data
*
* @var array
*/
public $userdata = array();
/**
* Current time
*
* @var int
*/
public $now;
// ------------------------------------------------------------------------
/**
* Default userdata keys
*
* @var array
*/
protected $defaults = array(
'session_id' => NULL,
'ip_address' => NULL,
'user_agent' => NULL,
'last_activity' => NULL
);
/**
* Data needs DB update flag
*
* @var bool
*/
protected $data_dirty = FALSE;
/**
* Standardize newlines flag
*
* @var bool
*/
protected $_standardize_newlines;
// ------------------------------------------------------------------------
/**
* Initialize session driver object
*
* @return void
*/
protected function initialize()
{
// Set all the session preferences, which can either be set
// manually via the $params array or via the config file
$prefs = array(
'sess_encrypt_cookie',
'sess_use_database',
'sess_table_name',
'sess_expiration',
'sess_expire_on_close',
'sess_match_ip',
'sess_match_useragent',
'sess_cookie_name',
'cookie_path',
'cookie_domain',
'cookie_secure',
'cookie_httponly',
'sess_time_to_update',
'time_reference',
'cookie_prefix',
'encryption_key',
);
$this->_standardize_newlines = (bool) config_item('standardize_newlines');
foreach ($prefs as $key)
{
$this->$key = isset($this->_parent->params[$key])
? $this->_parent->params[$key]
: $this->CI->config->item($key);
}
if (empty($this->encryption_key))
{
show_error('In order to use the Cookie Session driver you are required to set an encryption key in your config file.');
}
// Do we need encryption? If so, load the encryption class
if ($this->sess_encrypt_cookie === TRUE)
{
$this->CI->load->library('encryption');
}
// Check for database
if ($this->sess_use_database === TRUE && $this->sess_table_name !== '')
{
// Load database driver
$this->CI->load->database();
// Register shutdown function
register_shutdown_function(array($this, '_update_db'));
}
// Set the "now" time. Can either be GMT or server time, based on the config prefs.
// We use this to set the "last activity" time
$this->now = $this->_get_time();
// Set the session length. If the session expiration is
// set to zero we'll set the expiration two years from now.
if ($this->sess_expiration === 0)
{
$this->sess_expiration = (60*60*24*365*2);
}
// Set the cookie name
$this->sess_cookie_name = $this->cookie_prefix.$this->sess_cookie_name;
// Run the Session routine. If a session doesn't exist we'll
// create a new one. If it does, we'll update it.
if ( ! $this->_sess_read())
{
$this->_sess_create();
}
else
{
$this->_sess_update();
}
// Delete expired sessions if necessary
$this->_sess_gc();
}
// ------------------------------------------------------------------------
/**
* Write the session data
*
* @return void
*/
public function sess_save()
{
// Check for database
if ($this->sess_use_database === TRUE)
{
// Mark custom data as dirty so we know to update the DB
$this->data_dirty = TRUE;
}
// Write the cookie
$this->_set_cookie();
}
// ------------------------------------------------------------------------
/**
* Destroy the current session
*
* @return void
*/
public function sess_destroy()
{
// Kill the session DB row
if ($this->sess_use_database === TRUE && isset($this->userdata['session_id']))
{
$this->CI->db->delete($this->sess_table_name, array('session_id' => $this->userdata['session_id']));
$this->data_dirty = FALSE;
}
// Kill the cookie
$this->_setcookie($this->sess_cookie_name, '', ($this->now - 31500000),
$this->cookie_path, $this->cookie_domain, 0);
// Kill session data
$this->userdata = array();
}
// ------------------------------------------------------------------------
/**
* Regenerate the current session
*
* Regenerate the session id
*
* @param bool Destroy session data flag (default: false)
* @return void
*/
public function sess_regenerate($destroy = FALSE)
{
// Check destroy flag
if ($destroy)
{
// Destroy old session and create new one
$this->sess_destroy();
$this->_sess_create();
}
else
{
// Just force an update to recreate the id
$this->_sess_update(TRUE);
}
}
// ------------------------------------------------------------------------
/**
* Get a reference to user data array
*
* @return array Reference to userdata
*/
public function &get_userdata()
{
return $this->userdata;
}
// ------------------------------------------------------------------------
/**
* Fetch the current session data if it exists
*
* @return bool
*/
protected function _sess_read()
{
// Fetch the cookie
$session = $this->CI->input->cookie($this->sess_cookie_name);
// No cookie? Goodbye cruel world!...
if ($session === NULL)
{
log_message('debug', 'A session cookie was not found.');
return FALSE;
}
if ($this->sess_encrypt_cookie === TRUE)
{
$session = $this->CI->encryption->decrypt($session);
if ($session === FALSE)
{
log_message('error', 'Session: Unable to decrypt the session cookie, possibly due to a HMAC mismatch.');
return FALSE;
}
}
else
{
if (($len = strlen($session) - 40) <= 0)
{
log_message('error', 'Session: The session cookie was not signed.');
return FALSE;
}
// Check cookie authentication
$hmac = substr($session, $len);
$session = substr($session, 0, $len);
// Time-attack-safe comparison
$hmac_check = hash_hmac('sha1', $session, $this->encryption_key);
$diff = 0;
for ($i = 0; $i < 40; $i++)
{
$diff |= ord($hmac[$i]) ^ ord($hmac_check[$i]);
}
if ($diff !== 0)
{
log_message('error', 'Session: HMAC mismatch. The session cookie data did not match what was expected.');
$this->sess_destroy();
return FALSE;
}
}
// Unserialize the session array
$session = @unserialize($session);
// Is the session data we unserialized an array with the correct format?
if ( ! is_array($session) OR ! isset($session['session_id'], $session['ip_address'], $session['user_agent'], $session['last_activity']))
{
log_message('debug', 'Session: Wrong cookie data format');
$this->sess_destroy();
return FALSE;
}
// Is the session current?
if (($session['last_activity'] + $this->sess_expiration) < $this->now OR $session['last_activity'] > $this->now)
{
log_message('debug', 'Session: Expired');
$this->sess_destroy();
return FALSE;
}
// Does the IP match?
if ($this->sess_match_ip === TRUE && $session['ip_address'] !== $this->CI->input->ip_address())
{
log_message('debug', 'Session: IP address mismatch');
$this->sess_destroy();
return FALSE;
}
// Does the User Agent Match?
if ($this->sess_match_useragent === TRUE &&
trim($session['user_agent']) !== trim(substr($this->CI->input->user_agent(), 0, 120)))
{
log_message('debug', 'Session: User Agent string mismatch');
$this->sess_destroy();
return FALSE;
}
// Is there a corresponding session in the DB?
if ($this->sess_use_database === TRUE)
{
$this->CI->db->where('session_id', $session['session_id']);
if ($this->sess_match_ip === TRUE)
{
$this->CI->db->where('ip_address', $session['ip_address']);
}
if ($this->sess_match_useragent === TRUE)
{
$this->CI->db->where('user_agent', $session['user_agent']);
}
// Is caching in effect? Turn it off
$db_cache = $this->CI->db->cache_on;
$this->CI->db->cache_off();
$query = $this->CI->db->get($this->sess_table_name);
// Was caching in effect?
if ($db_cache)
{
// Turn it back on
$this->CI->db->cache_on();
}
// No result? Kill it!
if (empty($query) OR $query->num_rows() === 0)
{
log_message('debug', 'Session: No match found in our database');
$this->sess_destroy();
return FALSE;
}
// Is there custom data? If so, add it to the main session array
$row = $query->row();
if ( ! empty($row->user_data))
{
$custom_data = unserialize(trim($row->user_data));
if (is_array($custom_data))
{
$session = $session + $custom_data;
}
}
}
// Session is valid!
$this->userdata = $session;
return TRUE;
}
// ------------------------------------------------------------------------
/**
* Create a new session
*
* @return void
*/
protected function _sess_create()
{
// Initialize userdata
$this->userdata = array(
'session_id' => $this->_make_sess_id(),
'ip_address' => $this->CI->input->ip_address(),
'user_agent' => trim(substr($this->CI->input->user_agent(), 0, 120)),
'last_activity' => $this->now,
);
log_message('debug', 'Session: Creating new session ('.$this->userdata['session_id'].')');
// Check for database
if ($this->sess_use_database === TRUE)
{
// Add empty user_data field and save the data to the DB
$this->CI->db->set('user_data', '')->insert($this->sess_table_name, $this->userdata);
}
// Write the cookie
$this->_set_cookie();
}
// ------------------------------------------------------------------------
/**
* Update an existing session
*
* @param bool Force update flag (default: false)
* @return void
*/
protected function _sess_update($force = FALSE)
{
// We only update the session every five minutes by default (unless forced)
if ( ! $force && ($this->userdata['last_activity'] + $this->sess_time_to_update) >= $this->now)
{
return;
}
// Update last activity to now
$this->userdata['last_activity'] = $this->now;
// Save the old session id so we know which DB record to update
$old_sessid = $this->userdata['session_id'];
// Changing the session ID during an AJAX call causes problems
if ( ! $this->CI->input->is_ajax_request())
{
// Get new id
$this->userdata['session_id'] = $this->_make_sess_id();
log_message('debug', 'Session: Regenerate ID');
}
// Check for database
if ($this->sess_use_database === TRUE)
{
$this->CI->db->where('session_id', $old_sessid);
if ($this->sess_match_ip === TRUE)
{
$this->CI->db->where('ip_address', $this->CI->input->ip_address());
}
if ($this->sess_match_useragent === TRUE)
{
$this->CI->db->where('user_agent', trim(substr($this->CI->input->user_agent(), 0, 120)));
}
// Update the session ID and last_activity field in the DB
$this->CI->db->update($this->sess_table_name,
array(
'last_activity' => $this->now,
'session_id' => $this->userdata['session_id']
)
);
}
// Write the cookie
$this->_set_cookie();
}
// ------------------------------------------------------------------------
/**
* Update database with current data
*
* This gets called from the shutdown function and also
* registered with PHP to run at the end of the request
* so it's guaranteed to update even when a fatal error
* occurs. The first call makes the update and clears the
* dirty flag so it won't happen twice.
*
* @return void
*/
public function _update_db()
{
// Check for database and dirty flag and unsaved
if ($this->sess_use_database === TRUE && $this->data_dirty === TRUE)
{
// Set up activity and data fields to be set
// If we don't find custom data, user_data will remain an empty string
$set = array(
'last_activity' => $this->userdata['last_activity'],
'user_data' => ''
);
// Get the custom userdata, leaving out the defaults
// (which get stored in the cookie)
$userdata = array_diff_key($this->userdata, $this->defaults);
// Did we find any custom data?
if ( ! empty($userdata))
{
// Serialize the custom data array so we can store it
$set['user_data'] = serialize($userdata);
}
// Reset query builder values.
$this->CI->db->reset_query();
// Run the update query
// Any time we change the session id, it gets updated immediately,
// so our where clause below is always safe
$this->CI->db->where('session_id', $this->userdata['session_id']);
if ($this->sess_match_ip === TRUE)
{
$this->CI->db->where('ip_address', $this->CI->input->ip_address());
}
if ($this->sess_match_useragent === TRUE)
{
$this->CI->db->where('user_agent', trim(substr($this->CI->input->user_agent(), 0, 120)));
}
$this->CI->db->update($this->sess_table_name, $set);
// Clear dirty flag to prevent double updates
$this->data_dirty = FALSE;
log_message('debug', 'CI_Session Data Saved To DB');
}
}
// ------------------------------------------------------------------------
/**
* Generate a new session id
*
* @return string Hashed session id
*/
protected function _make_sess_id()
{
$new_sessid = '';
do
{
$new_sessid .= mt_rand();
}
while (strlen($new_sessid) < 32);
// To make the session ID even more secure we'll combine it with the user's IP
$new_sessid .= $this->CI->input->ip_address();
// Turn it into a hash and return
return md5(uniqid($new_sessid, TRUE));
}
// ------------------------------------------------------------------------
/**
* Get the "now" time
*
* @return int Time
*/
protected function _get_time()
{
if ($this->time_reference === 'local' OR $this->time_reference === date_default_timezone_get())
{
return time();
}
$datetime = new DateTime('now', new DateTimeZone($this->time_reference));
sscanf($datetime->format('j-n-Y G:i:s'), '%d-%d-%d %d:%d:%d', $day, $month, $year, $hour, $minute, $second);
return mktime($hour, $minute, $second, $month, $day, $year);
}
// ------------------------------------------------------------------------
/**
* Write the session cookie
*
* @return void
*/
protected function _set_cookie()
{
// Get userdata (only defaults if database)
$cookie_data = ($this->sess_use_database === TRUE)
? array_intersect_key($this->userdata, $this->defaults)
: $this->userdata;
// The Input class will do this and since we use HMAC verification,
// unless we standardize here as well, the hash won't match.
if ($this->_standardize_newlines)
{
foreach (array_keys($this->userdata) as $key)
{
$this->userdata[$key] = preg_replace('/(?:\r\n|[\r\n])/', PHP_EOL, $this->userdata[$key]);
}
}
// Serialize the userdata for the cookie
$cookie_data = serialize($cookie_data);
if ($this->sess_encrypt_cookie === TRUE)
{
$cookie_data = $this->CI->encryption->encrypt($cookie_data);
}
else
{
// Require message authentication
$cookie_data .= hash_hmac('sha1', $cookie_data, $this->encryption_key);
}
$expire = ($this->sess_expire_on_close === TRUE) ? 0 : $this->sess_expiration + time();
// Set the cookie
$this->_setcookie($this->sess_cookie_name, $cookie_data, $expire, $this->cookie_path, $this->cookie_domain,
$this->cookie_secure, $this->cookie_httponly);
}
// ------------------------------------------------------------------------
/**
* Set a cookie with the system
*
* This abstraction of the setcookie call allows overriding for unit testing
*
* @param string Cookie name
* @param string Cookie value
* @param int Expiration time
* @param string Cookie path
* @param string Cookie domain
* @param bool Secure connection flag
* @param bool HTTP protocol only flag
* @return void
*/
protected function _setcookie($name, $value = '', $expire = 0, $path = '', $domain = '', $secure = FALSE, $httponly = FALSE)
{
setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
}
// ------------------------------------------------------------------------
/**
* Garbage collection
*
* This deletes expired session rows from database
* if the probability percentage is met
*
* @return void
*/
protected function _sess_gc()
{
if ($this->sess_use_database !== TRUE)
{
return;
}
$probability = ini_get('session.gc_probability');
$divisor = ini_get('session.gc_divisor');
if (mt_rand(1, $divisor) <= $probability)
{
$expire = $this->now - $this->sess_expiration;
$this->CI->db->delete($this->sess_table_name, 'last_activity < '.$expire);
log_message('debug', 'Session garbage collection performed.');
}
}
}
/* End of file Session_cookie.php */
/* Location: ./system/libraries/Session/drivers/Session_cookie.php */