Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / session / SessionBackend.php
blobcafff37808f087ae3694aff94e505f33828fc06b
1 <?php
2 /**
3 * MediaWiki session backend
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
20 * @file
21 * @ingroup Session
24 namespace MediaWiki\Session;
26 use InvalidArgumentException;
27 use MediaWiki\Deferred\DeferredUpdates;
28 use MediaWiki\HookContainer\HookContainer;
29 use MediaWiki\HookContainer\HookRunner;
30 use MediaWiki\MainConfigNames;
31 use MediaWiki\MediaWikiServices;
32 use MediaWiki\Request\WebRequest;
33 use MediaWiki\User\User;
34 use MWRestrictions;
35 use Psr\Log\LoggerInterface;
36 use Wikimedia\AtEase\AtEase;
37 use Wikimedia\ObjectCache\CachedBagOStuff;
39 /**
40 * This is the actual workhorse for Session.
42 * Most code does not need to use this class, you want \MediaWiki\Session\Session.
43 * The exceptions are SessionProviders and SessionMetadata hook functions,
44 * which get an instance of this class rather than Session.
46 * The reasons for this split are:
47 * 1. A session can be attached to multiple requests, but we want the Session
48 * object to have some features that correspond to just one of those
49 * requests.
50 * 2. We want reasonable garbage collection behavior, but we also want the
51 * SessionManager to hold a reference to every active session so it can be
52 * saved when the request ends.
54 * @ingroup Session
55 * @since 1.27
57 final class SessionBackend {
58 private SessionId $id;
60 /** @var bool */
61 private $persist = false;
63 /** @var bool */
64 private $remember = false;
66 /** @var bool */
67 private $forceHTTPS = false;
69 /** @var array|null */
70 private $data = null;
72 /** @var bool */
73 private $forcePersist = false;
75 /**
76 * The reason for the next persistSession/unpersistSession call. Only used for logging. Can be:
77 * - 'renew': triggered by a renew() call)
78 * - 'no-store': the session was not found in the session store
79 * - 'no-expiry': there was no expiry * in the session store data; this probably shouldn't happen
80 * - null otherwise.
81 * @var string|null
83 private $persistenceChangeType;
85 /**
86 * The data from the previous logPersistenceChange() log event. Used for deduplication.
87 * @var array
89 private $persistenceChangeData = [];
91 /** @var bool */
92 private $metaDirty = false;
94 /** @var bool */
95 private $dataDirty = false;
97 /** @var string Used to detect subarray modifications */
98 private $dataHash = null;
100 private CachedBagOStuff $store;
101 private LoggerInterface $logger;
102 private HookRunner $hookRunner;
104 /** @var int */
105 private $lifetime;
107 private User $user;
109 /** @var int */
110 private $curIndex = 0;
112 /** @var WebRequest[] Session requests */
113 private $requests = [];
115 /** @var SessionProvider provider */
116 private $provider;
118 /** @var array|null provider-specified metadata */
119 private $providerMetadata = null;
121 /** @var int */
122 private $expires = 0;
124 /** @var int */
125 private $loggedOut = 0;
127 /** @var int */
128 private $delaySave = 0;
130 /** @var bool */
131 private $usePhpSessionHandling;
132 /** @var bool */
133 private $checkPHPSessionRecursionGuard = false;
135 /** @var bool */
136 private $shutdown = false;
139 * @param SessionId $id
140 * @param SessionInfo $info Session info to populate from
141 * @param CachedBagOStuff $store Backend data store
142 * @param LoggerInterface $logger
143 * @param HookContainer $hookContainer
144 * @param int $lifetime Session data lifetime in seconds
146 public function __construct(
147 SessionId $id, SessionInfo $info, CachedBagOStuff $store, LoggerInterface $logger,
148 HookContainer $hookContainer, $lifetime
150 $phpSessionHandling = MediaWikiServices::getInstance()->getMainConfig()
151 ->get( MainConfigNames::PHPSessionHandling );
152 $this->usePhpSessionHandling = $phpSessionHandling !== 'disable';
154 if ( $info->getUserInfo() && !$info->getUserInfo()->isVerified() ) {
155 throw new InvalidArgumentException(
156 "Refusing to create session for unverified user {$info->getUserInfo()}"
159 if ( $info->getProvider() === null ) {
160 throw new InvalidArgumentException( 'Cannot create session without a provider' );
162 if ( $info->getId() !== $id->getId() ) {
163 throw new InvalidArgumentException( 'SessionId and SessionInfo don\'t match' );
166 $this->id = $id;
167 $this->user = $info->getUserInfo()
168 ? $info->getUserInfo()->getUser()
169 : MediaWikiServices::getInstance()->getUserFactory()->newAnonymous();
170 $this->store = $store;
171 $this->logger = $logger;
172 $this->hookRunner = new HookRunner( $hookContainer );
173 $this->lifetime = $lifetime;
174 $this->provider = $info->getProvider();
175 $this->persist = $info->wasPersisted();
176 $this->remember = $info->wasRemembered();
177 $this->forceHTTPS = $info->forceHTTPS();
178 $this->providerMetadata = $info->getProviderMetadata();
180 $blob = $store->get( $store->makeKey( 'MWSession', (string)$this->id ) );
181 if ( !is_array( $blob ) ||
182 !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] ) ||
183 !isset( $blob['data'] ) || !is_array( $blob['data'] )
185 $this->data = [];
186 $this->dataDirty = true;
187 $this->metaDirty = true;
188 $this->persistenceChangeType = 'no-store';
189 $this->logger->debug(
190 'SessionBackend "{session}" is unsaved, marking dirty in constructor',
192 'session' => $this->id->__toString(),
193 ] );
194 } else {
195 $this->data = $blob['data'];
196 if ( isset( $blob['metadata']['loggedOut'] ) ) {
197 $this->loggedOut = (int)$blob['metadata']['loggedOut'];
199 if ( isset( $blob['metadata']['expires'] ) ) {
200 $this->expires = (int)$blob['metadata']['expires'];
201 } else {
202 $this->metaDirty = true;
203 $this->persistenceChangeType = 'no-expiry';
204 $this->logger->debug(
205 'SessionBackend "{session}" metadata dirty due to missing expiration timestamp',
207 'session' => $this->id->__toString(),
208 ] );
211 $this->dataHash = md5( serialize( $this->data ) );
215 * Return a new Session for this backend
216 * @param WebRequest $request
217 * @return Session
219 public function getSession( WebRequest $request ) {
220 $index = ++$this->curIndex;
221 $this->requests[$index] = $request;
222 $session = new Session( $this, $index, $this->logger );
223 return $session;
227 * Deregister a Session
228 * @internal For use by \MediaWiki\Session\Session::__destruct() only
229 * @param int $index
231 public function deregisterSession( $index ) {
232 unset( $this->requests[$index] );
233 if ( !$this->shutdown && !count( $this->requests ) ) {
234 $this->save( true );
235 $this->provider->getManager()->deregisterSessionBackend( $this );
240 * Shut down a session
241 * @internal For use by \MediaWiki\Session\SessionManager::shutdown() only
243 public function shutdown() {
244 $this->save( true );
245 $this->shutdown = true;
249 * Returns the session ID.
250 * @return string
252 public function getId() {
253 return (string)$this->id;
257 * Fetch the SessionId object
258 * @internal For internal use by WebRequest
259 * @return SessionId
261 public function getSessionId() {
262 return $this->id;
266 * Changes the session ID
267 * @return string New ID (might be the same as the old)
269 public function resetId() {
270 if ( $this->provider->persistsSessionId() ) {
271 $oldId = (string)$this->id;
272 $restart = $this->usePhpSessionHandling && $oldId === session_id() &&
273 PHPSessionHandler::isEnabled();
275 if ( $restart ) {
276 // If this session is the one behind PHP's $_SESSION, we need
277 // to close then reopen it.
278 session_write_close();
281 $this->provider->getManager()->changeBackendId( $this );
282 $this->provider->sessionIdWasReset( $this, $oldId );
283 $this->metaDirty = true;
284 $this->logger->debug(
285 'SessionBackend "{session}" metadata dirty due to ID reset (formerly "{oldId}")',
287 'session' => $this->id->__toString(),
288 'oldId' => $oldId,
289 ] );
291 if ( $restart ) {
292 session_id( (string)$this->id );
293 AtEase::quietCall( 'session_start' );
296 $this->autosave();
298 // Delete the data for the old session ID now
299 $this->store->delete( $this->store->makeKey( 'MWSession', $oldId ) );
302 return (string)$this->id;
306 * Fetch the SessionProvider for this session
307 * @return SessionProviderInterface
309 public function getProvider() {
310 return $this->provider;
314 * Indicate whether this session is persisted across requests
316 * For example, if cookies are set.
318 * @return bool
320 public function isPersistent() {
321 return $this->persist;
325 * Make this session persisted across requests
327 * If the session is already persistent, equivalent to calling
328 * $this->renew().
330 public function persist() {
331 if ( !$this->persist ) {
332 $this->persist = true;
333 $this->forcePersist = true;
334 $this->metaDirty = true;
335 $this->logger->debug(
336 'SessionBackend "{session}" force-persist due to persist()',
338 'session' => $this->id->__toString(),
339 ] );
340 $this->autosave();
341 } else {
342 $this->renew();
347 * Make this session not persisted across requests
349 public function unpersist() {
350 if ( $this->persist ) {
351 // Close the PHP session, if we're the one that's open
352 if ( $this->usePhpSessionHandling && PHPSessionHandler::isEnabled() &&
353 session_id() === (string)$this->id
355 $this->logger->debug(
356 'SessionBackend "{session}" Closing PHP session for unpersist',
357 [ 'session' => $this->id->__toString() ]
359 session_write_close();
360 session_id( '' );
363 $this->persist = false;
364 $this->forcePersist = true;
365 $this->metaDirty = true;
367 // Delete the session data, so the local cache-only write in
368 // self::save() doesn't get things out of sync with the backend.
369 $this->store->delete( $this->store->makeKey( 'MWSession', (string)$this->id ) );
371 $this->autosave();
376 * Indicate whether the user should be remembered independently of the
377 * session ID.
378 * @return bool
380 public function shouldRememberUser() {
381 return $this->remember;
385 * Set whether the user should be remembered independently of the session
386 * ID.
387 * @param bool $remember
389 public function setRememberUser( $remember ) {
390 if ( $this->remember !== (bool)$remember ) {
391 $this->remember = (bool)$remember;
392 $this->metaDirty = true;
393 $this->logger->debug(
394 'SessionBackend "{session}" metadata dirty due to remember-user change',
396 'session' => $this->id->__toString(),
397 ] );
398 $this->autosave();
403 * Returns the request associated with a Session
404 * @param int $index Session index
405 * @return WebRequest
407 public function getRequest( $index ) {
408 if ( !isset( $this->requests[$index] ) ) {
409 throw new InvalidArgumentException( 'Invalid session index' );
411 return $this->requests[$index];
415 * Returns the authenticated user for this session
416 * @return User
418 public function getUser(): User {
419 return $this->user;
423 * Fetch the rights allowed the user when this session is active.
424 * @return null|string[] Allowed user rights, or null to allow all.
426 public function getAllowedUserRights() {
427 return $this->provider->getAllowedUserRights( $this );
431 * Fetch any restrictions imposed on logins or actions when this
432 * session is active.
433 * @return MWRestrictions|null
435 public function getRestrictions(): ?MWRestrictions {
436 return $this->provider->getRestrictions( $this->providerMetadata );
440 * Indicate whether the session user info can be changed
441 * @return bool
443 public function canSetUser() {
444 return $this->provider->canChangeUser();
448 * Set a new user for this session
449 * @note This should only be called when the user has been authenticated via a login process
450 * @param User $user User to set on the session.
451 * This may become a "UserValue" in the future, or User may be refactored
452 * into such.
454 public function setUser( $user ) {
455 if ( !$this->canSetUser() ) {
456 throw new \BadMethodCallException(
457 'Cannot set user on this session; check $session->canSetUser() first'
461 $this->user = $user;
462 $this->metaDirty = true;
463 $this->logger->debug(
464 'SessionBackend "{session}" metadata dirty due to user change',
466 'session' => $this->id->__toString(),
467 ] );
468 $this->autosave();
472 * Get a suggested username for the login form
473 * @param int $index Session index
474 * @return string|null
476 public function suggestLoginUsername( $index ) {
477 if ( !isset( $this->requests[$index] ) ) {
478 throw new InvalidArgumentException( 'Invalid session index' );
480 return $this->provider->suggestLoginUsername( $this->requests[$index] );
484 * Whether HTTPS should be forced
485 * @return bool
487 public function shouldForceHTTPS() {
488 return $this->forceHTTPS;
492 * Set whether HTTPS should be forced
493 * @param bool $force
495 public function setForceHTTPS( $force ) {
496 if ( $this->forceHTTPS !== (bool)$force ) {
497 $this->forceHTTPS = (bool)$force;
498 $this->metaDirty = true;
499 $this->logger->debug(
500 'SessionBackend "{session}" metadata dirty due to force-HTTPS change',
502 'session' => $this->id->__toString(),
503 ] );
504 $this->autosave();
509 * Fetch the "logged out" timestamp
510 * @return int
512 public function getLoggedOutTimestamp() {
513 return $this->loggedOut;
517 * @param int|null $ts
519 public function setLoggedOutTimestamp( $ts = null ) {
520 $ts = (int)$ts;
521 if ( $this->loggedOut !== $ts ) {
522 $this->loggedOut = $ts;
523 $this->metaDirty = true;
524 $this->logger->debug(
525 'SessionBackend "{session}" metadata dirty due to logged-out-timestamp change',
527 'session' => $this->id->__toString(),
528 ] );
529 $this->autosave();
534 * Fetch provider metadata
535 * @note For use by SessionProvider subclasses only
536 * @return array|null
538 public function getProviderMetadata() {
539 return $this->providerMetadata;
543 * @note For use by SessionProvider subclasses only
544 * @param array|null $metadata
546 public function setProviderMetadata( $metadata ) {
547 if ( $metadata !== null && !is_array( $metadata ) ) {
548 throw new InvalidArgumentException( '$metadata must be an array or null' );
550 if ( $this->providerMetadata !== $metadata ) {
551 $this->providerMetadata = $metadata;
552 $this->metaDirty = true;
553 $this->logger->debug(
554 'SessionBackend "{session}" metadata dirty due to provider metadata change',
556 'session' => $this->id->__toString(),
557 ] );
558 $this->autosave();
563 * Fetch the session data array
565 * Note the caller is responsible for calling $this->dirty() if anything in
566 * the array is changed.
568 * @internal For use by \MediaWiki\Session\Session only.
569 * @return array
571 public function &getData() {
572 return $this->data;
576 * Add data to the session.
578 * Overwrites any existing data under the same keys.
580 * @param array $newData Key-value pairs to add to the session
582 public function addData( array $newData ) {
583 $data = &$this->getData();
584 foreach ( $newData as $key => $value ) {
585 if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
586 $data[$key] = $value;
587 $this->dataDirty = true;
588 $this->logger->debug(
589 'SessionBackend "{session}" data dirty due to addData(): {callers}',
591 'session' => $this->id->__toString(),
592 'callers' => wfGetAllCallers( 5 ),
593 ] );
599 * Mark data as dirty
600 * @internal For use by \MediaWiki\Session\Session only.
602 public function dirty() {
603 $this->dataDirty = true;
604 $this->logger->debug(
605 'SessionBackend "{session}" data dirty due to dirty(): {callers}',
607 'session' => $this->id->__toString(),
608 'callers' => wfGetAllCallers( 5 ),
609 ] );
613 * Renew the session by resaving everything
615 * Resets the TTL in the backend store if the session is near expiring, and
616 * re-persists the session to any active WebRequests if persistent.
618 public function renew() {
619 if ( time() + $this->lifetime / 2 > $this->expires ) {
620 $this->metaDirty = true;
621 $this->logger->debug(
622 'SessionBackend "{callers}" metadata dirty for renew(): {callers}',
624 'session' => $this->id->__toString(),
625 'callers' => wfGetAllCallers( 5 ),
626 ] );
627 if ( $this->persist ) {
628 $this->persistenceChangeType = 'renew';
629 $this->forcePersist = true;
630 $this->logger->debug(
631 'SessionBackend "{session}" force-persist for renew(): {callers}',
633 'session' => $this->id->__toString(),
634 'callers' => wfGetAllCallers( 5 ),
635 ] );
638 $this->autosave();
642 * Delay automatic saving while multiple updates are being made
644 * Calls to save() will not be delayed.
646 * @return \Wikimedia\ScopedCallback When this goes out of scope, a save will be triggered
648 public function delaySave() {
649 $this->delaySave++;
650 return new \Wikimedia\ScopedCallback( function () {
651 if ( --$this->delaySave <= 0 ) {
652 $this->delaySave = 0;
653 $this->save();
655 } );
659 * Save the session, unless delayed
660 * @see SessionBackend::save()
662 private function autosave() {
663 if ( $this->delaySave <= 0 ) {
664 $this->save();
669 * Save the session
671 * Update both the backend data and the associated WebRequest(s) to
672 * reflect the state of the SessionBackend. This might include
673 * persisting or unpersisting the session.
675 * @param bool $closing Whether the session is being closed
677 public function save( $closing = false ) {
678 $anon = $this->user->isAnon();
680 if ( !$anon && $this->provider->getManager()->isUserSessionPrevented( $this->user->getName() ) ) {
681 $this->logger->debug(
682 'SessionBackend "{session}" not saving, user {user} was ' .
683 'passed to SessionManager::preventSessionsForUser',
685 'session' => $this->id->__toString(),
686 'user' => $this->user->__toString(),
687 ] );
688 return;
691 // Ensure the user has a token
692 // @codeCoverageIgnoreStart
693 if ( !$anon && defined( 'MW_PHPUNIT_TEST' ) && MediaWikiServices::getInstance()->isStorageDisabled() ) {
694 // Avoid making DB queries in non-database tests. We don't need to save the token when using
695 // fake users, and it would probably be ignored anyway.
696 return;
698 if ( !$anon && !$this->user->getToken( false ) ) {
699 $this->logger->debug(
700 'SessionBackend "{session}" creating token for user {user} on save',
702 'session' => $this->id->__toString(),
703 'user' => $this->user->__toString(),
704 ] );
705 $this->user->setToken();
706 if ( !MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
707 // Promise that the token set here will be valid; save it at end of request
708 $user = $this->user;
709 DeferredUpdates::addCallableUpdate( static function () use ( $user ) {
710 $user->saveSettings();
711 } );
713 $this->metaDirty = true;
715 // @codeCoverageIgnoreEnd
717 if ( !$this->metaDirty && !$this->dataDirty &&
718 $this->dataHash !== md5( serialize( $this->data ) )
720 $this->logger->debug(
721 'SessionBackend "{session}" data dirty due to hash mismatch, {expected} !== {got}',
723 'session' => $this->id->__toString(),
724 'expected' => $this->dataHash,
725 'got' => md5( serialize( $this->data ) ),
726 ] );
727 $this->dataDirty = true;
730 if ( !$this->metaDirty && !$this->dataDirty && !$this->forcePersist ) {
731 return;
734 $this->logger->debug(
735 'SessionBackend "{session}" save: dataDirty={dataDirty} ' .
736 'metaDirty={metaDirty} forcePersist={forcePersist}',
738 'session' => $this->id->__toString(),
739 'dataDirty' => (int)$this->dataDirty,
740 'metaDirty' => (int)$this->metaDirty,
741 'forcePersist' => (int)$this->forcePersist,
742 ] );
744 // Persist or unpersist to the provider, if necessary
745 if ( $this->metaDirty || $this->forcePersist ) {
746 if ( $this->persist ) {
747 foreach ( $this->requests as $request ) {
748 $request->setSessionId( $this->getSessionId() );
749 $this->logPersistenceChange( $request, true );
750 $this->provider->persistSession( $this, $request );
752 if ( !$closing ) {
753 $this->checkPHPSession();
755 } else {
756 foreach ( $this->requests as $request ) {
757 if ( $request->getSessionId() === $this->id ) {
758 $this->logPersistenceChange( $request, false );
759 $this->provider->unpersistSession( $request );
765 $this->forcePersist = false;
766 $this->persistenceChangeType = null;
768 if ( !$this->metaDirty && !$this->dataDirty ) {
769 return;
772 // Save session data to store, if necessary
773 $metadata = $origMetadata = [
774 'provider' => (string)$this->provider,
775 'providerMetadata' => $this->providerMetadata,
776 'userId' => $anon ? 0 : $this->user->getId(),
777 'userName' => MediaWikiServices::getInstance()->getUserNameUtils()
778 ->isValid( $this->user->getName() ) ? $this->user->getName() : null,
779 'userToken' => $anon ? null : $this->user->getToken(),
780 'remember' => !$anon && $this->remember,
781 'forceHTTPS' => $this->forceHTTPS,
782 'expires' => time() + $this->lifetime,
783 'loggedOut' => $this->loggedOut,
784 'persisted' => $this->persist,
787 $this->hookRunner->onSessionMetadata( $this, $metadata, $this->requests );
789 foreach ( $origMetadata as $k => $v ) {
790 if ( $metadata[$k] !== $v ) {
791 throw new \UnexpectedValueException( "SessionMetadata hook changed metadata key \"$k\"" );
795 $flags = $this->persist ? 0 : CachedBagOStuff::WRITE_CACHE_ONLY;
796 $this->store->set(
797 $this->store->makeKey( 'MWSession', (string)$this->id ),
799 'data' => $this->data,
800 'metadata' => $metadata,
802 $metadata['expires'],
803 $flags
806 $this->metaDirty = false;
807 $this->dataDirty = false;
808 $this->dataHash = md5( serialize( $this->data ) );
809 $this->expires = $metadata['expires'];
813 * For backwards compatibility, open the PHP session when the global
814 * session is persisted
816 private function checkPHPSession() {
817 if ( !$this->checkPHPSessionRecursionGuard ) {
818 $this->checkPHPSessionRecursionGuard = true;
819 $reset = new \Wikimedia\ScopedCallback( function () {
820 $this->checkPHPSessionRecursionGuard = false;
821 } );
823 if ( $this->usePhpSessionHandling && session_id() === '' && PHPSessionHandler::isEnabled() &&
824 SessionManager::getGlobalSession()->getId() === (string)$this->id
826 $this->logger->debug(
827 'SessionBackend "{session}" Taking over PHP session',
829 'session' => $this->id->__toString(),
830 ] );
831 session_id( (string)$this->id );
832 AtEase::quietCall( 'session_start' );
838 * Helper method for logging persistSession/unpersistSession calls.
839 * @param WebRequest $request
840 * @param bool $persist True when persisting, false when unpersisting
842 private function logPersistenceChange( WebRequest $request, bool $persist ) {
843 if ( !$this->isPersistent() && !$persist ) {
844 // FIXME SessionManager calls unpersistSession() on anonymous requests (and the cookie
845 // filtering in WebResponse makes it a noop). Skip those.
846 return;
849 $verb = $persist ? 'Persisting' : 'Unpersisting';
850 if ( $this->persistenceChangeType === 'renew' ) {
851 $message = "$verb session for renewal";
852 } elseif ( $this->persistenceChangeType === 'no-store' ) {
853 $message = "$verb session due to no pre-existing stored session";
854 } elseif ( $this->persistenceChangeType === 'no-expiry' ) {
855 $message = "$verb session due to lack of stored expiry";
856 } elseif ( $this->persistenceChangeType === null ) {
857 $message = "$verb session for unknown reason";
860 // Because SessionManager repeats session loading several times in the same request,
861 // it will try to persist or unpersist several times. WebResponse deduplicates, but
862 // we want to deduplicate logging as well since the volume is already fairly large.
863 $id = $this->getId();
864 $user = $this->getUser()->isAnon() ? '<anon>' : $this->getUser()->getName();
865 if ( $this->persistenceChangeData
866 && $this->persistenceChangeData['id'] === $id
867 && $this->persistenceChangeData['user'] === $user
868 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable message always set
869 && $this->persistenceChangeData['message'] === $message
871 return;
873 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable message always set
874 $this->persistenceChangeData = [ 'id' => $id, 'user' => $user, 'message' => $message ];
876 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable,PhanPossiblyUndeclaredVariable message always set
877 $this->logger->info( $message, [
878 'id' => $id,
879 'provider' => get_class( $this->getProvider() ),
880 'user' => $user,
881 'clientip' => $request->getIP(),
882 'userAgent' => $request->getHeader( 'user-agent' ),
883 ] );