MediaWiki REL1_35
SessionBackend.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
29use Psr\Log\LoggerInterface;
30use User;
31use WebRequest;
32use Wikimedia\AtEase\AtEase;
33
52final class SessionBackend {
54 private $id;
55
57 private $persist = false;
58
60 private $remember = false;
61
63 private $forceHTTPS = false;
64
66 private $data = null;
67
69 private $forcePersist = false;
70
72 private $metaDirty = false;
73
75 private $dataDirty = false;
76
78 private $dataHash = null;
79
81 private $store;
82
84 private $logger;
85
87 private $hookRunner;
88
90 private $lifetime;
91
93 private $user;
94
96 private $curIndex = 0;
97
99 private $requests = [];
100
102 private $provider;
103
105 private $providerMetadata = null;
106
108 private $expires = 0;
109
111 private $loggedOut = 0;
112
114 private $delaySave = 0;
115
120
122 private $shutdown = false;
123
132 public function __construct(
133 SessionId $id, SessionInfo $info, CachedBagOStuff $store, LoggerInterface $logger,
134 HookContainer $hookContainer, $lifetime
135 ) {
136 $phpSessionHandling = \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' );
137 $this->usePhpSessionHandling = $phpSessionHandling !== 'disable';
138
139 if ( $info->getUserInfo() && !$info->getUserInfo()->isVerified() ) {
140 throw new \InvalidArgumentException(
141 "Refusing to create session for unverified user {$info->getUserInfo()}"
142 );
143 }
144 if ( $info->getProvider() === null ) {
145 throw new \InvalidArgumentException( 'Cannot create session without a provider' );
146 }
147 if ( $info->getId() !== $id->getId() ) {
148 throw new \InvalidArgumentException( 'SessionId and SessionInfo don\'t match' );
149 }
150
151 $this->id = $id;
152 $this->user = $info->getUserInfo() ? $info->getUserInfo()->getUser() : new User;
153 $this->store = $store;
154 $this->logger = $logger;
155 $this->hookRunner = new HookRunner( $hookContainer );
156 $this->lifetime = $lifetime;
157 $this->provider = $info->getProvider();
158 $this->persist = $info->wasPersisted();
159 $this->remember = $info->wasRemembered();
160 $this->forceHTTPS = $info->forceHTTPS();
161 $this->providerMetadata = $info->getProviderMetadata();
162
163 $blob = $store->get( $store->makeKey( 'MWSession', (string)$this->id ) );
164 if ( !is_array( $blob ) ||
165 !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] ) ||
166 !isset( $blob['data'] ) || !is_array( $blob['data'] )
167 ) {
168 $this->data = [];
169 $this->dataDirty = true;
170 $this->metaDirty = true;
171 $this->logger->debug(
172 'SessionBackend "{session}" is unsaved, marking dirty in constructor',
173 [
174 'session' => $this->id->__toString(),
175 ] );
176 } else {
177 $this->data = $blob['data'];
178 if ( isset( $blob['metadata']['loggedOut'] ) ) {
179 $this->loggedOut = (int)$blob['metadata']['loggedOut'];
180 }
181 if ( isset( $blob['metadata']['expires'] ) ) {
182 $this->expires = (int)$blob['metadata']['expires'];
183 } else {
184 $this->metaDirty = true;
185 $this->logger->debug(
186 'SessionBackend "{session}" metadata dirty due to missing expiration timestamp',
187 [
188 'session' => $this->id->__toString(),
189 ] );
190 }
191 }
192 $this->dataHash = md5( serialize( $this->data ) );
193 }
194
200 public function getSession( WebRequest $request ) {
201 $index = ++$this->curIndex;
202 $this->requests[$index] = $request;
203 $session = new Session( $this, $index, $this->logger );
204 return $session;
205 }
206
212 public function deregisterSession( $index ) {
213 unset( $this->requests[$index] );
214 if ( !$this->shutdown && !count( $this->requests ) ) {
215 $this->save( true );
216 $this->provider->getManager()->deregisterSessionBackend( $this );
217 }
218 }
219
224 public function shutdown() {
225 $this->save( true );
226 $this->shutdown = true;
227 }
228
233 public function getId() {
234 return (string)$this->id;
235 }
236
242 public function getSessionId() {
243 return $this->id;
244 }
245
250 public function resetId() {
251 if ( $this->provider->persistsSessionId() ) {
252 $oldId = (string)$this->id;
253 $restart = $this->usePhpSessionHandling && $oldId === session_id() &&
255
256 if ( $restart ) {
257 // If this session is the one behind PHP's $_SESSION, we need
258 // to close then reopen it.
259 session_write_close();
260 }
261
262 $this->provider->getManager()->changeBackendId( $this );
263 $this->provider->sessionIdWasReset( $this, $oldId );
264 $this->metaDirty = true;
265 $this->logger->debug(
266 'SessionBackend "{session}" metadata dirty due to ID reset (formerly "{oldId}")',
267 [
268 'session' => $this->id->__toString(),
269 'oldId' => $oldId,
270 ] );
271
272 if ( $restart ) {
273 session_id( (string)$this->id );
274 AtEase::quietCall( 'session_start' );
275 }
276
277 $this->autosave();
278
279 // Delete the data for the old session ID now
280 $this->store->delete( $this->store->makeKey( 'MWSession', $oldId ) );
281 }
282
283 return $this->id;
284 }
285
290 public function getProvider() {
291 return $this->provider;
292 }
293
301 public function isPersistent() {
302 return $this->persist;
303 }
304
311 public function persist() {
312 if ( !$this->persist ) {
313 $this->persist = true;
314 $this->forcePersist = true;
315 $this->metaDirty = true;
316 $this->logger->debug(
317 'SessionBackend "{session}" force-persist due to persist()',
318 [
319 'session' => $this->id->__toString(),
320 ] );
321 $this->autosave();
322 } else {
323 $this->renew();
324 }
325 }
326
330 public function unpersist() {
331 if ( $this->persist ) {
332 // Close the PHP session, if we're the one that's open
333 if ( $this->usePhpSessionHandling && PHPSessionHandler::isEnabled() &&
334 session_id() === (string)$this->id
335 ) {
336 $this->logger->debug(
337 'SessionBackend "{session}" Closing PHP session for unpersist',
338 [ 'session' => $this->id->__toString() ]
339 );
340 session_write_close();
341 session_id( '' );
342 }
343
344 $this->persist = false;
345 $this->forcePersist = true;
346 $this->metaDirty = true;
347
348 // Delete the session data, so the local cache-only write in
349 // self::save() doesn't get things out of sync with the backend.
350 $this->store->delete( $this->store->makeKey( 'MWSession', (string)$this->id ) );
351
352 $this->autosave();
353 }
354 }
355
361 public function shouldRememberUser() {
362 return $this->remember;
363 }
364
370 public function setRememberUser( $remember ) {
371 if ( $this->remember !== (bool)$remember ) {
372 $this->remember = (bool)$remember;
373 $this->metaDirty = true;
374 $this->logger->debug(
375 'SessionBackend "{session}" metadata dirty due to remember-user change',
376 [
377 'session' => $this->id->__toString(),
378 ] );
379 $this->autosave();
380 }
381 }
382
388 public function getRequest( $index ) {
389 if ( !isset( $this->requests[$index] ) ) {
390 throw new \InvalidArgumentException( 'Invalid session index' );
391 }
392 return $this->requests[$index];
393 }
394
399 public function getUser() {
400 return $this->user;
401 }
402
407 public function getAllowedUserRights() {
408 return $this->provider->getAllowedUserRights( $this );
409 }
410
415 public function canSetUser() {
416 return $this->provider->canChangeUser();
417 }
418
426 public function setUser( $user ) {
427 if ( !$this->canSetUser() ) {
428 throw new \BadMethodCallException(
429 'Cannot set user on this session; check $session->canSetUser() first'
430 );
431 }
432
433 $this->user = $user;
434 $this->metaDirty = true;
435 $this->logger->debug(
436 'SessionBackend "{session}" metadata dirty due to user change',
437 [
438 'session' => $this->id->__toString(),
439 ] );
440 $this->autosave();
441 }
442
448 public function suggestLoginUsername( $index ) {
449 if ( !isset( $this->requests[$index] ) ) {
450 throw new \InvalidArgumentException( 'Invalid session index' );
451 }
452 return $this->provider->suggestLoginUsername( $this->requests[$index] );
453 }
454
459 public function shouldForceHTTPS() {
460 return $this->forceHTTPS;
461 }
462
467 public function setForceHTTPS( $force ) {
468 if ( $this->forceHTTPS !== (bool)$force ) {
469 $this->forceHTTPS = (bool)$force;
470 $this->metaDirty = true;
471 $this->logger->debug(
472 'SessionBackend "{session}" metadata dirty due to force-HTTPS change',
473 [
474 'session' => $this->id->__toString(),
475 ] );
476 $this->autosave();
477 }
478 }
479
484 public function getLoggedOutTimestamp() {
485 return $this->loggedOut;
486 }
487
492 public function setLoggedOutTimestamp( $ts = null ) {
493 $ts = (int)$ts;
494 if ( $this->loggedOut !== $ts ) {
495 $this->loggedOut = $ts;
496 $this->metaDirty = true;
497 $this->logger->debug(
498 'SessionBackend "{session}" metadata dirty due to logged-out-timestamp change',
499 [
500 'session' => $this->id->__toString(),
501 ] );
502 $this->autosave();
503 }
504 }
505
511 public function getProviderMetadata() {
513 }
514
520 public function setProviderMetadata( $metadata ) {
521 if ( $metadata !== null && !is_array( $metadata ) ) {
522 throw new \InvalidArgumentException( '$metadata must be an array or null' );
523 }
524 if ( $this->providerMetadata !== $metadata ) {
525 $this->providerMetadata = $metadata;
526 $this->metaDirty = true;
527 $this->logger->debug(
528 'SessionBackend "{session}" metadata dirty due to provider metadata change',
529 [
530 'session' => $this->id->__toString(),
531 ] );
532 $this->autosave();
533 }
534 }
535
545 public function &getData() {
546 return $this->data;
547 }
548
556 public function addData( array $newData ) {
557 $data = &$this->getData();
558 foreach ( $newData as $key => $value ) {
559 if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
560 $data[$key] = $value;
561 $this->dataDirty = true;
562 $this->logger->debug(
563 'SessionBackend "{session}" data dirty due to addData(): {callers}',
564 [
565 'session' => $this->id->__toString(),
566 'callers' => wfGetAllCallers( 5 ),
567 ] );
568 }
569 }
570 }
571
576 public function dirty() {
577 $this->dataDirty = true;
578 $this->logger->debug(
579 'SessionBackend "{session}" data dirty due to dirty(): {callers}',
580 [
581 'session' => $this->id->__toString(),
582 'callers' => wfGetAllCallers( 5 ),
583 ] );
584 }
585
592 public function renew() {
593 if ( time() + $this->lifetime / 2 > $this->expires ) {
594 $this->metaDirty = true;
595 $this->logger->debug(
596 'SessionBackend "{callers}" metadata dirty for renew(): {callers}',
597 [
598 'session' => $this->id->__toString(),
599 'callers' => wfGetAllCallers( 5 ),
600 ] );
601 if ( $this->persist ) {
602 $this->forcePersist = true;
603 $this->logger->debug(
604 'SessionBackend "{session}" force-persist for renew(): {callers}',
605 [
606 'session' => $this->id->__toString(),
607 'callers' => wfGetAllCallers( 5 ),
608 ] );
609 }
610 }
611 $this->autosave();
612 }
613
621 public function delaySave() {
622 $this->delaySave++;
623 return new \Wikimedia\ScopedCallback( function () {
624 if ( --$this->delaySave <= 0 ) {
625 $this->delaySave = 0;
626 $this->save();
627 }
628 } );
629 }
630
635 private function autosave() {
636 if ( $this->delaySave <= 0 ) {
637 $this->save();
638 }
639 }
640
650 public function save( $closing = false ) {
651 $anon = $this->user->isAnon();
652
653 if ( !$anon && $this->provider->getManager()->isUserSessionPrevented( $this->user->getName() ) ) {
654 $this->logger->debug(
655 'SessionBackend "{session}" not saving, user {user} was ' .
656 'passed to SessionManager::preventSessionsForUser',
657 [
658 'session' => $this->id->__toString(),
659 'user' => $this->user->__toString(),
660 ] );
661 return;
662 }
663
664 // Ensure the user has a token
665 // @codeCoverageIgnoreStart
666 if ( !$anon && !$this->user->getToken( false ) ) {
667 $this->logger->debug(
668 'SessionBackend "{session}" creating token for user {user} on save',
669 [
670 'session' => $this->id->__toString(),
671 'user' => $this->user->__toString(),
672 ] );
673 $this->user->setToken();
674 if ( !wfReadOnly() ) {
675 // Promise that the token set here will be valid; save it at end of request
677 \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
679 } );
680 }
681 $this->metaDirty = true;
682 }
683 // @codeCoverageIgnoreEnd
684
685 if ( !$this->metaDirty && !$this->dataDirty &&
686 $this->dataHash !== md5( serialize( $this->data ) )
687 ) {
688 $this->logger->debug(
689 'SessionBackend "{session}" data dirty due to hash mismatch, {expected} !== {got}',
690 [
691 'session' => $this->id->__toString(),
692 'expected' => $this->dataHash,
693 'got' => md5( serialize( $this->data ) ),
694 ] );
695 $this->dataDirty = true;
696 }
697
698 if ( !$this->metaDirty && !$this->dataDirty && !$this->forcePersist ) {
699 return;
700 }
701
702 $this->logger->debug(
703 'SessionBackend "{session}" save: dataDirty={dataDirty} ' .
704 'metaDirty={metaDirty} forcePersist={forcePersist}',
705 [
706 'session' => $this->id->__toString(),
707 'dataDirty' => (int)$this->dataDirty,
708 'metaDirty' => (int)$this->metaDirty,
709 'forcePersist' => (int)$this->forcePersist,
710 ] );
711
712 // Persist or unpersist to the provider, if necessary
713 if ( $this->metaDirty || $this->forcePersist ) {
714 if ( $this->persist ) {
715 foreach ( $this->requests as $request ) {
716 $request->setSessionId( $this->getSessionId() );
717 $this->provider->persistSession( $this, $request );
718 }
719 if ( !$closing ) {
720 $this->checkPHPSession();
721 }
722 } else {
723 foreach ( $this->requests as $request ) {
724 if ( $request->getSessionId() === $this->id ) {
725 $this->provider->unpersistSession( $request );
726 }
727 }
728 }
729 }
730
731 $this->forcePersist = false;
732
733 if ( !$this->metaDirty && !$this->dataDirty ) {
734 return;
735 }
736
737 // Save session data to store, if necessary
738 $metadata = $origMetadata = [
739 'provider' => (string)$this->provider,
740 'providerMetadata' => $this->providerMetadata,
741 'userId' => $anon ? 0 : $this->user->getId(),
742 'userName' => User::isValidUserName( $this->user->getName() ) ? $this->user->getName() : null,
743 'userToken' => $anon ? null : $this->user->getToken(),
744 'remember' => !$anon && $this->remember,
745 'forceHTTPS' => $this->forceHTTPS,
746 'expires' => time() + $this->lifetime,
747 'loggedOut' => $this->loggedOut,
748 'persisted' => $this->persist,
749 ];
750
751 $this->hookRunner->onSessionMetadata( $this, $metadata, $this->requests );
752
753 foreach ( $origMetadata as $k => $v ) {
754 if ( $metadata[$k] !== $v ) {
755 throw new \UnexpectedValueException( "SessionMetadata hook changed metadata key \"$k\"" );
756 }
757 }
758
759 $flags = $this->persist ? 0 : CachedBagOStuff::WRITE_CACHE_ONLY;
760 $flags |= CachedBagOStuff::WRITE_SYNC; // write to all datacenters
761 $this->store->set(
762 $this->store->makeKey( 'MWSession', (string)$this->id ),
763 [
764 'data' => $this->data,
765 'metadata' => $metadata,
766 ],
767 $metadata['expires'],
768 $flags
769 );
770
771 $this->metaDirty = false;
772 $this->dataDirty = false;
773 $this->dataHash = md5( serialize( $this->data ) );
774 $this->expires = $metadata['expires'];
775 }
776
781 private function checkPHPSession() {
782 if ( !$this->checkPHPSessionRecursionGuard ) {
783 $this->checkPHPSessionRecursionGuard = true;
784 $reset = new \Wikimedia\ScopedCallback( function () {
785 $this->checkPHPSessionRecursionGuard = false;
786 } );
787
788 if ( $this->usePhpSessionHandling && session_id() === '' && PHPSessionHandler::isEnabled() &&
789 SessionManager::getGlobalSession()->getId() === (string)$this->id
790 ) {
791 $this->logger->debug(
792 'SessionBackend "{session}" Taking over PHP session',
793 [
794 'session' => $this->id->__toString(),
795 ] );
796 session_id( (string)$this->id );
797 AtEase::quietCall( 'session_start' );
798 }
799 }
800 }
801
802}
serialize()
wfReadOnly()
Check whether the wiki is in read-only mode.
wfGetAllCallers( $limit=3)
Return a string consisting of callers in the stack.
Wrapper around a BagOStuff that caches data in memory.
get( $key, $flags=0)
Get an item with the given key.
makeKey( $class,... $components)
Make a cache key, scoped to this instance's keyspace.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
static isEnabled()
Test whether the handler is installed and enabled.
This is the actual workhorse for Session.
setLoggedOutTimestamp( $ts=null)
Set the "logged out" timestamp.
addData(array $newData)
Add data to the session.
__construct(SessionId $id, SessionInfo $info, CachedBagOStuff $store, LoggerInterface $logger, HookContainer $hookContainer, $lifetime)
suggestLoginUsername( $index)
Get a suggested username for the login form.
isPersistent()
Indicate whether this session is persisted across requests.
getSession(WebRequest $request)
Return a new Session for this backend.
shouldForceHTTPS()
Whether HTTPS should be forced.
& getData()
Fetch the session data array.
getProviderMetadata()
Fetch provider metadata.
deregisterSession( $index)
Deregister a Session.
getSessionId()
Fetch the SessionId object.
canSetUser()
Indicate whether the session user info can be changed.
delaySave()
Delay automatic saving while multiple updates are being made.
checkPHPSession()
For backwards compatibility, open the PHP session when the global session is persisted.
unpersist()
Make this session not persisted across requests.
WebRequest[] $requests
Session requests.
setProviderMetadata( $metadata)
Set provider metadata.
array null $providerMetadata
provider-specified metadata
setForceHTTPS( $force)
Set whether HTTPS should be forced.
getProvider()
Fetch the SessionProvider for this session.
getId()
Returns the session ID.
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
resetId()
Changes the session ID.
setUser( $user)
Set a new user for this session.
save( $closing=false)
Save the session.
getUser()
Returns the authenticated user for this session.
autosave()
Save the session, unless delayed.
renew()
Renew the session by resaving everything.
SessionProvider $provider
provider
string $dataHash
Used to detect subarray modifications.
getAllowedUserRights()
Fetch the rights allowed the user when this session is active.
getRequest( $index)
Returns the request associated with a Session.
persist()
Make this session persisted across requests.
setRememberUser( $remember)
Set whether the user should be remembered independently of the session ID.
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Value object holding the session ID in a manner that can be globally updated.
Definition SessionId.php:40
Value object returned by SessionProvider.
getProviderMetadata()
Return provider metadata.
getId()
Return the session ID.
getProvider()
Return the provider.
getUserInfo()
Return the user.
wasPersisted()
Return whether the session is persisted.
wasRemembered()
Return whether the user was remembered.
forceHTTPS()
Whether this session should only be used over HTTPS.
static getGlobalSession()
Get the "global" session.
A SessionProvider provides SessionInfo and support for Session.
Manages data for an an authenticated session.
Definition Session.php:48
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
static isValidUserName( $name)
Is the input a valid username?
Definition User.php:979
saveSettings()
Save this user's settings into the database.
Definition User.php:3445
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...