Show:

File: platform/classes/Q/Session.php

<?php

/**
 * @module Q
 */
/**
 * Session-related functionality
 * @class Q_Session
 */
class Q_Session
{
	/**
	 * @property $session_save_path
	 * @type string
	 * @static
	 * @protected
	 */
	static protected $session_save_path;
	/**
	 * @property $session_db_connection
	 * @type Db
	 * @static
	 * @protected
	 */
	static protected $session_db_connection;
	/**
	 * @property $session_db_table
	 * @type string
	 * @static
	 * @protected
	 */
	static protected $session_db_table;
	/**
	 * @property $session_db_data_field
	 * @type string
	 * @static
	 * @protected
	 */
	static protected $session_db_data_field;
	/**
	 * @property $session_db_id_field
	 * @type string
	 * @static
	 * @protected
	 */
	static protected $session_db_id_field;
	/**
	 * @property $session_db_updated_field
	 * @type string
	 * @static
	 * @protected
	 */
	static protected $session_db_updated_field;
	/**
	 * @property $session_db_duration_field
	 * @type string
	 * @static
	 * @protected
	 */
	static protected $session_db_duration_field;
	/**
	 * @property $session_db_platform_field
	 * @type string
	 * @static
	 * @protected
	 */
	static protected $session_db_platform_field;
	/**
	 * @property $session_db
	 * @type boolean
	 * @static
	 * @protected
	 */
	static protected $session_db;
	/**
	 * @property $session_db_row
	 * @type Db_Row
	 * @static
	 * @protected
	 */
	static protected $session_db_row;
	/**
	 * @property $session_db_row_class
	 * @type string
	 * @static
	 * @protected
	 */
	static protected $session_db_row_class;
	/**
	 * @property $sess_data
	 * @type boolean
	 * @static
	 */
	static protected $sess_data;
	
	/**
	 * @method name
	 * @static
	 * @param {string} [$name=null]
	 * @return {string}
	 */
	static function name($name = null)
	{
		/**
		 * @event Q/session/name {before}
		 * @param {string} name
		 * @return {string}
		 */
		if ($name2 = Q::event('Q/session/name', compact('name'), 'before')) {
			return $name2;
		}
		if (isset($name)) {
			return session_name($name);
		}
		return session_name();
	}
	
	/**
	 * @method id
	 * @static
	 * @param {string} [$id=null] Pass a new session id, if you want to change it
	 * @return {string} The current session id
	 */
	static function id ($id = null)
	{
		/**
		 * @event Q/session/id {before}
		 * @param {string} id
		 * @return {string}
		 */
		$id = Q::event('Q/session/id', array(), 'before', false, $id);
		if (isset($id)) {
			return session_id($id);
		}
		return session_id();
	}
	
	/**
	 * @method savePath
	 * @static
	 * @param {string} [$savePath=null]
	 * @return {string}
	 */
	static function savePath ($savePath = null)
	{
		/**
		 * @event Q/session/savePath {before}
		 * @param {string} savePath
		 * @return {string}
		 */
		if ($savePath2 = Q::event('Q/session/savePath', compact('savePath'), 'before')) {
			return $savePath2;
		}
		if (isset($savePath)) {
			return session_save_path($savePath);
		}
		$sp = session_save_path();
		// A workaround for some systems:
		if (empty($sp)) {
			$sp = Q_FILES_DIR.DS.'Q'.DS.'sessions';
			session_save_path($sp);
		}
		return $sp;
	}
	
	/**
	 * @method init
	 * @static
	 */
	static function init()
	{
		if (self::$inited) {
			return false;
		}
		
		ini_set("session.entropy_file", "/dev/urandom");
		ini_set("session.entropy_length", "512");
		ini_set("session.hash_function", "1");
		$name = Q_Config::get('Q', 'session', 'name', 'Q_sessionId');
		$durationName = self::durationName();
		$duration = Q_Config::get('Q', 'session', 'durations', $durationName, 0);
		$baseUrl = Q_Request::baseUrl();
		$parts = parse_url($baseUrl);
		$path = !empty($parts['path']) ? $parts['path'] : '/';
		$domain = '.'.$parts['host'];

		Q::event('Q/session/init', array(
			'name' => &$name,
			'duration' => &$duration,
			'path' => &$path,
			'domain' => &$domain
		), 'before');

		Q_Session::name($name);
		session_set_cookie_params($duration, $path, $domain, false, false);
		
		if (Q_Config::get('Q', 'session', 'appendSuffix', false)
		or isset($_GET[$name])) {
			if (self::id()) {
				$s = "?$name=".self::id();
				$suffix = Q_Uri::suffix();
				$suffix[$baseUrl] = isset($suffix[$baseUrl])
					? $suffix[$baseUrl].$s
					: $s;
				Q_Uri::suffix($suffix);
			}
		}
		
		self::$inited = true;
		return true;
	}
	
