[ Mini Kiebo ]
Server: Windows NT DESKTOP-5B8S0D4 6.2 build 9200 (Windows 8 Professional Edition) i586
Path:
D:
/
Backup
/
05122024
/
htdocs
/
jurnal-kesmas
/
v1
/
lib
/
pkp
/
classes
/
session
/
[
Home
]
File: SessionManager.php
<?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'); }