Completed
Branch master (5cbada)
by
unknown
28:59
created

SessionManager::invalidateSessionsForUser()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 8
c 1
b 0
f 1
nc 4
nop 1
dl 0
loc 13
rs 9.4285
1
<?php
2
/**
3
 * MediaWiki\Session entry point
4
 *
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.
9
 *
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.
14
 *
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
19
 *
20
 * @file
21
 * @ingroup Session
22
 */
23
24
namespace MediaWiki\Session;
25
26
use MWException;
27
use Psr\Log\LoggerInterface;
28
use BagOStuff;
29
use CachedBagOStuff;
30
use Config;
31
use FauxRequest;
32
use User;
33
use WebRequest;
34
35
/**
36
 * This serves as the entry point to the MediaWiki session handling system.
37
 *
38
 * @ingroup Session
39
 * @since 1.27
40
 */
41
final class SessionManager implements SessionManagerInterface {
42
	/** @var SessionManager|null */
43
	private static $instance = null;
44
45
	/** @var Session|null */
46
	private static $globalSession = null;
47
48
	/** @var WebRequest|null */
49
	private static $globalSessionRequest = null;
50
51
	/** @var LoggerInterface */
52
	private $logger;
53
54
	/** @var Config */
55
	private $config;
56
57
	/** @var CachedBagOStuff|null */
58
	private $store;
59
60
	/** @var SessionProvider[] */
61
	private $sessionProviders = null;
62
63
	/** @var string[] */
64
	private $varyCookies = null;
65
66
	/** @var array */
67
	private $varyHeaders = null;
68
69
	/** @var SessionBackend[] */
70
	private $allSessionBackends = [];
71
72
	/** @var SessionId[] */
73
	private $allSessionIds = [];
74
75
	/** @var string[] */
76
	private $preventUsers = [];
77
78
	/**
79
	 * Get the global SessionManager
80
	 * @return SessionManagerInterface
81
	 *  (really a SessionManager, but this is to make IDEs less confused)
82
	 */
83
	public static function singleton() {
84
		if ( self::$instance === null ) {
85
			self::$instance = new self();
86
		}
87
		return self::$instance;
88
	}
89
90
	/**
91
	 * Get the "global" session
92
	 *
93
	 * If PHP's session_id() has been set, returns that session. Otherwise
94
	 * returns the session for RequestContext::getMain()->getRequest().
95
	 *
96
	 * @return Session
97
	 */
98
	public static function getGlobalSession() {
99
		if ( !PHPSessionHandler::isEnabled() ) {
100
			$id = '';
101
		} else {
102
			$id = session_id();
103
		}
104
105
		$request = \RequestContext::getMain()->getRequest();
106
		if (
107
			!self::$globalSession // No global session is set up yet
108
			|| self::$globalSessionRequest !== $request // The global WebRequest changed
109
			|| $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id()
110
		) {
111
			self::$globalSessionRequest = $request;
112
			if ( $id === '' ) {
113
				// session_id() wasn't used, so fetch the Session from the WebRequest.
114
				// We use $request->getSession() instead of $singleton->getSessionForRequest()
115
				// because doing the latter would require a public
116
				// "$request->getSessionId()" method that would confuse end
117
				// users by returning SessionId|null where they'd expect it to
118
				// be short for $request->getSession()->getId(), and would
119
				// wind up being a duplicate of the code in
120
				// $request->getSession() anyway.
121
				self::$globalSession = $request->getSession();
122
			} else {
123
				// Someone used session_id(), so we need to follow suit.
124
				// Note this overwrites whatever session might already be
125
				// associated with $request with the one for $id.
126
				self::$globalSession = self::singleton()->getSessionById( $id, true, $request )
127
					?: $request->getSession();
128
			}
129
		}
130
		return self::$globalSession;
131
	}
132
133
	/**
134
	 * @param array $options
135
	 *  - config: Config to fetch configuration from. Defaults to the default 'main' config.
136
	 *  - logger: LoggerInterface to use for logging. Defaults to the 'session' channel.
137
	 *  - store: BagOStuff to store session data in.
138
	 */
139
	public function __construct( $options = [] ) {
140
		if ( isset( $options['config'] ) ) {
141
			$this->config = $options['config'];
142
			if ( !$this->config instanceof Config ) {
143
				throw new \InvalidArgumentException(
144
					'$options[\'config\'] must be an instance of Config'
145
				);
146
			}
147
		} else {
148
			$this->config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
0 ignored issues
show
Deprecated Code introduced by
The method ConfigFactory::getDefaultInstance() has been deprecated with message: since 1.27, use MediaWikiServices::getConfigFactory() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
149
		}
150
151
		if ( isset( $options['logger'] ) ) {
152
			if ( !$options['logger'] instanceof LoggerInterface ) {
153
				throw new \InvalidArgumentException(
154
					'$options[\'logger\'] must be an instance of LoggerInterface'
155
				);
156
			}
157
			$this->setLogger( $options['logger'] );
158
		} else {
159
			$this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
160
		}
161
162
		if ( isset( $options['store'] ) ) {
163
			if ( !$options['store'] instanceof BagOStuff ) {
164
				throw new \InvalidArgumentException(
165
					'$options[\'store\'] must be an instance of BagOStuff'
166
				);
167
			}
168
			$store = $options['store'];
169
		} else {
170
			$store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
171
		}
172
		$this->store = $store instanceof CachedBagOStuff ? $store : new CachedBagOStuff( $store );
173
174
		register_shutdown_function( [ $this, 'shutdown' ] );
175
	}
176
177
	public function setLogger( LoggerInterface $logger ) {
178
		$this->logger = $logger;
179
	}
180
181
	public function getSessionForRequest( WebRequest $request ) {
182
		$info = $this->getSessionInfoForRequest( $request );
183
184
		if ( !$info ) {
185
			$session = $this->getEmptySession( $request );
186
		} else {
187
			$session = $this->getSessionFromInfo( $info, $request );
188
		}
189
		return $session;
190
	}
191
192
	public function getSessionById( $id, $create = false, WebRequest $request = null ) {
193
		if ( !self::validateSessionId( $id ) ) {
194
			throw new \InvalidArgumentException( 'Invalid session ID' );
195
		}
196
		if ( !$request ) {
197
			$request = new FauxRequest;
198
		}
199
200
		$session = null;
201
		$info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => $id, 'idIsSafe' => true ] );