	/**
	 * @method start
	 * @static
	 * @return {boolean}
	 */
	static function start()
	{
		if (self::id()) {
			// Session has already started
			return false;
		}
		/**
		 * @event Q/session/start {before}
		 * @return {false}
		 *	Return false to cancel session start
		 */
		if (false === Q::event('Q/session/start', array(), 'before')) {
			return false;
		}
		if (Q_Config::get('Q', 'session', 'custom', true)) {
			session_set_save_handler(
				array(__CLASS__, 'openHandler'), 
				array(__CLASS__, 'closeHandler'), 
				array(__CLASS__, 'readHandler'), 
				array(__CLASS__, 'writeHandler'), 
				array(__CLASS__, 'destroyHandler'),
				array(__CLASS__, 'gcHandler')
			);
		}
		if (!empty($_SESSION)) {
			$pre_SESSION = $_SESSION;
		}
		self::init();
		$name = Q_Session::name();
		$id = isset($_REQUEST[$name])
			? $_REQUEST[$name]
			: isset($_COOKIE[$name])
				? $_COOKIE[$name]
				: null;

		$isNew = false;
		if (!self::isValidId($id)) {
			// The session id was probably not generated by us, generate a new one
			/**
			 * This hook is called before a new session id is generated.
			 * You can return false to cancel starting the session.
			 * @event Q/session/generate {before}
			 * @param {string} id An invalid id, if any, that was passed by the client
			 * @return {boolean}
			 */
			if (false === Q::event('Q/session/generate', compact('id'), 'before')) {
				return false;
			}
			$id = self::generateId();
			$isNew = true;
		}

		try {
			if ($id) {
				self::processDbInfo();
				if (self::$session_db_connection) {
					$id_field = self::$session_db_id_field;
					$data_field = self::$session_db_data_field;
					$updated_field = self::$session_db_updated_field;
					$duration_field = self::$session_db_duration_field;
					$platform_field = self::$session_db_platform_field;
					$class = self::$session_db_row_class;
					$row = new $class();
					$row->$id_field = $id;
					if ($row->retrieve()) {
						self::$session_db_row = $row;
					} else {
						// Start a new session with our own id
						$id = $row->$id_field = self::generateId();
						$row->$data_field = "";
						$row->$updated_field = date('Y-m-d H:i:s');
						$row->$duration_field = Q_Config::get(
							'Q', 'session', 'durations', Q_Request::formFactor(),
							Q_Config::expect('Q', 'session', 'durations', 'session')
						);
						if ($platform_field) {
							$platform = Q_Request::platform();
							$row->$platform_field = $platform ? $platform : null;
						}
						if (false !== Q::event(
							'Q/session/save',
							array(
								'row' => $row,
								'id_field' => $id_field,
								'data_field' => $data_field,
								'updated_field' => $updated_field,
								'duration_field' => $duration_field,
								'platform_field' => $platform_field,
								'inserting' => true
							),
							'before'
						)) {
							$row->save();
							self::$session_db_row = $row;
						}
					}
				}
				self::id($id);
			}
			if (!empty($_SERVER['HTTP_HOST'])) {
				$durationName = self::durationName();
				$duration = Q_Config::get('Q', 'session', 'durations', $durationName, 0);
				Q_Response::setCookie(self::name(), $id, $duration ? time()+$duration : 0);
			} else if (empty($_SESSION)) {
				$_SESSION = array();
			}
			ini_set('session.use_cookies', 0); // we are gonna handle the cookies, thanks
			session_cache_limiter(''); // don't send the cache limiter headers either
			session_start();
		} catch (Exception $e) {
			$app = Q_Config::get('Q', 'app', null);
			$prefix = $app ? "$app/" : '';
			if (empty($_SERVER['HTTP_HOST'])) {
				echo "Warning: Ignoring Q_Session::start() called before running {$prefix}scripts/Q/install.php --all".PHP_EOL;
				$message = $e->getMessage();
				$file = $e->getFile();
				$line = $e->getLine();
				if (is_callable(array($e, 'getTraceAsStringEx'))) {
					$trace_string = $e->getTraceAsStringEx();
				} else {
					$trace_string = $e->getTraceAsString();
				}
				echo "$message\n(in $file line $line)\n$trace_string".PHP_EOL;
			} else {
				Q_Cache::clear(true);
				Q::log($e);
				throw new Q_Exception("Please run {$prefix}scripts/Q/install.php --all");
			}
		}
		// merge in all the stuff that was added to $_SESSION
		// before we started it.
		if (isset($pre_SESSION)) {
			foreach ($pre_SESSION as $k => $v) {
				$_SESSION[$k] = $v;
			}
		}
		if (isset($_SESSION['Q']['notices'])) {
			foreach ($_SESSION['Q']['notices'] as $k => $v) {
				Q_Response::setNotice($k, $v['notice'], $v['options']);
			}
		}
		if (!empty($_SESSION['Q']['terminated'])) {
			throw new Q_Exception_SessionTerminated(array(
				'id' => Q_Session::id()
			));
		}
		$original = null;
		$changed = array();
		if (Q_Config::get('Q', 'session', 'userAgentInfo', null)) {
			$arr = isset($_SESSION['Q']) ? $_SESSION['Q'] : array();
			$userAgentInfo = Q_Request::userAgentInfo();
			foreach ($userAgentInfo as $k => $v) {
				if (isset($arr[$k])) {
					$original[$k] = $arr[$k];
					if ($arr[$k] !== $v) {
						$changed[$k] = $v;
					}
				}
			}
			$_SESSION['Q'] = array_merge($arr, $userAgentInfo);
		}
		/**
		 * This is a hook for after the session starts.
		 * You may want to do extra security checks here.
		 * @event Q/session/start {after}
		 * @param {array} $original The userAgentInfo values that were in the session originally
		 * @param {array} $changed Whether any of the userAgentInfo values changed.
		 * @param {boolean} $isNew Whether a new session has just been started
		 * @param {string} $id The id of the session
		 */
		Q::event('Q/session/start', array('original', 'changed', 'isNew', 'id'), 'after');
		return true;
	}
	
