Completed
Branch master (1b526b)
by
unknown
29:06
created

SessionManager::getEmptySessionInternal()   C

Complexity

Conditions 16
Paths 110

Size

Total Lines 59
Code Lines 37

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 59
rs 6.0476
cc 16
eloc 37
nc 110
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
		global $wgAuth;
306
307
		$user->setToken();
308
		$user->saveSettings();
309
310
		$wgAuth->getUserInstance( $user )->resetAuthToken();
311
312
		foreach ( $this->getProviders() as $provider ) {
313
			$provider->invalidateSessionsForUser( $user );
314
		}
315
	}
316
317
	public function getVaryHeaders() {
318
		// @codeCoverageIgnoreStart
319
		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...
320
			return [];
321
		}
322
		// @codeCoverageIgnoreEnd
323
		if ( $this->varyHeaders === null ) {
324
			$headers = [];
325
			foreach ( $this->getProviders() as $provider ) {
326
				foreach ( $provider->getVaryHeaders() as $header => $options ) {
327
					if ( !isset( $headers[$header] ) ) {
328
						$headers[$header] = [];
329
					}
330
					if ( is_array( $options ) ) {
331
						$headers[$header] = array_unique( array_merge( $headers[$header], $options ) );
332
					}
333
				}
334
			}
335
			$this->varyHeaders = $headers;
336
		}
337
		return $this->varyHeaders;
338
	}
339
340
	public function getVaryCookies() {
341
		// @codeCoverageIgnoreStart
342
		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...
343
			return [];
344
		}
345
		// @codeCoverageIgnoreEnd
346
		if ( $this->varyCookies === null ) {
347
			$cookies = [];
348
			foreach ( $this->getProviders() as $provider ) {
349
				$cookies = array_merge( $cookies, $provider->getVaryCookies() );
350
			}
351
			$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...
352
		}
353
		return $this->varyCookies;
354
	}
355
356
	/**
357
	 * Validate a session ID
358
	 * @param string $id
359
	 * @return bool
360
	 */
361
	public static function validateSessionId( $id ) {
362
		return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
363
	}
364
365
	/**
366
	 * @name Internal methods
367
	 * @{
368
	 */
369
370
	/**
371
	 * Auto-create the given user, if necessary
372
	 * @private Don't call this yourself. Let Setup.php do it for you at the right time.
373
	 * @note This more properly belongs in AuthManager, but we need it now.
374
	 *  When AuthManager comes, this will be deprecated and will pass-through
375
	 *  to the corresponding AuthManager method.
376
	 * @param User $user User to auto-create
377
	 * @return bool Success
378
	 */
379
	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...
380
		global $wgAuth;
381
382
		$logger = self::singleton()->logger;
383
384
		// Much of this code is based on that in CentralAuth
385
386
		// Try the local user from the slave DB
387
		$localId = User::idFromName( $user->getName() );
388
		$flags = 0;
389
390
		// Fetch the user ID from the master, so that we don't try to create the user
391
		// when they already exist, due to replication lag
392
		// @codeCoverageIgnoreStart
393
		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...
394
			$localId = User::idFromName( $user->getName(), User::READ_LATEST );
395
			$flags = User::READ_LATEST;
396
		}
397
		// @codeCoverageIgnoreEnd
398
399
		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...
400
			// User exists after all.
401
			$user->setId( $localId );
402
			$user->loadFromId( $flags );
403
			return false;
404
		}
405
406
		// Denied by AuthPlugin? But ignore AuthPlugin itself.
407
		if ( get_class( $wgAuth ) !== 'AuthPlugin' && !$wgAuth->autoCreate() ) {
408
			$logger->debug( __METHOD__ . ': denied by AuthPlugin' );
409
			$user->setId( 0 );
410
			$user->loadFromId();
411
			return false;
412
		}
413
414
		// Wiki is read-only?
415 View Code Duplication
		if ( wfReadOnly() ) {
416
			$logger->debug( __METHOD__ . ': denied by wfReadOnly()' );
417
			$user->setId( 0 );
418
			$user->loadFromId();
419
			return false;
420
		}
421
422
		$userName = $user->getName();
423
424
		// Check the session, if we tried to create this user already there's
