<?php
/**
 * CodeIgniter
 *
 * An open source application development framework for PHP 5.2.4 or newer
 *
 * NOTICE OF LICENSE
 *
 * Licensed under the Open Software License version 3.0
 *
 * This source file is subject to the Open Software License (OSL 3.0) that is
 * bundled with this package in the files license.txt / license.rst.  It is
 * also available through the world wide web at this URL:
 * http://opensource.org/licenses/OSL-3.0
 * If you did not receive a copy of the license and are unable to obtain it
 * through the world wide web, please send an email to
 * licensing@ellislab.com so we can send you a copy immediately.
 *
 * @package		CodeIgniter
 * @author		EllisLab Dev Team
 * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
 * @license		http://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
 * @link		http://codeigniter.com
 * @since		Version 2.0
 * @filesource
 */
defined('BASEPATH') OR exit('No direct script access allowed');

/**
 * CodeIgniter Session Class
 *
 * The user interface defined by EllisLabs, now with puggable drivers to manage different storage mechanisms.
 * By default, the cookie session driver will load, but the 'sess_driver' config/param item (see above) can be
 * used to specify the 'native' driver, or any other you might create.
 * Once loaded, this driver setup is a drop-in replacement for the former CI_Session library, taking its place as the
 * 'session' member of the global controller framework (e.g.: $CI->session or $this->session).
 * In keeping with the CI_Driver methodology, multiple drivers may be loaded, although this might be a bit confusing.
 * The CI_Session library class keeps track of the most recently loaded driver as "current" to call for driver methods.
 * Ideally, one driver is loaded and all calls go directly through the main library interface. However, any methods
 * called through the specific driver will switch the "current" driver to itself before invoking the library method
 * (which will then call back into the driver for low-level operations). So, alternation between two drivers can be
 * achieved by specifying which driver to use for each call (e.g.: $this->session->native->set_userdata('foo', 'bar');
 * $this->session->cookie->userdata('foo'); $this->session->native->unset_userdata('foo');). Notice in the previous
 * example that the _native_ userdata value 'foo' would be set to 'bar', which would NOT be returned by the call for
 * the _cookie_ userdata 'foo', nor would the _cookie_ value be unset by the call to unset the _native_ 'foo' value.
 *
 * @package		CodeIgniter
 * @subpackage	Libraries
 * @category	Sessions
 * @author		EllisLab Dev Team
 * @link		http://codeigniter.com/user_guide/libraries/sessions.html
 */
class CI_Session extends CI_Driver_Library {

	/**
	 * Initialization parameters
	 *
	 * @var	array
	 */
	public $params = array();

	/**
	 * Valid drivers list
	 *
	 * @var	array
	 */
	public $valid_drivers = array('native',	'cookie');

	/**
	 * Current driver in use
	 *
	 * @var	string
	 */
	public $current = NULL;

	/**
	 * User data
	 *
	 * @var	array
	 */
	protected $userdata = array();

	// ------------------------------------------------------------------------

	const FLASHDATA_KEY = 'flash';
	const FLASHDATA_NEW = ':new:';
	const FLASHDATA_OLD = ':old:';
	const FLASHDATA_EXP = ':exp:';
	const EXPIRATION_KEY = '__expirations';
	const TEMP_EXP_DEF = 300;

	// ------------------------------------------------------------------------