	/**
	 * You can call this function to clear out the contents of 
	 * a session, but keep its ID.
	 * @method clear
	 * @static
	 */
	static function clear()
	{
		session_unset();
		$_SESSION = array();
	}
	
	static function destroy()
	{
		session_destroy();
		self::clear();
		if (ini_get("session.use_cookies")) {
		    // note - we no use session_get_cookie_params();
		    Q_Response::clearCookie(self::name());
		}
	}
	
	/**
	 * You should use this instead of simply calling session_regenerate_id().
	 * Generates a new session id signed with "Q"/"external"/"secret", and
	 * clones the current session data into it.
	 * @method regenerateId
	 * @static
	 * @param {boolean} [$destroy_old_session=false] Set to true if you want to get rid
	 *  of the old session (to save space or for security purposes).
	 * @param {integer|string} [$duration=null] Set the duration of the regenerated session,
	 *  otherwise it will use the default duration for Q_Session::durationName().
	 *  See Q/session/durations config field. Pass 0 to expire at the end of browser session.
	 * @return {string} The new session id.
	 */
	static function regenerateId($destroy_old_session = false, $duration = null)
	{
		$old_SESSION = $_SESSION;
		if ($destroy_old_session) {
			self::destroy();
		}

		// we have to re-set all the handlers, due to a bug in PHP 5.2
		if (Q_Config::get('Q', 'session', 'custom', true)) {
			session_set_save_handler(
				array(__CLASS__, 'openHandler'), 
				array(__CLASS__, 'closeHandler'), 
				array(__CLASS__, 'readHandler'), 
				array(__CLASS__, 'writeHandler'), 
				array(__CLASS__, 'destroyHandler'),
				array(__CLASS__, 'gcHandler')
			);
		}
		session_id($sid = self::generateId()); // generate a new session id
		session_start(); // start a new session
		if (!empty($_SERVER['HTTP_HOST'])) {
			// set the new cookie
			if (!isset($duration)) {
				$duration = self::durationName();
			}
			if (is_string($duration)) {
				$duration = Q_Config::get('Q', 'session', 'durations', $duration, 0);
			};
			Q_Response::setCookie(self::name(), $sid, $duration ? time()+$duration : 0);
		}
		$_SESSION = $old_SESSION; // restore $_SESSION, which will be saved when session closes

		return $sid;
	}