202
203
		// If we already have the backend loaded, use it directly
204
		if ( isset( $this->allSessionBackends[$id] ) ) {
205
			return $this->getSessionFromInfo( $info, $request );
206
		}
207
208
		// Test if the session is in storage, and if so try to load it.
209
		$key = wfMemcKey( 'MWSession', $id );
210
		if ( is_array( $this->store->get( $key ) ) ) {
211
			$create = false; // If loading fails, don't bother creating because it probably will fail too.
212
			if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
213
				$session = $this->getSessionFromInfo( $info, $request );
214
			}
215
		}
216
217
		if ( $create && $session === null ) {
218
			$ex = null;
219
			try {
220
				$session = $this->getEmptySessionInternal( $request, $id );
221
			} catch ( \Exception $ex ) {
222
				$this->logger->error( 'Failed to create empty session: {exception}',
223
					[
224
						'method' => __METHOD__,
225
						'exception' => $ex,
226
				] );
227
				$session = null;
228
			}
229
		}
230
231
		return $session;
232
	}
233
234
	public function getEmptySession( WebRequest $request = null ) {
235
		return $this->getEmptySessionInternal( $request );
236
	}
237
238
	/**
239
	 * @see SessionManagerInterface::getEmptySession
240
	 * @param WebRequest|null $request
241
	 * @param string|null $id ID to force on the new session
242
	 * @return Session
243
	 */
244
	private function getEmptySessionInternal( WebRequest $request = null, $id = null ) {
245
		if ( $id !== null ) {
246
			if ( !self::validateSessionId( $id ) ) {
247
				throw new \InvalidArgumentException( 'Invalid session ID' );
248
			}
249
250
			$key = wfMemcKey( 'MWSession', $id );
251
			if ( is_array( $this->store->get( $key ) ) ) {
252
				throw new \InvalidArgumentException( 'Session ID already exists' );
253
			}
254
		}
255
		if ( !$request ) {
256
			$request = new FauxRequest;
257
		}
258
259
		$infos = [];
260
		foreach ( $this->getProviders() as $provider ) {
261
			$info = $provider->newSessionInfo( $id );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $info is correct as $provider->newSessionInfo($id) (which targets MediaWiki\Session\Sessio...vider::newSessionInfo()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
262
			if ( !$info ) {
263
				continue;
264
			}
265
			if ( $info->getProvider() !== $provider ) {
266
				throw new \UnexpectedValueException(
267
					"$provider returned an empty session info for a different provider: $info"
268
				);
269
			}
270
			if ( $id !== null && $info->getId() !== $id ) {
271
				throw new \UnexpectedValueException(
272
					"$provider returned empty session info with a wrong id: " .
273
						$info->getId() . ' != ' . $id
274
				);
275
			}
276
			if ( !$info->isIdSafe() ) {
277
				throw new \UnexpectedValueException(
278
					"$provider returned empty session info with id flagged unsafe"
279
				);
280
			}
281
			$compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1;
282
			if ( $compare > 0 ) {
283
				continue;
284
			}
285
			if ( $compare === 0 ) {
286
				$infos[] = $info;
287
			} else {
288
				$infos = [ $info ];
289
			}
290
		}
291
292
		// Make sure there's exactly one
293
		if ( count( $infos ) > 1 ) {
294
			throw new \UnexpectedValueException(
295
				'Multiple empty sessions tied for top priority: ' . implode( ', ', $infos )
296
			);
297
		} elseif ( count( $infos ) < 1 ) {
298
			throw new \UnexpectedValueException( 'No provider could provide an empty session!' );
299
		}
300
301
		return $this->getSessionFromInfo( $infos[0], $request );
302
	}
303
304
	public function invalidateSessionsForUser( User $user ) {
305
		$user->setToken();
306
		$user->saveSettings();
307
308
		$authUser = \MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ] );