	/**
	 * CI_Session constructor
	 *
	 * The constructor loads the configured driver ('sess_driver' in config.php or as a parameter), running
	 * routines in its constructor, and manages flashdata aging.
	 *
	 * @param	array	Configuration parameters
	 * @return	void
	 */
	public function __construct(array $params = array())
	{
		$_config =& get_instance()->config;

		// No sessions under CLI
		if (is_cli())
		{
			return;
		}

		log_message('debug', 'CI_Session Class Initialized');

		// Add possible extra entries to our valid drivers list
		$drivers = isset($params['sess_valid_drivers']) ? $params['sess_valid_drivers'] : $_config->item('sess_valid_drivers');
		if ( ! empty($drivers))
		{
			$drivers = array_map('strtolower', (array) $drivers);
			$this->valid_drivers = array_merge($this->valid_drivers, array_diff($drivers, $this->valid_drivers));
		}

		// Get driver to load
		$driver = isset($params['sess_driver']) ? $params['sess_driver'] : $_config->item('sess_driver');
		if ( ! $driver)
		{
			log_message('debug', "Session: No driver name is configured, defaulting to 'cookie'.");
			$driver = 'cookie';
		}

		if ( ! in_array($driver, $this->valid_drivers))
		{
			log_message('error', 'Session: Configured driver name is not valid, aborting.');
			return;
		}

		// Save a copy of parameters in case drivers need access
		$this->params = $params;

		// Load driver and get array reference
		$this->load_driver($driver);

		// Delete 'old' flashdata (from last request)
		$this->_flashdata_sweep();

		// Mark all new flashdata as old (data will be deleted before next request)
		$this->_flashdata_mark();

		// Delete expired tempdata
		$this->_tempdata_sweep();

		log_message('debug', 'CI_Session routines successfully run');
	}

	// ------------------------------------------------------------------------

	/**
	 * Loads session storage driver
	 *
	 * @param	string	Driver classname
	 * @return	object	Loaded driver object
	 */
	public function load_driver($driver)
	{
		// Save reference to most recently loaded driver as library default and sync userdata
		$this->current = parent::load_driver($driver);
		$this->userdata =& $this->current->get_userdata();
		return $this->current;
	}

	// ------------------------------------------------------------------------

	/**
	 * Select default session storage driver
	 *
	 * @param	string	Driver name
	 * @return	void
	 */
	public function select_driver($driver)
	{
		// Validate driver name
		$prefix = (string) get_instance()->config->item('subclass_prefix');
		$child = strtolower(str_replace(array('CI_', $prefix, $this->lib_name.'_'), '', $driver));
		if (in_array($child, array_map('strtolower', $this->valid_drivers)))
		{
			// See if driver is loaded
			if (isset($this->$child))
			{
				// See if driver is already current
				if ($this->$child !== $this->current)
				{
					// Make driver current and sync userdata
					$this->current = $this->$child;
					$this->userdata =& $this->current->get_userdata();
				}
			}
			else
			{
				// Load new driver
				$this->load_driver($child);
			}
		}
	}

	// ------------------------------------------------------------------------

	/**
	 * Destroy the current session
	 *
	 * @return	void
	 */
	public function sess_destroy()
	{
		// Just call destroy on driver
		$this->current->sess_destroy();
	}

	// ------------------------------------------------------------------------

	/**
	 * Regenerate the current session
	 *
	 * @param	bool	Destroy session data flag (default: false)
	 * @return	void
	 */
	public function sess_regenerate($destroy = FALSE)
	{
		// Call regenerate on driver and resync userdata
		$this->current->sess_regenerate($destroy);
		$this->userdata =& $this->current->get_userdata();
	}

	// ------------------------------------------------------------------------

	/**
	 * Fetch a specific item from the session array
	 *
	 * @param	string	Item key
	 * @return	string	Item value or NULL if not found
	 */
	public function userdata($item = NULL)
	{
		if (isset($item))
		{
			return isset($this->userdata[$item]) ? $this->userdata[$item] : NULL;
		}

		return isset($this->userdata) ? $this->userdata : array();
	}

	// ------------------------------------------------------------------------

	/**
	 * Fetch all session data
	 *
	 * @deprecated	3.0.0	Use userdata() with no parameters instead
	 * @return	array	User data array
	 */
	public function all_userdata()
	{
		return isset($this->userdata) ? $this->userdata : array();
	}

	// ------------------------------------------------------------------------

	/**
	 * Add or change data in the "userdata" array
	 *
	 * @param	mixed	Item name or array of items
	 * @param	string	Item value or empty string
	 * @return	void
	 */
	public function set_userdata($newdata, $newval = '')
	{
		// Wrap params as array if singular
		if (is_string($newdata))
		{
			$newdata = array($newdata => $newval);
		}

		// Set each name/value pair
		if (count($newdata) > 0)
		{
			foreach ($newdata as $key => $val)
			{
				$this->userdata[$key] = $val;
			}
		}

		// Tell driver data changed
		$this->current->sess_save();
	}