425
		// no point in retrying.
426
		$session = self::getGlobalSession();
427
		$reason = $session->get( 'MWSession::AutoCreateBlacklist' );
428 View Code Duplication
		if ( $reason ) {
429
			$logger->debug( __METHOD__ . ": blacklisted in session ($reason)" );
430
			$user->setId( 0 );
431
			$user->loadFromId();
432
			return false;
433
		}
434
435
		// Is the IP user able to create accounts?
436
		$anon = new User;
437
		if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
438
			|| $anon->isBlockedFromCreateAccount()
439
		) {
440
			// Blacklist the user to avoid repeated DB queries subsequently
441
			$logger->debug( __METHOD__ . ': user is blocked from this wiki, blacklisting' );
442
			$session->set( 'MWSession::AutoCreateBlacklist', 'blocked', 600 );
443
			$session->persist();
444
			$user->setId( 0 );
445
			$user->loadFromId();
446
			return false;
447
		}
448
449
		// Check for validity of username
450
		if ( !User::isCreatableName( $userName ) ) {
451
			$logger->debug( __METHOD__ . ': Invalid username, blacklisting' );
452
			$session->set( 'MWSession::AutoCreateBlacklist', 'invalid username', 600 );
453
			$session->persist();
454
			$user->setId( 0 );
455
			$user->loadFromId();
456
			return false;
457
		}
458
459
		// Give other extensions a chance to stop auto creation.
460
		$user->loadDefaults( $userName );
461
		$abortMessage = '';
462
		if ( !\Hooks::run( 'AbortAutoAccount', [ $user, &$abortMessage ] ) ) {
463
			// In this case we have no way to return the message to the user,
464
			// but we can log it.
465
			$logger->debug( __METHOD__ . ": denied by hook: $abortMessage" );
466
			$session->set( 'MWSession::AutoCreateBlacklist', "hook aborted: $abortMessage", 600 );
467
			$session->persist();
468
			$user->setId( 0 );
469
			$user->loadFromId();
470
			return false;
471
		}
472
473
		// Make sure the name has not been changed
474
		if ( $user->getName() !== $userName ) {
475
			$user->setId( 0 );
476
			$user->loadFromId();
477
			throw new \UnexpectedValueException(
478
				'AbortAutoAccount hook tried to change the user name'
479
			);
480
		}
481
482
		// Ignore warnings about master connections/writes...hard to avoid here
483
		\Profiler::instance()->getTransactionProfiler()->resetExpectations();
484
485
		$cache = \ObjectCache::getLocalClusterInstance();
486
		$backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $userName ) );
487
		if ( $cache->get( $backoffKey ) ) {
488
			$logger->debug( __METHOD__ . ': denied by prior creation attempt failures' );
489
			$user->setId( 0 );
490
			$user->loadFromId();
491
			return false;
492
		}
493
494
		// Checks passed, create the user...
495
		$from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
496
		$logger->info( __METHOD__ . ': creating new user ({username}) - from: {url}',
497
			[
498
				'username' => $userName,
499
				'url' => $from,
500
		] );
501
502
		try {
503
			// Insert the user into the local DB master
504
			$status = $user->addToDatabase();
505
			if ( !$status->isOK() ) {
506
				// @codeCoverageIgnoreStart
507
				// double-check for a race condition (T70012)
508
				$id = User::idFromName( $user->getName(), User::READ_LATEST );
509
				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...
510
					$logger->info( __METHOD__ . ': tried to autocreate existing user',
511
						[
512
							'username' => $userName,
513
						] );
514
				} else {
515
					$logger->error(
516
						__METHOD__ . ': failed with message ' . $status->getWikiText( false, false, 'en' ),
517
						[
518
							'username' => $userName,
519
						]
520
					);
521
				}
522
				$user->setId( $id );
523
				$user->loadFromId( User::READ_LATEST );
524
				return false;
525
				// @codeCoverageIgnoreEnd
526
			}
527
		} catch ( \Exception $ex ) {
528
			// @codeCoverageIgnoreStart
529
			$logger->error( __METHOD__ . ': failed with exception {exception}', [
530
				'exception' => $ex,
531
				'username' => $userName,
532
			] );
533
			// Do not keep throwing errors for a while
534
			$cache->set( $backoffKey, 1, 600 );
535
			// Bubble up error; which should normally trigger DB rollbacks
536
			throw $ex;
537
			// @codeCoverageIgnoreEnd
538
		}