0 ignored issues
show
Deprecated Code introduced by
The method MediaWiki\Auth\AuthManager::callLegacyAuthPlugin() has been deprecated with message: For backwards compatibility only, should be avoided in new code

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
309
		if ( $authUser ) {
310
			$authUser->resetAuthToken();
311
		}
312
313
		foreach ( $this->getProviders() as $provider ) {
314
			$provider->invalidateSessionsForUser( $user );
315
		}
316
	}
317
318
	public function getVaryHeaders() {
319
		// @codeCoverageIgnoreStart
320
		if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of MW_NO_SESSION (integer) and 'warn' (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
321
			return [];
322
		}
323
		// @codeCoverageIgnoreEnd
324
		if ( $this->varyHeaders === null ) {
325
			$headers = [];
326
			foreach ( $this->getProviders() as $provider ) {
327
				foreach ( $provider->getVaryHeaders() as $header => $options ) {
328
					if ( !isset( $headers[$header] ) ) {
329
						$headers[$header] = [];
330
					}
331
					if ( is_array( $options ) ) {
332
						$headers[$header] = array_unique( array_merge( $headers[$header], $options ) );
333
					}
334
				}
335
			}
336
			$this->varyHeaders = $headers;
337
		}
338
		return $this->varyHeaders;
339
	}
340
341
	public function getVaryCookies() {
342
		// @codeCoverageIgnoreStart
343
		if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of MW_NO_SESSION (integer) and 'warn' (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
344
			return [];
345
		}
346
		// @codeCoverageIgnoreEnd
347
		if ( $this->varyCookies === null ) {
348
			$cookies = [];
349
			foreach ( $this->getProviders() as $provider ) {
350
				$cookies = array_merge( $cookies, $provider->getVaryCookies() );
351
			}
352
			$this->varyCookies = array_values( array_unique( $cookies ) );
0 ignored issues
show
Documentation Bug introduced by
It seems like array_values(array_unique($cookies)) of type array<integer,?> is incompatible with the declared type array<integer,string> of property $varyCookies.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
353
		}
354
		return $this->varyCookies;
355
	}
356
357
	/**
358
	 * Validate a session ID
359
	 * @param string $id
360
	 * @return bool
361
	 */
362
	public static function validateSessionId( $id ) {
363
		return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
364
	}
365
366
	/**
367
	 * @name Internal methods
368
	 * @{
369
	 */
370
371
	/**
372
	 * Auto-create the given user, if necessary
373
	 * @private Don't call this yourself. Let Setup.php do it for you at the right time.
374
	 * @deprecated since 1.27, use MediaWiki\Auth\AuthManager::autoCreateUser instead
375
	 * @param User $user User to auto-create
376
	 * @return bool Success
377
	 */
378
	public static function autoCreateUser( User $user ) {
0 ignored issues
show
Coding Style introduced by
autoCreateUser uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
379
		global $wgAuth, $wgDisableAuthManager;
380
381
		// @codeCoverageIgnoreStart
382
		if ( !$wgDisableAuthManager ) {
383
			wfDeprecated( __METHOD__, '1.27' );
384
			return \MediaWiki\Auth\AuthManager::singleton()->autoCreateUser(
385
				$user,
386
				\MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSION,
387
				false
388
			)->isGood();
389
		}
390
		// @codeCoverageIgnoreEnd
391
392
		$logger = self::singleton()->logger;
393
394
		// Much of this code is based on that in CentralAuth
395
396
		// Try the local user from the slave DB
397
		$localId = User::idFromName( $user->getName() );
398
		$flags = 0;
399
400
		// Fetch the user ID from the master, so that we don't try to create the user
401
		// when they already exist, due to replication lag
402
		// @codeCoverageIgnoreStart
403 View Code Duplication
		if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $localId of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
404
			$localId = User::idFromName( $user->getName(), User::READ_LATEST );
405
			$flags = User::READ_LATEST;
406
		}
407
		// @codeCoverageIgnoreEnd
408
409
		if ( $localId ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $localId of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
410
			// User exists after all.
411
			$user->setId( $localId );
412
			$user->loadFromId( $flags );
413
			return false;
414
		}
415
416
		// Denied by AuthPlugin? But ignore AuthPlugin itself.
417
		if ( get_class( $wgAuth ) !== 'AuthPlugin' && !$wgAuth->autoCreate() ) {
418
			$logger->debug( __METHOD__ . ': denied by AuthPlugin' );
419
			$user->setId( 0 );
420
			$user->loadFromId();
421
			return false;
422
		}
423
424
		// Wiki is read-only?
425 View Code Duplication
		if ( wfReadOnly() ) {
426
			$logger->debug( __METHOD__ . ': denied by wfReadOnly()' );
427
			$user->setId( 0 );
428
			$user->loadFromId();
429
			return false;
430
		}
431
432
		$userName = $user->getName();
433
434
		// Check the session, if we tried to create this user already there's
435
		// no point in retrying.
436
		$session = self::getGlobalSession();
437
		$reason = $session->get( 'MWSession::AutoCreateBlacklist' );