	//
	// Session handling functions
	//
	
	/**
	 * @method openHandler
	 * @static
	 * @param {string} $save_path
	 * @param {string} $session_name
	 * @return {boolean}
	 * @throws {Q_Exception_WrongType}
	 */
	static function openHandler ($save_path, $session_name)
	{
		$db_info = self::processDbInfo();
		
		/**
		 * @event Q/session/open {before}
		 * @param {string} save_path
		 * @param {string} session_name
		 * @param {Db_Interface} session_db_connection
		 */
		Q::event('Q/session/open', 
			compact('save_path', 'session_name', 'db_info'), 
			'before'
		);

		self::$session_save_path = $save_path;
		/**
		 * @event Q/session/open {after}
		 * @param {string} save_path
		 * @param {string} session_name
		 * @param {Db_Interface} session_db_connection
		 */
		Q::event('Q/session/open', 
			compact('save_path', 'session_name', 'session_db_connection'), 
			'after'
		);
		return true;
	}

	/**
	 * @method closeHandler
	 * @static
	 */
	static function closeHandler ()
	{
		/**
		 * @event Q/session/close {before}
		 */
		if (false === Q::event('Q/session/close', array(), 'before')) {
			return false;
		}
		return true;
	}

	/**
	 * @method readHandler
	 * @static
	 * @param {string} $id
	 * @return {string}
	 */
	static function readHandler ($id)
	{
		/**
		 * @event Q/session/read {before}
		 * @param {string} 'save_path'
		 * @param {Db_Interface} 'session_db_connection'
		 * @return {string}
		 */
		$result = Q::event('Q/session/read', 
			array(
				'save_path' => self::$session_save_path,
				'session_db_connection' => self::$session_db_connection
			), 
			'before'
		);
		if (isset($result)) {
			return $result;
		}
		if (empty(self::$session_save_path)) {
			self::$session_save_path = self::savePath();
		}
		if (! empty(self::$session_db_connection)) {
			$id_field = self::$session_db_id_field;
			$data_field = self::$session_db_data_field;
			if (!self::$session_db_row
			or self::$session_db_row->$id_field != $id) {
				$class = self::$session_db_row_class;
				$row = new $class();
				$row->$id_field = $id;
				$row->retrieve();
				self::$session_db_row = $row;
			}
			$result = isset(self::$session_db_row->$data_field)
				? self::$session_db_row->$data_field : '';
		} else {
			$duration_name = self::durationName();
			$id1 = substr($id, 0, 4);
			$id2 = substr($id, 4);
			$sess_file = self::$session_save_path . DS . "$duration_name/$id1/$id2";
			if (!file_exists($sess_file)) {
				$result = '';
			} else {
				$result = (string) file_get_contents($sess_file);
			}
		}
		self::$sess_data = $result;
		/**
		 * @event Q/session/read {after}
		 * @param {string} 'save_path'
		 * @param {Db_Interface} 'session_db_connection'
		 * @return {string}
		 */
		$result = Q::event('Q/session/read', 
			array(
				'save_path' => self::$session_save_path,
				'session_db_connection' => self::$session_db_connection,
				'sess_data' => $result
			), 
			'after',
			false,
			$result
		);
		return $result;
	}

