3 namespace wcf\system\session
;
5 use ParagonIE\ConstantTime\Hex
;
6 use wcf\data\session\Session
as LegacySession
;
7 use wcf\data\session\SessionEditor
;
8 use wcf\data\user\User
;
9 use wcf\data\user\UserEditor
;
10 use wcf\system\application\ApplicationHandler
;
11 use wcf\system\cache\builder\SpiderCacheBuilder
;
12 use wcf\system\cache\builder\UserGroupOptionCacheBuilder
;
13 use wcf\system\cache\builder\UserGroupPermissionCacheBuilder
;
14 use wcf\system\database\DatabaseException
;
15 use wcf\system\database\util\PreparedStatementConditionBuilder
;
16 use wcf\system\event\EventHandler
;
17 use wcf\system\exception\PermissionDeniedException
;
18 use wcf\system\page\PageLocationManager
;
19 use wcf\system\request\RouteHandler
;
20 use wcf\system\SingletonFactory
;
21 use wcf\system\user\storage\UserStorageHandler
;
23 use wcf\system\WCFACP
;
24 use wcf\util\CryptoUtil
;
25 use wcf\util\HeaderUtil
;
26 use wcf\util\UserUtil
;
31 * @author Tim Duesterhus, Alexander Ebert
32 * @copyright 2001-2020 WoltLab GmbH
33 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
34 * @package WoltLabSuite\Core\System\Session
36 * @property-read string $sessionID unique textual identifier of the session
37 * @property-read int|null $userID id of the user the session belongs to or `null` if the session belongs to a guest
38 * @property-read int|null $pageID id of the latest page visited
39 * @property-read int|null $pageObjectID id of the object the latest page visited belongs to
40 * @property-read int|null $parentPageID id of the parent page of latest page visited
41 * @property-read int|null $parentPageObjectID id of the object the parent page of latest page visited belongs to
42 * @property-read int $spiderID id of the spider the session belongs to
44 final class SessionHandler
extends SingletonFactory
47 * prevents update on shutdown
50 protected $doNotUpdate = false;
53 * disables page tracking
56 protected $disableTracking = false;
59 * group data and permissions
65 * true if within ACP or WCFSetup
68 protected $isACP = false;
71 * language id for active user
74 protected $languageID = 0;
77 * language ids for active user
80 protected $languageIDs;
90 protected $legacySession;
108 protected $variables = [];
111 * indicates if session variables changed and must be saved upon shutdown
114 protected $variablesChanged = false;
117 * true if this is a new session
120 protected $firstVisit = false;
123 * list of names of permissions only available for users
126 protected $usersOnlyPermissions = [];
133 private const GUEST_SESSION_LIFETIME
= 2 * 3600;
135 private const USER_SESSION_LIFETIME
= 60 * 86400;
137 private const CHANGE_USER_AFTER_MULTIFACTOR_KEY
= self
::class . "\0__changeUserAfterMultifactor__";
139 private const PENDING_USER_LIFETIME
= 15 * 60;
141 private const REAUTHENTICATION_KEY
= self
::class . "\0__reauthentication__";
143 private const REAUTHENTICATION_HARD_LIMIT
= 12 * 3600;
145 private const REAUTHENTICATION_SOFT_LIMIT
= 2 * 3600;
147 private const REAUTHENTICATION_SOFT_LIMIT_ACP
= 2 * 3600;
149 private const REAUTHENTICATION_GRACE_PERIOD
= 15 * 60;
152 * Provides access to session data.
157 public function __get($key)
161 return $this->sessionID
;
163 return $this->user
->userID
;
165 return $this->getSpiderID(UserUtil
::getUserAgent());
169 case 'parentPageObjectID':
170 return $this->legacySession
->{$key};
172 /** @deprecated 5.4 - The below values are deprecated. */
174 return UserUtil
::getIpAddress();
176 return UserUtil
::getUserAgent();
178 return UserUtil
::getRequestURI();
179 case 'requestMethod':
180 return !empty($_SERVER['REQUEST_METHOD']) ? \
substr($_SERVER['REQUEST_METHOD'], 0, 7) : '';
181 case 'lastActivityTime':
192 protected function init()
194 $this->isACP
= (\
class_exists(WCFACP
::class, false) ||
!PACKAGE_ID
);
195 $this->usersOnlyPermissions
= UserGroupOptionCacheBuilder
::getInstance()->getData([], 'usersOnlyOptions');
199 * @deprecated 5.4 - This method is a noop. The cookie suffix is determined automatically.
201 public function setCookieSuffix()
206 * @deprecated 5.4 - This method is a noop. Cookie handling works automatically.
208 public function setHasValidCookie($hasValidCookie)
213 * Parses the session cookie value, returning an array with the stored fields.
215 * The return array is guaranteed to have a `sessionId` key.
217 private function parseCookie(string $value): array
219 $length = \
mb_strlen($value, '8bit');
221 throw new \
InvalidArgumentException(\
sprintf(
222 'Expected at least 1 Byte, %d given.',
227 $version = \
unpack('Cversion', $value)['version'];
228 if (!\
in_array($version, [1], true)) {
229 throw new \
InvalidArgumentException(\
sprintf(
230 'Unknown version %d',
235 if ($version === 1) {
236 if ($length !== 22) {
237 throw new \
InvalidArgumentException(\
sprintf(
238 'Expected exactly 22 Bytes, %d given.',
242 $data = \
unpack('Cversion/A20sessionId/Ctimestep', $value);
243 $data['sessionId'] = Hex
::encode($data['sessionId']);
248 throw new \
LogicException('Unreachable');
252 * Extracts the data from the session cookie.
254 * @see SessionHandler::parseCookie()
257 private function getParsedCookieData(): ?
array
259 $cookieName = COOKIE_PREFIX
. "user_session";
261 if (!empty($_COOKIE[$cookieName])) {
264 'sessionId' => $_COOKIE[$cookieName],
268 $cookieData = CryptoUtil
::getValueFromSignedString($_COOKIE[$cookieName]);
270 // Check whether the sessionId was correctly signed.
276 return $this->parseCookie($cookieData);
277 } catch (\InvalidArgumentException
$e) {
286 * Returns the session ID stored in the session cookie or `null`.
288 private function getSessionIdFromCookie(): ?
string
290 $cookieData = $this->getParsedCookieData();
293 return $cookieData['sessionId'];
300 * Returns the current time step. The time step changes
303 private function getCookieTimestep(): int
305 $window = (24 * 3600);
307 \assert
((self
::USER_SESSION_LIFETIME
/ $window) < 0xFF);
309 return \floor
(TIME_NOW
/ $window) & 0xFF;
313 * Returns the signed session data for use in a cookie.
315 private function getCookieValue(): string
318 return $this->sessionID
;
321 return CryptoUtil
::createSignedString(\
pack(
324 Hex
::decode($this->sessionID
),
325 $this->getCookieTimestep()
330 * Returns true if client provided a valid session cookie.
335 public function hasValidCookie(): bool
337 return $this->getSessionIdFromCookie() === $this->sessionID
;
341 * @deprecated 5.4 - Sessions are managed automatically. Use loadFromCookie().
343 public function load($sessionEditorClassName, $sessionID)
346 if (!empty($sessionID)) {
347 $hasSession = $this->getExistingSession($sessionID);
356 * Loads the session matching the session cookie.
358 public function loadFromCookie()
360 $sessionID = $this->getSessionIdFromCookie();
364 $hasSession = $this->getExistingSession($sessionID);
368 $this->maybeRefreshCookie();
375 * Refreshes the session cookie, extending the expiry.
377 private function maybeRefreshCookie(): void
379 // Guests use short-lived sessions with an actual session cookie.
380 if (!$this->user
->userID
) {
384 $cookieData = $this->getParsedCookieData();
386 // No refresh is needed if the timestep matches up.
387 if (isset($cookieData['timestep']) && $cookieData['timestep'] === $this->getCookieTimestep()) {
391 // Refresh the cookie.
392 HeaderUtil
::setCookie(
394 $this->getCookieValue(),
395 TIME_NOW +
(self
::USER_SESSION_LIFETIME
* 2)
400 * Initializes session system.
402 public function initSession()
404 $this->defineConstants();
406 // assign language and style id
407 $this->languageID
= $this->getVar('languageID') ?
: $this->user
->languageID
;
408 $this->styleID
= $this->getVar('styleID') ?
: $this->user
->styleID
;
410 // https://github.com/WoltLab/WCF/issues/2568
411 if ($this->getVar('__wcfIsFirstVisit') === true) {
412 $this->firstVisit
= true;
413 $this->unregister('__wcfIsFirstVisit');
418 * Disables update on shutdown.
420 public function disableUpdate()
422 $this->doNotUpdate
= true;
426 * Disables page tracking.
428 public function disableTracking()
430 $this->disableTracking
= true;
434 * Defines global wcf constants related to session.
436 protected function defineConstants()
439 if (!\
defined('SECURITY_TOKEN')) {
440 \
define('SECURITY_TOKEN', $this->getSecurityToken());
442 if (!\
defined('SECURITY_TOKEN_INPUT_TAG')) {
444 'SECURITY_TOKEN_INPUT_TAG',
445 '<input type="hidden" name="t" value="' . $this->getSecurityToken() . '">'
451 * Initializes security token.
453 protected function initSecurityToken()
456 if (!empty($_COOKIE['XSRF-TOKEN'])) {
457 // We intentionally do not extract the signed value and instead just verify the correctness.
459 // The reason is that common JavaScript frameworks can use the contents of the `XSRF-TOKEN` cookie as-is,
460 // without performing any processing on it, improving interoperability. Leveraging this JavaScript framework
461 // feature requires the author of the controller to check the value within the `X-XSRF-TOKEN` request header
462 // instead of the WoltLab Suite specific `t` parameter, though.
464 // The only reason we sign the cookie is that an XSS vulnerability or a rogue application on a subdomain
465 // is not able to create a valid `XSRF-TOKEN`, e.g. by setting the `XSRF-TOKEN` cookie to the static
466 // value `1234`, possibly allowing later exploitation.
467 if (!PACKAGE_ID || CryptoUtil
::validateSignedString($_COOKIE['XSRF-TOKEN'])) {
468 $xsrfToken = $_COOKIE['XSRF-TOKEN'];
474 $xsrfToken = CryptoUtil
::createSignedString(\random_bytes
(16));
476 $xsrfToken = Hex
::encode(\random_bytes
(16));
479 // We construct the cookie manually instead of using HeaderUtil::setCookie(), because:
480 // 1) We don't want the prefix. The `XSRF-TOKEN` cookie name is a standard name across applications
481 // and it is supported by default in common JavaScript frameworks.
482 // 2) We want to set the SameSite=strict parameter.
483 // 3) We don't want the HttpOnly parameter.
484 $sameSite = $cookieDomain = '';
486 if (ApplicationHandler
::getInstance()->isMultiDomainSetup()) {
487 // We need to specify the cookieDomain in a multi domain set-up, because
488 // otherwise no cookies are sent to subdomains.
489 $cookieDomain = HeaderUtil
::getCookieDomain();
490 $cookieDomain = ($cookieDomain !== null ?
'; domain=' . $cookieDomain : '');
492 // SameSite=strict is not supported in a multi domain set-up, because
493 // it breaks cross-application requests.
494 $sameSite = '; SameSite=strict';
498 'set-cookie: XSRF-TOKEN=' . \rawurlencode
($xsrfToken) . '; path=/' . $cookieDomain . (RouteHandler
::secureConnection() ?
'; secure' : '') . $sameSite,
503 $this->xsrfToken
= $xsrfToken;
507 * Returns security token.
511 public function getSecurityToken()
513 if ($this->xsrfToken
=== null) {
514 $this->initSecurityToken();
517 return $this->xsrfToken
;
521 * Validates the given security token, returns false if
522 * given token is invalid.
524 * @param string $token
527 public function checkSecurityToken($token)
529 // The output of CryptoUtil::createSignedString() is not url-safe. For compatibility
530 // reasons the SECURITY_TOKEN in URLs might not be encoded, turning the '+' into a space.
531 // Convert it back before comparing.
532 $token = \
str_replace(' ', '+', $token);
534 return \
hash_equals($this->getSecurityToken(), $token);
538 * Registers a session variable.
541 * @param mixed $value
543 public function register($key, $value)
545 $scope = $this->isACP ?
'acp' : 'frontend';
547 $this->variables
[$scope][$key] = $value;
548 $this->variablesChanged
= true;
552 * Unsets a session variable.
556 public function unregister($key)
558 $scope = $this->isACP ?
'acp' : 'frontend';
560 unset($this->variables
[$scope][$key]);
561 $this->variablesChanged
= true;
565 * Returns the value of a session variable or `null` if the session
566 * variable does not exist.
571 public function getVar($key)
573 $scope = $this->isACP ?
'acp' : 'frontend';
575 if (isset($this->variables
[$scope][$key])) {
576 return $this->variables
[$scope][$key];
581 * Returns the user object of this session.
585 public function getUser()
591 * Tries to read existing session identified by the given session id. Returns whether
592 * a session could be found.
594 protected function getExistingSession(string $sessionID): bool
597 FROM wcf" . WCF_N
. "_user_session
598 WHERE sessionID = ?";
599 $statement = WCF
::getDB()->prepareStatement($sql);
600 $statement->execute([
603 $row = $statement->fetchSingleRow();
609 // Check whether the session technically already expired.
610 $lifetime = ($row['userID'] ? self
::USER_SESSION_LIFETIME
: self
::GUEST_SESSION_LIFETIME
);
611 if ($row['lastActivityTime'] < (TIME_NOW
- $lifetime)) {
615 $variables = @\
unserialize($row['sessionVariables']);
616 // Check whether the session variables became corrupted.
617 if (!\
is_array($variables)) {
621 $this->sessionID
= $sessionID;
622 $this->user
= new User($row['userID']);
623 $this->variables
= $variables;
625 $sql = "UPDATE wcf" . WCF_N
. "_user_session
629 WHERE sessionID = ?";
630 $statement = WCF
::getDB()->prepareStatement($sql);
631 $statement->execute([
632 UserUtil
::getIpAddress(),
633 UserUtil
::getUserAgent(),
638 // Fetch legacy session.
639 $condition = new PreparedStatementConditionBuilder();
641 if ($row['userID']) {
642 // The `userID IS NOT NULL` condition technically is redundant, but is added for
643 // clarity and consistency with the guest case below.
644 $condition->add('userID IS NOT NULL');
645 $condition->add('userID = ?', [$row['userID']]);
647 $condition->add('userID IS NULL');
648 $condition->add('(sessionID = ? OR spiderID = ?)', [
650 $this->getSpiderID(UserUtil
::getUserAgent()),
655 FROM wcf" . WCF_N
. "_session
657 $statement = WCF
::getDB()->prepareStatement($sql);
658 $statement->execute($condition->getParameters());
659 $this->legacySession
= $statement->fetchSingleObject(LegacySession
::class);
661 if (!$this->legacySession
) {
662 $this->createLegacySession();
669 * Creates a new session.
671 protected function create()
673 $this->sessionID
= Hex
::encode(\random_bytes
(20));
680 // Create new session.
681 $sql = "INSERT INTO wcf" . WCF_N
. "_user_session
682 (sessionID, ipAddress, userAgent, lastActivityTime, sessionVariables)
683 VALUES (?, ?, ?, ?, ?)";
684 $statement = WCF
::getDB()->prepareStatement($sql);
685 $statement->execute([
687 UserUtil
::getIpAddress(),
688 UserUtil
::getUserAgent(),
690 \
serialize($variables),
693 $this->variables
= $variables;
694 $this->user
= new User(null);
695 $this->firstVisit
= true;
697 HeaderUtil
::setCookie(
699 $this->getCookieValue()
702 // Maintain legacy session table for users online list.
703 $this->createLegacySession();
706 private function createLegacySession()
708 $spiderID = $this->getSpiderID(UserUtil
::getUserAgent());
712 'sessionID' => $this->sessionID
,
713 'userID' => $this->user
->userID
,
714 'ipAddress' => UserUtil
::getIpAddress(),
715 'userAgent' => UserUtil
::getUserAgent(),
716 'lastActivityTime' => TIME_NOW
,
717 'requestURI' => UserUtil
::getRequestURI(),
718 'requestMethod' => !empty($_SERVER['REQUEST_METHOD']) ? \
substr($_SERVER['REQUEST_METHOD'], 0, 7) : '',
721 if ($spiderID !== null) {
722 $sessionData['spiderID'] = $spiderID;
725 $this->legacySession
= SessionEditor
::create($sessionData);
729 * Returns the value of the permission with the given name.
731 * @param string $permission
732 * @return mixed permission value
734 public function getPermission($permission)
736 // check if a users only permission is checked for a guest and return
737 // false if that is the case
738 if (!$this->user
->userID
&& \
in_array($permission, $this->usersOnlyPermissions
)) {
742 $this->loadGroupData();
744 if (!isset($this->groupData
[$permission])) {
748 return $this->groupData
[$permission];
752 * Returns true if a permission was set to 'Never'. This is required to preserve
753 * compatibility, while preventing ACLs from overruling a 'Never' setting.
755 * @param string $permission
758 public function getNeverPermission($permission)
760 $this->loadGroupData();
762 return isset($this->groupData
['__never'][$permission]);
766 * Checks if the active user has the given permissions and throws a
767 * PermissionDeniedException if that isn't the case.
769 * @param string[] $permissions list of permissions where each one must pass
770 * @throws PermissionDeniedException
772 public function checkPermissions(array $permissions)
774 foreach ($permissions as $permission) {
775 if (!$this->getPermission($permission)) {
776 throw new PermissionDeniedException();
782 * Loads group data from cache.
784 protected function loadGroupData()
786 if ($this->groupData
!== null) {
790 // work-around for setup process (package wcf does not exist yet)
792 $sql = "SELECT groupID
793 FROM wcf" . WCF_N
. "_user_to_group
795 $statement = WCF
::getDB()->prepareStatement($sql);
796 $statement->execute([$this->user
->userID
]);
797 $groupIDs = $statement->fetchAll(\PDO
::FETCH_COLUMN
);
799 $groupIDs = $this->user
->getGroupIDs();
802 // get group data from cache
803 $this->groupData
= UserGroupPermissionCacheBuilder
::getInstance()->getData($groupIDs);
804 if (isset($this->groupData
['groupIDs']) && $this->groupData
['groupIDs'] != $groupIDs) {
805 $this->groupData
= [];
810 * Returns language ids for active user.
814 public function getLanguageIDs()
816 $this->loadLanguageIDs();
818 return $this->languageIDs
;
822 * Loads language ids for active user.
824 protected function loadLanguageIDs()
826 if ($this->languageIDs
!== null) {
830 $this->languageIDs
= [];
832 if (!$this->user
->userID
) {
836 // work-around for setup process (package wcf does not exist yet)
838 $sql = "SELECT languageID
839 FROM wcf" . WCF_N
. "_user_to_language
841 $statement = WCF
::getDB()->prepareStatement($sql);
842 $statement->execute([$this->user
->userID
]);
843 $this->languageIDs
= $statement->fetchAll(\PDO
::FETCH_COLUMN
);
845 $this->languageIDs
= $this->user
->getLanguageIDs();
850 * If multi-factor authentication is enabled for the given user then
851 * - the userID will be stored in the session variables, and
852 * - `true` is returned.
854 * - `changeUser()` will be called, and
855 * - `false` is returned.
857 * If `true` is returned you should perform a redirect to `MultifactorAuthenticationForm`.
861 public function changeUserAfterMultifactorAuthentication(User
$user): bool
863 if ($user->multifactorActive
) {
864 $this->register(self
::CHANGE_USER_AFTER_MULTIFACTOR_KEY
, [
865 'userId' => $user->userID
,
866 'expires' => TIME_NOW + self
::PENDING_USER_LIFETIME
,
868 $this->setLanguageID($user->languageID
);
872 $this->changeUser($user);
879 * Applies the pending user change, calling `changeUser()` for the user returned
880 * by `getPendingUserChange()`.
882 * As a safety check you must provide the `$expectedUser` as a parameter, it must match the
883 * data stored within the session.
885 * @throws \RuntimeException If the `$expectedUser` does not match.
886 * @throws \BadMethodCallException If `getPendingUserChange()` returns `null`.
887 * @see SessionHandler::getPendingUserChange()
890 public function applyPendingUserChange(User
$expectedUser): void
892 $user = $this->getPendingUserChange();
893 $this->clearPendingUserChange();
895 if ($user->userID
!== $expectedUser->userID
) {
896 throw new \
RuntimeException('Mismatching expectedUser.');
900 throw new \
BadMethodCallException('No pending user change.');
903 $this->changeUser($user);
907 * Returns the pending user change initiated by `changeUserAfterMultifactorAuthentication()`.
909 * @see SessionHandler::changeUserAfterMultifactorAuthentication()
912 public function getPendingUserChange(): ?User
914 $data = $this->getVar(self
::CHANGE_USER_AFTER_MULTIFACTOR_KEY
);
919 $userId = $data['userId'];
920 $expires = $data['expires'];
922 if ($expires < TIME_NOW
) {
926 $user = new User($userId);
928 if (!$user->userID
) {
936 * Clears a pending user change, reverses the effects of `changeUserAfterMultifactorAuthentication()`.
938 * @see SessionHandler::changeUserAfterMultifactorAuthentication()
941 public function clearPendingUserChange(): void
943 $this->unregister(self
::CHANGE_USER_AFTER_MULTIFACTOR_KEY
);
947 * Stores a new user object in this session, e.g. a user was guest because not
948 * logged in, after the login his old session is used to store his full data.
951 * @param bool $hideSession if true, database won't be updated
953 public function changeUser(User
$user, $hideSession = false)
955 $eventParameters = ['user' => $user, 'hideSession' => $hideSession];
957 EventHandler
::getInstance()->fireAction($this, 'beforeChangeUser', $eventParameters);
959 $user = $eventParameters['user'];
960 $hideSession = $eventParameters['hideSession'];
962 // skip changeUserVirtual, if session will not be persistent anyway
964 $this->changeUserVirtual($user);
967 // update user reference
971 $this->groupData
= null;
972 $this->languageIDs
= null;
973 $this->languageID
= $this->user
->languageID
;
974 $this->styleID
= $this->user
->styleID
;
977 WCF
::setLanguage($this->languageID ?
: 0);
979 // in some cases the language id can be stuck in the session variables
980 $this->unregister('languageID');
982 EventHandler
::getInstance()->fireAction($this, 'afterChangeUser');
986 * Changes the user stored in the session.
989 * @throws DatabaseException
991 protected function changeUserVirtual(User
$user)
993 // We must delete the old session to not carry over any state across different users.
996 // If the target user is a registered user ...
998 // ... we create a new session with a new session ID ...
1001 // ... delete the newly created legacy session ...
1002 $sql = "DELETE FROM wcf" . WCF_N
. "_session
1003 WHERE sessionID = ?";
1004 $statement = WCF
::getDB()->prepareStatement($sql);
1005 $statement->execute([$this->sessionID
]);
1007 // ... perform the login ...
1008 $sql = "UPDATE wcf" . WCF_N
. "_user_session
1010 WHERE sessionID = ?";
1011 $statement = WCF
::getDB()->prepareStatement($sql);
1012 $statement->execute([
1017 // ... and reload the session with the updated information.
1018 $hasSession = $this->getExistingSession($this->sessionID
);
1021 throw new \
LogicException('Unreachable');
1027 * Checks whether the user needs to authenticate themselves once again
1028 * to access a security critical area.
1030 * If `true` is returned you should perform a redirect to `ReAuthenticationForm`,
1031 * otherwise the user is sufficiently authenticated and may proceed.
1033 * @throws \BadMethodCallException If the current user is a guest.
1036 public function needsReauthentication(): bool
1038 if (!$this->getUser()->userID
) {
1039 throw new \
BadMethodCallException('The current user is a guest.');
1042 // Reauthentication for third party authentication is not supported.
1043 if ($this->getUser()->authData
) {
1047 $data = $this->getVar(self
::REAUTHENTICATION_KEY
);
1049 // Request a new authentication if no stored information is available.
1054 $lastAuthentication = $data['lastAuthentication'];
1055 $lastCheck = $data['lastCheck'];
1057 // Request a new authentication if the hard limit since the last authentication
1059 if ($lastAuthentication < (TIME_NOW
- self
::REAUTHENTICATION_HARD_LIMIT
)) {
1063 $softLimit = self
::REAUTHENTICATION_SOFT_LIMIT
;
1065 $softLimit = self
::REAUTHENTICATION_SOFT_LIMIT_ACP
;
1067 // If both the debug mode and the developer tools are enabled the
1068 // reauthentication soft limit within the ACP matches the hard limit.
1070 // This allows for a continous access to the ACP and specifically the
1071 // developer tools within a single workday without needing to re-login
1072 // just because one spent 15 minutes within the IDE.
1073 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
1074 $softLimit = self
::REAUTHENTICATION_HARD_LIMIT
;
1078 // Request a new authentication if the soft limit since the last authentication
1080 if ($lastAuthentication < (TIME_NOW
- $softLimit)) {
1081 // ... and the grace period since the last check is also exceeded.
1082 if ($lastCheck < (TIME_NOW
- self
::REAUTHENTICATION_GRACE_PERIOD
)) {
1087 // If we reach this point we determined that a new authentication is not necessary.
1089 ($lastAuthentication >= TIME_NOW
- $softLimit)
1090 ||
($lastAuthentication >= TIME_NOW
- self
::REAUTHENTICATION_HARD_LIMIT
1091 && $lastCheck >= TIME_NOW
- self
::REAUTHENTICATION_GRACE_PERIOD
)
1094 // Update the lastCheck timestamp to make sure that the grace period works properly.
1096 // The grace period allows the user to complete their action if the soft limit
1097 // expires between loading a form and actually submitting that form, provided that
1098 // the user does not take longer than the grace period to fill in the form.
1099 $data['lastCheck'] = TIME_NOW
;
1100 $this->register(self
::REAUTHENTICATION_KEY
, $data);
1106 * Registers that the user performed reauthentication successfully.
1108 * This method should be considered to be semi-public and is intended to be used
1109 * by `ReAuthenticationForm` only.
1111 * @throws \BadMethodCallException If the current user is a guest.
1112 * @see SessionHandler::needsReauthentication()
1115 public function registerReauthentication(): void
1117 if (!$this->getUser()->userID
) {
1118 throw new \
BadMethodCallException('The current user is a guest.');
1121 $this->register(self
::REAUTHENTICATION_KEY
, [
1122 'lastAuthentication' => TIME_NOW
,
1123 'lastCheck' => TIME_NOW
,
1128 * Clears that the user performed reauthentication successfully.
1130 * After this method is called `needsReauthentication()` will return true until
1131 * `registerReauthentication()` is called again.
1133 * @throws \BadMethodCallException If the current user is a guest.
1134 * @see SessionHandler::needsReauthentication()
1135 * @see SessionHandler::registerReauthentication()
1138 public function clearReauthentication(): void
1140 if (!$this->getUser()->userID
) {
1141 throw new \
BadMethodCallException('The current user is a guest.');
1144 $this->unregister(self
::REAUTHENTICATION_KEY
);
1148 * Updates user session on shutdown.
1150 public function update()
1152 if ($this->doNotUpdate
) {
1156 if ($this->variablesChanged
) {
1157 $sql = "UPDATE wcf" . WCF_N
. "_user_session
1158 SET sessionVariables = ?
1159 WHERE sessionID = ?";
1160 $statement = WCF
::getDB()->prepareStatement($sql);
1161 $statement->execute([
1162 \
serialize($this->variables
),
1166 // Reset the flag, because the variables are no longer dirty.
1167 $this->variablesChanged
= false;
1171 'ipAddress' => $this->ipAddress
,
1172 'userAgent' => $this->userAgent
,
1173 'requestURI' => $this->requestURI
,
1174 'requestMethod' => $this->requestMethod
,
1175 'lastActivityTime' => TIME_NOW
,
1176 'userID' => $this->user
->userID
,
1177 'sessionID' => $this->sessionID
,
1179 if (!\
class_exists('wcf\system\CLIWCF', false) && !$this->disableTracking
) {
1180 $pageLocations = PageLocationManager
::getInstance()->getLocations();
1181 if (isset($pageLocations[0])) {
1182 $data['pageID'] = $pageLocations[0]['pageID'];
1183 $data['pageObjectID'] = ($pageLocations[0]['pageObjectID'] ?
: null);
1184 $data['parentPageID'] = null;
1185 $data['parentPageObjectID'] = null;
1187 for ($i = 1, $length = \
count($pageLocations); $i < $length; $i++
) {
1188 if (!empty($pageLocations[$i]['useAsParentLocation'])) {
1189 $data['parentPageID'] = $pageLocations[$i]['pageID'];
1190 $data['parentPageObjectID'] = ($pageLocations[$i]['pageObjectID'] ?
: null);
1197 if ($this->legacySession
) {
1198 $sessionEditor = new SessionEditor($this->legacySession
);
1199 $sessionEditor->update($data);
1204 * @deprecated 5.4 - This method is a noop. The lastActivityTime is always updated immediately after loading.
1206 public function keepAlive()
1211 * Deletes this session and its related data.
1213 public function delete()
1216 if ($this->user
->userID
) {
1217 self
::resetSessions([$this->user
->userID
]);
1219 // update last activity time
1220 $editor = new UserEditor($this->user
);
1221 $editor->update(['lastActivityTime' => TIME_NOW
]);
1224 $this->deleteUserSession($this->sessionID
);
1228 * Prunes expired sessions.
1230 public function prune()
1232 $sql = "DELETE FROM wcf" . WCF_N
. "_user_session
1233 WHERE (lastActivityTime < ? AND userID IS NULL)
1234 OR (lastActivityTime < ? AND userID IS NOT NULL)";
1235 $statement = WCF
::getDB()->prepareStatement($sql);
1236 $statement->execute([
1237 TIME_NOW
- self
::GUEST_SESSION_LIFETIME
,
1238 TIME_NOW
- self
::USER_SESSION_LIFETIME
,
1241 // Legacy sessions live 120 minutes, they will be re-created on demand.
1242 $sql = "DELETE FROM wcf" . WCF_N
. "_session
1243 WHERE lastActivityTime < ?";
1244 $statement = WCF
::getDB()->prepareStatement($sql);
1245 $statement->execute([
1246 TIME_NOW
- (3600 * 2),
1251 * Deletes this session if:
1252 * - it is newly created in this request, and
1253 * - it belongs to a guest.
1255 * This method is useful if you have controllers that are likely to be
1256 * accessed by a user agent that is not going to re-use sessions (e.g.
1257 * curl in a cronjob). It immediately remove the session that was created
1258 * just for that request and that is not going to be used ever again.
1262 public function deleteIfNew()
1264 if ($this->isFirstVisit() && !$this->getUser()->userID
) {
1270 * Returns currently active language id.
1274 public function getLanguageID()
1276 return $this->languageID
;
1280 * Sets the currently active language id.
1282 * @param int $languageID
1284 public function setLanguageID($languageID)
1286 $this->languageID
= $languageID;
1287 $this->register('languageID', $this->languageID
);
1291 * Returns currently active style id.
1295 public function getStyleID()
1297 return $this->styleID
;
1301 * Sets the currently active style id.
1303 * @param int $styleID
1305 public function setStyleID($styleID)
1307 $this->styleID
= $styleID;
1308 $this->register('styleID', $this->styleID
);
1312 * Resets session-specific storage data.
1314 * @param int[] $userIDs
1316 public static function resetSessions(array $userIDs = [])
1318 if (!empty($userIDs)) {
1319 UserStorageHandler
::getInstance()->reset($userIDs, 'groupIDs');
1320 UserStorageHandler
::getInstance()->reset($userIDs, 'languageIDs');
1322 UserStorageHandler
::getInstance()->resetAll('groupIDs');
1323 UserStorageHandler
::getInstance()->resetAll('languageIDs');
1328 * Returns the spider id for given user agent.
1330 * @param string $userAgent
1333 protected function getSpiderID($userAgent)
1335 $spiderList = SpiderCacheBuilder
::getInstance()->getData();
1336 $userAgent = \
strtolower($userAgent);
1338 foreach ($spiderList as $spider) {
1339 if (\
strpos($userAgent, $spider->spiderIdentifier
) !== false) {
1340 return $spider->spiderID
;
1346 * Returns true if this is a new session.
1350 public function isFirstVisit()
1352 return $this->firstVisit
;
1356 * Returns all user sessions for a specific user.
1359 * @throws \InvalidArgumentException if the given user is a guest.
1362 public function getUserSessions(User
$user): array
1364 return $this->getSessions($user, false);
1368 * Returns all acp sessions for a specific user.
1371 * @throws \InvalidArgumentException if the given user is a guest.
1374 public function getAcpSessions(User
$user): array
1376 return $this->getSessions($user, true);
1380 * Returns all sessions for a specific user.
1383 * @throws \InvalidArgumentException if the given user is a guest.
1386 private function getSessions(User
$user): array
1388 if (!$user->userID
) {
1389 throw new \
InvalidArgumentException("The given user is a guest.");
1393 FROM wcf" . WCF_N
. "_user_session
1395 $statement = WCF
::getDB()->prepareStatement($sql);
1396 $statement->execute([$user->userID
]);
1399 while ($row = $statement->fetchArray()) {
1400 $sessions[] = new Session($row);
1407 * Deletes the user sessions for a specific user, except the session with the given session id.
1409 * If the given session id is `null` or unknown, all sessions of the user will be deleted.
1411 * @throws \InvalidArgumentException if the given user is a guest.
1414 public function deleteUserSessionsExcept(User
$user, ?
string $sessionID = null): void
1416 if (!$user->userID
) {
1417 throw new \
InvalidArgumentException("The given user is a guest.");
1420 $conditionBuilder = new PreparedStatementConditionBuilder();
1421 $conditionBuilder->add('userID = ?', [$user->userID
]);
1423 if ($sessionID !== null) {
1424 $conditionBuilder->add('sessionID <> ?', [$sessionID]);
1427 $sql = "DELETE FROM wcf" . WCF_N
. "_user_session
1428 " . $conditionBuilder;
1429 $statement = WCF
::getDB()->prepareStatement($sql);
1430 $statement->execute($conditionBuilder->getParameters());
1432 // Delete legacy session.
1433 $sql = "DELETE FROM wcf" . WCF_N
. "_session
1434 " . $conditionBuilder;
1435 $statement = WCF
::getDB()->prepareStatement($sql);
1436 $statement->execute($conditionBuilder->getParameters());
1440 * Deletes a user session with the given session ID.
1444 public function deleteUserSession(string $sessionID): void
1446 $sql = "DELETE FROM wcf" . WCF_N
. "_user_session
1447 WHERE sessionID = ?";
1448 $statement = WCF
::getDB()->prepareStatement($sql);
1449 $statement->execute([$sessionID]);
1451 // Delete legacy session.
1452 $sql = "DELETE FROM wcf" . WCF_N
. "_session
1453 WHERE sessionID = ?";
1454 $statement = WCF
::getDB()->prepareStatement($sql);
1455 $statement->execute([$sessionID]);