438 View Code Duplication
		if ( $reason ) {
439
			$logger->debug( __METHOD__ . ": blacklisted in session ($reason)" );
440
			$user->setId( 0 );
441
			$user->loadFromId();
442
			return false;
443
		}
444
445
		// Is the IP user able to create accounts?
446
		$anon = new User;
447
		if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
448
			|| $anon->isBlockedFromCreateAccount()
449
		) {
450
			// Blacklist the user to avoid repeated DB queries subsequently
451
			$logger->debug( __METHOD__ . ': user is blocked from this wiki, blacklisting' );
452
			$session->set( 'MWSession::AutoCreateBlacklist', 'blocked', 600 );
453
			$session->persist();
454
			$user->setId( 0 );
455
			$user->loadFromId();
456
			return false;
457
		}
458
459
		// Check for validity of username
460
		if ( !User::isCreatableName( $userName ) ) {
461
			$logger->debug( __METHOD__ . ': Invalid username, blacklisting' );
462
			$session->set( 'MWSession::AutoCreateBlacklist', 'invalid username', 600 );
463
			$session->persist();
464
			$user->setId( 0 );
465
			$user->loadFromId();
466
			return false;
467
		}
468
469
		// Give other extensions a chance to stop auto creation.
470
		$user->loadDefaults( $userName );
471
		$abortMessage = '';
472
		if ( !\Hooks::run( 'AbortAutoAccount', [ $user, &$abortMessage ] ) ) {
473
			// In this case we have no way to return the message to the user,
474
			// but we can log it.
475
			$logger->debug( __METHOD__ . ": denied by hook: $abortMessage" );
476
			$session->set( 'MWSession::AutoCreateBlacklist', "hook aborted: $abortMessage", 600 );
477
			$session->persist();
478
			$user->setId( 0 );
479
			$user->loadFromId();
480
			return false;
481
		}
482
483
		// Make sure the name has not been changed
484
		if ( $user->getName() !== $userName ) {
485
			$user->setId( 0 );
486
			$user->loadFromId();
487
			throw new \UnexpectedValueException(
488
				'AbortAutoAccount hook tried to change the user name'
489
			);
490
		}
491
492
		// Ignore warnings about master connections/writes...hard to avoid here
493
		\Profiler::instance()->getTransactionProfiler()->resetExpectations();
494
495
		$cache = \ObjectCache::getLocalClusterInstance();
496
		$backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $userName ) );
497
		if ( $cache->get( $backoffKey ) ) {
498
			$logger->debug( __METHOD__ . ': denied by prior creation attempt failures' );
499
			$user->setId( 0 );
500
			$user->loadFromId();
501
			return false;
502
		}
503
504
		// Checks passed, create the user...
505
		$from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
506
		$logger->info( __METHOD__ . ': creating new user ({username}) - from: {url}',
507
			[
508
				'username' => $userName,
509
				'url' => $from,
510
		] );
511
512
		try {
513
			// Insert the user into the local DB master
514
			$status = $user->addToDatabase();
515
			if ( !$status->isOK() ) {
516
				// @codeCoverageIgnoreStart
517
				// double-check for a race condition (T70012)
518
				$id = User::idFromName( $user->getName(), User::READ_LATEST );
519
				if ( $id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
520
					$logger->info( __METHOD__ . ': tried to autocreate existing user',
521
						[
522
							'username' => $userName,
523
						] );
524
				} else {
525
					$logger->error(
526
						__METHOD__ . ': failed with message ' . $status->getWikiText( false, false, 'en' ),
527
						[
528
							'username' => $userName,
529
						]
530
					);
531
				}
532
				$user->setId( $id );
533
				$user->loadFromId( User::READ_LATEST );
534
				return false;
535
				// @codeCoverageIgnoreEnd
536
			}
537
		} catch ( \Exception $ex ) {
538
			// @codeCoverageIgnoreStart
539
			$logger->error( __METHOD__ . ': failed with exception {exception}', [
540
				'exception' => $ex,
541
				'username' => $userName,
542
			] );
543
			// Do not keep throwing errors for a while
544
			$cache->set( $backoffKey, 1, 600 );
545
			// Bubble up error; which should normally trigger DB rollbacks
546
			throw $ex;
547
			// @codeCoverageIgnoreEnd
548
		}
549
550
		# Notify AuthPlugin
551
		// @codeCoverageIgnoreStart
552
		$tmpUser = $user;
553
		$wgAuth->initUser( $tmpUser, true );
554
		if ( $tmpUser !== $user ) {
555
			$logger->warning( __METHOD__ . ': ' .
556
				get_class( $wgAuth ) . '::initUser() replaced the user object' );
557
		}
558
		// @codeCoverageIgnoreEnd
559
560
		# Notify hooks (e.g. Newuserlog)
561
		\Hooks::run( 'AuthPluginAutoCreate', [ $user ] );
562
		\Hooks::run( 'LocalUserCreated', [ $user, true ] );
563
564
		$user->saveSettings();
565
566
		# Update user count
567
		\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
568
569
		# Watch user's userpage and talk page
570
		$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
571
572
		return true;
573
	}