	/**
	 * @method writeHandler
	 * @static
	 * @param {string} $id
	 * @param {string} $sess_data
	 * @return {boolean}
	 */
	static function writeHandler ($id, $sess_data)
	{
		try {
			// if the request is AJAX request that came without session cookie, then do not write session, ignore it
			if (Q_Request::isAjax() && !isset($_COOKIE[self::name()])) {
				return false;
			}

			// don't save sessions when running from command-line (cli)
			if(php_sapi_name() == 'cli') {
				return false;
			}

			if (self::$preventWrite) {
				return false;
			}
			
			$our_SESSION = $_SESSION;
			$old_data = self::$sess_data;
			$changed = ($sess_data !== $old_data);
			$result = false;

			/**
			 * @event Q/session/write {before}
			 * @param {string} id
			 * @param {string} sess_data
			 * @param {string} old_data
			 * @param {boolean} changed
			 * @return {boolean}
			 */
			if (false === Q::event(
				'Q/session/write', 
				compact('id', 'sess_data', 'old_data', 'changed'), 
				'before'
			)) {
				return false;
			}
			if (empty(self::$session_save_path)) {
				self::$session_save_path = self::savePath();
			}
			if (!empty(self::$session_db_connection)) {
				// Create a new row to be saved in the session table
				$db_row_class = self::$session_db_row_class;
				// Make sure it has a primary key!
				if (count(self::$session_db_row->getPrimaryKey()) != 1) {
					throw new Q_Exception(
						"The primary key of " . self::$session_db_row_class 
						. " has to consist of exactly 1 field!"
					);
				}
				$id_field = self::$session_db_id_field;
				$data_field = self::$session_db_data_field;
				$updated_field = self::$session_db_updated_field;
				$duration_field = self::$session_db_duration_field;
				$platform_field = self::$session_db_platform_field;
				$row = self::$session_db_row;
				$row->$id_field = $id;
			} else {
				$duration_name = self::durationName();
				$id1 = substr($id, 0, 4);
				$id2 = substr($id, 4);
				$ssp = self::$session_save_path;
				$sess_file = $ssp . DS . "$duration_name/$id1/$id2";
				$dir = $ssp . DS . "$duration_name/$id1/";
			}
			if ($changed) {
				// Apparently, we want to save some changes.
				// The convention to avoid locking is that everything
				// stored in sessions must be mergeable using the
				// Q_Tree merge algorithm.
				// So we will retrieve the latest session data again,
				// merge our changes over it, and save.
				$params = array(
					'changed' => $changed,
					'sess_data' => $sess_data,
					'old_data' => $old_data
				);
				if (!empty(self::$session_db_connection)) {
					$row->retrieve(null, false, array(
						'begin' => true,
						'ignoreCache' => true
					));
					$existing_data = Q::ifset($row, $data_field, "");
					$params = array_merge($params, array(
						'id_field' => $id_field,
						'data_field' => $data_field,
						'duration_field' => $duration_field,
						'platform_field' => $platform_field,
						'updated_field' => $updated_field,
						'row' => $row
					));
				} else {
					if (!is_dir($dir)) {
						mkdir($dir, fileperms($ssp), true);
					}
					if (!is_writable($dir)) {
						// alert the developer to this problem
						Q::log("$sess_file is not writable", 'fatal');
						die("$sess_file is not writable");
					}
					if (file_exists($sess_file)) {
						$file = fopen($sess_file, "r+");
						flock($file, LOCK_EX);
						$maxlength = Q_Config::get('Q', 'session', 'maxlength', 4095);
						$existing_data = fread($file, $maxlength);
					} else {
						$file = fopen($sess_file, "w");
						flock($file, LOCK_EX);
						$existing_data = '';
					}
				}
				$_SESSION = self::unserialize($existing_data);
				$t = new Q_Tree($_SESSION);
				$t->merge($our_SESSION);
				$_SESSION = $t->getAll();
				$params['existing_data'] = $existing_data;
				$params['merged_data'] = $merged_data = session_encode();
				/**
				 * @event Q/session/save {before}
				 * @param {string} sess_data
				 * @param {string} old_data
				 * @param {string} existing_data
				 * @param {string} merged_data
				 * @param {boolean} changed
				 * @param {Db_Row} row
				 * @return {boolean}
				 */
				Q::event('Q/session/save', $params, 'before');
				if (! empty(self::$session_db_connection)) {
					$row->$data_field = $merged_data;
					$row->$duration_field = Q_Config::get(
						'Q', 'session', 'durations', Q_Request::formFactor(),
						Q_Config::expect('Q', 'session', 'durations', 'session')
					);
					if ($platform_field) {
						$platform = Q_Request::platform();
						$row->$platform_field = $platform ? $platform : null;
					}
					$row->save(false, true);
					$result = true;
				} else {
					if (!$file) {
						throw new Q_Exception_MissingFile(array('filename' => $sess_file));
					}
					ftruncate($file, 0);
					rewind($file);
					$result = fwrite($file, $merged_data);
					flock($file, LOCK_UN);
					fclose($file);
				}
			} else {
				$result = true;
			}
			/**
			 * @event Q/session/write {after}
			 * @param {string} id
			 * @param {boolean} changed
			 * @param {string} sess_data
			 * @param {string} old_data
			 * @param {string} existing_data
			 * @param {string} merged_data
			 * @param {string} data_field
			 * @param {string} updated_field
			 * @param {string} duration_field
			 * @param {string} platform_field
			 * @param {string} sess_file
			 * @param {integer} row
			 * @return {mixed}
			 */
			$result = Q::event(
				'Q/session/write', 
				compact(
					'id', 'data_field', 'updated_field', 'duration_field', 'platform_field',
					'sess_file', 'row',
					'changed', 'sess_data', 'old_data', 'existing_data', 'merged_data'
				), 
				'after',
				false,
				$result
			);
			return $result;
		} catch (Exception $e) {
			Q::log("Exception when writing session $id: " . $e->getMessage());
			throw $e;
		}
	}