539
540
		# Notify AuthPlugin
541
		// @codeCoverageIgnoreStart
542
		$tmpUser = $user;
543
		$wgAuth->initUser( $tmpUser, true );
544
		if ( $tmpUser !== $user ) {
545
			$logger->warning( __METHOD__ . ': ' .
546
				get_class( $wgAuth ) . '::initUser() replaced the user object' );
547
		}
548
		// @codeCoverageIgnoreEnd
549
550
		# Notify hooks (e.g. Newuserlog)
551
		\Hooks::run( 'AuthPluginAutoCreate', [ $user ] );
552
		\Hooks::run( 'LocalUserCreated', [ $user, true ] );
553
554
		$user->saveSettings();
555
556
		# Update user count
557
		\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
558
559
		# Watch user's userpage and talk page
560
		$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
561
562
		return true;
563
	}
564
565
	/**
566
	 * Prevent future sessions for the user
567
	 *
568
	 * The intention is that the named account will never again be usable for
569
	 * normal login (i.e. there is no way to undo the prevention of access).
570
	 *
571
	 * @private For use from \User::newSystemUser only
572
	 * @param string $username
573
	 */
574
	public function preventSessionsForUser( $username ) {
575
		$this->preventUsers[$username] = true;
576
577
		// Instruct the session providers to kill any other sessions too.
578
		foreach ( $this->getProviders() as $provider ) {
579
			$provider->preventSessionsForUser( $username );
580
		}
581
	}
582
583
	/**
584
	 * Test if a user is prevented
585
	 * @private For use from SessionBackend only
586
	 * @param string $username
587
	 * @return bool
588
	 */
589
	public function isUserSessionPrevented( $username ) {
590
		return !empty( $this->preventUsers[$username] );
591
	}
592
593
	/**
594
	 * Get the available SessionProviders
595
	 * @return SessionProvider[]
596
	 */
597
	protected function getProviders() {
598
		if ( $this->sessionProviders === null ) {
599
			$this->sessionProviders = [];
600
			foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
601
				$provider = \ObjectFactory::getObjectFromSpec( $spec );
602
				$provider->setLogger( $this->logger );
603
				$provider->setConfig( $this->config );
604
				$provider->setManager( $this );
605
				if ( isset( $this->sessionProviders[(string)$provider] ) ) {
606
					throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
607
				}
608
				$this->sessionProviders[(string)$provider] = $provider;
609
			}
610
		}
611
		return $this->sessionProviders;
612
	}
613
614
	/**
615
	 * Get a session provider by name
616
	 *
617
	 * Generally, this will only be used by internal implementation of some
618
	 * special session-providing mechanism. General purpose code, if it needs
619
	 * to access a SessionProvider at all, will use Session::getProvider().
620
	 *
621
	 * @param string $name
622
	 * @return SessionProvider|null
623
	 */
624
	public function getProvider( $name ) {
625
		$providers = $this->getProviders();
626
		return isset( $providers[$name] ) ? $providers[$name] : null;
627
	}
628
629
	/**
630
	 * Save all active sessions on shutdown
631
	 * @private For internal use with register_shutdown_function()
632
	 */
