MediaWiki REL1_34
SessionBackend.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
27use Psr\Log\LoggerInterface;
28use User;
29use WebRequest;
30use Wikimedia\AtEase\AtEase;
31
50final class SessionBackend {
52 private $id;
53
55 private $persist = false;
56
58 private $remember = false;
59
61 private $forceHTTPS = false;
62
64 private $data = null;
65
67 private $forcePersist = false;
68
70 private $metaDirty = false;
71
73 private $dataDirty = false;
74
76 private $dataHash = null;
77
79 private $store;
80
82 private $logger;
83
85 private $lifetime;
86
88 private $user;
89
91 private $curIndex = 0;
92
94 private $requests = [];
95
97 private $provider;
98
100 private $providerMetadata = null;
101
103 private $expires = 0;
104
106 private $loggedOut = 0;
107
109 private $delaySave = 0;
110
115
117 private $shutdown = false;
118
126 public function __construct(
128 ) {
129 $phpSessionHandling = \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' );
130 $this->usePhpSessionHandling = $phpSessionHandling !== 'disable';
131
132 if ( $info->getUserInfo() && !$info->getUserInfo()->isVerified() ) {
133 throw new \InvalidArgumentException(
134 "Refusing to create session for unverified user {$info->getUserInfo()}"
135 );
136 }
137 if ( $info->getProvider() === null ) {
138 throw new \InvalidArgumentException( 'Cannot create session without a provider' );
139 }
140 if ( $info->getId() !== $id->getId() ) {
141 throw new \InvalidArgumentException( 'SessionId and SessionInfo don\'t match' );
142 }
143
144 $this->id = $id;
145 $this->user = $info->getUserInfo() ? $info->getUserInfo()->getUser() : new User;
146 $this->store = $store;
147 $this->logger = $logger;
148 $this->lifetime = $lifetime;
149 $this->provider = $info->getProvider();
150 $this->persist = $info->wasPersisted();
151 $this->remember = $info->wasRemembered();
152 $this->forceHTTPS = $info->forceHTTPS();
153 $this->providerMetadata = $info->getProviderMetadata();
154
155 $blob = $store->get( $store->makeKey( 'MWSession', (string)$this->id ) );
156 if ( !is_array( $blob ) ||
157 !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] ) ||
158 !isset( $blob['data'] ) || !is_array( $blob['data'] )
159 ) {
160 $this->data = [];
161 $this->dataDirty = true;
162 $this->metaDirty = true;
163 $this->logger->debug(
164 'SessionBackend "{session}" is unsaved, marking dirty in constructor',
165 [
166 'session' => $this->id,
167 ] );
168 } else {
169 $this->data = $blob['data'];
170 if ( isset( $blob['metadata']['loggedOut'] ) ) {
171 $this->loggedOut = (int)$blob['metadata']['loggedOut'];
172 }
173 if ( isset( $blob['metadata']['expires'] ) ) {
174 $this->expires = (int)$blob['metadata']['expires'];
175 } else {
176 $this->metaDirty = true;
177 $this->logger->debug(
178 'SessionBackend "{session}" metadata dirty due to missing expiration timestamp',
179 [
180 'session' => $this->id,
181 ] );
182 }
183 }
184 $this->dataHash = md5( serialize( $this->data ) );
185 }
186
192 public function getSession( WebRequest $request ) {
193 $index = ++$this->curIndex;
194 $this->requests[$index] = $request;
195 $session = new Session( $this, $index, $this->logger );
196 return $session;
197 }
198
204 public function deregisterSession( $index ) {
205 unset( $this->requests[$index] );
206 if ( !$this->shutdown && !count( $this->requests ) ) {
207 $this->save( true );
208 $this->provider->getManager()->deregisterSessionBackend( $this );
209 }
210 }
211
216 public function shutdown() {
217 $this->save( true );
218 $this->shutdown = true;
219 }
220
225 public function getId() {
226 return (string)$this->id;
227 }
228
234 public function getSessionId() {
235 return $this->id;
236 }
237
242 public function resetId() {
243 if ( $this->provider->persistsSessionId() ) {
244 $oldId = (string)$this->id;
245 $restart = $this->usePhpSessionHandling && $oldId === session_id() &&
247
248 if ( $restart ) {
249 // If this session is the one behind PHP's $_SESSION, we need
250 // to close then reopen it.
251 session_write_close();
252 }
253
254 $this->provider->getManager()->changeBackendId( $this );
255 $this->provider->sessionIdWasReset( $this, $oldId );
256 $this->metaDirty = true;
257 $this->logger->debug(
258 'SessionBackend "{session}" metadata dirty due to ID reset (formerly "{oldId}")',
259 [
260 'session' => $this->id,
261 'oldId' => $oldId,
262 ] );
263
264 if ( $restart ) {
265 session_id( (string)$this->id );
266 AtEase::quietCall( 'session_start' );
267 }
268
269 $this->autosave();
270
271 // Delete the data for the old session ID now
272 $this->store->delete( $this->store->makeKey( 'MWSession', $oldId ) );
273 }
274
275 return $this->id;
276 }
277
282 public function getProvider() {
283 return $this->provider;
284 }
285
293 public function isPersistent() {
294 return $this->persist;
295 }
296
303 public function persist() {
304 if ( !$this->persist ) {
305 $this->persist = true;
306 $this->forcePersist = true;
307 $this->metaDirty = true;
308 $this->logger->debug(
309 'SessionBackend "{session}" force-persist due to persist()',
310 [
311 'session' => $this->id,
312 ] );
313 $this->autosave();
314 } else {
315 $this->renew();
316 }
317 }
318
322 public function unpersist() {
323 if ( $this->persist ) {
324 // Close the PHP session, if we're the one that's open
325 if ( $this->usePhpSessionHandling && PHPSessionHandler::isEnabled() &&
326 session_id() === (string)$this->id
327 ) {
328 $this->logger->debug(
329 'SessionBackend "{session}" Closing PHP session for unpersist',
330 [ 'session' => $this->id ]
331 );
332 session_write_close();
333 session_id( '' );
334 }
335
336 $this->persist = false;
337 $this->forcePersist = true;
338 $this->metaDirty = true;
339
340 // Delete the session data, so the local cache-only write in
341 // self::save() doesn't get things out of sync with the backend.
342 $this->store->delete( $this->store->makeKey( 'MWSession', (string)$this->id ) );
343
344 $this->autosave();
345 }
346 }
347
353 public function shouldRememberUser() {
354 return $this->remember;
355 }
356
362 public function setRememberUser( $remember ) {
363 if ( $this->remember !== (bool)$remember ) {
364 $this->remember = (bool)$remember;
365 $this->metaDirty = true;
366 $this->logger->debug(
367 'SessionBackend "{session}" metadata dirty due to remember-user change',
368 [
369 'session' => $this->id,
370 ] );
371 $this->autosave();
372 }
373 }
374
380 public function getRequest( $index ) {
381 if ( !isset( $this->requests[$index] ) ) {
382 throw new \InvalidArgumentException( 'Invalid session index' );
383 }
384 return $this->requests[$index];
385 }
386
391 public function getUser() {
392 return $this->user;
393 }
394
399 public function getAllowedUserRights() {
400 return $this->provider->getAllowedUserRights( $this );
401 }
402
407 public function canSetUser() {
408 return $this->provider->canChangeUser();
409 }
410
418 public function setUser( $user ) {
419 if ( !$this->canSetUser() ) {
420 throw new \BadMethodCallException(
421 'Cannot set user on this session; check $session->canSetUser() first'
422 );
423 }
424
425 $this->user = $user;
426 $this->metaDirty = true;
427 $this->logger->debug(
428 'SessionBackend "{session}" metadata dirty due to user change',
429 [
430 'session' => $this->id,
431 ] );
432 $this->autosave();
433 }
434
440 public function suggestLoginUsername( $index ) {
441 if ( !isset( $this->requests[$index] ) ) {
442 throw new \InvalidArgumentException( 'Invalid session index' );
443 }
444 return $this->provider->suggestLoginUsername( $this->requests[$index] );
445 }
446
451 public function shouldForceHTTPS() {
452 return $this->forceHTTPS;
453 }
454
459 public function setForceHTTPS( $force ) {
460 if ( $this->forceHTTPS !== (bool)$force ) {
461 $this->forceHTTPS = (bool)$force;
462 $this->metaDirty = true;
463 $this->logger->debug(
464 'SessionBackend "{session}" metadata dirty due to force-HTTPS change',
465 [
466 'session' => $this->id,
467 ] );
468 $this->autosave();
469 }
470 }
471
476 public function getLoggedOutTimestamp() {
477 return $this->loggedOut;
478 }
479
484 public function setLoggedOutTimestamp( $ts = null ) {
485 $ts = (int)$ts;
486 if ( $this->loggedOut !== $ts ) {
487 $this->loggedOut = $ts;
488 $this->metaDirty = true;
489 $this->logger->debug(
490 'SessionBackend "{session}" metadata dirty due to logged-out-timestamp change',
491 [
492 'session' => $this->id,
493 ] );
494 $this->autosave();
495 }
496 }
497
503 public function getProviderMetadata() {
505 }
506
512 public function setProviderMetadata( $metadata ) {
513 if ( $metadata !== null && !is_array( $metadata ) ) {
514 throw new \InvalidArgumentException( '$metadata must be an array or null' );
515 }
516 if ( $this->providerMetadata !== $metadata ) {
517 $this->providerMetadata = $metadata;
518 $this->metaDirty = true;
519 $this->logger->debug(
520 'SessionBackend "{session}" metadata dirty due to provider metadata change',
521 [
522 'session' => $this->id,
523 ] );
524 $this->autosave();
525 }
526 }
527
537 public function &getData() {
538 return $this->data;
539 }
540
548 public function addData( array $newData ) {
549 $data = &$this->getData();
550 foreach ( $newData as $key => $value ) {
551 if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
552 $data[$key] = $value;
553 $this->dataDirty = true;
554 $this->logger->debug(
555 'SessionBackend "{session}" data dirty due to addData(): {callers}',
556 [
557 'session' => $this->id,
558 'callers' => wfGetAllCallers( 5 ),
559 ] );
560 }
561 }
562 }
563
568 public function dirty() {
569 $this->dataDirty = true;
570 $this->logger->debug(
571 'SessionBackend "{session}" data dirty due to dirty(): {callers}',
572 [
573 'session' => $this->id,
574 'callers' => wfGetAllCallers( 5 ),
575 ] );
576 }
577
584 public function renew() {
585 if ( time() + $this->lifetime / 2 > $this->expires ) {
586 $this->metaDirty = true;
587 $this->logger->debug(
588 'SessionBackend "{callers}" metadata dirty for renew(): {callers}',
589 [
590 'session' => $this->id,
591 'callers' => wfGetAllCallers( 5 ),
592 ] );
593 if ( $this->persist ) {
594 $this->forcePersist = true;
595 $this->logger->debug(
596 'SessionBackend "{session}" force-persist for renew(): {callers}',
597 [
598 'session' => $this->id,
599 'callers' => wfGetAllCallers( 5 ),
600 ] );
601 }
602 }
603 $this->autosave();
604 }
605
613 public function delaySave() {
614 $this->delaySave++;
615 return new \Wikimedia\ScopedCallback( function () {
616 if ( --$this->delaySave <= 0 ) {
617 $this->delaySave = 0;
618 $this->save();
619 }
620 } );
621 }
622
627 private function autosave() {
628 if ( $this->delaySave <= 0 ) {
629 $this->save();
630 }
631 }
632
642 public function save( $closing = false ) {
643 $anon = $this->user->isAnon();
644
645 if ( !$anon && $this->provider->getManager()->isUserSessionPrevented( $this->user->getName() ) ) {
646 $this->logger->debug(
647 'SessionBackend "{session}" not saving, user {user} was ' .
648 'passed to SessionManager::preventSessionsForUser',
649 [
650 'session' => $this->id,
651 'user' => $this->user,
652 ] );
653 return;
654 }
655
656 // Ensure the user has a token
657 // @codeCoverageIgnoreStart
658 if ( !$anon && !$this->user->getToken( false ) ) {
659 $this->logger->debug(
660 'SessionBackend "{session}" creating token for user {user} on save',
661 [
662 'session' => $this->id,
663 'user' => $this->user,
664 ] );
665 $this->user->setToken();
666 if ( !wfReadOnly() ) {
667 // Promise that the token set here will be valid; save it at end of request
669 \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
671 } );
672 }
673 $this->metaDirty = true;
674 }
675 // @codeCoverageIgnoreEnd
676
677 if ( !$this->metaDirty && !$this->dataDirty &&
678 $this->dataHash !== md5( serialize( $this->data ) )
679 ) {
680 $this->logger->debug(
681 'SessionBackend "{session}" data dirty due to hash mismatch, {expected} !== {got}',
682 [
683 'session' => $this->id,
684 'expected' => $this->dataHash,
685 'got' => md5( serialize( $this->data ) ),
686 ] );
687 $this->dataDirty = true;
688 }
689
690 if ( !$this->metaDirty && !$this->dataDirty && !$this->forcePersist ) {
691 return;
692 }
693
694 $this->logger->debug(
695 'SessionBackend "{session}" save: dataDirty={dataDirty} ' .
696 'metaDirty={metaDirty} forcePersist={forcePersist}',
697 [
698 'session' => $this->id,
699 'dataDirty' => (int)$this->dataDirty,
700 'metaDirty' => (int)$this->metaDirty,
701 'forcePersist' => (int)$this->forcePersist,
702 ] );
703
704 // Persist or unpersist to the provider, if necessary
705 if ( $this->metaDirty || $this->forcePersist ) {
706 if ( $this->persist ) {
707 foreach ( $this->requests as $request ) {
708 $request->setSessionId( $this->getSessionId() );
709 $this->provider->persistSession( $this, $request );
710 }
711 if ( !$closing ) {
712 $this->checkPHPSession();
713 }
714 } else {
715 foreach ( $this->requests as $request ) {
716 if ( $request->getSessionId() === $this->id ) {
717 $this->provider->unpersistSession( $request );
718 }
719 }
720 }
721 }
722
723 $this->forcePersist = false;
724
725 if ( !$this->metaDirty && !$this->dataDirty ) {
726 return;
727 }
728
729 // Save session data to store, if necessary
730 $metadata = $origMetadata = [
731 'provider' => (string)$this->provider,
732 'providerMetadata' => $this->providerMetadata,
733 'userId' => $anon ? 0 : $this->user->getId(),
734 'userName' => User::isValidUserName( $this->user->getName() ) ? $this->user->getName() : null,
735 'userToken' => $anon ? null : $this->user->getToken(),
736 'remember' => !$anon && $this->remember,
737 'forceHTTPS' => $this->forceHTTPS,
738 'expires' => time() + $this->lifetime,
739 'loggedOut' => $this->loggedOut,
740 'persisted' => $this->persist,
741 ];
742
743 \Hooks::run( 'SessionMetadata', [ $this, &$metadata, $this->requests ] );
744
745 foreach ( $origMetadata as $k => $v ) {
746 if ( $metadata[$k] !== $v ) {
747 throw new \UnexpectedValueException( "SessionMetadata hook changed metadata key \"$k\"" );
748 }
749 }
750
751 $flags = $this->persist ? 0 : CachedBagOStuff::WRITE_CACHE_ONLY;
752 $flags |= CachedBagOStuff::WRITE_SYNC; // write to all datacenters
753 $this->store->set(
754 $this->store->makeKey( 'MWSession', (string)$this->id ),
755 [
756 'data' => $this->data,
757 'metadata' => $metadata,
758 ],
759 $metadata['expires'],
760 $flags
761 );
762
763 $this->metaDirty = false;
764 $this->dataDirty = false;
765 $this->dataHash = md5( serialize( $this->data ) );
766 $this->expires = $metadata['expires'];
767 }
768
773 private function checkPHPSession() {
774 if ( !$this->checkPHPSessionRecursionGuard ) {
775 $this->checkPHPSessionRecursionGuard = true;
776 $reset = new \Wikimedia\ScopedCallback( function () {
777 $this->checkPHPSessionRecursionGuard = false;
778 } );
779
780 if ( $this->usePhpSessionHandling && session_id() === '' && PHPSessionHandler::isEnabled() &&
781 SessionManager::getGlobalSession()->getId() === (string)$this->id
782 ) {
783 $this->logger->debug(
784 'SessionBackend "{session}" Taking over PHP session',
785 [
786 'session' => $this->id,
787 ] );
788 session_id( (string)$this->id );
789 AtEase::quietCall( 'session_start' );
790 }
791 }
792 }
793
794}
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.
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.
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.
__construct(SessionId $id, SessionInfo $info, CachedBagOStuff $store, LoggerInterface $logger, $lifetime)
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:38
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:51
static isValidUserName( $name)
Is the input a valid username?
Definition User.php:959
saveSettings()
Save this user's settings into the database.
Definition User.php:4027
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...