	/**
	 * @method destroyHandler
	 * @static
	 * @param {string} $id
	 * @return {boolean}
	 */
	static function destroyHandler ($id)
	{
		/**
		* @event Q/session/destroy {before}
		* @param {string} id
		* @return {false}
		*/
		if (false === Q::event(
			'Q/session/destroy', 
			compact('id'), 
			'before'
		)) {
			return false;
		}
		self::clearNonce();
		if (! empty(self::$session_db_connection)) {
			self::$session_db
				->delete(self::$session_db_table)
				->where(array(self::$session_db_id_field => $id))
				->execute();
			$result = true;
		} else {
			$sess_file = self::$session_save_path . DS . "$id";
			if (!file_exists($sess_file)) {
				return false;
			}
			$result = unlink($sess_file);
		}
		/**
		 * @event Q/session/destroy {after}
		 * @param {string} id
		 * @return {mixed}
		 */
		$result = Q::event(
			'Q/session/destroy',
			compact('id'),
			'after',
			false,
			$result
		);
		return $result;
	}

	/**
	 * @method gcHandler
	 * @static
	 * @param {integer} $max_duration
	 */
	static function gcHandler ($max_duration)
	{
		$proceed = Q_Config::get('Q', 'session', 'gc', true);
		if ($proceed) {
			self::gc($max_duration);
		}
	}
	
	/**
	 * @method gc
	 * @static
	 * @param {integer} $max_duration
	 */
	static function gc($max_duration)
	{
		$id = self::id();
		/**
		 * @event Q/session/gc {before}
		 * @param {string} id
		 * @param {integer} max_duration
		 * @return {false}
		 */
		if (false === Q::event(
			'Q/session/gc', 
			compact('id', 'max_duration'), 
			'before'
		)) {
			return false;
		}
		$durations = Q_Config::get('Q', 'session', 'durations', array());
		foreach ($durations as $k => $v) {
			if ($v === null) {
				$v = $max_duration;
			}
			if (!$v) {
				continue;
			}
			$since_time = time() - $v;
			if (! empty(self::$session_db_connection)) {
				$datetime = date('Y-m-d H:i:s', $since_time);
				self::$session_db
					->delete(self::$session_db_table)
					->where(array(
						self::$session_db_updated_field . '<' => $datetime,
						self::$session_db_duration_field => $v
					))->execute();
			} else {
				foreach (glob(self::$session_save_path . "/$k/*/*") as $filename) {
					$mtime = filemtime($filename);
					if ($mtime < $since_time) {
						unlink($filename);
					}
				}
			}
		}
		/**
		 * @event Q/session/gc {after}
		 * @param {string} id
		 * @param {integer} max_duration
		 * @param {integer} since_time
		 */
		Q::event(
			'Q/session/gc', 
			compact('id', 'max_duration', 'since_time'), 
			'after'
		);
		return true;
	}
	
	/**
	 * Gets nonce from the session
	 * @method getNonce
	 * @return {string}
	 */
	static function getNonce()
	{
		return isset($_SESSION['Q']['nonce'])
			? $_SESSION['Q']['nonce']
			: null;
	}
	