633
	public function shutdown() {
634
		if ( $this->allSessionBackends ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->allSessionBackends of type MediaWiki\Session\SessionBackend[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
635
			$this->logger->debug( 'Saving all sessions on shutdown' );
636
			if ( session_id() !== '' ) {
637
				// @codeCoverageIgnoreStart
638
				session_write_close();
639
			}
640
			// @codeCoverageIgnoreEnd
641
			foreach ( $this->allSessionBackends as $backend ) {
642
				$backend->shutdown();
643
			}
644
		}
645
	}
646
647
	/**
648
	 * Fetch the SessionInfo(s) for a request
649
	 * @param WebRequest $request
650
	 * @return SessionInfo|null
651
	 */
652
	private function getSessionInfoForRequest( WebRequest $request ) {
653
		// Call all providers to fetch "the" session
654
		$infos = [];
655
		foreach ( $this->getProviders() as $provider ) {
656
			$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...
657
			if ( !$info ) {
658
				continue;
659
			}
660
			if ( $info->getProvider() !== $provider ) {
661
				throw new \UnexpectedValueException(
662
					"$provider returned session info for a different provider: $info"
663
				);
664
			}
665
			$infos[] = $info;
666
		}
667
668
		// Sort the SessionInfos. Then find the first one that can be
669
		// successfully loaded, and then all the ones after it with the same
670
		// priority.
671
		usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' );
672
		$retInfos = [];
673
		while ( $infos ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $infos of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
674
			$info = array_pop( $infos );
675
			if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
676
				$retInfos[] = $info;
677
				while ( $infos ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $infos of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
678
					$info = array_pop( $infos );
679
					if ( SessionInfo::compare( $retInfos[0], $info ) ) {
680
						// We hit a lower priority, stop checking.
681
						break;
682
					}
683
					if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
684
						// This is going to error out below, but we want to
685
						// provide a complete list.
686
						$retInfos[] = $info;
687
					} else {
688
						// Session load failed, so unpersist it from this request
689
						$info->getProvider()->unpersistSession( $request );
690
					}
691
				}
692
			} else {
693
				// Session load failed, so unpersist it from this request
694
				$info->getProvider()->unpersistSession( $request );
695
			}
696
		}
697
698
		if ( count( $retInfos ) > 1 ) {
699
			$ex = new \OverflowException(
700
				'Multiple sessions for this request tied for top priority: ' . implode( ', ', $retInfos )
701
			);
702
			$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...
703
			throw $ex;
704
		}
705
706
		return $retInfos ? $retInfos[0] : null;
707
	}
708
709
	/**
710
	 * Load and verify the session info against the store
711
	 *
712
	 * @param SessionInfo &$info Will likely be replaced with an updated SessionInfo instance
713
	 * @param WebRequest $request
714
	 * @return bool Whether the session info matches the stored data (if any)
715
	 */