574
575
	/**
576
	 * Prevent future sessions for the user
577
	 *
578
	 * The intention is that the named account will never again be usable for
579
	 * normal login (i.e. there is no way to undo the prevention of access).
580
	 *
581
	 * @private For use from \User::newSystemUser only
582
	 * @param string $username
583
	 */
584
	public function preventSessionsForUser( $username ) {
585
		$this->preventUsers[$username] = true;
586
587
		// Instruct the session providers to kill any other sessions too.
588
		foreach ( $this->getProviders() as $provider ) {
589
			$provider->preventSessionsForUser( $username );
590
		}
591
	}
592
593
	/**
594
	 * Test if a user is prevented
595
	 * @private For use from SessionBackend only
596
	 * @param string $username
597
	 * @return bool
598
	 */
599
	public function isUserSessionPrevented( $username ) {
600
		return !empty( $this->preventUsers[$username] );
601
	}
602
603
	/**
604
	 * Get the available SessionProviders
605
	 * @return SessionProvider[]
606
	 */
607
	protected function getProviders() {
608
		if ( $this->sessionProviders === null ) {
609
			$this->sessionProviders = [];
610
			foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
611
				$provider = \ObjectFactory::getObjectFromSpec( $spec );
612
				$provider->setLogger( $this->logger );
613
				$provider->setConfig( $this->config );
614
				$provider->setManager( $this );
615
				if ( isset( $this->sessionProviders[(string)$provider] ) ) {
616
					throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
617
				}
618
				$this->sessionProviders[(string)$provider] = $provider;
619
			}
620
		}
621
		return $this->sessionProviders;
622
	}
623
624
	/**
625
	 * Get a session provider by name
626
	 *
627
	 * Generally, this will only be used by internal implementation of some
628
	 * special session-providing mechanism. General purpose code, if it needs
629
	 * to access a SessionProvider at all, will use Session::getProvider().
630
	 *
631
	 * @param string $name
632
	 * @return SessionProvider|null
633
	 */
634
	public function getProvider( $name ) {
635
		$providers = $this->getProviders();
636
		return isset( $providers[$name] ) ? $providers[$name] : null;
637
	}
638
639
	/**
640
	 * Save all active sessions on shutdown
641
	 * @private For internal use with register_shutdown_function()
642
	 */
643
	public function shutdown() {
644
		if ( $this->allSessionBackends ) {
645
			$this->logger->debug( 'Saving all sessions on shutdown' );
646
			if ( session_id() !== '' ) {
647
				// @codeCoverageIgnoreStart
648
				session_write_close();
649
			}
650
			// @codeCoverageIgnoreEnd
651
			foreach ( $this->allSessionBackends as $backend ) {
652
				$backend->shutdown();
653
			}
654
		}
655
	}
656
657
	/**
658
	 * Fetch the SessionInfo(s) for a request
659
	 * @param WebRequest $request
660
	 * @return SessionInfo|null
661
	 */
662
	private function getSessionInfoForRequest( WebRequest $request ) {
663
		// Call all providers to fetch "the" session
664
		$infos = [];
665
		foreach ( $this->getProviders() as $provider ) {
666
			$info = $provider->provideSessionInfo( $request );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $info is correct as $provider->provideSessionInfo($request) (which targets MediaWiki\Session\Sessio...r::provideSessionInfo()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
667
			if ( !$info ) {
668
				continue;
669
			}
670
			if ( $info->getProvider() !== $provider ) {
671
				throw new \UnexpectedValueException(
672
					"$provider returned session info for a different provider: $info"
673
				);
674
			}
675
			$infos[] = $info;
676
		}
677
678
		// Sort the SessionInfos. Then find the first one that can be
679
		// successfully loaded, and then all the ones after it with the same
680
		// priority.
681
		usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' );
682
		$retInfos = [];
683
		while ( $infos ) {
684
			$info = array_pop( $infos );
685
			if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
686
				$retInfos[] = $info;
687
				while ( $infos ) {
688
					$info = array_pop( $infos );
689
					if ( SessionInfo::compare( $retInfos[0], $info ) ) {
690
						// We hit a lower priority, stop checking.
691
						break;
692
					}
693
					if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
694
						// This is going to error out below, but we want to
695
						// provide a complete list.
696
						$retInfos[] = $info;
697
					} else {
698
						// Session load failed, so unpersist it from this request
699
						$info->getProvider()->unpersistSession( $request );
700
					}
701
				}
702
			} else {
703
				// Session load failed, so unpersist it from this request
704
				$info->getProvider()->unpersistSession( $request );
705
			}
706
		}
707
708
		if ( count( $retInfos ) > 1 ) {
709
			$ex = new \OverflowException(
710
				'Multiple sessions for this request tied for top priority: ' . implode( ', ', $retInfos )
711
			);
712
			$ex->sessionInfos = $retInfos;
0 ignored issues
show
Bug introduced by
The property sessionInfos does not seem to exist in OverflowException.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
713
			throw $ex;
714
		}
715
716
		return $retInfos ? $retInfos[0] : null;
717
	}