	// ------------------------------------------------------------------------

	/**
	 * Delete a session variable from the "userdata" array
	 *
	 * @param	mixed	Item name or array of item names
	 * @return	void
	 */
	public function unset_userdata($newdata)
	{
		// Wrap single name as array
		if (is_string($newdata))
		{
			$newdata = array($newdata => '');
		}

		// Unset each item name
		if (count($newdata) > 0)
		{
			foreach (array_keys($newdata) as $key)
			{
				unset($this->userdata[$key]);
			}
		}

		// Tell driver data changed
		$this->current->sess_save();
	}

	// ------------------------------------------------------------------------

	/**
	 * Determine if an item exists
	 *
	 * @param	string	Item name
	 * @return	bool
	 */
	public function has_userdata($item)
	{
		return isset($this->userdata[$item]);
	}

	// ------------------------------------------------------------------------

	/**
	 * Add or change flashdata, only available until the next request
	 *
	 * @param	mixed	Item name or array of items
	 * @param	string	Item value or empty string
	 * @return	void
	 */
	public function set_flashdata($newdata, $newval = '')
	{
		// Wrap item as array if singular
		if (is_string($newdata))
		{
			$newdata = array($newdata => $newval);
		}

		// Prepend each key name and set value
		if (count($newdata) > 0)
		{
			foreach ($newdata as $key => $val)
			{
				$flashdata_key = self::FLASHDATA_KEY.self::FLASHDATA_NEW.$key;
				$this->set_userdata($flashdata_key, $val);
			}
		}
	}

	// ------------------------------------------------------------------------

	/**
	 * Keeps existing flashdata available to next request.
	 *
	 * @param	mixed	Item key(s)
	 * @return	void
	 */
	public function keep_flashdata($key)
	{

		if (is_array($key))
		{
			foreach ($key as $k)
			{
				$this->keep_flashdata($k);
			}

			return;
		}

		// 'old' flashdata gets removed. Here we mark all flashdata as 'new' to preserve it from _flashdata_sweep()
		// Note the function will return NULL if the $key provided cannot be found
		$old_flashdata_key = self::FLASHDATA_KEY.self::FLASHDATA_OLD.$key;
		$value = $this->userdata($old_flashdata_key);

		$new_flashdata_key = self::FLASHDATA_KEY.self::FLASHDATA_NEW.$key;
		$this->set_userdata($new_flashdata_key, $value);
	}

	// ------------------------------------------------------------------------

	/**
	 * Fetch a specific flashdata item from the session array
	 *
	 * @param	string	Item key
	 * @return	string
	 */
	public function flashdata($key = NULL)
	{
		if (isset($key))
		{
			return $this->userdata(self::FLASHDATA_KEY.self::FLASHDATA_OLD.$key);
		}

		// Get our flashdata items from userdata
		$out = array();
		foreach ($this->userdata() as $key => $val)
		{
			if (strpos($key, self::FLASHDATA_KEY.self::FLASHDATA_OLD) !== FALSE)
			{
				$key = str_replace(self::FLASHDATA_KEY.self::FLASHDATA_OLD, '', $key);
				$out[$key] = $val;
			}
		}

		return $out;
	}

	// ------------------------------------------------------------------------

	/**
	 * Add or change tempdata, only available until expiration
	 *
	 * @param	mixed	Item name or array of items
	 * @param	string	Item value or empty string
	 * @param	int	Item lifetime in seconds or 0 for default
	 * @return	void
	 */
	public function set_tempdata($newdata, $newval = '', $expire = 0)
	{
		// Set expiration time
		$expire = time() + ($expire ? $expire : self::TEMP_EXP_DEF);

		// Wrap item as array if singular
		if (is_string($newdata))
		{
			$newdata = array($newdata => $newval);
		}

		// Get or create expiration list
		$expirations = $this->userdata(self::EXPIRATION_KEY);
		if ( ! $expirations)
		{
			$expirations = array();
		}

		// Prepend each key name and set value
		if (count($newdata) > 0)
		{
			foreach ($newdata as $key => $val)
			{
				$tempdata_key = self::FLASHDATA_KEY.self::FLASHDATA_EXP.$key;
				$expirations[$tempdata_key] = $expire;
				$this->set_userdata($tempdata_key, $val);
			}
		}

		// Update expiration list
		$this->set_userdata(self::EXPIRATION_KEY, $expirations);
	}

