first commit
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @defgroup session Session
|
||||
* Implements session concerns such as the session manager, session objects, etc.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/session/Session.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class Session
|
||||
*
|
||||
* @ingroup session
|
||||
*
|
||||
* @see SessionDAO
|
||||
*
|
||||
* @brief Maintains user state information from one request to the next.
|
||||
*/
|
||||
|
||||
namespace PKP\session;
|
||||
|
||||
use APP\facades\Repo;
|
||||
use PKP\config\Config;
|
||||
use PKP\user\User;
|
||||
|
||||
class Session extends \PKP\core\DataObject
|
||||
{
|
||||
/** @var User User object associated with this session */
|
||||
public $user;
|
||||
|
||||
|
||||
/**
|
||||
* Get a session variable's value.
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
public function getSessionVar($key)
|
||||
{
|
||||
return $_SESSION[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a session variable's value.
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
public function setSessionVar($key, $value)
|
||||
{
|
||||
$_SESSION[$key] = $value;
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset (delete) a session variable.
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
public function unsetSessionVar($key)
|
||||
{
|
||||
if (isset($_SESSION[$key])) {
|
||||
unset($_SESSION[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Get/set methods
|
||||
//
|
||||
|
||||
/**
|
||||
* Get user ID (0 if anonymous user).
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUserId()
|
||||
{
|
||||
return $this->getData('userId');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user ID.
|
||||
*
|
||||
* @param ?int $userId
|
||||
*/
|
||||
public function setUserId($userId)
|
||||
{
|
||||
if (!isset($userId) || empty($userId)) {
|
||||
$this->user = null;
|
||||
$userId = null;
|
||||
} elseif ($userId != $this->getData('userId')) {
|
||||
$this->user = Repo::user()->get($userId);
|
||||
if (!isset($this->user)) {
|
||||
$userId = null;
|
||||
}
|
||||
}
|
||||
$this->setData('userId', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IP address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->getData('ipAddress');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set IP address.
|
||||
*
|
||||
* @param string $ipAddress
|
||||
*/
|
||||
public function setIpAddress($ipAddress)
|
||||
{
|
||||
$this->setData('ipAddress', $ipAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user agent.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserAgent()
|
||||
{
|
||||
return $this->getData('userAgent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user agent.
|
||||
*
|
||||
* @param string $userAgent
|
||||
*/
|
||||
public function setUserAgent($userAgent)
|
||||
{
|
||||
$this->setData('userAgent', $userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time (in seconds) since session was created.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSecondsCreated()
|
||||
{
|
||||
return $this->getData('created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set time (in seconds) since session was created.
|
||||
*
|
||||
* @param int $created
|
||||
*/
|
||||
public function setSecondsCreated($created)
|
||||
{
|
||||
$this->setData('created', $created);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time (in seconds) since session was last used.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSecondsLastUsed()
|
||||
{
|
||||
return $this->getData('lastUsed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set time (in seconds) since session was last used.
|
||||
*
|
||||
* @param int $lastUsed
|
||||
*/
|
||||
public function setSecondsLastUsed($lastUsed)
|
||||
{
|
||||
$this->setData('lastUsed', $lastUsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if session is to be saved across browser sessions.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getRemember()
|
||||
{
|
||||
return $this->getData('remember');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether session is to be saved across browser sessions.
|
||||
*
|
||||
* @param bool $remember
|
||||
*/
|
||||
public function setRemember($remember)
|
||||
{
|
||||
$this->setData('remember', $remember);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all session parameters.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSessionData()
|
||||
{
|
||||
return $this->getData('data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set session parameters.
|
||||
*
|
||||
* @param string $data
|
||||
*/
|
||||
public function setSessionData($data)
|
||||
{
|
||||
$this->setData('data', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the domain with which the session is registered
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDomain()
|
||||
{
|
||||
return $this->getData('domain');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the domain with which the session is registered
|
||||
*
|
||||
* @param string $data
|
||||
*/
|
||||
public function setDomain($data)
|
||||
{
|
||||
$this->setData('domain', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user associated with this session (null if anonymous user).
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function &getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a usable CSRF token (generating if necessary).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCSRFToken()
|
||||
{
|
||||
$csrf = $this->getSessionVar('csrf');
|
||||
if (!is_array($csrf) || time() > $csrf['timestamp'] + (60 * 60)) { // 1 hour token expiry
|
||||
// Generate random data
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
$data = openssl_random_pseudo_bytes(128);
|
||||
} elseif (function_exists('random_bytes')) {
|
||||
$data = random_bytes(128);
|
||||
} else {
|
||||
$data = sha1(random_int(0, PHP_INT_MAX));
|
||||
}
|
||||
|
||||
// Hash the data
|
||||
$token = null;
|
||||
$salt = Config::getVar('security', 'salt');
|
||||
$algos = hash_algos();
|
||||
foreach (['sha256', 'sha1', 'md5'] as $algo) {
|
||||
if (in_array($algo, $algos)) {
|
||||
$token = hash_hmac($algo, $data, $salt);
|
||||
}
|
||||
}
|
||||
if (!$token) {
|
||||
$token = md5($data . $salt);
|
||||
}
|
||||
|
||||
$csrf = $this->setSessionVar('csrf', [
|
||||
'timestamp' => time(),
|
||||
'token' => $token,
|
||||
]);
|
||||
} else {
|
||||
// Extend timeout of CSRF token
|
||||
$csrf['timestamp'] = time();
|
||||
$this->setSessionVar('csrf', $csrf);
|
||||
}
|
||||
return $csrf['token'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\session\Session', '\Session');
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/session/SessionDAO.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class SessionDAO
|
||||
*
|
||||
* @ingroup session
|
||||
*
|
||||
* @see Session
|
||||
*
|
||||
* @brief Operations for retrieving and modifying Session objects.
|
||||
*/
|
||||
|
||||
namespace PKP\session;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PKP\db\DAO;
|
||||
|
||||
class SessionDAO extends DAO
|
||||
{
|
||||
/**
|
||||
* Instantiate and return a new data object.
|
||||
*/
|
||||
public function newDataObject()
|
||||
{
|
||||
return new Session();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a session by ID.
|
||||
*
|
||||
* @param string $sessionId
|
||||
*
|
||||
* @return Session
|
||||
*/
|
||||
public function getSession($sessionId)
|
||||
{
|
||||
$result = $this->retrieve('SELECT * FROM sessions WHERE session_id = ?', [$sessionId]);
|
||||
|
||||
if ($row = (array) $result->current()) {
|
||||
$session = $this->newDataObject();
|
||||
$session->setId($row['session_id']);
|
||||
$session->setUserId($row['user_id']);
|
||||
$session->setIpAddress($row['ip_address']);
|
||||
$session->setUserAgent($row['user_agent']);
|
||||
$session->setSecondsCreated($row['created']);
|
||||
$session->setSecondsLastUsed($row['last_used']);
|
||||
$session->setRemember($row['remember']);
|
||||
$session->setSessionData($row['data']);
|
||||
$session->setDomain($row['domain']);
|
||||
return $session;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new session.
|
||||
*
|
||||
* @param Session $session
|
||||
*/
|
||||
public function insertObject($session)
|
||||
{
|
||||
$this->update(
|
||||
'INSERT INTO sessions
|
||||
(session_id, ip_address, user_agent, created, last_used, remember, data, domain)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
$session->getId(),
|
||||
$session->getIpAddress(),
|
||||
substr($session->getUserAgent(), 0, 255),
|
||||
(int) $session->getSecondsCreated(),
|
||||
(int) $session->getSecondsLastUsed(),
|
||||
$session->getRemember() ? 1 : 0,
|
||||
$session->getSessionData(),
|
||||
$session->getDomain()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing session.
|
||||
*
|
||||
* @param Session $session
|
||||
*
|
||||
* @return int Number of affected rows
|
||||
*/
|
||||
public function updateObject($session)
|
||||
{
|
||||
return $this->update(
|
||||
'UPDATE sessions
|
||||
SET
|
||||
user_id = ?,
|
||||
ip_address = ?,
|
||||
user_agent = ?,
|
||||
created = ?,
|
||||
last_used = ?,
|
||||
remember = ?,
|
||||
data = ?,
|
||||
domain = ?
|
||||
WHERE session_id = ?',
|
||||
[
|
||||
$session->getUserId() == '' ? null : (int) $session->getUserId(),
|
||||
$session->getIpAddress(),
|
||||
substr($session->getUserAgent(), 0, 255),
|
||||
(int) $session->getSecondsCreated(),
|
||||
(int) $session->getSecondsLastUsed(),
|
||||
$session->getRemember() ? 1 : 0,
|
||||
$session->getSessionData(),
|
||||
$session->getDomain(),
|
||||
$session->getId()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a session.
|
||||
*
|
||||
* @param Session $session
|
||||
*/
|
||||
public function deleteObject($session)
|
||||
{
|
||||
$this->deleteById($session->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a session by ID.
|
||||
*
|
||||
* @param string $sessionId
|
||||
*/
|
||||
public function deleteById($sessionId)
|
||||
{
|
||||
$this->update('DELETE FROM sessions WHERE session_id = ?', [$sessionId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete sessions by user ID.
|
||||
*
|
||||
* @param string $userId
|
||||
*/
|
||||
public function deleteByUserId($userId)
|
||||
{
|
||||
$this->update(
|
||||
'DELETE FROM sessions WHERE user_id = ?',
|
||||
[(int) $userId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all sessions older than the specified time.
|
||||
*
|
||||
* @param int $lastUsed cut-off time in seconds for not-remembered sessions
|
||||
* @param int $lastUsedRemember optional, cut-off time in seconds for remembered sessions
|
||||
*/
|
||||
public function deleteByLastUsed($lastUsed, $lastUsedRemember = 0)
|
||||
{
|
||||
if ($lastUsedRemember == 0) {
|
||||
$this->update(
|
||||
'DELETE FROM sessions WHERE (last_used < ? AND remember = 0)',
|
||||
[(int) $lastUsed]
|
||||
);
|
||||
} else {
|
||||
$this->update(
|
||||
'DELETE FROM sessions WHERE (last_used < ? AND remember = 0) OR (last_used < ? AND remember = 1)',
|
||||
[(int) $lastUsed, (int) $lastUsedRemember]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all sessions.
|
||||
*/
|
||||
public function deleteAllSessions()
|
||||
{
|
||||
$this->update('DELETE FROM sessions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a session exists with the specified ID.
|
||||
*
|
||||
* @param string $sessionId
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function sessionExistsById($sessionId)
|
||||
{
|
||||
$result = $this->retrieve('SELECT COUNT(*) AS row_count FROM sessions WHERE session_id = ?', [$sessionId]);
|
||||
$row = $result->current();
|
||||
return $row ? (bool) $row->row_count : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete given user's all sessions or except for the given session id
|
||||
*
|
||||
* @param int $userId The target user id for whom to invalidate sessions
|
||||
*
|
||||
*/
|
||||
public function deleteUserSessions(int $userId, string $excludableSessionId = null)
|
||||
{
|
||||
DB::table('sessions')
|
||||
->where('user_id', $userId)
|
||||
->when($excludableSessionId, fn ($query) => $query->where('session_id', '<>', $excludableSessionId))
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\session\SessionDAO', '\SessionDAO');
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/session/SessionManager.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class SessionManager
|
||||
*
|
||||
* @ingroup session
|
||||
*
|
||||
* @brief Implements PHP methods for a custom session storage handler (see http://php.net/session).
|
||||
*/
|
||||
|
||||
namespace PKP\session;
|
||||
|
||||
use APP\core\Application;
|
||||
use Carbon\Carbon;
|
||||
use PKP\config\Config;
|
||||
use PKP\core\PKPRequest;
|
||||
use PKP\core\Registry;
|
||||
use PKP\db\DAORegistry;
|
||||
use SessionHandlerInterface;
|
||||
|
||||
class SessionManager implements SessionHandlerInterface
|
||||
{
|
||||
/** The DAO for accessing Session objects */
|
||||
private SessionDao $sessionDao;
|
||||
|
||||
/** The Session associated with the current request */
|
||||
private ?Session $userSession = null;
|
||||
|
||||
private PKPRequest $request;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Initialize session configuration and set PHP session handlers.
|
||||
* Attempts to rejoin a user's session if it exists, or create a new session otherwise.
|
||||
*
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
$this->sessionDao = DAORegistry::getDAO('SessionDAO');
|
||||
$this->request = Application::get()->getRequest();
|
||||
|
||||
$this->configure();
|
||||
$this->start();
|
||||
|
||||
// If there's a session assigned to the session ID
|
||||
if ($this->userSession) {
|
||||
// Validate and refresh it
|
||||
if ($this->isValid($this->userSession)) {
|
||||
return $this->refresh();
|
||||
}
|
||||
// When invalid, regenerates the session ID without destroying the failed session (it might belong to another user)
|
||||
session_regenerate_id();
|
||||
}
|
||||
|
||||
$this->createSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the session manager.
|
||||
*
|
||||
*/
|
||||
public static function getManager(): static
|
||||
{
|
||||
return Registry::get('sessionManager') ?? Registry::get('sessionManager', true, new static());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate given user's all sessions or except for the given session id
|
||||
*
|
||||
* @param int $userId The target user id for whom to invalidate sessions
|
||||
*
|
||||
*/
|
||||
public function invalidateSessions(int $userId, string $excludableSessionId = null): bool
|
||||
{
|
||||
$this->getSessionDao()->deleteUserSessions($userId, $excludableSessionId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Session DAO instance associated with the current request
|
||||
*
|
||||
*/
|
||||
public function getSessionDao(): SessionDao
|
||||
{
|
||||
return $this->sessionDao;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session associated with the current request.
|
||||
*/
|
||||
public function getUserSession(): Session
|
||||
{
|
||||
return $this->userSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a session.
|
||||
* Does nothing; only here to satisfy PHP session handler requirements.
|
||||
*/
|
||||
public function open(string $path, string $name): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a session.
|
||||
* Does nothing; only here to satisfy PHP session handler requirements.
|
||||
*/
|
||||
public function close(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read session data from database.
|
||||
*/
|
||||
public function read(string $sessionId): string
|
||||
{
|
||||
$this->userSession ??= $this->sessionDao->getSession($sessionId);
|
||||
return $this->userSession?->getSessionData() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Save session data to database.
|
||||
*/
|
||||
public function write(string $sessionId, string $data): bool
|
||||
{
|
||||
if ($this->userSession) {
|
||||
$this->userSession->setSessionData($data);
|
||||
$this->sessionDao->updateObject($this->userSession);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy (delete) a session.
|
||||
*/
|
||||
public function destroy(string $sessionId): bool
|
||||
{
|
||||
$this->sessionDao->deleteById($sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage collect unused session data.
|
||||
*
|
||||
* @todo Use $lifetime instead of assuming 24 hours?
|
||||
*
|
||||
* @param int $lifetime the number of seconds after which data will be seen as "garbage" and cleaned up
|
||||
*/
|
||||
public function gc(int $lifetime): int|false
|
||||
{
|
||||
$sessionLifetimeInDays = max(0, Config::getVar('general', 'session_lifetime'));
|
||||
$lastUsedRemember = $sessionLifetimeInDays ? Carbon::now()->subDays($sessionLifetimeInDays)->getTimestamp() : 0;
|
||||
$this->sessionDao->deleteByLastUsed(Carbon::now()->subDay()->getTimestamp(), $lastUsedRemember);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the session ID for the current user session.
|
||||
* This is useful to guard against the "session fixation" form of hijacking
|
||||
* by changing the user's session ID after they have logged in (in case the
|
||||
* original session ID had been pre-populated).
|
||||
*/
|
||||
public function regenerateSessionId(): bool
|
||||
{
|
||||
// Indirectly calls $this->destroy() with the old session ID
|
||||
if (!session_regenerate_id(true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->userSession->setId(session_id());
|
||||
$this->sessionDao->insertObject($this->userSession);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the lifetime of the current session cookie.
|
||||
*
|
||||
*/
|
||||
public function updateSessionLifetime(int $expireTime = 0): bool
|
||||
{
|
||||
$options = session_get_cookie_params();
|
||||
unset($options['lifetime']);
|
||||
$options['expires'] = $expireTime;
|
||||
return setcookie(session_name(), session_id(), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves whether the session initialization is disabled
|
||||
*/
|
||||
public static function isDisabled(): bool
|
||||
{
|
||||
return defined('SESSION_DISABLE_INIT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the session initialization
|
||||
*
|
||||
* @todo Drop the constant definition once it's safe
|
||||
*/
|
||||
public static function disable(): void
|
||||
{
|
||||
// Constant kept for backwards compatibility with applications <= 3.3.0
|
||||
if (!defined('SESSION_DISABLE_INIT')) {
|
||||
define('SESSION_DISABLE_INIT', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves whether the user has a session ID
|
||||
*/
|
||||
public static function hasSession(): bool
|
||||
{
|
||||
// If the session isn't disabled and a cookie is present or a session was started in the current request
|
||||
return !static::isDisabled() && (isset($_COOKIE[Config::getVar('general', 'session_cookie_name')]) || !!session_id());
|
||||
}
|
||||
|
||||
private function configure(): void
|
||||
{
|
||||
$domain = $this->request->getServerHost(includePort: false);
|
||||
|
||||
// Configure PHP session parameters
|
||||
ini_set('session.name', Config::getVar('general', 'session_cookie_name'));
|
||||
ini_set('session.cookie_lifetime', 0);
|
||||
ini_set('session.cookie_path', Config::getVar('general', 'session_cookie_path', $this->request->getBasePath() . '/'));
|
||||
ini_set('session.cookie_domain', $domain);
|
||||
ini_set('session.cookie_httponly', 1);
|
||||
ini_set('session.cookie_samesite', Config::getVar('general', 'same_site', 'Lax'));
|
||||
ini_set('session.cookie_secure', Config::getVar('security', 'force_ssl'));
|
||||
ini_set('session.use_trans_sid', 0);
|
||||
ini_set('session.serialize_handler', 'php');
|
||||
ini_set('session.use_cookies', 1);
|
||||
ini_set('session.gc_probability', 1);
|
||||
ini_set('session.gc_maxlifetime', 60 * 60);
|
||||
ini_set('session.cache_limiter', 'none');
|
||||
|
||||
session_set_save_handler($this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the session
|
||||
*
|
||||
* In case there are many session cookies, it attempts to find the best one and clear the remaining, considering the issues below:
|
||||
* Empty domains: Old applications (e.g. OJS 2.x) didn't store the domain, therefore users revising the application after a migration might be affected, we'll drop it.
|
||||
* Subdomains mixed with parent domains: Users visiting "journal.sfu.ca", then "www.journal.sfu.ca" might end up with N+1 session cookies, this is problematic, we'll try to drop the excess.
|
||||
* Domain cookies belonging to different paths or expired sessions: They will be kept (until the user clears his cookies) and probably trigger the extra checks.
|
||||
*/
|
||||
private function start(): void
|
||||
{
|
||||
$sessionIds = collect($this->getSessionIds());
|
||||
// Standard flow with a single session ID
|
||||
if ($sessionIds->count() < 2) {
|
||||
session_start();
|
||||
return;
|
||||
}
|
||||
|
||||
$requestDomain = $this->request->getServerHost(includePort: false);
|
||||
/** @var \Illuminate\Support\Collection<int,Session> */
|
||||
$sessions = $sessionIds
|
||||
// Attempts to map the ID to an active session
|
||||
->map(fn (string $sessionId) => $this->sessionDao->getSession($sessionId))
|
||||
// Only sessions with valid domains (empty domains are also accepted)
|
||||
->filter(fn (?Session $session) => $session && str_ends_with(strtolower($requestDomain), strtolower($session->getDomain())));
|
||||
|
||||
/** @var ?Session */
|
||||
$bestSession = $sessions->reduce(function (?Session $best, Session $current): ?Session {
|
||||
// Skip invalid sessions
|
||||
if (!$this->isValid($current)) {
|
||||
return $best;
|
||||
}
|
||||
// Give priority to logged in sessions
|
||||
if ($current->getUserId() && !$best?->getUserId()) {
|
||||
return $current;
|
||||
}
|
||||
// Give priority to the session which was used most recently
|
||||
return $current->getSecondsLastUsed() > (int) $best?->getSecondsLastUsed() ? $current : $best;
|
||||
});
|
||||
|
||||
/** @var \Illuminate\Support\Collection<int,string> */
|
||||
$domains = $sessions->map(fn (Session $session) => $session->getDomain() ?: $requestDomain)->unique();
|
||||
// Prefers the parent domain (smaller length) to define the session, fallbacks to the request domain
|
||||
$bestDomain = $domains->reduce(fn (?string $best, string $current) => $best && strlen($best) <= strlen($current) ? $best : $current) ?: $requestDomain;
|
||||
|
||||
// Ensures the session domain isn't empty
|
||||
$bestSession?->setDomain($bestDomain);
|
||||
// Updates the domain setting while the session is closed
|
||||
ini_set('session.cookie_domain', $bestDomain);
|
||||
|
||||
// Seed the session with the proper ID
|
||||
session_id($bestSession?->getId() ?? session_create_id());
|
||||
session_start();
|
||||
|
||||
// The session cookies must be dropped **after** the session is started, otherwise PHP will not send the headers to clear them
|
||||
$this->clearDiscardedSessions($domains->toArray(), $bestDomain);
|
||||
// Ensures the domain is updated (data will be saved once the session gets closed)
|
||||
$this->userSession?->setDomain($bestDomain);
|
||||
$this->updateSessionLifetime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears discarded session cookies
|
||||
*
|
||||
* @param string[] $domains
|
||||
*/
|
||||
private function clearDiscardedSessions(array $domains, string $bestDomain): void
|
||||
{
|
||||
// Includes non-specified/empty domain (cleanup deprecated domainless cookie from OJS 2.x)
|
||||
$domains[] = '';
|
||||
$requestDomain = $this->request->getServerHost(includePort: false);
|
||||
// Includes the request domain if it's not the domain used by the session
|
||||
if ($requestDomain !== $bestDomain) {
|
||||
$domains[] = $requestDomain;
|
||||
}
|
||||
|
||||
// Drops only the cookies (the session data will be cleared by the garbage collector, if we attempt to drop them here we may affect other users)
|
||||
foreach (array_unique($domains) as $domain) {
|
||||
setcookie(session_name(), '', ['domain' => $domain, 'path' => ini_get('session.cookie_path')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves whether the given session is valid
|
||||
*/
|
||||
private function isValid(Session $session): bool
|
||||
{
|
||||
// Same IP address (if IP validation is enabled)
|
||||
return (!Config::getVar('security', 'session_check_ip') || $session->getIpAddress() === $this->request->getRemoteAddr())
|
||||
// Same user agent
|
||||
&& $session->getUserAgent() === substr($this->request->getUserAgent(), 0, 255)
|
||||
// Compatible domain
|
||||
&& (!$session->getDomain() || str_ends_with(strtolower($this->request->getServerHost(includePort: false)), strtolower($session->getDomain())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the session expiration
|
||||
*/
|
||||
private function refresh(): void
|
||||
{
|
||||
// Update existing session's timestamp; will be saved when write is called
|
||||
$this->userSession->setSecondsLastUsed(time());
|
||||
if (!$this->userSession->getRemember()) {
|
||||
return;
|
||||
}
|
||||
// Update session timestamp for remembered sessions so it doesn't expire in the middle of a browser session
|
||||
$lifetime = max(0, Config::getVar('general', 'session_lifetime'));
|
||||
$this->userSession->setRemember((bool) $lifetime);
|
||||
$this->updateSessionLifetime($lifetime ? Carbon::now()->addDays($lifetime)->getTimestamp() : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new session
|
||||
*/
|
||||
private function createSession(): void
|
||||
{
|
||||
$now = time();
|
||||
|
||||
$this->userSession = $this->sessionDao->newDataObject();
|
||||
$this->userSession->setId(session_id());
|
||||
$this->userSession->setIpAddress($this->request->getRemoteAddr());
|
||||
$this->userSession->setUserAgent($this->request->getUserAgent());
|
||||
$this->userSession->setSecondsCreated($now);
|
||||
$this->userSession->setSecondsLastUsed($now);
|
||||
$this->userSession->setDomain(ini_get('session.cookie_domain'));
|
||||
$this->userSession->setSessionData('');
|
||||
|
||||
$this->sessionDao->insertObject($this->userSession);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve session IDs sent by the browser
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function getSessionIds(): array
|
||||
{
|
||||
$ids = [];
|
||||
foreach (explode('; ', $_SERVER['HTTP_COOKIE'] ?? '') as $cookie) {
|
||||
$nameValue = explode('=', $cookie, 2);
|
||||
$value = trim(urldecode($nameValue[1] ?? ''));
|
||||
if ($nameValue[0] === session_name() && strlen($value)) {
|
||||
$ids[$value] = 0;
|
||||
}
|
||||
}
|
||||
return array_keys($ids);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\session\SessionManager', '\SessionManager');
|
||||
}
|
||||
Reference in New Issue
Block a user