718
719
	/**
720
	 * Load and verify the session info against the store
721
	 *
722
	 * @param SessionInfo &$info Will likely be replaced with an updated SessionInfo instance
723
	 * @param WebRequest $request
724
	 * @return bool Whether the session info matches the stored data (if any)
725
	 */
726
	private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) {
727
		$key = wfMemcKey( 'MWSession', $info->getId() );
728
		$blob = $this->store->get( $key );
729
730
		// If we got data from the store and the SessionInfo says to force use,
731
		// "fail" means to delete the data from the store and retry. Otherwise,
732
		// "fail" is just return false.
733
		if ( $info->forceUse() && $blob !== false ) {
734
			$failHandler = function () use ( $key, &$info, $request ) {
735
				$this->store->delete( $key );
736
				return $this->loadSessionInfoFromStore( $info, $request );
737
			};
738
		} else {
739
			$failHandler = function () {
740
				return false;
741
			};
742
		}
743
744
		$newParams = [];
745
746
		if ( $blob !== false ) {
747
			// Sanity check: blob must be an array, if it's saved at all
748
			if ( !is_array( $blob ) ) {
749
				$this->logger->warning( 'Session "{session}": Bad data', [
750
					'session' => $info,
751
				] );
752
				$this->store->delete( $key );
753
				return $failHandler();
754
			}
755
756
			// Sanity check: blob has data and metadata arrays
757
			if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
758
				!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
759
			) {
760
				$this->logger->warning( 'Session "{session}": Bad data structure', [
761
					'session' => $info,
762
				] );
763
				$this->store->delete( $key );
764
				return $failHandler();
765
			}
766
767
			$data = $blob['data'];
768
			$metadata = $blob['metadata'];
769
770
			// Sanity check: metadata must be an array and must contain certain
771
			// keys, if it's saved at all
772
			if ( !array_key_exists( 'userId', $metadata ) ||
773
				!array_key_exists( 'userName', $metadata ) ||
774
				!array_key_exists( 'userToken', $metadata ) ||
775
				!array_key_exists( 'provider', $metadata )
776
			) {
777
				$this->logger->warning( 'Session "{session}": Bad metadata', [
778
					'session' => $info,
779
				] );
780
				$this->store->delete( $key );
781
				return $failHandler();
782
			}
783
784
			// First, load the provider from metadata, or validate it against the metadata.
785
			$provider = $info->getProvider();
786
			if ( $provider === null ) {
787
				$newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
788
				if ( !$provider ) {
789
					$this->logger->warning(
790
						'Session "{session}": Unknown provider ' . $metadata['provider'],
791
						[
792
							'session' => $info,
793
						]
794
					);
795
					$this->store->delete( $key );
796
					return $failHandler();
797
				}
798
			} elseif ( $metadata['provider'] !== (string)$provider ) {
799
				$this->logger->warning( 'Session "{session}": Wrong provider ' .
800
					$metadata['provider'] . ' !== ' . $provider,
801
					[
802
						'session' => $info,
803
				] );
804
				return $failHandler();
805
			}
806
807
			// Load provider metadata from metadata, or validate it against the metadata
808
			$providerMetadata = $info->getProviderMetadata();
809
			if ( isset( $metadata['providerMetadata'] ) ) {
810
				if ( $providerMetadata === null ) {
811
					$newParams['metadata'] = $metadata['providerMetadata'];
812
				} else {
813
					try {
814
						$newProviderMetadata = $provider->mergeMetadata(
815
							$metadata['providerMetadata'], $providerMetadata
816
						);
817
						if ( $newProviderMetadata !== $providerMetadata ) {
818
							$newParams['metadata'] = $newProviderMetadata;
819
						}
820
					} catch ( MetadataMergeException $ex ) {
821
						$this->logger->warning(
822
							'Session "{session}": Metadata merge failed: {exception}',
823
							[
824
								'session' => $info,
825
								'exception' => $ex,
826
							] + $ex->getContext()
827
						);
828
						return $failHandler();
829
					}
830
				}
831
			}
832
833
			// Next, load the user from metadata, or validate it against the metadata.
834
			$userInfo = $info->getUserInfo();
835
			if ( !$userInfo ) {
836
				// For loading, id is preferred to name.
837
				try {
838
					if ( $metadata['userId'] ) {
839
						$userInfo = UserInfo::newFromId( $metadata['userId'] );
840
					} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
841
						$userInfo = UserInfo::newFromName( $metadata['userName'] );
842
					} else {
843
						$userInfo = UserInfo::newAnonymous();
844
					}
845
				} catch ( \InvalidArgumentException $ex ) {
846
					$this->logger->error( 'Session "{session}": {exception}', [
847
						'session' => $info,
848
						'exception' => $ex,
849
					] );
850
					return $failHandler();
851
				}
852
				$newParams['userInfo'] = $userInfo;
853
			} else {
854
				// User validation passes if user ID matches, or if there
855
				// is no saved ID and the names match.
856
				if ( $metadata['userId'] ) {
857
					if ( $metadata['userId'] !== $userInfo->getId() ) {
858
						$this->logger->warning(
859
							'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
860
							[
861
								'session' => $info,
862
								'uid_a' => $metadata['userId'],
863
								'uid_b' => $userInfo->getId(),
864
						] );
865
						return $failHandler();
866
					}
867
868
					// If the user was renamed, probably best to fail here.
869 View Code Duplication
					if ( $metadata['userName'] !== null &&
870
						$userInfo->getName() !== $metadata['userName']
871
					) {
872
						$this->logger->warning(
873
							'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
874
							[
875
								'session' => $info,
876
								'uname_a' => $metadata['userName'],
877
								'uname_b' => $userInfo->getName(),
878
						] );
879
						return $failHandler();
880
					}
881
882 View Code Duplication
				} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
883
					if ( $metadata['userName'] !== $userInfo->getName() ) {
884
						$this->logger->warning(
885
							'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
886
							[
887
								'session' => $info,
888
								'uname_a' => $metadata['userName'],
889
								'uname_b' => $userInfo->getName(),
890
						] );
891
						return $failHandler();
892
					}
893
				} elseif ( !$userInfo->isAnon() ) {
894
					// Metadata specifies an anonymous user, but the passed-in
895
					// user isn't anonymous.
896
					$this->logger->warning(
897
						'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
898
						[
899
							'session' => $info,
900
					] );
901
					return $failHandler();
902
				}