716
	private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) {
717
		$key = wfMemcKey( 'MWSession', $info->getId() );
718
		$blob = $this->store->get( $key );
719
720
		// If we got data from the store and the SessionInfo says to force use,
721
		// "fail" means to delete the data from the store and retry. Otherwise,
722
		// "fail" is just return false.
723
		if ( $info->forceUse() && $blob !== false ) {
724
			$failHandler = function () use ( $key, &$info, $request ) {
725
				$this->store->delete( $key );
726
				return $this->loadSessionInfoFromStore( $info, $request );
727
			};
728
		} else {
729
			$failHandler = function () {
730
				return false;
731
			};
732
		}
733
734
		$newParams = [];
735
736
		if ( $blob !== false ) {
737
			// Sanity check: blob must be an array, if it's saved at all
738
			if ( !is_array( $blob ) ) {
739
				$this->logger->warning( 'Session "{session}": Bad data', [
740
					'session' => $info,
741
				] );
742
				$this->store->delete( $key );
743
				return $failHandler();
744
			}
745
746
			// Sanity check: blob has data and metadata arrays
747
			if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
748
				!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
749
			) {
750
				$this->logger->warning( 'Session "{session}": Bad data structure', [
751
					'session' => $info,
752
				] );
753
				$this->store->delete( $key );
754
				return $failHandler();
755
			}
756
757
			$data = $blob['data'];
758
			$metadata = $blob['metadata'];
759
760
			// Sanity check: metadata must be an array and must contain certain
761
			// keys, if it's saved at all
762
			if ( !array_key_exists( 'userId', $metadata ) ||
763
				!array_key_exists( 'userName', $metadata ) ||
764
				!array_key_exists( 'userToken', $metadata ) ||
765
				!array_key_exists( 'provider', $metadata )
766
			) {
767
				$this->logger->warning( 'Session "{session}": Bad metadata', [
768
					'session' => $info,
769
				] );
770
				$this->store->delete( $key );
771
				return $failHandler();
772
			}
773
774
			// First, load the provider from metadata, or validate it against the metadata.
775
			$provider = $info->getProvider();
776
			if ( $provider === null ) {
777
				$newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
778
				if ( !$provider ) {
779
					$this->logger->warning(
780
						'Session "{session}": Unknown provider ' . $metadata['provider'],
781
						[
782
							'session' => $info,
783
						]
784
					);
785
					$this->store->delete( $key );
786
					return $failHandler();
787
				}
788
			} elseif ( $metadata['provider'] !== (string)$provider ) {
789
				$this->logger->warning( 'Session "{session}": Wrong provider ' .
790
					$metadata['provider'] . ' !== ' . $provider,
791
					[
792
						'session' => $info,
793
				] );
794
				return $failHandler();
795
			}
796
797
			// Load provider metadata from metadata, or validate it against the metadata
798
			$providerMetadata = $info->getProviderMetadata();
799
			if ( isset( $metadata['providerMetadata'] ) ) {
800
				if ( $providerMetadata === null ) {
801
					$newParams['metadata'] = $metadata['providerMetadata'];
802
				} else {
803
					try {
804
						$newProviderMetadata = $provider->mergeMetadata(
805
							$metadata['providerMetadata'], $providerMetadata
806
						);
807
						if ( $newProviderMetadata !== $providerMetadata ) {
808
							$newParams['metadata'] = $newProviderMetadata;
809
						}
810
					} catch ( MetadataMergeException $ex ) {
811
						$this->logger->warning(
812
							'Session "{session}": Metadata merge failed: {exception}',
813
							[
814
								'session' => $info,
815
								'exception' => $ex,
816
							] + $ex->getContext()
817
						);
818
						return $failHandler();
819
					}
820
				}
821
			}
822
823
			// Next, load the user from metadata, or validate it against the metadata.
824
			$userInfo = $info->getUserInfo();
825
			if ( !$userInfo ) {
826
				// For loading, id is preferred to name.
827
				try {
828
					if ( $metadata['userId'] ) {
829
						$userInfo = UserInfo::newFromId( $metadata['userId'] );
830
					} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
831
						$userInfo = UserInfo::newFromName( $metadata['userName'] );
832
					} else {
833
						$userInfo = UserInfo::newAnonymous();
834
					}
835
				} catch ( \InvalidArgumentException $ex ) {
836
					$this->logger->error( 'Session "{session}": {exception}', [
837
						'session' => $info,
838
						'exception' => $ex,
839
					] );
840
					return $failHandler();
841
				}
842
				$newParams['userInfo'] = $userInfo;
843
			} else {
844
				// User validation passes if user ID matches, or if there
845
				// is no saved ID and the names match.
846
				if ( $metadata['userId'] ) {
847
					if ( $metadata['userId'] !== $userInfo->getId() ) {
848
						$this->logger->warning(
849
							'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
850
							[
851
								'session' => $info,
852
								'uid_a' => $metadata['userId'],
853
								'uid_b' => $userInfo->getId(),
854
						] );
855
						return $failHandler();
856
					}
857
858
					// If the user was renamed, probably best to fail here.
859 View Code Duplication
					if ( $metadata['userName'] !== null &&
860
						$userInfo->getName() !== $metadata['userName']
861
					) {
862
						$this->logger->warning(
863
							'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
864
							[
865
								'session' => $info,
866
								'uname_a' => $metadata['userName'],
867
								'uname_b' => $userInfo->getName(),
868
						] );
869
						return $failHandler();
870
					}
871
872 View Code Duplication
				} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
873
					if ( $metadata['userName'] !== $userInfo->getName() ) {
874
						$this->logger->warning(
875
							'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
876
							[
877
								'session' => $info,
878
								'uname_a' => $metadata['userName'],
879
								'uname_b' => $userInfo->getName(),
880
						] );
881
						return $failHandler();
882
					}
883
				} elseif ( !$userInfo->isAnon() ) {
884
					// Metadata specifies an anonymous user, but the passed-in
885
					// user isn't anonymous.
886
					$this->logger->warning(
887
						'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
888
						[
889
							'session' => $info,
890
					] );
891
					return $failHandler();
892
				}
893
			}
894
895
			// And if we have a token in the metadata, it must match the loaded/provided user.
