* @copyright Copyright 2007-2013, Josh Sherman * @license http://www.opensource.org/licenses/mit-license.html * @package PICKLES * @link https://github.com/joshtronic/pickles */ /** * Session Class * * Provides session handling via database instead of the file based session * handling built into PHP. Using this class requires an array to be * defined in place of the boolean true/false (on/off). If simply array(), * the datasource will default to the value in * $config['pickles']['datasource'] and if the table will default to * "sessions". The format is as follows: * * $config = array( * 'pickles' => array( * 'session' => array( * 'datasource' => 'mysql', * 'table' => 'sessions', * ) * ) * ); * * In addition to the configuration variables, a table in your database * must be created. The [MySQL] table schema is as follows: * * CREATE TABLE sessions ( * id varchar(32) COLLATE utf8_unicode_ci NOT NULL, * session text COLLATE utf8_unicode_ci NOT NULL, * expires_at datetime NOT NULL, * PRIMARY KEY (id), * INDEX (expires_at) * ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; * * Note: The reason for not using a model class was to avoid a naming * conflict between the Session model and the Session class itself. This * will eventually be resolved when I abandon full 5.x support and migrate * to 5.3+ (assuming that ever happens). */ class Session extends Object { /** * Handler * * What the session is being handled by. * * @access private * @var string */ private $handler = false; /** * Accessed At * * The UNIX timestamp of when the page was accessed. * * @access private * @var integer */ private $accessed_at = null; /** * Time to Live * * The number of seconds the session should remain active. Corresponds * to the INI variable session.gc_maxlifetime * * @access private * @var integer */ private $time_to_live = null; /** * Datasource * * Name of the datasource, defaults to whatever the default datasource * is defined to in config.php * * @access private * @var string */ private $datasource = null; /** * Table * * Name of the database table in the aforementioned datasource that * holds the session data. The expected schema is defined above. * * @access private * @var string */ private $table = null; /** * Database * * Our database object to interact with the aforementioned datasource * and table. This object is shared with other PICKLES internals. * * @access private * @var object */ private $db = null; /** * Constructor * * All of our set up logic for the session in contained here. This * object is initially instantiated from pickles.php and the session * callbacks are established here. All variables are driven from * php.ini and/or the site config. Once configured, the session is * started automatically. */ public function __construct() { if (!IS_CLI) { parent::__construct(); // Sets up our configuration variables if (isset($this->config->pickles['session'])) { $session = $this->config->pickles['session']; $version = 1; } if (isset($this->config->pickles['sessions'])) { $session = $this->config->pickles['sessions']; $version = 2; } $datasources = $this->config->datasources; $this->handler = 'files'; $datasource = false; $table = 'sessions'; if (isset($datasources[$session])) { $datasource = $datasources[$session]; $this->handler = $datasource['type']; if (isset($datasource['hostname'], $datasource['port'])) { $host = 'tcp://' . $datasource['hostname'] . ':' . $datasource['port']; } } switch ($this->handler) { case 'memcache': ini_set('session.save_handler', 'memcache'); ini_set('session.save_path', $host . '?persistent=1&weight=1&timeout=1&retry_interval=15'); break; // @todo memcached case 'mysql': // Sets our access time and time to live $this->accessed_at = time(); $this->time_to_live = ini_get('session.gc_maxlifetime'); $this->datasource = $datasource; $this->table = $table; // Gets a database instance $this->db = Database::getInstance($this->datasource); // Initializes the session $this->initialize(); break; case 'redis': // Keep in mind that the database value is ignored by phpredis $save_path = $host . '?weight=1' . (isset($datasource['database']) ? '&database=' . $datasource['database'] : '') . (isset($datasource['prefix']) ? '&prefix=' . $datasource['prefix'] : ''); ini_set('session.save_handler', 'redis'); ini_set('session.save_path', $save_path); break; default: case 'files': ini_set('session.save_handler', 'files'); break; } if (isset($_SERVER['HTTP_USER_AGENT']) && !String::isEmpty($_SERVER['HTTP_USER_AGENT']) && !preg_match('/(Baidu|Gigabot|Googlebot|libwww-perl|lwp-trivial|msnbot|SiteUptime|Slurp|WordPress|ZIBB|ZyBorg)/i', $_SERVER['HTTP_USER_AGENT'])) { session_start(); } } } /** * Destructor * * Runs garbage collection and closes the session. I'm not sure if the * garbage collection should stay as it could be accomplished via * php.ini variables. The session_write_close() is present to combat a * chicken and egg scenario in earlier versions of PHP 5. */ public function __destruct() { if ($this->handler == 'mysql') { $this->gc($this->time_to_live); session_write_close(); } } /** * Initializes the Session * * This method exists to combat the fact that calling session_destroy() * also clears out the save handler. Upon destorying a session this * method is called again so the save handler is all set. */ public function initialize() { // Sets up the session handler session_set_save_handler( array($this, 'open'), array($this, 'close'), array($this, 'read'), array($this, 'write'), array($this, 'destroy'), array($this, 'gc') ); register_shutdown_function('session_write_close'); } /** * Opens the Session * * Since the session is in the database, opens the database connection. * This step isn't really necessary as the Database object is smart * enough to open itself up upon execute. */ public function open() { session_regenerate_id(); return $this->db->open(); } /** * Closes the Session * * Same as above, but in reverse. */ public function close() { return $this->db->close(); } /** * Reads the Session * * Checks the database for the session ID and returns the session data. * * @param string $id session ID * @return string serialized session data */ public function read($id) { $sql = 'SELECT session FROM `' . $this->table . '` WHERE id = ?;'; $session = $this->db->fetch($sql, array($id)); return isset($session[0]['session']) ? $session[0]['session'] : ''; } /** * Writes the Session * * When there's changes to the session, writes the data to the * database. * * @param string $id session ID * @param string $session serialized session data * @return boolean whether the query executed correctly */ public function write($id, $session) { $sql = 'REPLACE INTO `' . $this->table . '` VALUES (?, ? ,?);'; $parameters = array($id, $session, date('Y-m-d H:i:s', strtotime('+' . $this->time_to_live . ' seconds'))); return $this->db->execute($sql, $parameters); } /** * Destroys the Session * * Deletes the session from the database. * * @param string $id session ID * @return boolean whether the query executed correctly */ public function destroy($id) { $sql = 'DELETE FROM `' . $this->table . '` WHERE id = ?;'; return $this->db->execute($sql, array($id)); } /** * Garbage Collector * * This is who you call when you got trash to be taken out. * * @param integer $time_to_live number of seconds a session is active * @return boolean whether the query executed correctly */ public function gc($time_to_live) { $sql = 'DELETE FROM `' . $this->table . '` WHERE expires_at < ?;'; $parameters = array(date('Y-m-d H:i:s', $this->accessed_at - $time_to_live)); return $this->db->execute($sql, $parameters); } } ?>