903
			}
904
905
			// And if we have a token in the metadata, it must match the loaded/provided user.
906
			if ( $metadata['userToken'] !== null &&
907
				$userInfo->getToken() !== $metadata['userToken']
908
			) {
909
				$this->logger->warning( 'Session "{session}": User token mismatch', [
910
					'session' => $info,
911
				] );
912
				return $failHandler();
913
			}
914
			if ( !$userInfo->isVerified() ) {
915
				$newParams['userInfo'] = $userInfo->verified();
916
			}
917
918
			if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
919
				$newParams['remembered'] = true;
920
			}
921
			if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
922
				$newParams['forceHTTPS'] = true;
923
			}
924
			if ( !empty( $metadata['persisted'] ) && !$info->wasPersisted() ) {
925
				$newParams['persisted'] = true;
926
			}
927
928
			if ( !$info->isIdSafe() ) {
929
				$newParams['idIsSafe'] = true;
930
			}
931
		} else {
932
			// No metadata, so we can't load the provider if one wasn't given.
933
			if ( $info->getProvider() === null ) {
934
				$this->logger->warning(
935
					'Session "{session}": Null provider and no metadata',
936
					[
937
						'session' => $info,
938
				] );
939
				return $failHandler();
940
			}
941
942
			// If no user was provided and no metadata, it must be anon.
943
			if ( !$info->getUserInfo() ) {
944
				if ( $info->getProvider()->canChangeUser() ) {
945
					$newParams['userInfo'] = UserInfo::newAnonymous();
946
				} else {
947
					$this->logger->info(
948
						'Session "{session}": No user provided and provider cannot set user',
949
						[
950
							'session' => $info,
951
					] );
952
					return $failHandler();
953
				}
954
			} elseif ( !$info->getUserInfo()->isVerified() ) {
955
				$this->logger->warning(
956
					'Session "{session}": Unverified user provided and no metadata to auth it',
957
					[
958
						'session' => $info,
959
				] );
960
				return $failHandler();
961
			}
962
963
			$data = false;
964
			$metadata = false;
965
966
			if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
967
				// The ID doesn't come from the user, so it should be safe
968
				// (and if not, nothing we can do about it anyway)
969
				$newParams['idIsSafe'] = true;
970
			}
971
		}
972
973
		// Construct the replacement SessionInfo, if necessary
974
		if ( $newParams ) {
975
			$newParams['copyFrom'] = $info;
976
			$info = new SessionInfo( $info->getPriority(), $newParams );
977
		}
978
979
		// Allow the provider to check the loaded SessionInfo
980
		$providerMetadata = $info->getProviderMetadata();
981
		if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
982
			return $failHandler();
983
		}
984
		if ( $providerMetadata !== $info->getProviderMetadata() ) {
985
			$info = new SessionInfo( $info->getPriority(), [
986
				'metadata' => $providerMetadata,
987
				'copyFrom' => $info,
988
			] );
989
		}
990
991
		// Give hooks a chance to abort. Combined with the SessionMetadata
992
		// hook, this can allow for tying a session to an IP address or the
993
		// like.
994
		$reason = 'Hook aborted';
995
		if ( !\Hooks::run(
996
			'SessionCheckInfo',
997
			[ &$reason, $info, $request, $metadata, $data ]
998
		) ) {
999
			$this->logger->warning( 'Session "{session}": ' . $reason, [
1000
				'session' => $info,
1001
			] );
1002
			return $failHandler();
1003
		}
1004
1005
		return true;
1006
	}
1007
1008
	/**
1009
	 * Create a session corresponding to the passed SessionInfo
1010
	 * @private For use by a SessionProvider that needs to specially create its
1011
	 *  own session.
1012
	 * @param SessionInfo $info
1013
	 * @param WebRequest $request
1014
	 * @return Session
1015
	 */
1016
	public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