896
			if ( $metadata['userToken'] !== null &&
897
				$userInfo->getToken() !== $metadata['userToken']
898
			) {
899
				$this->logger->warning( 'Session "{session}": User token mismatch', [
900
					'session' => $info,
901
				] );
902
				return $failHandler();
903
			}
904
			if ( !$userInfo->isVerified() ) {
905
				$newParams['userInfo'] = $userInfo->verified();
906
			}
907
908
			if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
909
				$newParams['remembered'] = true;
910
			}
911
			if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
912
				$newParams['forceHTTPS'] = true;
913
			}
914
			if ( !empty( $metadata['persisted'] ) && !$info->wasPersisted() ) {
915
				$newParams['persisted'] = true;
916
			}
917
918
			if ( !$info->isIdSafe() ) {
919
				$newParams['idIsSafe'] = true;
920
			}
921
		} else {
922
			// No metadata, so we can't load the provider if one wasn't given.
923
			if ( $info->getProvider() === null ) {
924
				$this->logger->warning(
925
					'Session "{session}": Null provider and no metadata',
926
					[
927
						'session' => $info,
928
				] );
929
				return $failHandler();
930
			}
931
932
			// If no user was provided and no metadata, it must be anon.
933
			if ( !$info->getUserInfo() ) {
934
				if ( $info->getProvider()->canChangeUser() ) {
935
					$newParams['userInfo'] = UserInfo::newAnonymous();
936
				} else {
937
					$this->logger->info(
938
						'Session "{session}": No user provided and provider cannot set user',
939
						[
940
							'session' => $info,
941
					] );
942
					return $failHandler();
943
				}
944
			} elseif ( !$info->getUserInfo()->isVerified() ) {
945
				$this->logger->warning(
946
					'Session "{session}": Unverified user provided and no metadata to auth it',
947
					[
948
						'session' => $info,
949
				] );
950
				return $failHandler();
951
			}
952
953
			$data = false;
954
			$metadata = false;
955
956
			if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
957
				// The ID doesn't come from the user, so it should be safe
958
				// (and if not, nothing we can do about it anyway)
959
				$newParams['idIsSafe'] = true;
960
			}
961
		}
962
963
		// Construct the replacement SessionInfo, if necessary
964
		if ( $newParams ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newParams of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
965
			$newParams['copyFrom'] = $info;
966
			$info = new SessionInfo( $info->getPriority(), $newParams );
967
		}
968
969
		// Allow the provider to check the loaded SessionInfo
970
		$providerMetadata = $info->getProviderMetadata();
971
		if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
972
			return $failHandler();
973
		}
974
		if ( $providerMetadata !== $info->getProviderMetadata() ) {
975
			$info = new SessionInfo( $info->getPriority(), [
976
				'metadata' => $providerMetadata,
977
				'copyFrom' => $info,
978
			] );
979
		}
980
981
		// Give hooks a chance to abort. Combined with the SessionMetadata
982
		// hook, this can allow for tying a session to an IP address or the
983
		// like.
984
		$reason = 'Hook aborted';
985
		if ( !\Hooks::run(
986
			'SessionCheckInfo',
987
			[ &$reason, $info, $request, $metadata, $data ]
988
		) ) {
989
			$this->logger->warning( 'Session "{session}": ' . $reason, [
990
				'session' => $info,
991
			] );
992
			return $failHandler();
993
		}
994
995
		return true;
996
	}
997
998
	/**
999
	 * Create a session corresponding to the passed SessionInfo
1000
	 * @private For use by a SessionProvider that needs to specially create its
1001
	 *  own session.
1002
	 * @param SessionInfo $info
1003
	 * @param WebRequest $request
1004
	 * @return Session
1005
	 */
1006
	public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
1007
		// @codeCoverageIgnoreStart
1008
		if ( defined( 'MW_NO_SESSION' ) ) {
1009
			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...
1010
				// Undocumented safety case for converting existing entry points
1011
				$this->logger->error( 'Sessions are supposed to be disabled for this entry point', [
1012
					'exception' => new \BadMethodCallException( 'Sessions are disabled for this entry point' ),
1013
				] );
1014
			} else {
1015
				throw new \BadMethodCallException( 'Sessions are disabled for this entry point' );
1016
			}
1017
		}
