store = $store ?: new Storage\NativeStorage(new Handler\FilesystemHandler); if ($dispatcher) { $this->setDispatcher($dispatcher); } $this->setOptions($options); $this->setState(SessionState::INACTIVE); } /** * Adds a validator to the session * * @param ValidatorInterface $validator The session validator * * @return void * * @since 2.0.0 */ public function addValidator(ValidatorInterface $validator): void { $this->sessionValidators[] = $validator; } /** * Get expiration time in seconds * * @return integer The session expiration time in seconds * * @since 1.0 */ public function getExpire() { return $this->expire; } /** * Get current state of session * * @return string The session state * * @since 1.0 */ public function getState() { return $this->state; } /** * Get a session token. * * Tokens are used to secure forms from spamming attacks. Once a token has been generated the system will check the request to see if * it is present, if not it will invalidate the session. * * @param boolean $forceNew If true, forces a new token to be created * * @return string * * @since 1.0 */ public function getToken($forceNew = false) { // Ensure the session token exists and create it if necessary if (!$this->has('session.token') || $forceNew) { $this->set('session.token', $this->createToken()); } return $this->get('session.token'); } /** * Check if the session has the given token. * * @param string $token Hashed token to be verified * @param boolean $forceExpire If true, expires the session * * @return boolean * * @since 1.0 */ public function hasToken($token, $forceExpire = true) { $result = $this->get('session.token') === $token; if (!$result && $forceExpire) { $this->setState(SessionState::EXPIRED); } return $result; } /** * Retrieve an external iterator. * * @return \ArrayIterator Return an ArrayIterator of $_SESSION. * * @since 1.0 */ #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->all()); } /** * Get session name * * @return string The session name * * @since 1.0 */ public function getName() { return $this->store->getName(); } /** * Set the session name * * @param string $name The session name * * @return $this * * @since 2.0.0 */ public function setName(string $name) { $this->store->setName($name); return $this; } /** * Get session id * * @return string The session id * * @since 1.0 */ public function getId() { return $this->store->getId(); } /** * Set the session ID * * @param string $id The session ID * * @return $this * * @since 2.0.0 */ public function setId(string $id) { $this->store->setId($id); return $this; } /** * Check if the session is active * * @return boolean * * @since 1.0 */ public function isActive() { if ($this->getState() === SessionState::ACTIVE) { return $this->store->isActive(); } return false; } /** * Check whether this session is currently created * * @return boolean * * @since 1.0 */ public function isNew() { $counter = $this->get('session.counter'); return $counter === 1; } /** * Check if the session is started * * @return boolean * * @since 2.0.0 */ public function isStarted(): bool { return $this->store->isStarted(); } /** * Get data from the session store * * @param string $name Name of a variable * @param mixed $default Default value of a variable if not set * * @return mixed Value of a variable * * @since 1.0 */ public function get($name, $default = null) { if (!$this->isActive()) { $this->start(); } return $this->store->get($name, $default); } /** * Set data into the session store. * * @param string $name Name of a variable. * @param mixed $value Value of a variable. * * @return mixed Old value of a variable. * * @since 1.0 */ public function set($name, $value = null) { if (!$this->isActive()) { $this->start(); } return $this->store->set($name, $value); } /** * Check whether data exists in the session store * * @param string $name Name of variable * * @return boolean True if the variable exists * * @since 1.0 */ public function has($name) { if (!$this->isActive()) { $this->start(); } return $this->store->has($name); } /** * Unset a variable from the session store * * @param string $name Name of variable * * @return mixed The value from session or NULL if not set * * @since 2.0.0 */ public function remove(string $name) { if (!$this->isActive()) { $this->start(); } return $this->store->remove($name); } /** * Clears all variables from the session store * * @return void * * @since 1.0 */ public function clear() { if (!$this->isActive()) { $this->start(); } $this->store->clear(); } /** * Retrieves all variables from the session store * * @return array * * @since 2.0.0 */ public function all(): array { if (!$this->isActive()) { $this->start(); } return $this->store->all(); } /** * Start a session. * * @return void * * @since 1.0 */ public function start() { if ($this->isStarted()) { return; } $this->store->start(); $this->setState(SessionState::ACTIVE); // Initialise the session $this->setCounter(); $this->setTimers(); // Perform security checks if (!$this->validate()) { // If the session isn't valid because it expired try to restart it or destroy it. if ($this->getState() === SessionState::EXPIRED) { $this->restart(); } else { $this->destroy(); } } if ($this->dispatcher) { if (!empty($this->dispatcher->getListeners('onAfterSessionStart'))) { trigger_deprecation( 'joomla/session', '2.0.0', 'The `onAfterSessionStart` event is deprecated and will be removed in 3.0, use the %s::START event instead.', SessionEvents::class ); // Dispatch deprecated event $this->dispatcher->dispatch('onAfterSessionStart', new SessionEvent('onAfterSessionStart', $this)); } // Dispatch new event $this->dispatcher->dispatch(SessionEvents::START, new SessionEvent(SessionEvents::START, $this)); } } /** * Frees all session variables and destroys all data registered to a session * * This method resets the $_SESSION variable and destroys all of the data associated * with the current session in its storage (file or DB). It forces new session to be * started after this method is called. * * @return boolean * * @see session_destroy() * @see session_unset() * @since 1.0 */ public function destroy() { // Session was already destroyed if ($this->getState() === SessionState::DESTROYED) { return true; } $this->clear(); $this->fork(true); $this->setState(SessionState::DESTROYED); return true; } /** * Restart an expired or locked session. * * @return boolean True on success * * @see destroy * @since 1.0 */ public function restart() { // Backup existing session data $data = $this->all(); $this->destroy(); if ($this->getState() !== SessionState::DESTROYED) { // @TODO :: generated error here return false; } // Restart the session $this->store->start(); $this->setState(SessionState::ACTIVE); // Initialise the session $this->setCounter(); $this->setTimers(); // Restore the data foreach ($data as $key => $value) { $this->set($key, $value); } // If the restarted session cannot be validated then it will be destroyed if (!$this->validate(true)) { $this->destroy(); } if ($this->dispatcher) { if (!empty($this->dispatcher->getListeners('onAfterSessionRestart'))) { trigger_deprecation( 'joomla/session', '2.0.0', 'The `onAfterSessionRestart` event is deprecated and will be removed in 3.0, use the %s::RESTART event instead.', SessionEvents::class ); // Dispatch deprecated event $this->dispatcher->dispatch('onAfterSessionRestart', new SessionEvent('onAfterSessionRestart', $this)); } // Dispatch new event $this->dispatcher->dispatch(SessionEvents::RESTART, new SessionEvent(SessionEvents::RESTART, $this)); } return true; } /** * Create a new session and copy variables from the old one * * @param boolean $destroy Whether to delete the old session or leave it to garbage collection. * * @return boolean True on success * * @since 1.0 */ public function fork($destroy = false) { $result = $this->store->regenerate($destroy); if ($result) { $this->setTimers(); } return $result; } /** * Writes session data and ends session * * Session data is usually stored after your script terminated without the need * to call {@link Session::close()}, but as session data is locked to prevent concurrent * writes only one script may operate on a session at any time. When using * framesets together with sessions you will experience the frames loading one * by one due to this locking. You can reduce the time needed to load all the * frames by ending the session as soon as all changes to session variables are * done. * * @return void * * @see session_write_close() * @since 1.0 */ public function close() { $this->store->close(); $this->setState(SessionState::CLOSED); } /** * Perform session data garbage collection * * @return integer|boolean Number of deleted sessions on success or boolean false on failure or if the function is unsupported * * @see session_gc() * @since 2.0.0 */ public function gc() { if (!$this->isActive()) { $this->start(); } return $this->store->gc(); } /** * Aborts the current session * * @return boolean * * @see session_abort() * @since 2.0.0 */ public function abort(): bool { if (!$this->isActive()) { return true; } return $this->store->abort(); } /** * Create a token string * * @return string * * @since 1.3.1 */ protected function createToken(): string { /* * We are returning a 32 character string. * The bin2hex() function will double the length of the hexadecimal value returned by random_bytes(), * so generate the token from a 16 byte random value */ return bin2hex(random_bytes(16)); } /** * Set counter of session usage * * @return boolean True on success * * @since 1.0 */ protected function setCounter() { $counter = $this->get('session.counter', 0); $counter++; $this->set('session.counter', $counter); return true; } /** * Set the session expiration * * @param integer $expire Maximum age of unused session in seconds * * @return $this * * @since 1.3.0 */ protected function setExpire($expire) { $this->expire = $expire; return $this; } /** * Set the session state * * @param string $state Internal state * * @return $this * * @since 1.3.0 */ protected function setState($state) { $this->state = $state; return $this; } /** * Set the session timers * * @return boolean True on success * * @since 1.0 */ protected function setTimers() { if (!$this->has('session.timer.start')) { $start = time(); $this->set('session.timer.start', $start); $this->set('session.timer.last', $start); $this->set('session.timer.now', $start); } $this->set('session.timer.last', $this->get('session.timer.now')); $this->set('session.timer.now', time()); return true; } /** * Set additional session options * * @param array $options List of parameter * * @return boolean True on success * * @since 1.0 */ protected function setOptions(array $options) { // Set name if (isset($options['name'])) { $this->setName($options['name']); } // Set id if (isset($options['id'])) { $this->setId($options['id']); } // Set expire time if (isset($options['expire'])) { $this->setExpire($options['expire']); } // Sync the session maxlifetime if (!headers_sent()) { ini_set('session.gc_maxlifetime', $this->getExpire()); } return true; } /** * Do some checks for security reasons * * If one check fails, session data has to be cleaned. * * @param boolean $restart Reactivate session * * @return boolean True on success * * @see http://shiflett.org/articles/the-truth-about-sessions * @since 1.0 */ protected function validate($restart = false) { // Allow to restart a session if ($restart) { $this->setState(SessionState::ACTIVE); } // Check if session has expired if ($this->expire) { $curTime = $this->get('session.timer.now', 0); $maxTime = $this->get('session.timer.last', 0) + $this->expire; // Empty session variables if ($maxTime < $curTime) { $this->setState(SessionState::EXPIRED); return false; } } try { foreach ($this->sessionValidators as $validator) { $validator->validate($restart); } } catch (Exception\InvalidSessionException $e) { $this->setState(SessionState::ERROR); return false; } return true; } }