1017
		// @codeCoverageIgnoreStart
1018
		if ( defined( 'MW_NO_SESSION' ) ) {
1019
			if ( MW_NO_SESSION === 'warn' ) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of MW_NO_SESSION (integer) and 'warn' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
1020
				// Undocumented safety case for converting existing entry points
1021
				$this->logger->error( 'Sessions are supposed to be disabled for this entry point', [
1022
					'exception' => new \BadMethodCallException( 'Sessions are disabled for this entry point' ),
1023
				] );
1024
			} else {
1025
				throw new \BadMethodCallException( 'Sessions are disabled for this entry point' );
1026
			}
1027
		}
1028
		// @codeCoverageIgnoreEnd
1029
1030
		$id = $info->getId();
1031
1032
		if ( !isset( $this->allSessionBackends[$id] ) ) {
1033
			if ( !isset( $this->allSessionIds[$id] ) ) {
1034
				$this->allSessionIds[$id] = new SessionId( $id );
1035
			}
1036
			$backend = new SessionBackend(
1037
				$this->allSessionIds[$id],
1038
				$info,
1039
				$this->store,
0 ignored issues
show
Bug introduced by
It seems like $this->store can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1040
				$this->logger,
1041
				$this->config->get( 'ObjectCacheSessionExpiry' )
1042
			);
1043
			$this->allSessionBackends[$id] = $backend;
1044
			$delay = $backend->delaySave();
1045
		} else {
1046
			$backend = $this->allSessionBackends[$id];
1047
			$delay = $backend->delaySave();
1048
			if ( $info->wasPersisted() ) {
1049
				$backend->persist();
1050
			}
1051
			if ( $info->wasRemembered() ) {
1052
				$backend->setRememberUser( true );
1053
			}
1054
		}
1055
1056
		$request->setSessionId( $backend->getSessionId() );
1057
		$session = $backend->getSession( $request );
1058
1059
		if ( !$info->isIdSafe() ) {
1060
			$session->resetId();
1061
		}
1062
1063
		\ScopedCallback::consume( $delay );
1064
		return $session;
1065
	}
1066
1067
	/**
1068
	 * Deregister a SessionBackend
1069
	 * @private For use from \MediaWiki\Session\SessionBackend only
1070
	 * @param SessionBackend $backend
1071
	 */
1072
	public function deregisterSessionBackend( SessionBackend $backend ) {
1073
		$id = $backend->getId();
1074 View Code Duplication
		if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
1075
			$this->allSessionBackends[$id] !== $backend ||
1076
			$this->allSessionIds[$id] !== $backend->getSessionId()
1077
		) {
1078
			throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
1079
		}
1080
1081
		unset( $this->allSessionBackends[$id] );
1082
		// Explicitly do not unset $this->allSessionIds[$id]
1083
	}
1084
1085
	/**
1086
	 * Change a SessionBackend's ID
1087
	 * @private For use from \MediaWiki\Session\SessionBackend only
1088
	 * @param SessionBackend $backend
1089
	 */
1090
	public function changeBackendId( SessionBackend $backend ) {
1091
		$sessionId = $backend->getSessionId();
1092
		$oldId = (string)$sessionId;
1093 View Code Duplication
		if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
1094
			$this->allSessionBackends[$oldId] !== $backend ||
1095
			$this->allSessionIds[$oldId] !== $sessionId
1096
		) {
1097
			throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
1098
		}
1099
1100
		$newId = $this->generateSessionId();
1101
1102
		unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
1103
		$sessionId->setId( $newId );
0 ignored issues
show
Security Bug introduced by
It seems like $newId defined by $this->generateSessionId() on line 1100 can also be of type false; however, MediaWiki\Session\SessionId::setId() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1104
		$this->allSessionBackends[$newId] = $backend;
1105
		$this->allSessionIds[$newId] = $sessionId;
1106
	}
1107
1108
	/**
1109
	 * Generate a new random session ID
1110
	 * @return string
1111
	 */
1112
	public function generateSessionId() {
1113
		do {
1114
			$id = \Wikimedia\base_convert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
1115
			$key = wfMemcKey( 'MWSession', $id );
1116
		} while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
1117
		return $id;
1118
	}
1119
1120
	/**
1121
	 * Call setters on a PHPSessionHandler
1122
	 * @private Use PhpSessionHandler::install()
1123
	 * @param PHPSessionHandler $handler
1124
	 */
1125
	public function setupPHPSessionHandler( PHPSessionHandler $handler ) {
1126
		$handler->setManager( $this, $this->store, $this->logger );
0 ignored issues
show
Bug introduced by
It seems like $this->store can be null; however, setManager() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1127
	}
1128
1129
	/**
1130
	 * Reset the internal caching for unit testing
1131
	 */
1132
	public static function resetCache() {
1133
		if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
1134
			// @codeCoverageIgnoreStart
1135
			throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
1136
			// @codeCoverageIgnoreEnd
1137
		}
1138
1139
		self::$globalSession = null;
1140
		self::$globalSessionRequest = null;
1141
	}
1142
1143
	/**@}*/
1144
1145
}
1146