1018
		// @codeCoverageIgnoreEnd
1019
1020
		$id = $info->getId();
1021
1022
		if ( !isset( $this->allSessionBackends[$id] ) ) {
1023
			if ( !isset( $this->allSessionIds[$id] ) ) {
1024
				$this->allSessionIds[$id] = new SessionId( $id );
1025
			}
1026
			$backend = new SessionBackend(
1027
				$this->allSessionIds[$id],
1028
				$info,
1029
				$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...
1030
				$this->logger,
1031
				$this->config->get( 'ObjectCacheSessionExpiry' )
1032
			);
1033
			$this->allSessionBackends[$id] = $backend;
1034
			$delay = $backend->delaySave();
1035
		} else {
1036
			$backend = $this->allSessionBackends[$id];
1037
			$delay = $backend->delaySave();
1038
			if ( $info->wasPersisted() ) {
1039
				$backend->persist();
1040
			}
1041
			if ( $info->wasRemembered() ) {
1042
				$backend->setRememberUser( true );
1043
			}
1044
		}
1045
1046
		$request->setSessionId( $backend->getSessionId() );
1047
		$session = $backend->getSession( $request );
1048
1049
		if ( !$info->isIdSafe() ) {
1050
			$session->resetId();
1051
		}
1052
1053
		\ScopedCallback::consume( $delay );
1054
		return $session;
1055
	}
1056
1057
	/**
1058
	 * Deregister a SessionBackend
1059
	 * @private For use from \MediaWiki\Session\SessionBackend only
1060
	 * @param SessionBackend $backend
1061
	 */
1062
	public function deregisterSessionBackend( SessionBackend $backend ) {
1063
		$id = $backend->getId();
1064 View Code Duplication
		if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
1065
			$this->allSessionBackends[$id] !== $backend ||
1066
			$this->allSessionIds[$id] !== $backend->getSessionId()
1067
		) {
1068
			throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
1069
		}
1070
1071
		unset( $this->allSessionBackends[$id] );
1072
		// Explicitly do not unset $this->allSessionIds[$id]
1073
	}
1074
1075
	/**
1076
	 * Change a SessionBackend's ID
1077
	 * @private For use from \MediaWiki\Session\SessionBackend only
1078
	 * @param SessionBackend $backend
1079
	 */
1080
	public function changeBackendId( SessionBackend $backend ) {
1081
		$sessionId = $backend->getSessionId();
1082
		$oldId = (string)$sessionId;
1083 View Code Duplication
		if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
1084
			$this->allSessionBackends[$oldId] !== $backend ||
1085
			$this->allSessionIds[$oldId] !== $sessionId
1086
		) {
1087
			throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
1088
		}
1089
1090
		$newId = $this->generateSessionId();
1091
1092
		unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
1093
		$sessionId->setId( $newId );
0 ignored issues
show
Security Bug introduced by
It seems like $newId defined by $this->generateSessionId() on line 1090 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...
1094
		$this->allSessionBackends[$newId] = $backend;
1095
		$this->allSessionIds[$newId] = $sessionId;
1096
	}
1097
1098
	/**
1099
	 * Generate a new random session ID
1100
	 * @return string
1101
	 */
1102
	public function generateSessionId() {
1103
		do {
1104
			$id = \Wikimedia\base_convert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
1105
			$key = wfMemcKey( 'MWSession', $id );
1106
		} while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
1107
		return $id;
1108
	}
1109
1110
	/**
1111
	 * Call setters on a PHPSessionHandler
1112
	 * @private Use PhpSessionHandler::install()
1113
	 * @param PHPSessionHandler $handler
1114
	 */
1115
	public function setupPHPSessionHandler( PHPSessionHandler $handler ) {
1116
		$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...
1117
	}
1118
1119
	/**
1120
	 * Reset the internal caching for unit testing
1121
	 */
1122
	public static function resetCache() {
1123
		if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
1124
			// @codeCoverageIgnoreStart
1125
			throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
1126
			// @codeCoverageIgnoreEnd
1127
		}
1128
1129
		self::$globalSession = null;
1130
		self::$globalSessionRequest = null;
1131
	}
1132
1133
	/**@}*/
1134
1135
}
1136