	/**
	 * Sets a nonce in the session ['Q']['nonce'] field and in cookie 'Q_nonce'
	 * @method setNonce
	 * @param {boolean} [$overwrite=false] If true, sets a new nonce even if one is already there.
	 */
	static function setNonce($overwrite = false)
	{
		self::start();
		if ($overwrite or !isset($_SESSION['Q']['nonce'])) {
			if (is_callable('random_bytes')) {
				$_SESSION['Q']['nonce'] = bin2hex(random_bytes(32));
			} else {
				$_SESSION['Q']['nonce'] = sha1(mt_rand().microtime());
			}
		}
		if (!empty($_SERVER['HTTP_HOST'])) {
			$durationName = self::durationName();
			$duration = Q_Config::get('Q', 'session', 'durations', $durationName, 0);
			Q_Response::setCookie('Q_nonce', $_SESSION['Q']['nonce'], $duration ? time()+$duration : 0);
		}
		Q_Session::$nonceWasSet = true;
	}
	
	/**
	 * Clears the nonce in the session ['Q']['nonce'] field and in cookie 'Q_nonce'
	 * @method clearNonce
	 */
	static function clearNonce($overwrite = false)
	{
		self::start();
		$_SESSION['Q']['nonce'] = null;
		if (!empty($_SERVER['HTTP_HOST'])) {
			Q_Response::clearCookie('Q_nonce');
		}
	}
	
	static function durationName()
	{
		$ff = Q_Request::formFactor();
		$duration = Q_Config::get('Q', 'session', 'durations', $ff, null);
		return isset($duration) ? $ff : 'session';
	}
	
	static function processDbInfo()
	{
		static $db_info = null;
		
		if ($db_info) {
			return $db_info;
		}
		if (!$db_info) {
			$db_info = Q_Config::get('Q', 'session', 'db', null);
		}
		if (!$db_info) {
			return null;
		}
		
		$session_db_connection = isset($db_info['connection']) ? $db_info['connection'] : null;

		// use the DB for session
		$session_db_data_field = isset($db_info['dataField']) ? $db_info['dataField'] : null;
		if (empty($session_db_data_field)) {
			throw new Q_Exception_WrongType(array(
				'field' => 'session_db_data_field', 
				'type' => 'string'
			));
		}
		$session_db_id_field = isset($db_info['idField']) ? $db_info['idField'] : null;
		if (empty($session_db_id_field)) {
			throw new Q_Exception_WrongType(array(
				'field' => 'session_db_id_field', 
				'type' => 'string'
			));
		}
		$session_db_updated_field = isset($db_info['updatedField']) ? $db_info['updatedField'] : null;
		if (empty($session_db_updated_field)) {
			throw new Q_Exception_WrongType(array(
				'field' => 'session_db_updated_field', 
				'type' => 'string'
			));
		}
		$session_db_duration_field = isset($db_info['durationField']) ? $db_info['durationField'] : null;
		if (empty($session_db_duration_field)) {
			throw new Q_Exception_WrongType(array(
				'field' => 'session_db_duration_field', 
				'type' => 'string'
			));
		}
		$session_db_platform_field = isset($db_info['platformField']) ? $db_info['platformField'] : null;
		$session_db_row_class = isset($db_info['rowClass']) ? $db_info['rowClass'] : null;
		if (empty($session_db_row_class)
		or ! class_exists($session_db_row_class)) {
			throw new Q_Exception_WrongType(array(
				'field' => 'session_db_row_class', 
				'type' => 'a class name'
			));
		}
		$session_db_table = call_user_func(array($session_db_row_class, 'table'));
		$class = $session_db_row_class;
		$ancestors = array($class);
		while ($class = get_parent_class($class))
			$ancestors[] = $class;
		if (! in_array('Db_Row', $ancestors)) {
			throw new Q_Exception_WrongType(array(
				'field' => 'session_db_row_class', 
				'type' => 'name of a class that extends Db_Row'
			));
		}

		self::$session_db_connection = $session_db_connection;
		self::$session_db_table = $session_db_table;
		self::$session_db_data_field = $session_db_data_field;
		self::$session_db_id_field = $session_db_id_field;
		self::$session_db_updated_field = $session_db_updated_field;
		self::$session_db_duration_field = $session_db_duration_field;
		self::$session_db_platform_field = $session_db_platform_field;
		self::$session_db_row_class = $session_db_row_class;
		self::$session_db = Db::connect(self::$session_db_connection);
		
		return $db_info;
	}
	
	/**
	 * Get the session Db_Row, if it has been retrieved, otherwise null
	 * @method row
	 * @static
	 * @return {Db_Row|null}
	 */
	static function row()
	{
		return self::$session_db_row;
	}