	// ------------------------------------------------------------------------

	/**
	 * Delete a temporary session variable from the "userdata" array
	 *
	 * @param	mixed	Item name or array of item names
	 * @return	void
	 */
	public function unset_tempdata($newdata)
	{
		// Get expirations list
		$expirations = $this->userdata(self::EXPIRATION_KEY);
		if (empty($expirations))
		{
			// Nothing to do
			return;
		}

		// Wrap single name as array
		if (is_string($newdata))
		{
			$newdata = array($newdata => '');
		}

		// Prepend each item name and unset
		if (count($newdata) > 0)
		{
			foreach (array_keys($newdata) as $key)
			{
				$tempdata_key = self::FLASHDATA_KEY.self::FLASHDATA_EXP.$key;
				unset($expirations[$tempdata_key]);
				$this->unset_userdata($tempdata_key);
			}
		}

		// Update expiration list
		$this->set_userdata(self::EXPIRATION_KEY, $expirations);
	}

	// ------------------------------------------------------------------------

	/**
	 * Fetch a specific tempdata item from the session array
	 *
	 * @param	string	Item key
	 * @return	string
	 */
	public function tempdata($key = NULL)
	{
		if (isset($key))
		{
			return $this->userdata(self::FLASHDATA_KEY.self::FLASHDATA_EXP.$key);
		}

		// Get our tempdata items from userdata
		$out = array();
		foreach ($this->userdata() as $key => $val)
		{
			if (strpos($key, self::FLASHDATA_KEY.self::FLASHDATA_EXP) !== FALSE)
			{
				$key = str_replace(self::FLASHDATA_KEY.self::FLASHDATA_EXP, '', $key);
				$out[$key] = $val;
			}
		}

		return $out;
	}

	// ------------------------------------------------------------------------

	/**
	 * Identifies flashdata as 'old' for removal
	 * when _flashdata_sweep() runs.
	 *
	 * @return	void
	 */
	protected function _flashdata_mark()
	{
		foreach ($this->userdata() as $name => $value)
		{
			$parts = explode(self::FLASHDATA_NEW, $name);
			if (count($parts) === 2)
			{
				$this->set_userdata(self::FLASHDATA_KEY.self::FLASHDATA_OLD.$parts[1], $value);
				$this->unset_userdata($name);
			}
		}
	}

	// ------------------------------------------------------------------------

	/**
	 * Removes all flashdata marked as 'old'
	 *
	 * @return	void
	 */
	protected function _flashdata_sweep()
	{
		$userdata = $this->userdata();
		foreach (array_keys($userdata) as $key)
		{
			if (strpos($key, self::FLASHDATA_OLD))
			{
				$this->unset_userdata($key);
			}
		}
	}

	// ------------------------------------------------------------------------

	/**
	 * Removes all expired tempdata
	 *
	 * @return	void
	 */
	protected function _tempdata_sweep()
	{
		// Get expirations list
		$expirations = $this->userdata(self::EXPIRATION_KEY);
		if (empty($expirations))
		{
			// Nothing to do
			return;
		}

		// Unset expired elements
		$now = time();
		$userdata = $this->userdata();
		foreach (array_keys($userdata) as $key)
		{
			if (strpos($key, self::FLASHDATA_EXP) && $expirations[$key] < $now)
			{
				unset($expirations[$key]);
				$this->unset_userdata($key);
			}
		}

		// Update expiration list
		$this->set_userdata(self::EXPIRATION_KEY, $expirations);
	}

}

// ------------------------------------------------------------------------

