31use Psr\Log\LoggerInterface;
34use Wikimedia\AtEase\AtEase;
59 private $persist =
false;
62 private $remember =
false;
65 private $forceHTTPS =
false;
71 private $forcePersist =
false;
81 private $persistenceChangeType;
87 private $persistenceChangeData = [];
90 private $metaDirty =
false;
93 private $dataDirty =
false;
96 private $dataHash =
null;
114 private $curIndex = 0;
117 private $requests = [];
123 private $providerMetadata =
null;
126 private $expires = 0;
129 private $loggedOut = 0;
132 private $delaySave = 0;
135 private $usePhpSessionHandling;
137 private $checkPHPSessionRecursionGuard =
false;
140 private $shutdown =
false;
156 $this->usePhpSessionHandling = $phpSessionHandling !==
'disable';
159 throw new \InvalidArgumentException(
160 "Refusing to create session for unverified user {$info->getUserInfo()}"
164 throw new \InvalidArgumentException(
'Cannot create session without a provider' );
167 throw new \InvalidArgumentException(
'SessionId and SessionInfo don\'t match' );
172 $this->store = $store;
173 $this->logger = $logger;
174 $this->hookRunner =
new HookRunner( $hookContainer );
175 $this->lifetime = $lifetime;
182 $blob = $store->
get( $store->
makeKey(
'MWSession', (
string)$this->id ) );
183 if ( !is_array(
$blob ) ||
184 !isset(
$blob[
'metadata'] ) || !is_array(
$blob[
'metadata'] ) ||
185 !isset(
$blob[
'data'] ) || !is_array(
$blob[
'data'] )
188 $this->dataDirty =
true;
189 $this->metaDirty =
true;
190 $this->persistenceChangeType =
'no-store';
191 $this->logger->debug(
192 'SessionBackend "{session}" is unsaved, marking dirty in constructor',
194 'session' => $this->id->__toString(),
197 $this->data =
$blob[
'data'];
198 if ( isset(
$blob[
'metadata'][
'loggedOut'] ) ) {
199 $this->loggedOut = (int)
$blob[
'metadata'][
'loggedOut'];
201 if ( isset(
$blob[
'metadata'][
'expires'] ) ) {
202 $this->expires = (int)
$blob[
'metadata'][
'expires'];
204 $this->metaDirty =
true;
205 $this->persistenceChangeType =
'no-expiry';
206 $this->logger->debug(
207 'SessionBackend "{session}" metadata dirty due to missing expiration timestamp',
209 'session' => $this->id->__toString(),
213 $this->dataHash = md5(
serialize( $this->data ) );
222 $index = ++$this->curIndex;
223 $this->requests[$index] = $request;
224 $session =
new Session( $this, $index, $this->logger );
234 unset( $this->requests[$index] );
235 if ( !$this->
shutdown && !count( $this->requests ) ) {
237 $this->provider->getManager()->deregisterSessionBackend( $this );
255 return (
string)$this->id;
272 if ( $this->provider->persistsSessionId() ) {
273 $oldId = (string)$this->
id;
274 $restart = $this->usePhpSessionHandling && $oldId === session_id() &&
280 session_write_close();
283 $this->provider->getManager()->changeBackendId( $this );
284 $this->provider->sessionIdWasReset( $this, $oldId );
285 $this->metaDirty =
true;
286 $this->logger->debug(
287 'SessionBackend "{session}" metadata dirty due to ID reset (formerly "{oldId}")',
289 'session' => $this->id->__toString(),
294 session_id( (
string)$this->
id );
295 AtEase::quietCall(
'session_start' );
301 $this->store->delete( $this->store->makeKey(
'MWSession', $oldId ) );
312 return $this->provider;
323 return $this->persist;
335 $this->forcePersist =
true;
336 $this->metaDirty =
true;
337 $this->logger->debug(
338 'SessionBackend "{session}" force-persist due to persist()',
340 'session' => $this->id->__toString(),
355 session_id() === (
string)$this->
id
357 $this->logger->debug(
358 'SessionBackend "{session}" Closing PHP session for unpersist',
359 [
'session' => $this->id->__toString() ]
361 session_write_close();
366 $this->forcePersist =
true;
367 $this->metaDirty =
true;
371 $this->store->delete( $this->store->makeKey(
'MWSession', (
string)$this->id ) );
383 return $this->remember;
392 if ( $this->remember !== (
bool)$remember ) {
393 $this->remember = (bool)$remember;
394 $this->metaDirty =
true;
395 $this->logger->debug(
396 'SessionBackend "{session}" metadata dirty due to remember-user change',
398 'session' => $this->id->__toString(),
410 if ( !isset( $this->requests[$index] ) ) {
411 throw new \InvalidArgumentException(
'Invalid session index' );
413 return $this->requests[$index];
429 return $this->provider->getAllowedUserRights( $this );
437 return $this->provider->canChangeUser();
449 throw new \BadMethodCallException(
450 'Cannot set user on this session; check $session->canSetUser() first'
455 $this->metaDirty =
true;
456 $this->logger->debug(
457 'SessionBackend "{session}" metadata dirty due to user change',
459 'session' => $this->id->__toString(),
470 if ( !isset( $this->requests[$index] ) ) {
471 throw new \InvalidArgumentException(
'Invalid session index' );
473 return $this->provider->suggestLoginUsername( $this->requests[$index] );
481 return $this->forceHTTPS;
489 if ( $this->forceHTTPS !== (
bool)$force ) {
490 $this->forceHTTPS = (bool)$force;
491 $this->metaDirty =
true;
492 $this->logger->debug(
493 'SessionBackend "{session}" metadata dirty due to force-HTTPS change',
495 'session' => $this->id->__toString(),
506 return $this->loggedOut;
514 if ( $this->loggedOut !== $ts ) {
515 $this->loggedOut = $ts;
516 $this->metaDirty =
true;
517 $this->logger->debug(
518 'SessionBackend "{session}" metadata dirty due to logged-out-timestamp change',
520 'session' => $this->id->__toString(),
532 return $this->providerMetadata;
540 if ( $metadata !==
null && !is_array( $metadata ) ) {
541 throw new \InvalidArgumentException(
'$metadata must be an array or null' );
543 if ( $this->providerMetadata !== $metadata ) {
544 $this->providerMetadata = $metadata;
545 $this->metaDirty =
true;
546 $this->logger->debug(
547 'SessionBackend "{session}" metadata dirty due to provider metadata change',
549 'session' => $this->id->__toString(),
577 foreach ( $newData as $key => $value ) {
578 if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
579 $data[$key] = $value;
580 $this->dataDirty =
true;
581 $this->logger->debug(
582 'SessionBackend "{session}" data dirty due to addData(): {callers}',
584 'session' => $this->id->__toString(),
596 $this->dataDirty =
true;
597 $this->logger->debug(
598 'SessionBackend "{session}" data dirty due to dirty(): {callers}',
600 'session' => $this->id->__toString(),
612 if ( time() + $this->lifetime / 2 > $this->expires ) {
613 $this->metaDirty =
true;
614 $this->logger->debug(
615 'SessionBackend "{callers}" metadata dirty for renew(): {callers}',
617 'session' => $this->id->__toString(),
621 $this->persistenceChangeType =
'renew';
622 $this->forcePersist =
true;
623 $this->logger->debug(
624 'SessionBackend "{session}" force-persist for renew(): {callers}',
626 'session' => $this->id->__toString(),
643 return new \Wikimedia\ScopedCallback(
function () {
655 private function autosave() {
670 public function save( $closing =
false ) {
671 $anon = $this->user->isAnon();
673 if ( !$anon && $this->provider->getManager()->isUserSessionPrevented( $this->user->getName() ) ) {
674 $this->logger->debug(
675 'SessionBackend "{session}" not saving, user {user} was ' .
676 'passed to SessionManager::preventSessionsForUser',
678 'session' => $this->id->__toString(),
679 'user' => $this->user->__toString(),
686 if ( !$anon && !$this->user->getToken(
false ) ) {
687 $this->logger->debug(
688 'SessionBackend "{session}" creating token for user {user} on save',
690 'session' => $this->id->__toString(),
691 'user' => $this->user->__toString(),
693 $this->user->setToken();
697 \DeferredUpdates::addCallableUpdate(
static function () use ( $user ) {
701 $this->metaDirty =
true;
705 if ( !$this->metaDirty && !$this->dataDirty &&
706 $this->dataHash !== md5(
serialize( $this->data ) )
708 $this->logger->debug(
709 'SessionBackend "{session}" data dirty due to hash mismatch, {expected} !== {got}',
711 'session' => $this->id->__toString(),
712 'expected' => $this->dataHash,
713 'got' => md5(
serialize( $this->data ) ),
715 $this->dataDirty =
true;
718 if ( !$this->metaDirty && !$this->dataDirty && !$this->forcePersist ) {
722 $this->logger->debug(
723 'SessionBackend "{session}" save: dataDirty={dataDirty} ' .
724 'metaDirty={metaDirty} forcePersist={forcePersist}',
726 'session' => $this->id->__toString(),
727 'dataDirty' => (
int)$this->dataDirty,
728 'metaDirty' => (
int)$this->metaDirty,
729 'forcePersist' => (
int)$this->forcePersist,
733 if ( $this->metaDirty || $this->forcePersist ) {
735 foreach ( $this->requests as $request ) {
737 $this->logPersistenceChange( $request,
true );
738 $this->provider->persistSession( $this, $request );
741 $this->checkPHPSession();
744 foreach ( $this->requests as $request ) {
745 if ( $request->getSessionId() === $this->id ) {
746 $this->logPersistenceChange( $request,
false );
747 $this->provider->unpersistSession( $request );
753 $this->forcePersist =
false;
754 $this->persistenceChangeType =
null;
756 if ( !$this->metaDirty && !$this->dataDirty ) {
761 $metadata = $origMetadata = [
762 'provider' => (string)$this->provider,
763 'providerMetadata' => $this->providerMetadata,
764 'userId' => $anon ? 0 : $this->user->getId(),
766 ->isValid( $this->user->getName() ) ? $this->user->getName() :
null,
767 'userToken' => $anon ? null : $this->user->getToken(),
768 'remember' => !$anon && $this->remember,
769 'forceHTTPS' => $this->forceHTTPS,
770 'expires' => time() + $this->lifetime,
771 'loggedOut' => $this->loggedOut,
772 'persisted' => $this->persist,
775 $this->hookRunner->onSessionMetadata( $this, $metadata, $this->requests );
777 foreach ( $origMetadata as $k => $v ) {
778 if ( $metadata[$k] !== $v ) {
779 throw new \UnexpectedValueException(
"SessionMetadata hook changed metadata key \"$k\"" );
783 $flags = $this->
persist ? 0 : CachedBagOStuff::WRITE_CACHE_ONLY;
785 $this->store->makeKey(
'MWSession', (
string)$this->id ),
787 'data' => $this->data,
788 'metadata' => $metadata,
790 $metadata[
'expires'],
794 $this->metaDirty =
false;
795 $this->dataDirty =
false;
796 $this->dataHash = md5(
serialize( $this->data ) );
797 $this->expires = $metadata[
'expires'];
804 private function checkPHPSession() {
805 if ( !$this->checkPHPSessionRecursionGuard ) {
806 $this->checkPHPSessionRecursionGuard =
true;
807 $reset = new \Wikimedia\ScopedCallback(
function () {
808 $this->checkPHPSessionRecursionGuard =
false;
814 $this->logger->debug(
815 'SessionBackend "{session}" Taking over PHP session',
817 'session' => $this->id->__toString(),
819 session_id( (
string)$this->
id );
820 AtEase::quietCall(
'session_start' );
830 private function logPersistenceChange(
WebRequest $request,
bool $persist ) {
837 $verb = $persist ?
'Persisting' :
'Unpersisting';
838 if ( $this->persistenceChangeType ===
'renew' ) {
839 $message =
"$verb session for renewal";
840 } elseif ( $this->persistenceChangeType ===
'no-store' ) {
841 $message =
"$verb session due to no pre-existing stored session";
842 } elseif ( $this->persistenceChangeType ===
'no-expiry' ) {
843 $message =
"$verb session due to lack of stored expiry";
844 } elseif ( $this->persistenceChangeType ===
null ) {
845 $message =
"$verb session for unknown reason";
851 $id = $this->
getId();
853 if ( $this->persistenceChangeData
854 && $this->persistenceChangeData[
'id'] === $id
855 && $this->persistenceChangeData[
'user'] === $user
857 && $this->persistenceChangeData[
'message'] === $message
862 $this->persistenceChangeData = [
'id' => $id,
'user' => $user,
'message' => $message ];
865 $this->logger->info( $message, [
869 'clientip' => $request->
getIP(),
870 'userAgent' => $request->
getHeader(
'user-agent' ),
wfGetAllCallers( $limit=3)
Return a string consisting of callers in the stack.
Wrapper around a BagOStuff that caches data in memory.
makeKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
get( $key, $flags=0)
Get an item.
A class containing constants representing the names of configuration variables.
const PHPSessionHandling
Name constant for the PHPSessionHandling setting, for use with Config::get()
saveSettings()
Save this user's settings into the database.
isAnon()
Get whether the user is anonymous.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
getIP()
Work out the IP address based on various globals For trusted proxies, use the XFF client IP (first of...
getHeader( $name, $flags=0)
Get a request header, or false if it isn't set.