	/**
	 * Generates a session id, signed with "Q"/"external"/"secret"
	 * so that the web server won't have to deal with session ids we haven't issued.
	 * @return {string}
	 */
	static function generateId()
	{
		if (is_callable('random_bytes')) {
			$id = bin2hex(random_bytes(16));
		} else {
			$id = str_replace('-', '', Q_Utils::uuid());
		}
		$secret = Q_Config::get('Q', 'external', 'secret', null);
		if (isset($secret)) {
			$sig = Q_Utils::signature($id, "$secret");
			$id .= substr($sig, 0, 32);
		}
		$id = base64_encode(pack('H*', $id));
		return str_replace(
			array('z', '+', '/', '='), 
			array('zz', 'za', 'zb', 'zc'), 
			$id
		);
	}
	/**
	 * @param string $id
	 *
	 * @return array
	 * @throws Q_Exception
	 * @throws TypeError
	*/
	protected static function decodeId($id)
	{
		if (!$id) {
			return array(false, '', '');
		}
		$result = '';
		$len = strlen($id);
		$i = 0;
		$replacements = array(
			'z' => 'z',
			'a' => '+',
			'b' => '/',
			'c' => '='
		);
		while ($i < $len-1) {
			$r = $id[$i];
			$c1 = $id[$i];
			++$i;
			if ($c1 == 'z') {
				$c2 = $id[$i];
				if (isset($replacements[$c2])) {
					$r = $replacements[$c2];
					++$i;
				}
			}
			$result .= $r;
		}
		if ($i < $len) {
			$result .= $id[$i];
		}
		$result = bin2hex(base64_decode($result));
		$a = substr($result, 0, 32);
		$b = substr($result, 32, 32);
		$secret = Q_Config::get('Q', 'external', 'secret', null);
		$c = isset($secret)
			? Q_Utils::hashEquals($b, substr(Q_Utils::signature($a, $secret), 0, 32))
			: true;
		return array($c, $a, $b);
	}
	
	/**
	 * Verifies a session id, that it was correctly signed with "Q"/"external"/"secret"
	 * so that the web server won't have to deal with session ids we haven't issued.
	 * @param {string} $id
	 * @return {boolean}
	 */
	static function isValidId($id)
	{
		$results = self::decodeId($id);
		return $results[0];
	}
	
	/**
	 * Unserialize a session string stored by PHP using the same
	 * session.serialize_handler as the current one.
	 * @param {string} $session_data
	 * @return {array} The session data
	 */
    static function unserialize($session_data) {
        $method = ini_get("session.serialize_handler");
        switch ($method) {
            case "php":
                return self::unserialize_php($session_data);
                break;
            case "php_binary":
                return self::unserialize_phpbinary($session_data);
                break;
            default:
                throw new Exception("Unsupported session.serialize_handler: " . $method . ". Supported: php, php_binary");
        }
    }

    protected static function unserialize_php($session_data) {
        $return_data = array();
        $offset = 0;
        while ($offset < strlen($session_data)) {
            if (!strstr(substr($session_data, $offset), "|")) {
                throw new Exception("invalid data, remaining: " . substr($session_data, $offset));
            }
            $pos = strpos($session_data, "|", $offset);
            $num = $pos - $offset;
            $varname = substr($session_data, $offset, $num);
            $offset += $num + 1;
            $data = unserialize(substr($session_data, $offset));
            $return_data[$varname] = $data;
            $offset += strlen(serialize($data));
        }
        return $return_data;
    }

    protected static function unserialize_phpbinary($session_data) {
        $return_data = array();
        $offset = 0;
        while ($offset < strlen($session_data)) {
            $num = ord($session_data[$offset]);
            $offset += 1;
            $varname = substr($session_data, $offset, $num);
            $offset += $num;
            $data = unserialize(substr($session_data, $offset));
            $return_data[$varname] = $data;
            $offset += strlen(serialize($data));
        }
        return $return_data;
    }
	
	/**
	 * @property $inited
	 * @type boolean
	 * @static
	 * @protected
	 */
	protected static $inited = false;
	
	/**
	 * @property $nonceWasSet
	 * @type boolean
	 * @static
	 * @public
	 */
	public static $nonceWasSet = false;

	/**
	 * Set this to true to prevent writing the session to disk
	 * @property $preventWrite
	 * @type boolean
	 * @static
	 * @public
	 */
	public static $preventWrite = false;
}