/**
 * CI_Session_driver Class
 *
 * Extend this class to make a new CI_Session driver.
 * A CI_Session driver basically manages an array of name/value pairs with some sort of storage mechanism.
 * To make a new driver, derive from (extend) CI_Session_driver. Overload the initialize method and read or create
 * session data. Then implement a save handler to write changed data to storage (sess_save), a destroy handler
 * to remove deleted data (sess_destroy), and an access handler to expose the data (get_userdata).
 * Put your driver in the libraries/Session/drivers folder anywhere in the loader paths. This includes the
 * application directory, the system directory, or any path you add with $CI->load->add_package_path().
 * Your driver must be named CI_Session_<name>, and your filename must be Session_<name>.php,
 * preferably also capitalized. (e.g.: CI_Session_foo in libraries/Session/drivers/Session_foo.php)
 * Then specify the driver by setting 'sess_driver' in your config file or as a parameter when loading the CI_Session
 * object. (e.g.: $config['sess_driver'] = 'foo'; OR $CI->load->driver('session', array('sess_driver' => 'foo')); )
 * Already provided are the Native driver, which manages the native PHP $_SESSION array, and
 * the Cookie driver, which manages the data in a browser cookie, with optional extra storage in a database table.
 *
 * @package		CodeIgniter
 * @subpackage	Libraries
 * @category	Sessions
 * @author		EllisLab Dev Team
 */
abstract class CI_Session_driver extends CI_Driver {

	/**
	 * CI Singleton
	 *
	 * @see	get_instance()
	 * @var	object
	 */
	protected $CI;

	// ------------------------------------------------------------------------

	/**
	 * Constructor
	 *
	 * Gets the CI singleton, so that individual drivers
	 * don't have to do it separately.
	 *
	 * @return	void
	 */
	public function __construct()
	{
		$this->CI =& get_instance();
	}

	// ------------------------------------------------------------------------

	/**
	 * Decorate
	 *
	 * Decorates the child with the parent driver lib's methods and properties
	 *
	 * @param	object	Parent library object
	 * @return	void
	 */
	public function decorate($parent)
	{
		// Call base class decorate first
		parent::decorate($parent);

		// Call initialize method now that driver has access to $this->_parent
		$this->initialize();
	}

	// ------------------------------------------------------------------------

	/**
	 * __call magic method
	 *
	 * Handles access to the parent driver library's methods
	 *
	 * @param	string	Library method name
	 * @param	array	Method arguments (default: none)
	 * @return	mixed
	 */
	public function __call($method, $args = array())
	{
		// Make sure the parent library uses this driver
		$this->_parent->select_driver(get_class($this));
		return parent::__call($method, $args);
	}

	// ------------------------------------------------------------------------

	/**
	 * Initialize driver
	 *
	 * @return	void
	 */
	protected function initialize()
	{
		// Overload this method to implement initialization
	}

	// ------------------------------------------------------------------------

	/**
	 * Save the session data
	 *
	 * Data in the array has changed - perform any storage synchronization
	 * necessary. The child class MUST implement this abstract method!
	 *
	 * @return	void
	 */
	abstract public function sess_save();

	// ------------------------------------------------------------------------

	/**
	 * Destroy the current session
	 *
	 * Clean up storage for this session - it has been terminated.
	 * The child class MUST implement this abstract method!
	 *
	 * @return	void
	 */
	abstract public function sess_destroy();

	// ------------------------------------------------------------------------

	/**
	 * Regenerate the current session
	 *
	 * Regenerate the session ID.
	 * The child class MUST implement this abstract method!
	 *
	 * @param	bool	Destroy session data flag (default: false)
	 * @return	void
	 */
	abstract public function sess_regenerate($destroy = FALSE);

	// ------------------------------------------------------------------------

	/**
	 * Get a reference to user data array
	 *
	 * Give array access to the main CI_Session object.
	 * The child class MUST implement this abstract method!
	 *
	 * @return	array	Reference to userdata
	 */
	abstract public function &get_userdata();

}

/* End of file Session.php */
/* Location: ./system/libraries/Session/Session.php */