AuthManager   F
last analyzed

Complexity

Total Complexity 323

Size/Duplication

Total Lines 2364
Duplicated Lines 16.67 %

Coupling/Cohesion

Components 2
Dependencies 37

Importance

Changes 0
Metric Value
dl 394
loc 2364
rs 0.5217
c 0
b 0
f 0
wmc 323
lcom 2
cbo 37

42 Methods

Rating   Name   Duplication   Size   Complexity  
A singleton() 0 9 2
A __construct() 0 5 1
A setLogger() 0 3 1
A getRequest() 0 3 1
B forcePrimaryAuthenticationProviders() 7 42 5
A callLegacyAuthPlugin() 0 9 3
A canAuthenticateNow() 0 3 1
D beginAuthentication() 13 101 15
F continueAuthentication() 100 295 52
C securitySensitiveOperationStatus() 0 63 16
A userCanAuthenticate() 0 8 3
A normalizeUsername() 0 10 3
A revokeAccessForUser() 0 6 1
B allowsAuthenticationDataChange() 0 18 5
A changeAuthenticationData() 0 12 2
A canCreateAccounts() 10 10 4
C canCreateAccount() 0 42 8
C checkAccountCreatePermissions() 0 42 8
C beginAccountCreation() 34 95 11
F continueAccountCreation() 90 384 49
A canLinkAccounts() 8 8 3
D beginAccountLink() 38 103 14
C continueAccountLink() 29 86 12
C getAuthenticationRequests() 22 54 13
D getAuthenticationRequestsInternal() 0 58 15
B fillRequests() 0 10 5
A userExists() 0 9 3
A allowsPropertyChange() 0 10 3
B getAuthenticationProvider() 0 22 5
A setAuthenticationSessionData() 0 9 2
A getAuthenticationSessionData() 0 8 3
A removeAuthenticationSessionData() 0 12 4
B providerArrayFromSpecs() 7 35 6
A getConfiguration() 0 3 2
A getPreAuthenticationProviders() 0 9 2
A getPrimaryAuthenticationProviders() 0 9 2
A getSecondaryAuthenticationProviders() 0 9 2
A setSessionDataForUser() 0 20 3
A setDefaultUserOptions() 0 12 3
B callMethodOnProviders() 0 15 5
A resetCache() 0 9 2
D autoCreateUser() 36 217 23

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AuthManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AuthManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Authentication (and possibly Authorization in the future) system 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 Auth
22
 */
23
24
namespace MediaWiki\Auth;
25
26
use Config;
27
use Psr\Log\LoggerAwareInterface;
28
use Psr\Log\LoggerInterface;
29
use Status;
30
use StatusValue;
31
use User;
32
use WebRequest;
33
34
/**
35
 * This serves as the entry point to the authentication system.
36
 *
37
 * In the future, it may also serve as the entry point to the authorization
38
 * system.
39
 *
40
 * If you are looking at this because you are working on an extension that creates its own
41
 * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
42
 * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
43
 * or the createaccount API. Trying to call this class directly will very likely end up in
44
 * security vulnerabilities or broken UX in edge cases.
45
 *
46
 * If you are working on an extension that needs to integrate with the authentication system
47
 * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
48
 * need to write an AuthenticationProvider.
49
 *
50
 * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
51
 * you are looking for. If you want to change user data, use User::changeAuthenticationData().
52
 * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
53
 * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
54
 * responsibility to ensure that the user can authenticate somehow (see especially
55
 * PrimaryAuthenticationProvider::autoCreatedAccount()).
56
 * If you are writing code that is not associated with such a provider and needs to create accounts
57
 * programmatically for real users, you should rethink your architecture. There is no good way to
58
 * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
59
 * cannot provide any means for users to access the accounts it would create.
60
 *
61
 * The two main control flows when using this class are as follows:
62
 * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
63
 *   the requests with data (by using them to build a HTMLForm and have the user fill it, or by
64
 *   exposing a form specification via the API, so that the client can build it), and pass them to
65
 *   the appropriate begin* method. That will return either a success/failure response, or more
66
 *   requests to fill (either by building a form or by redirecting the user to some external
67
 *   provider which will send the data back), in which case they need to be submitted to the
68
 *   appropriate continue* method and that step has to be repeated until the response is a success
69
 *   or failure response. AuthManager will use the session to maintain internal state during the
70
 *   process.
71
 * * Code doing an authentication data change will call getAuthenticationRequests(), select
72
 *   a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
73
 *   changeAuthenticationData(). If the data change is user-initiated, the whole process needs
74
 *   to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
75
 *   a non-OK status.
76
 *
77
 * @ingroup Auth
78
 * @since 1.27
79
 * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
80
 */
81
class AuthManager implements LoggerAwareInterface {
82
	/** Log in with an existing (not necessarily local) user */
83
	const ACTION_LOGIN = 'login';
84
	/** Continue a login process that was interrupted by the need for user input or communication
85
	 * with an external provider */
86
	const ACTION_LOGIN_CONTINUE = 'login-continue';
87
	/** Create a new user */
88
	const ACTION_CREATE = 'create';
89
	/** Continue a user creation process that was interrupted by the need for user input or
90
	 * communication with an external provider */
91
	const ACTION_CREATE_CONTINUE = 'create-continue';
92
	/** Link an existing user to a third-party account */
93
	const ACTION_LINK = 'link';
94
	/** Continue a user linking process that was interrupted by the need for user input or
95
	 * communication with an external provider */
96
	const ACTION_LINK_CONTINUE = 'link-continue';
97
	/** Change a user's credentials */
98
	const ACTION_CHANGE = 'change';
99
	/** Remove a user's credentials */
100
	const ACTION_REMOVE = 'remove';
101
	/** Like ACTION_REMOVE but for linking providers only */
102
	const ACTION_UNLINK = 'unlink';
103
104
	/** Security-sensitive operations are ok. */
105
	const SEC_OK = 'ok';
106
	/** Security-sensitive operations should re-authenticate. */
107
	const SEC_REAUTH = 'reauth';
108
	/** Security-sensitive should not be performed. */
109
	const SEC_FAIL = 'fail';
110
111
	/** Auto-creation is due to SessionManager */
112
	const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
113
114
	/** @var AuthManager|null */
115
	private static $instance = null;
116
117
	/** @var WebRequest */
118
	private $request;
119
120
	/** @var Config */
121
	private $config;
122
123
	/** @var LoggerInterface */
124
	private $logger;
125
126
	/** @var AuthenticationProvider[] */
127
	private $allAuthenticationProviders = [];
128
129
	/** @var PreAuthenticationProvider[] */
130
	private $preAuthenticationProviders = null;
131
132
	/** @var PrimaryAuthenticationProvider[] */
133
	private $primaryAuthenticationProviders = null;
134
135
	/** @var SecondaryAuthenticationProvider[] */
136
	private $secondaryAuthenticationProviders = null;
137
138
	/** @var CreatedAccountAuthenticationRequest[] */
139
	private $createdAccountAuthenticationRequests = [];
140
141
	/**
142
	 * Get the global AuthManager
143
	 * @return AuthManager
144
	 */
145
	public static function singleton() {
146
		if ( self::$instance === null ) {
147
			self::$instance = new self(
148
				\RequestContext::getMain()->getRequest(),
149
				\ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
150
			);
151
		}
152
		return self::$instance;
153
	}
154
155
	/**
156
	 * @param WebRequest $request
157
	 * @param Config $config
158
	 */
159
	public function __construct( WebRequest $request, Config $config ) {
160
		$this->request = $request;
161
		$this->config = $config;
162
		$this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
163
	}
164
165
	/**
166
	 * @param LoggerInterface $logger
167
	 */
168
	public function setLogger( LoggerInterface $logger ) {
169
		$this->logger = $logger;
170
	}
171
172
	/**
173
	 * @return WebRequest
174
	 */
175
	public function getRequest() {
176
		return $this->request;
177
	}
178
179
	/**
180
	 * Force certain PrimaryAuthenticationProviders
181
	 * @deprecated For backwards compatibility only
182
	 * @param PrimaryAuthenticationProvider[] $providers
183
	 * @param string $why
184
	 */
185
	public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
186
		$this->logger->warning( "Overriding AuthManager primary authn because $why" );
187
188
		if ( $this->primaryAuthenticationProviders !== null ) {
189
			$this->logger->warning(
190
				'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
191
			);
192
193
			$this->allAuthenticationProviders = array_diff_key(
194
				$this->allAuthenticationProviders,
195
				$this->primaryAuthenticationProviders
196
			);
197
			$session = $this->request->getSession();
198
			$session->remove( 'AuthManager::authnState' );
199
			$session->remove( 'AuthManager::accountCreationState' );
200
			$session->remove( 'AuthManager::accountLinkState' );
201
			$this->createdAccountAuthenticationRequests = [];
202
		}
203
204
		$this->primaryAuthenticationProviders = [];
205
		foreach ( $providers as $provider ) {
206
			if ( !$provider instanceof PrimaryAuthenticationProvider ) {
207
				throw new \RuntimeException(
208
					'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
209
						get_class( $provider )
210
				);
211
			}
212
			$provider->setLogger( $this->logger );
213
			$provider->setManager( $this );
214
			$provider->setConfig( $this->config );
215
			$id = $provider->getUniqueId();
216 View Code Duplication
			if ( isset( $this->allAuthenticationProviders[$id] ) ) {
217
				throw new \RuntimeException(
218
					"Duplicate specifications for id $id (classes " .
219
						get_class( $provider ) . ' and ' .
220
						get_class( $this->allAuthenticationProviders[$id] ) . ')'
221
				);
222
			}
223
			$this->allAuthenticationProviders[$id] = $provider;
224
			$this->primaryAuthenticationProviders[$id] = $provider;
225
		}
226
	}
227
228
	/**
229
	 * Call a legacy AuthPlugin method, if necessary
230
	 * @codeCoverageIgnore
231
	 * @deprecated For backwards compatibility only, should be avoided in new code
232
	 * @param string $method AuthPlugin method to call
233
	 * @param array $params Parameters to pass
234
	 * @param mixed $return Return value if AuthPlugin wasn't called
235
	 * @return mixed Return value from the AuthPlugin method, or $return
236
	 */
237
	public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
238
		global $wgAuth;
239
240
		if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
241
			return call_user_func_array( [ $wgAuth, $method ], $params );
242
		} else {
243
			return $return;
244
		}
245
	}
246
247
	/**
248
	 * @name Authentication
249
	 * @{
250
	 */
251
252
	/**
253
	 * Indicate whether user authentication is possible
254
	 *
255
	 * It may not be if the session is provided by something like OAuth
256
	 * for which each individual request includes authentication data.
257
	 *
258
	 * @return bool
259
	 */
260
	public function canAuthenticateNow() {
261
		return $this->request->getSession()->canSetUser();
262
	}
263
264
	/**
265
	 * Start an authentication flow
266
	 *
267
	 * In addition to the AuthenticationRequests returned by
268
	 * $this->getAuthenticationRequests(), a client might include a
269
	 * CreateFromLoginAuthenticationRequest from a previous login attempt to
270
	 * preserve state.
271
	 *
272
	 * Instead of the AuthenticationRequests returned by
273
	 * $this->getAuthenticationRequests(), a client might pass a
274
	 * CreatedAccountAuthenticationRequest from an account creation that just
275
	 * succeeded to log in to the just-created account.
276
	 *
277
	 * @param AuthenticationRequest[] $reqs
278
	 * @param string $returnToUrl Url that REDIRECT responses should eventually
279
	 *  return to.
280
	 * @return AuthenticationResponse See self::continueAuthentication()
281
	 */
282
	public function beginAuthentication( array $reqs, $returnToUrl ) {
283
		$session = $this->request->getSession();
284
		if ( !$session->canSetUser() ) {
285
			// Caller should have called canAuthenticateNow()
286
			$session->remove( 'AuthManager::authnState' );
287
			throw new \LogicException( 'Authentication is not possible now' );
288
		}
289
290
		$guessUserName = null;
291 View Code Duplication
		foreach ( $reqs as $req ) {
292
			$req->returnToUrl = $returnToUrl;
293
			// @codeCoverageIgnoreStart
294
			if ( $req->username !== null && $req->username !== '' ) {
295
				if ( $guessUserName === null ) {
296
					$guessUserName = $req->username;
297
				} elseif ( $guessUserName !== $req->username ) {
298
					$guessUserName = null;
299
					break;
300
				}
301
			}
302
			// @codeCoverageIgnoreEnd
303
		}
304
305
		// Check for special-case login of a just-created account
306
		$req = AuthenticationRequest::getRequestByClass(
307
			$reqs, CreatedAccountAuthenticationRequest::class
308
		);
309
		if ( $req ) {
310
			if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
311
				throw new \LogicException(
312
					'CreatedAccountAuthenticationRequests are only valid on ' .
313
						'the same AuthManager that created the account'
314
				);
315
			}
316
317
			$user = User::newFromName( $req->username );
318
			// @codeCoverageIgnoreStart
319
			if ( !$user ) {
320
				throw new \UnexpectedValueException(
321
					"CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
322
				);
323
			} elseif ( $user->getId() != $req->id ) {
0 ignored issues
show
Bug introduced by
The property id does not seem to exist in MediaWiki\Auth\AuthenticationRequest.

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...
324
				throw new \UnexpectedValueException(
325
					"ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
326
				);
327
			}
328
			// @codeCoverageIgnoreEnd
329
330
			$this->logger->info( 'Logging in {user} after account creation', [
331
				'user' => $user->getName(),
332
			] );
333
			$ret = AuthenticationResponse::newPass( $user->getName() );
334
			$this->setSessionDataForUser( $user );
335
			$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
336
			$session->remove( 'AuthManager::authnState' );
337
			\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
338
			return $ret;
339
		}
340
341
		$this->removeAuthenticationSessionData( null );
342
343
		foreach ( $this->getPreAuthenticationProviders() as $provider ) {
344
			$status = $provider->testForAuthentication( $reqs );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testForAuthentication() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractPreAuthenticationProvider, MediaWiki\Auth\LegacyHookPreAuthenticationProvider, MediaWiki\Auth\ThrottlePreAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
345
			if ( !$status->isGood() ) {
346
				$this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
347
				$ret = AuthenticationResponse::newFail(
348
					Status::wrap( $status )->getMessage()
349
				);
350
				$this->callMethodOnProviders( 7, 'postAuthentication',
351
					[ User::newFromName( $guessUserName ) ?: null, $ret ]
352
				);
353
				\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
354
				return $ret;
355
			}
356
		}
357
358
		$state = [
359
			'reqs' => $reqs,
360
			'returnToUrl' => $returnToUrl,
361
			'guessUserName' => $guessUserName,
362
			'primary' => null,
363
			'primaryResponse' => null,
364
			'secondary' => [],
365
			'maybeLink' => [],
366
			'continueRequests' => [],
367
		];
368
369
		// Preserve state from a previous failed login
370
		$req = AuthenticationRequest::getRequestByClass(
371
			$reqs, CreateFromLoginAuthenticationRequest::class
372
		);
373
		if ( $req ) {
374
			$state['maybeLink'] = $req->maybeLink;
0 ignored issues
show
Bug introduced by
The property maybeLink does not seem to exist in MediaWiki\Auth\AuthenticationRequest.

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...
375
		}
376
377
		$session = $this->request->getSession();
378
		$session->setSecret( 'AuthManager::authnState', $state );
379
		$session->persist();
380
381
		return $this->continueAuthentication( $reqs );
382
	}
383
384
	/**
385
	 * Continue an authentication flow
386
	 *
387
	 * Return values are interpreted as follows:
388
	 * - status FAIL: Authentication failed. If $response->createRequest is
389
	 *   set, that may be passed to self::beginAuthentication() or to
390
	 *   self::beginAccountCreation() to preserve state.
391
	 * - status REDIRECT: The client should be redirected to the contained URL,
392
	 *   new AuthenticationRequests should be made (if any), then
393
	 *   AuthManager::continueAuthentication() should be called.
394
	 * - status UI: The client should be presented with a user interface for
395
	 *   the fields in the specified AuthenticationRequests, then new
396
	 *   AuthenticationRequests should be made, then
397
	 *   AuthManager::continueAuthentication() should be called.
398
	 * - status RESTART: The user logged in successfully with a third-party
399
	 *   service, but the third-party credentials aren't attached to any local
400
	 *   account. This could be treated as a UI or a FAIL.
401
	 * - status PASS: Authentication was successful.
402
	 *
403
	 * @param AuthenticationRequest[] $reqs
404
	 * @return AuthenticationResponse
405
	 */
406
	public function continueAuthentication( array $reqs ) {
407
		$session = $this->request->getSession();
408
		try {
409
			if ( !$session->canSetUser() ) {
410
				// Caller should have called canAuthenticateNow()
411
				// @codeCoverageIgnoreStart
412
				throw new \LogicException( 'Authentication is not possible now' );
413
				// @codeCoverageIgnoreEnd
414
			}
415
416
			$state = $session->getSecret( 'AuthManager::authnState' );
417
			if ( !is_array( $state ) ) {
418
				return AuthenticationResponse::newFail(
419
					wfMessage( 'authmanager-authn-not-in-progress' )
420
				);
421
			}
422
			$state['continueRequests'] = [];
423
424
			$guessUserName = $state['guessUserName'];
425
426
			foreach ( $reqs as $req ) {
427
				$req->returnToUrl = $state['returnToUrl'];
428
			}
429
430
			// Step 1: Choose an primary authentication provider, and call it until it succeeds.
431
432
			if ( $state['primary'] === null ) {
433
				// We haven't picked a PrimaryAuthenticationProvider yet
434
				// @codeCoverageIgnoreStart
435
				$guessUserName = null;
436 View Code Duplication
				foreach ( $reqs as $req ) {
437
					if ( $req->username !== null && $req->username !== '' ) {
438
						if ( $guessUserName === null ) {
439
							$guessUserName = $req->username;
440
						} elseif ( $guessUserName !== $req->username ) {
441
							$guessUserName = null;
442
							break;
443
						}
444
					}
445
				}
446
				$state['guessUserName'] = $guessUserName;
447
				// @codeCoverageIgnoreEnd
448
				$state['reqs'] = $reqs;
449
450
				foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
451
					$res = $provider->beginPrimaryAuthentication( $reqs );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method beginPrimaryAuthentication() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
452
					switch ( $res->status ) {
453
						case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
454
							$state['primary'] = $id;
455
							$state['primaryResponse'] = $res;
456
							$this->logger->debug( "Primary login with $id succeeded" );
457
							break 2;
458 View Code Duplication
						case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
459
							$this->logger->debug( "Login failed in primary authentication by $id" );
460
							if ( $res->createRequest || $state['maybeLink'] ) {
461
								$res->createRequest = new CreateFromLoginAuthenticationRequest(
462
									$res->createRequest, $state['maybeLink']
463
								);
464
							}
465
							$this->callMethodOnProviders( 7, 'postAuthentication',
466
								[ User::newFromName( $guessUserName ) ?: null, $res ]
467
							);
468
							$session->remove( 'AuthManager::authnState' );
469
							\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
470
							return $res;
471
						case AuthenticationResponse::ABSTAIN;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
472
							// Continue loop
473
							break;
474
						case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
475 View Code Duplication
						case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
476
							$this->logger->debug( "Primary login with $id returned $res->status" );
477
							$this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
478
							$state['primary'] = $id;
479
							$state['continueRequests'] = $res->neededRequests;
480
							$session->setSecret( 'AuthManager::authnState', $state );
481
							return $res;
482
483
							// @codeCoverageIgnoreStart
484
						default:
485
							throw new \DomainException(
486
								get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
487
							);
488
							// @codeCoverageIgnoreEnd
489
					}
490
				}
491
				if ( $state['primary'] === null ) {
492
					$this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
493
					$ret = AuthenticationResponse::newFail(
494
						wfMessage( 'authmanager-authn-no-primary' )
495
					);
496
					$this->callMethodOnProviders( 7, 'postAuthentication',
497
						[ User::newFromName( $guessUserName ) ?: null, $ret ]
498
					);
499
					$session->remove( 'AuthManager::authnState' );
500
					return $ret;
501
				}
502
			} elseif ( $state['primaryResponse'] === null ) {
503
				$provider = $this->getAuthenticationProvider( $state['primary'] );
504 View Code Duplication
				if ( !$provider instanceof PrimaryAuthenticationProvider ) {
505
					// Configuration changed? Force them to start over.
506
					// @codeCoverageIgnoreStart
507
					$ret = AuthenticationResponse::newFail(
508
						wfMessage( 'authmanager-authn-not-in-progress' )
509
					);
510
					$this->callMethodOnProviders( 7, 'postAuthentication',
511
						[ User::newFromName( $guessUserName ) ?: null, $ret ]
512
					);
513
					$session->remove( 'AuthManager::authnState' );
514
					return $ret;
515
					// @codeCoverageIgnoreEnd
516
				}
517
				$id = $provider->getUniqueId();
518
				$res = $provider->continuePrimaryAuthentication( $reqs );
519
				switch ( $res->status ) {
520
					case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
521
						$state['primaryResponse'] = $res;
522
						$this->logger->debug( "Primary login with $id succeeded" );
523
						break;
524 View Code Duplication
					case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
525
						$this->logger->debug( "Login failed in primary authentication by $id" );
526
						if ( $res->createRequest || $state['maybeLink'] ) {
527
							$res->createRequest = new CreateFromLoginAuthenticationRequest(
528
								$res->createRequest, $state['maybeLink']
529
							);
530
						}
531
						$this->callMethodOnProviders( 7, 'postAuthentication',
532
							[ User::newFromName( $guessUserName ) ?: null, $res ]
533
						);
534
						$session->remove( 'AuthManager::authnState' );
535
						\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
536
						return $res;
537
					case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
538
					case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
539
						$this->logger->debug( "Primary login with $id returned $res->status" );
540
						$this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
541
						$state['continueRequests'] = $res->neededRequests;
542
						$session->setSecret( 'AuthManager::authnState', $state );
543
						return $res;
544
					default:
545
						throw new \DomainException(
546
							get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
547
						);
548
				}
549
			}
550
551
			$res = $state['primaryResponse'];
552
			if ( $res->username === null ) {
553
				$provider = $this->getAuthenticationProvider( $state['primary'] );
554 View Code Duplication
				if ( !$provider instanceof PrimaryAuthenticationProvider ) {
555
					// Configuration changed? Force them to start over.
556
					// @codeCoverageIgnoreStart
557
					$ret = AuthenticationResponse::newFail(
558
						wfMessage( 'authmanager-authn-not-in-progress' )
559
					);
560
					$this->callMethodOnProviders( 7, 'postAuthentication',
561
						[ User::newFromName( $guessUserName ) ?: null, $ret ]
562
					);
563
					$session->remove( 'AuthManager::authnState' );
564
					return $ret;
565
					// @codeCoverageIgnoreEnd
566
				}
567
568
				if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
569
					$res->linkRequest &&
570
					 // don't confuse the user with an incorrect message if linking is disabled
571
					$this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
572
				) {
573
					$state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
574
					$msg = 'authmanager-authn-no-local-user-link';
575
				} else {
576
					$msg = 'authmanager-authn-no-local-user';
577
				}
578
				$this->logger->debug(
579
					"Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
580
				);
581
				$ret = AuthenticationResponse::newRestart( wfMessage( $msg ) );
582
				$ret->neededRequests = $this->getAuthenticationRequestsInternal(
583
					self::ACTION_LOGIN,
584
					[],
585
					$this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
586
				);
587 View Code Duplication
				if ( $res->createRequest || $state['maybeLink'] ) {
588
					$ret->createRequest = new CreateFromLoginAuthenticationRequest(
589
						$res->createRequest, $state['maybeLink']
590
					);
591
					$ret->neededRequests[] = $ret->createRequest;
592
				}
593
				$this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
594
				$session->setSecret( 'AuthManager::authnState', [
595
					'reqs' => [], // Will be filled in later
596
					'primary' => null,
597
					'primaryResponse' => null,
598
					'secondary' => [],
599
					'continueRequests' => $ret->neededRequests,
600
				] + $state );
601
				return $ret;
602
			}
603
604
			// Step 2: Primary authentication succeeded, create the User object
605
			// (and add the user locally if necessary)
606
607
			$user = User::newFromName( $res->username, 'usable' );
608
			if ( !$user ) {
609
				$provider = $this->getAuthenticationProvider( $state['primary'] );
610
				throw new \DomainException(
611
					get_class( $provider ) . " returned an invalid username: {$res->username}"
612
				);
613
			}
614
			if ( $user->getId() === 0 ) {
615
				// User doesn't exist locally. Create it.
616
				$this->logger->info( 'Auto-creating {user} on login', [
617
					'user' => $user->getName(),
618
				] );
619
				$status = $this->autoCreateUser( $user, $state['primary'], false );
620
				if ( !$status->isGood() ) {
621
					$ret = AuthenticationResponse::newFail(
622
						Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
623
					);
624
					$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
625
					$session->remove( 'AuthManager::authnState' );
626
					\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
627
					return $ret;
628
				}
629
			}
630
631
			// Step 3: Iterate over all the secondary authentication providers.
632
633
			$beginReqs = $state['reqs'];
634
635
			foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
636 View Code Duplication
				if ( !isset( $state['secondary'][$id] ) ) {
637
					// This provider isn't started yet, so we pass it the set
638
					// of reqs from beginAuthentication instead of whatever
639
					// might have been used by a previous provider in line.
640
					$func = 'beginSecondaryAuthentication';
641
					$res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method beginSecondaryAuthentication() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
642
				} elseif ( !$state['secondary'][$id] ) {
643
					$func = 'continueSecondaryAuthentication';
644
					$res = $provider->continueSecondaryAuthentication( $user, $reqs );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method continueSecondaryAuthentication() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
645
				} else {
646
					continue;
647
				}
648
				switch ( $res->status ) {
649
					case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
650
						$this->logger->debug( "Secondary login with $id succeeded" );
651
						// fall through
652
					case AuthenticationResponse::ABSTAIN;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
653
						$state['secondary'][$id] = true;
654
						break;
655 View Code Duplication
					case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
656
						$this->logger->debug( "Login failed in secondary authentication by $id" );
657
						$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
658
						$session->remove( 'AuthManager::authnState' );
659
						\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
660
						return $res;
661
					case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
662 View Code Duplication
					case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
663
						$this->logger->debug( "Secondary login with $id returned " . $res->status );
664
						$this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
665
						$state['secondary'][$id] = false;
666
						$state['continueRequests'] = $res->neededRequests;
667
						$session->setSecret( 'AuthManager::authnState', $state );
668
						return $res;
669
670
						// @codeCoverageIgnoreStart
671
					default:
672
						throw new \DomainException(
673
							get_class( $provider ) . "::{$func}() returned $res->status"
674
						);
675
						// @codeCoverageIgnoreEnd
676
				}
677
			}
678
679
			// Step 4: Authentication complete! Set the user in the session and
680
			// clean up.
681
682
			$this->logger->info( 'Login for {user} succeeded', [
683
				'user' => $user->getName(),
684
			] );
685
			/** @var RememberMeAuthenticationRequest $req */
686
			$req = AuthenticationRequest::getRequestByClass(
687
				$beginReqs, RememberMeAuthenticationRequest::class
688
			);
689
			$this->setSessionDataForUser( $user, $req && $req->rememberMe );
690
			$ret = AuthenticationResponse::newPass( $user->getName() );
691
			$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
692
			$session->remove( 'AuthManager::authnState' );
693
			$this->removeAuthenticationSessionData( null );
694
			\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
695
			return $ret;
696
		} catch ( \Exception $ex ) {
697
			$session->remove( 'AuthManager::authnState' );
698
			throw $ex;
699
		}
700
	}
701
702
	/**
703
	 * Whether security-sensitive operations should proceed.
704
	 *
705
	 * A "security-sensitive operation" is something like a password or email
706
	 * change, that would normally have a "reenter your password to confirm"
707
	 * box if we only supported password-based authentication.
708
	 *
709
	 * @param string $operation Operation being checked. This should be a
710
	 *  message-key-like string such as 'change-password' or 'change-email'.
711
	 * @return string One of the SEC_* constants.
712
	 */
713
	public function securitySensitiveOperationStatus( $operation ) {
714
		$status = self::SEC_OK;
715
716
		$this->logger->debug( __METHOD__ . ": Checking $operation" );
717
718
		$session = $this->request->getSession();
719
		$aId = $session->getUser()->getId();
720
		if ( $aId === 0 ) {
721
			// User isn't authenticated. DWIM?
722
			$status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
723
			$this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
724
			return $status;
725
		}
726
727
		if ( $session->canSetUser() ) {
728
			$id = $session->get( 'AuthManager:lastAuthId' );
729
			$last = $session->get( 'AuthManager:lastAuthTimestamp' );
730
			if ( $id !== $aId || $last === null ) {
731
				$timeSinceLogin = PHP_INT_MAX; // Forever ago
732
			} else {
733
				$timeSinceLogin = max( 0, time() - $last );
734
			}
735
736
			$thresholds = $this->config->get( 'ReauthenticateTime' );
737
			if ( isset( $thresholds[$operation] ) ) {
738
				$threshold = $thresholds[$operation];
739
			} elseif ( isset( $thresholds['default'] ) ) {
740
				$threshold = $thresholds['default'];
741
			} else {
742
				throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
743
			}
744
745
			if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
746
				$status = self::SEC_REAUTH;
747
			}
748
		} else {
749
			$timeSinceLogin = -1;
750
751
			$pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
752
			if ( isset( $pass[$operation] ) ) {
753
				$status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
754
			} elseif ( isset( $pass['default'] ) ) {
755
				$status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
756
			} else {
757
				throw new \UnexpectedValueException(
758
					'$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
759
				);
760
			}
761
		}
762
763
		\Hooks::run( 'SecuritySensitiveOperationStatus', [
764
			&$status, $operation, $session, $timeSinceLogin
765
		] );
766
767
		// If authentication is not possible, downgrade from "REAUTH" to "FAIL".
768
		if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
769
			$status = self::SEC_FAIL;
770
		}
771
772
		$this->logger->info( __METHOD__ . ": $operation is $status" );
773
774
		return $status;
775
	}
776
777
	/**
778
	 * Determine whether a username can authenticate
779
	 *
780
	 * This is mainly for internal purposes and only takes authentication data into account,
781
	 * not things like blocks that can change without the authentication system being aware.
782
	 *
783
	 * @param string $username MediaWiki username
784
	 * @return bool
785
	 */
786
	public function userCanAuthenticate( $username ) {
787
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
788
			if ( $provider->testUserCanAuthenticate( $username ) ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testUserCanAuthenticate() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
789
				return true;
790
			}
791
		}
792
		return false;
793
	}
794
795
	/**
796
	 * Provide normalized versions of the username for security checks
797
	 *
798
	 * Since different providers can normalize the input in different ways,
799
	 * this returns an array of all the different ways the name might be
800
	 * normalized for authentication.
801
	 *
802
	 * The returned strings should not be revealed to the user, as that might
803
	 * leak private information (e.g. an email address might be normalized to a
804
	 * username).
805
	 *
806
	 * @param string $username
807
	 * @return string[]
808
	 */
809
	public function normalizeUsername( $username ) {
810
		$ret = [];
811
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
812
			$normalized = $provider->providerNormalizeUsername( $username );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method providerNormalizeUsername() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
813
			if ( $normalized !== null ) {
814
				$ret[$normalized] = true;
815
			}
816
		}
817
		return array_keys( $ret );
818
	}
819
820
	/**@}*/
821
822
	/**
823
	 * @name Authentication data changing
824
	 * @{
825
	 */
826
827
	/**
828
	 * Revoke any authentication credentials for a user
829
	 *
830
	 * After this, the user should no longer be able to log in.
831
	 *
832
	 * @param string $username
833
	 */
834
	public function revokeAccessForUser( $username ) {
835
		$this->logger->info( 'Revoking access for {user}', [
836
			'user' => $username,
837
		] );
838
		$this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
839
	}
840
841
	/**
842
	 * Validate a change of authentication data (e.g. passwords)
843
	 * @param AuthenticationRequest $req
844
	 * @param bool $checkData If false, $req hasn't been loaded from the
845
	 *  submission so checks on user-submitted fields should be skipped. $req->username is
846
	 *  considered user-submitted for this purpose, even if it cannot be changed via
847
	 *  $req->loadFromSubmission.
848
	 * @return Status
849
	 */
850
	public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
851
		$any = false;
852
		$providers = $this->getPrimaryAuthenticationProviders() +
853
			$this->getSecondaryAuthenticationProviders();
854
		foreach ( $providers as $provider ) {
855
			$status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method providerAllowsAuthenticationDataChange() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
856
			if ( !$status->isGood() ) {
857
				return Status::wrap( $status );
858
			}
859
			$any = $any || $status->value !== 'ignored';
860
		}
861
		if ( !$any ) {
862
			$status = Status::newGood( 'ignored' );
863
			$status->warning( 'authmanager-change-not-supported' );
864
			return $status;
865
		}
866
		return Status::newGood();
867
	}
868
869
	/**
870
	 * Change authentication data (e.g. passwords)
871
	 *
872
	 * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
873
	 * result in a successful login in the future.
874
	 *
875
	 * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
876
	 * no longer result in a successful login.
877
	 *
878
	 * This method should only be called if allowsAuthenticationDataChange( $req, true )
879
	 * returned success.
880
	 *
881
	 * @param AuthenticationRequest $req
882
	 */
883
	public function changeAuthenticationData( AuthenticationRequest $req ) {
884
		$this->logger->info( 'Changing authentication data for {user} class {what}', [
885
			'user' => is_string( $req->username ) ? $req->username : '<no name>',
886
			'what' => get_class( $req ),
887
		] );
888
889
		$this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
890
891
		// When the main account's authentication data is changed, invalidate
892
		// all BotPasswords too.
893
		\BotPassword::invalidateAllPasswordsForUser( $req->username );
894
	}
895
896
	/**@}*/
897
898
	/**
899
	 * @name Account creation
900
	 * @{
901
	 */
902
903
	/**
904
	 * Determine whether accounts can be created
905
	 * @return bool
906
	 */
907 View Code Duplication
	public function canCreateAccounts() {
908
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
909
			switch ( $provider->accountCreationType() ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method accountCreationType() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
910
				case PrimaryAuthenticationProvider::TYPE_CREATE:
911
				case PrimaryAuthenticationProvider::TYPE_LINK:
912
					return true;
913
			}
914
		}
915
		return false;
916
	}
917
918
	/**
919
	 * Determine whether a particular account can be created
920
	 * @param string $username MediaWiki username
921
	 * @param array $options
922
	 *  - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
923
	 *  - creating: (bool) For internal use only. Never specify this.
924
	 * @return Status
925
	 */
926
	public function canCreateAccount( $username, $options = [] ) {
927
		// Back compat
928
		if ( is_int( $options ) ) {
929
			$options = [ 'flags' => $options ];
930
		}
931
		$options += [
932
			'flags' => User::READ_NORMAL,
933
			'creating' => false,
934
		];
935
		$flags = $options['flags'];
936
937
		if ( !$this->canCreateAccounts() ) {
938
			return Status::newFatal( 'authmanager-create-disabled' );
939
		}
940
941
		if ( $this->userExists( $username, $flags ) ) {
942
			return Status::newFatal( 'userexists' );
943
		}
944
945
		$user = User::newFromName( $username, 'creatable' );
946
		if ( !is_object( $user ) ) {
947
			return Status::newFatal( 'noname' );
948
		} else {
949
			$user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
950
			if ( $user->getId() !== 0 ) {
951
				return Status::newFatal( 'userexists' );
952
			}
953
		}
954
955
		// Denied by providers?
956
		$providers = $this->getPreAuthenticationProviders() +
957
			$this->getPrimaryAuthenticationProviders() +
958
			$this->getSecondaryAuthenticationProviders();
959
		foreach ( $providers as $provider ) {
960
			$status = $provider->testUserForCreation( $user, false, $options );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testUserForCreation() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractPreAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\LegacyHookPreAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider, MediaWiki\Auth\ThrottlePreAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
961
			if ( !$status->isGood() ) {
962
				return Status::wrap( $status );
963
			}
964
		}
965
966
		return Status::newGood();
967
	}
968
969
	/**
970
	 * Basic permissions checks on whether a user can create accounts
971
	 * @param User $creator User doing the account creation
972
	 * @return Status
973
	 */
974
	public function checkAccountCreatePermissions( User $creator ) {
975
		// Wiki is read-only?
976
		if ( wfReadOnly() ) {
977
			return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
978
		}
979
980
		// This is awful, this permission check really shouldn't go through Title.
981
		$permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
982
			->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
983
		if ( $permErrors ) {
984
			$status = Status::newGood();
985
			foreach ( $permErrors as $args ) {
986
				call_user_func_array( [ $status, 'fatal' ], $args );
987
			}
988
			return $status;
989
		}
990
991
		$block = $creator->isBlockedFromCreateAccount();
992
		if ( $block ) {
993
			$errorParams = [
994
				$block->getTarget(),
995
				$block->mReason ?: wfMessage( 'blockednoreason' )->text(),
996
				$block->getByName()
997
			];
998
999
			if ( $block->getType() === \Block::TYPE_RANGE ) {
1000
				$errorMessage = 'cantcreateaccount-range-text';
1001
				$errorParams[] = $this->getRequest()->getIP();
1002
			} else {
1003
				$errorMessage = 'cantcreateaccount-text';
1004
			}
1005
1006
			return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1007
		}
1008
1009
		$ip = $this->getRequest()->getIP();
1010
		if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1011
			return Status::newFatal( 'sorbs_create_account_reason' );
1012
		}
1013
1014
		return Status::newGood();
1015
	}
1016
1017
	/**
1018
	 * Start an account creation flow
1019
	 *
1020
	 * In addition to the AuthenticationRequests returned by
1021
	 * $this->getAuthenticationRequests(), a client might include a
1022
	 * CreateFromLoginAuthenticationRequest from a previous login attempt. If
1023
	 * <code>
1024
	 * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
1025
	 * </code>
1026
	 * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
1027
	 * should be omitted. If the CreateFromLoginAuthenticationRequest has a
1028
	 * username set, that username must be used for all other requests.
1029
	 *
1030
	 * @param User $creator User doing the account creation
1031
	 * @param AuthenticationRequest[] $reqs
1032
	 * @param string $returnToUrl Url that REDIRECT responses should eventually
1033
	 *  return to.
1034
	 * @return AuthenticationResponse
1035
	 */
1036
	public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1037
		$session = $this->request->getSession();
1038
		if ( !$this->canCreateAccounts() ) {
1039
			// Caller should have called canCreateAccounts()
1040
			$session->remove( 'AuthManager::accountCreationState' );
1041
			throw new \LogicException( 'Account creation is not possible' );
1042
		}
1043
1044
		try {
1045
			$username = AuthenticationRequest::getUsernameFromRequests( $reqs );
1046
		} catch ( \UnexpectedValueException $ex ) {
1047
			$username = null;
1048
		}
1049
		if ( $username === null ) {
1050
			$this->logger->debug( __METHOD__ . ': No username provided' );
1051
			return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1052
		}
1053
1054
		// Permissions check
1055
		$status = $this->checkAccountCreatePermissions( $creator );
1056 View Code Duplication
		if ( !$status->isGood() ) {
1057
			$this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1058
				'user' => $username,
1059
				'creator' => $creator->getName(),
1060
				'reason' => $status->getWikiText( null, null, 'en' )
1061
			] );
1062
			return AuthenticationResponse::newFail( $status->getMessage() );
1063
		}
1064
1065
		$status = $this->canCreateAccount(
1066
			$username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1067
		);
1068 View Code Duplication
		if ( !$status->isGood() ) {
1069
			$this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1070
				'user' => $username,
1071
				'creator' => $creator->getName(),
1072
				'reason' => $status->getWikiText( null, null, 'en' )
1073
			] );
1074
			return AuthenticationResponse::newFail( $status->getMessage() );
1075
		}
1076
1077
		$user = User::newFromName( $username, 'creatable' );
1078
		foreach ( $reqs as $req ) {
1079
			$req->username = $username;
1080
			$req->returnToUrl = $returnToUrl;
1081
			if ( $req instanceof UserDataAuthenticationRequest ) {
1082
				$status = $req->populateUser( $user );
0 ignored issues
show
Security Bug introduced by
It seems like $user defined by \User::newFromName($username, 'creatable') on line 1077 can also be of type false; however, MediaWiki\Auth\UserDataA...Request::populateUser() does only seem to accept object<User>, 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...
1083 View Code Duplication
				if ( !$status->isGood() ) {
1084
					$status = Status::wrap( $status );
1085
					$session->remove( 'AuthManager::accountCreationState' );
1086
					$this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1087
						'user' => $user->getName(),
1088
						'creator' => $creator->getName(),
1089
						'reason' => $status->getWikiText( null, null, 'en' ),
1090
					] );
1091
					return AuthenticationResponse::newFail( $status->getMessage() );
1092
				}
1093
			}
1094
		}
1095
1096
		$this->removeAuthenticationSessionData( null );
1097
1098
		$state = [
1099
			'username' => $username,
1100
			'userid' => 0,
1101
			'creatorid' => $creator->getId(),
1102
			'creatorname' => $creator->getName(),
1103
			'reqs' => $reqs,
1104
			'returnToUrl' => $returnToUrl,
1105
			'primary' => null,
1106
			'primaryResponse' => null,
1107
			'secondary' => [],
1108
			'continueRequests' => [],
1109
			'maybeLink' => [],
1110
			'ranPreTests' => false,
1111
		];
1112
1113
		// Special case: converting a login to an account creation
1114
		$req = AuthenticationRequest::getRequestByClass(
1115
			$reqs, CreateFromLoginAuthenticationRequest::class
1116
		);
1117 View Code Duplication
		if ( $req ) {
1118
			$state['maybeLink'] = $req->maybeLink;
0 ignored issues
show
Bug introduced by
The property maybeLink does not seem to exist in MediaWiki\Auth\AuthenticationRequest.

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...
1119
1120
			if ( $req->createRequest ) {
1121
				$reqs[] = $req->createRequest;
0 ignored issues
show
Bug introduced by
The property createRequest does not seem to exist in MediaWiki\Auth\AuthenticationRequest.

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...
1122
				$state['reqs'][] = $req->createRequest;
1123
			}
1124
		}
1125
1126
		$session->setSecret( 'AuthManager::accountCreationState', $state );
1127
		$session->persist();
1128
1129
		return $this->continueAccountCreation( $reqs );
1130
	}
1131
1132
	/**
1133
	 * Continue an account creation flow
1134
	 * @param AuthenticationRequest[] $reqs
1135
	 * @return AuthenticationResponse
1136
	 */
1137
	public function continueAccountCreation( array $reqs ) {
1138
		$session = $this->request->getSession();
1139
		try {
1140
			if ( !$this->canCreateAccounts() ) {
1141
				// Caller should have called canCreateAccounts()
1142
				$session->remove( 'AuthManager::accountCreationState' );
1143
				throw new \LogicException( 'Account creation is not possible' );
1144
			}
1145
1146
			$state = $session->getSecret( 'AuthManager::accountCreationState' );
1147
			if ( !is_array( $state ) ) {
1148
				return AuthenticationResponse::newFail(
1149
					wfMessage( 'authmanager-create-not-in-progress' )
1150
				);
1151
			}
1152
			$state['continueRequests'] = [];
1153
1154
			// Step 0: Prepare and validate the input
1155
1156
			$user = User::newFromName( $state['username'], 'creatable' );
1157 View Code Duplication
			if ( !is_object( $user ) ) {
1158
				$session->remove( 'AuthManager::accountCreationState' );
1159
				$this->logger->debug( __METHOD__ . ': Invalid username', [
1160
					'user' => $state['username'],
1161
				] );
1162
				return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1163
			}
1164
1165
			if ( $state['creatorid'] ) {
1166
				$creator = User::newFromId( $state['creatorid'] );
1167
			} else {
1168
				$creator = new User;
1169
				$creator->setName( $state['creatorname'] );
1170
			}
1171
1172
			// Avoid account creation races on double submissions
1173
			$cache = \ObjectCache::getLocalClusterInstance();
1174
			$lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1175 View Code Duplication
			if ( !$lock ) {
1176
				// Don't clear AuthManager::accountCreationState for this code
1177
				// path because the process that won the race owns it.
1178
				$this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1179
					'user' => $user->getName(),
1180
					'creator' => $creator->getName(),
1181
				] );
1182
				return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1183
			}
1184
1185
			// Permissions check
1186
			$status = $this->checkAccountCreatePermissions( $creator );
1187 View Code Duplication
			if ( !$status->isGood() ) {
1188
				$this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1189
					'user' => $user->getName(),
1190
					'creator' => $creator->getName(),
1191
					'reason' => $status->getWikiText( null, null, 'en' )
1192
				] );
1193
				$ret = AuthenticationResponse::newFail( $status->getMessage() );
1194
				$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1195
				$session->remove( 'AuthManager::accountCreationState' );
1196
				return $ret;
1197
			}
1198
1199
			// Load from master for existence check
1200
			$user->load( User::READ_LOCKING );
1201
1202
			if ( $state['userid'] === 0 ) {
1203 View Code Duplication
				if ( $user->getId() != 0 ) {
1204
					$this->logger->debug( __METHOD__ . ': User exists locally', [
1205
						'user' => $user->getName(),
1206
						'creator' => $creator->getName(),
1207
					] );
1208
					$ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1209
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1210
					$session->remove( 'AuthManager::accountCreationState' );
1211
					return $ret;
1212
				}
1213
			} else {
1214
				if ( $user->getId() == 0 ) {
1215
					$this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1216
						'user' => $user->getName(),
1217
						'creator' => $creator->getName(),
1218
						'expected_id' => $state['userid'],
1219
					] );
1220
					throw new \UnexpectedValueException(
1221
						"User \"{$state['username']}\" should exist now, but doesn't!"
1222
					);
1223
				}
1224
				if ( $user->getId() != $state['userid'] ) {
1225
					$this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1226
						'user' => $user->getName(),
1227
						'creator' => $creator->getName(),
1228
						'expected_id' => $state['userid'],
1229
						'actual_id' => $user->getId(),
1230
					] );
1231
					throw new \UnexpectedValueException(
1232
						"User \"{$state['username']}\" exists, but " .
1233
							"ID {$user->getId()} != {$state['userid']}!"
1234
					);
1235
				}
1236
			}
1237
			foreach ( $state['reqs'] as $req ) {
1238
				if ( $req instanceof UserDataAuthenticationRequest ) {
1239
					$status = $req->populateUser( $user );
1240 View Code Duplication
					if ( !$status->isGood() ) {
1241
						// This should never happen...
1242
						$status = Status::wrap( $status );
1243
						$this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1244
							'user' => $user->getName(),
1245
							'creator' => $creator->getName(),
1246
							'reason' => $status->getWikiText( null, null, 'en' ),
1247
						] );
1248
						$ret = AuthenticationResponse::newFail( $status->getMessage() );
1249
						$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1250
						$session->remove( 'AuthManager::accountCreationState' );
1251
						return $ret;
1252
					}
1253
				}
1254
			}
1255
1256
			foreach ( $reqs as $req ) {
1257
				$req->returnToUrl = $state['returnToUrl'];
1258
				$req->username = $state['username'];
1259
			}
1260
1261
			// Run pre-creation tests, if we haven't already
1262
			if ( !$state['ranPreTests'] ) {
1263
				$providers = $this->getPreAuthenticationProviders() +
1264
					$this->getPrimaryAuthenticationProviders() +
1265
					$this->getSecondaryAuthenticationProviders();
1266
				foreach ( $providers as $id => $provider ) {
1267
					$status = $provider->testForAccountCreation( $user, $creator, $reqs );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testForAccountCreation() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractPreAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\LegacyHookPreAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider, MediaWiki\Auth\ThrottlePreAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1268
					if ( !$status->isGood() ) {
1269
						$this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1270
							'user' => $user->getName(),
1271
							'creator' => $creator->getName(),
1272
						] );
1273
						$ret = AuthenticationResponse::newFail(
1274
							Status::wrap( $status )->getMessage()
1275
						);
1276
						$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1277
						$session->remove( 'AuthManager::accountCreationState' );
1278
						return $ret;
1279
					}
1280
				}
1281
1282
				$state['ranPreTests'] = true;
1283
			}
1284
1285
			// Step 1: Choose a primary authentication provider and call it until it succeeds.
1286
1287
			if ( $state['primary'] === null ) {
1288
				// We haven't picked a PrimaryAuthenticationProvider yet
1289
				foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1290
					if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method accountCreationType() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1291
						continue;
1292
					}
1293
					$res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method beginPrimaryAccountCreation() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1294
					switch ( $res->status ) {
1295
						case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1296
							$this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1297
								'user' => $user->getName(),
1298
								'creator' => $creator->getName(),
1299
							] );
1300
							$state['primary'] = $id;
1301
							$state['primaryResponse'] = $res;
1302
							break 2;
1303 View Code Duplication
						case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1304
							$this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1305
								'user' => $user->getName(),
1306
								'creator' => $creator->getName(),
1307
							] );
1308
							$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1309
							$session->remove( 'AuthManager::accountCreationState' );
1310
							return $res;
1311
						case AuthenticationResponse::ABSTAIN;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1312
							// Continue loop
1313
							break;
1314
						case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1315
						case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1316
							$this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1317
								'user' => $user->getName(),
1318
								'creator' => $creator->getName(),
1319
							] );
1320
							$this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1321
							$state['primary'] = $id;
1322
							$state['continueRequests'] = $res->neededRequests;
1323
							$session->setSecret( 'AuthManager::accountCreationState', $state );
1324
							return $res;
1325
1326
							// @codeCoverageIgnoreStart
1327
						default:
1328
							throw new \DomainException(
1329
								get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1330
							);
1331
							// @codeCoverageIgnoreEnd
1332
					}
1333
				}
1334 View Code Duplication
				if ( $state['primary'] === null ) {
1335
					$this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1336
						'user' => $user->getName(),
1337
						'creator' => $creator->getName(),
1338
					] );
1339
					$ret = AuthenticationResponse::newFail(
1340
						wfMessage( 'authmanager-create-no-primary' )
1341
					);
1342
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1343
					$session->remove( 'AuthManager::accountCreationState' );
1344
					return $ret;
1345
				}
1346
			} elseif ( $state['primaryResponse'] === null ) {
1347
				$provider = $this->getAuthenticationProvider( $state['primary'] );
1348
				if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1349
					// Configuration changed? Force them to start over.
1350
					// @codeCoverageIgnoreStart
1351
					$ret = AuthenticationResponse::newFail(
1352
						wfMessage( 'authmanager-create-not-in-progress' )
1353
					);
1354
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1355
					$session->remove( 'AuthManager::accountCreationState' );
1356
					return $ret;
1357
					// @codeCoverageIgnoreEnd
1358
				}
1359
				$id = $provider->getUniqueId();
1360
				$res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1361
				switch ( $res->status ) {
1362
					case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1363
						$this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1364
							'user' => $user->getName(),
1365
							'creator' => $creator->getName(),
1366
						] );
1367
						$state['primaryResponse'] = $res;
1368
						break;
1369 View Code Duplication
					case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1370
						$this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1371
							'user' => $user->getName(),
1372
							'creator' => $creator->getName(),
1373
						] );
1374
						$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1375
						$session->remove( 'AuthManager::accountCreationState' );
1376
						return $res;
1377
					case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1378
					case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1379
						$this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1380
							'user' => $user->getName(),
1381
							'creator' => $creator->getName(),
1382
						] );
1383
						$this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1384
						$state['continueRequests'] = $res->neededRequests;
1385
						$session->setSecret( 'AuthManager::accountCreationState', $state );
1386
						return $res;
1387
					default:
1388
						throw new \DomainException(
1389
							get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1390
						);
1391
				}
1392
			}
1393
1394
			// Step 2: Primary authentication succeeded, create the User object
1395
			// and add the user locally.
1396
1397
			if ( $state['userid'] === 0 ) {
1398
				$this->logger->info( 'Creating user {user} during account creation', [
1399
					'user' => $user->getName(),
1400
					'creator' => $creator->getName(),
1401
				] );
1402
				$status = $user->addToDatabase();
1403
				if ( !$status->isOK() ) {
1404
					// @codeCoverageIgnoreStart
1405
					$ret = AuthenticationResponse::newFail( $status->getMessage() );
1406
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1407
					$session->remove( 'AuthManager::accountCreationState' );
1408
					return $ret;
1409
					// @codeCoverageIgnoreEnd
1410
				}
1411
				$this->setDefaultUserOptions( $user, $creator->isAnon() );
1412
				\Hooks::run( 'LocalUserCreated', [ $user, false ] );
1413
				$user->saveSettings();
1414
				$state['userid'] = $user->getId();
1415
1416
				// Update user count
1417
				\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1418
1419
				// Watch user's userpage and talk page
1420
				$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1421
1422
				// Inform the provider
1423
				$logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
0 ignored issues
show
Bug introduced by
The variable $provider does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1424
1425
				// Log the creation
1426
				if ( $this->config->get( 'NewUserLog' ) ) {
1427
					$isAnon = $creator->isAnon();
1428
					$logEntry = new \ManualLogEntry(
1429
						'newusers',
1430
						$logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1431
					);
1432
					$logEntry->setPerformer( $isAnon ? $user : $creator );
1433
					$logEntry->setTarget( $user->getUserPage() );
1434
					/** @var CreationReasonAuthenticationRequest $req */
1435
					$req = AuthenticationRequest::getRequestByClass(
1436
						$state['reqs'], CreationReasonAuthenticationRequest::class
1437
					);
1438
					$logEntry->setComment( $req ? $req->reason : '' );
1439
					$logEntry->setParameters( [
1440
						'4::userid' => $user->getId(),
1441
					] );
1442
					$logid = $logEntry->insert();
1443
					$logEntry->publish( $logid );
1444
				}
1445
			}
1446
1447
			// Step 3: Iterate over all the secondary authentication providers.
1448
1449
			$beginReqs = $state['reqs'];
1450
1451
			foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1452 View Code Duplication
				if ( !isset( $state['secondary'][$id] ) ) {
1453
					// This provider isn't started yet, so we pass it the set
1454
					// of reqs from beginAuthentication instead of whatever
1455
					// might have been used by a previous provider in line.
1456
					$func = 'beginSecondaryAccountCreation';
1457
					$res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method beginSecondaryAccountCreation() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1458
				} elseif ( !$state['secondary'][$id] ) {
1459
					$func = 'continueSecondaryAccountCreation';
1460
					$res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method continueSecondaryAccountCreation() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1461
				} else {
1462
					continue;
1463
				}
1464
				switch ( $res->status ) {
1465
					case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1466
						$this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1467
							'user' => $user->getName(),
1468
							'creator' => $creator->getName(),
1469
						] );
1470
						// fall through
1471
					case AuthenticationResponse::ABSTAIN;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1472
						$state['secondary'][$id] = true;
1473
						break;
1474
					case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1475
					case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1476
						$this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1477
							'user' => $user->getName(),
1478
							'creator' => $creator->getName(),
1479
						] );
1480
						$this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1481
						$state['secondary'][$id] = false;
1482
						$state['continueRequests'] = $res->neededRequests;
1483
						$session->setSecret( 'AuthManager::accountCreationState', $state );
1484
						return $res;
1485
					case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1486
						throw new \DomainException(
1487
							get_class( $provider ) . "::{$func}() returned $res->status." .
1488
							' Secondary providers are not allowed to fail account creation, that' .
1489
							' should have been done via testForAccountCreation().'
1490
						);
1491
							// @codeCoverageIgnoreStart
1492
					default:
1493
						throw new \DomainException(
1494
							get_class( $provider ) . "::{$func}() returned $res->status"
1495
						);
1496
							// @codeCoverageIgnoreEnd
1497
				}
1498
			}
1499
1500
			$id = $user->getId();
1501
			$name = $user->getName();
1502
			$req = new CreatedAccountAuthenticationRequest( $id, $name );
1503
			$ret = AuthenticationResponse::newPass( $name );
1504
			$ret->loginRequest = $req;
1505
			$this->createdAccountAuthenticationRequests[] = $req;
1506
1507
			$this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1508
				'user' => $user->getName(),
1509
				'creator' => $creator->getName(),
1510
			] );
1511
1512
			$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1513
			$session->remove( 'AuthManager::accountCreationState' );
1514
			$this->removeAuthenticationSessionData( null );
1515
			return $ret;
1516
		} catch ( \Exception $ex ) {
1517
			$session->remove( 'AuthManager::accountCreationState' );
1518
			throw $ex;
1519
		}
1520
	}
1521
1522
	/**
1523
	 * Auto-create an account, and log into that account
1524
	 *
1525
	 * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
1526
	 * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
1527
	 * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
1528
	 * the username of a non-existing user from provideSessionInfo(). Calling this method
1529
	 * explicitly (e.g. from a maintenance script) is also fine.
1530
	 *
1531
	 * @param User $user User to auto-create
1532
	 * @param string $source What caused the auto-creation? This must be the ID
1533
	 *  of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
1534
	 * @param bool $login Whether to also log the user in
1535
	 * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1536
	 */
1537
	public function autoCreateUser( User $user, $source, $login = true ) {
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...
1538
		if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1539
			!$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1540
		) {
1541
			throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1542
		}
1543
1544
		$username = $user->getName();
1545
1546
		// Try the local user from the replica DB
1547
		$localId = User::idFromName( $username );
1548
		$flags = User::READ_NORMAL;
1549
1550
		// Fetch the user ID from the master, so that we don't try to create the user
1551
		// when they already exist, due to replication lag
1552
		// @codeCoverageIgnoreStart
1553
		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...
1554
			$localId = User::idFromName( $username, User::READ_LATEST );
1555
			$flags = User::READ_LATEST;
1556
		}
1557
		// @codeCoverageIgnoreEnd
1558
1559
		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...
1560
			$this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1561
				'username' => $username,
1562
			] );
1563
			$user->setId( $localId );
1564
			$user->loadFromId( $flags );
1565
			if ( $login ) {
1566
				$this->setSessionDataForUser( $user );
1567
			}
1568
			$status = Status::newGood();
1569
			$status->warning( 'userexists' );
1570
			return $status;
1571
		}
1572
1573
		// Wiki is read-only?
1574
		if ( wfReadOnly() ) {
1575
			$this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1576
				'username' => $username,
1577
				'reason' => wfReadOnlyReason(),
1578
			] );
1579
			$user->setId( 0 );
1580
			$user->loadFromId();
1581
			return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
1582
		}
1583
1584
		// Check the session, if we tried to create this user already there's
1585
		// no point in retrying.
1586
		$session = $this->request->getSession();
1587
		if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1588
			$this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1589
				'username' => $username,
1590
				'sessionid' => $session->getId(),
1591
			] );
1592
			$user->setId( 0 );
1593
			$user->loadFromId();
1594
			$reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1595
			if ( $reason instanceof StatusValue ) {
1596
				return Status::wrap( $reason );
1597
			} else {
1598
				return Status::newFatal( $reason );
1599
			}
1600
		}
1601
1602
		// Is the username creatable?
1603 View Code Duplication
		if ( !User::isCreatableName( $username ) ) {
1604
			$this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1605
				'username' => $username,
1606
			] );
1607
			$session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1608
			$user->setId( 0 );
1609
			$user->loadFromId();
1610
			return Status::newFatal( 'noname' );
1611
		}
1612
1613
		// Is the IP user able to create accounts?
1614
		$anon = new User;
1615
		if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1616
			$this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1617
				'username' => $username,
1618
				'ip' => $anon->getName(),
1619
			] );
1620
			$session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1621
			$session->persist();
1622
			$user->setId( 0 );
1623
			$user->loadFromId();
1624
			return Status::newFatal( 'authmanager-autocreate-noperm' );
1625
		}
1626
1627
		// Avoid account creation races on double submissions
1628
		$cache = \ObjectCache::getLocalClusterInstance();
1629
		$lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1630 View Code Duplication
		if ( !$lock ) {
1631
			$this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1632
				'user' => $username,
1633
			] );
1634
			$user->setId( 0 );
1635
			$user->loadFromId();
1636
			return Status::newFatal( 'usernameinprogress' );
1637
		}
1638
1639
		// Denied by providers?
1640
		$options = [
1641
			'flags' => User::READ_LATEST,
1642
			'creating' => true,
1643
		];
1644
		$providers = $this->getPreAuthenticationProviders() +
1645
			$this->getPrimaryAuthenticationProviders() +
1646
			$this->getSecondaryAuthenticationProviders();
1647
		foreach ( $providers as $provider ) {
1648
			$status = $provider->testUserForCreation( $user, $source, $options );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testUserForCreation() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractPreAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\LegacyHookPreAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider, MediaWiki\Auth\ThrottlePreAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1649 View Code Duplication
			if ( !$status->isGood() ) {
1650
				$ret = Status::wrap( $status );
1651
				$this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1652
					'username' => $username,
1653
					'reason' => $ret->getWikiText( null, null, 'en' ),
1654
				] );
1655
				$session->set( 'AuthManager::AutoCreateBlacklist', $status );
1656
				$user->setId( 0 );
1657
				$user->loadFromId();
1658
				return $ret;
1659
			}
1660
		}
1661
1662
		$backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1663 View Code Duplication
		if ( $cache->get( $backoffKey ) ) {
1664
			$this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1665
				'username' => $username,
1666
			] );
1667
			$user->setId( 0 );
1668
			$user->loadFromId();
1669
			return Status::newFatal( 'authmanager-autocreate-exception' );
1670
		}
1671
1672
		// Checks passed, create the user...
1673
		$from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
1674
		$this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1675
			'username' => $username,
1676
			'from' => $from,
1677
		] );
1678
1679
		// Ignore warnings about master connections/writes...hard to avoid here
1680
		$trxProfiler = \Profiler::instance()->getTransactionProfiler();
1681
		$old = $trxProfiler->setSilenced( true );
1682
		try {
1683
			$status = $user->addToDatabase();
1684
			if ( !$status->isOK() ) {
1685
				// Double-check for a race condition (T70012). We make use of the fact that when
1686
				// addToDatabase fails due to the user already existing, the user object gets loaded.
1687
				if ( $user->getId() ) {
1688
					$this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1689
						'username' => $username,
1690
					] );
1691
					if ( $login ) {
1692
						$this->setSessionDataForUser( $user );
1693
					}
1694
					$status = Status::newGood();
1695
					$status->warning( 'userexists' );
1696
				} else {
1697
					$this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1698
						'username' => $username,
1699
						'msg' => $status->getWikiText( null, null, 'en' )
1700
					] );
1701
					$user->setId( 0 );
1702
					$user->loadFromId();
1703
				}
1704
				return $status;
1705
			}
1706
		} catch ( \Exception $ex ) {
1707
			$trxProfiler->setSilenced( $old );
1708
			$this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1709
				'username' => $username,
1710
				'exception' => $ex,
1711
			] );
1712
			// Do not keep throwing errors for a while
1713
			$cache->set( $backoffKey, 1, 600 );
1714
			// Bubble up error; which should normally trigger DB rollbacks
1715
			throw $ex;
1716
		}
1717
1718
		$this->setDefaultUserOptions( $user, false );
1719
1720
		// Inform the providers
1721
		$this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1722
1723
		\Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1724
		\Hooks::run( 'LocalUserCreated', [ $user, true ] );
1725
		$user->saveSettings();
1726
1727
		// Update user count
1728
		\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1729
		// Watch user's userpage and talk page
1730
		\DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1731
			$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1732
		} );
1733
1734
		// Log the creation
1735
		if ( $this->config->get( 'NewUserLog' ) ) {
1736
			$logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1737
			$logEntry->setPerformer( $user );
1738
			$logEntry->setTarget( $user->getUserPage() );
1739
			$logEntry->setComment( '' );
1740
			$logEntry->setParameters( [
1741
				'4::userid' => $user->getId(),
1742
			] );
1743
			$logEntry->insert();
1744
		}
1745
1746
		$trxProfiler->setSilenced( $old );
1747
1748
		if ( $login ) {
1749
			$this->setSessionDataForUser( $user );
1750
		}
1751
1752
		return Status::newGood();
1753
	}
1754
1755
	/**@}*/
1756
1757
	/**
1758
	 * @name Account linking
1759
	 * @{
1760
	 */
1761
1762
	/**
1763
	 * Determine whether accounts can be linked
1764
	 * @return bool
1765
	 */
1766 View Code Duplication
	public function canLinkAccounts() {
1767
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1768
			if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method accountCreationType() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1769
				return true;
1770
			}
1771
		}
1772
		return false;
1773
	}
1774
1775
	/**
1776
	 * Start an account linking flow
1777
	 *
1778
	 * @param User $user User being linked
1779
	 * @param AuthenticationRequest[] $reqs
1780
	 * @param string $returnToUrl Url that REDIRECT responses should eventually
1781
	 *  return to.
1782
	 * @return AuthenticationResponse
1783
	 */
1784
	public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1785
		$session = $this->request->getSession();
1786
		$session->remove( 'AuthManager::accountLinkState' );
1787
1788
		if ( !$this->canLinkAccounts() ) {
1789
			// Caller should have called canLinkAccounts()
1790
			throw new \LogicException( 'Account linking is not possible' );
1791
		}
1792
1793
		if ( $user->getId() === 0 ) {
1794
			if ( !User::isUsableName( $user->getName() ) ) {
1795
				$msg = wfMessage( 'noname' );
1796
			} else {
1797
				$msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1798
			}
1799
			return AuthenticationResponse::newFail( $msg );
1800
		}
1801
		foreach ( $reqs as $req ) {
1802
			$req->username = $user->getName();
1803
			$req->returnToUrl = $returnToUrl;
1804
		}
1805
1806
		$this->removeAuthenticationSessionData( null );
1807
1808
		$providers = $this->getPreAuthenticationProviders();
1809
		foreach ( $providers as $id => $provider ) {
1810
			$status = $provider->testForAccountLink( $user );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testForAccountLink() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractPreAuthenticationProvider, MediaWiki\Auth\LegacyHookPreAuthenticationProvider, MediaWiki\Auth\ThrottlePreAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1811
			if ( !$status->isGood() ) {
1812
				$this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1813
					'user' => $user->getName(),
1814
				] );
1815
				$ret = AuthenticationResponse::newFail(
1816
					Status::wrap( $status )->getMessage()
1817
				);
1818
				$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1819
				return $ret;
1820
			}
1821
		}
1822
1823
		$state = [
1824
			'username' => $user->getName(),
1825
			'userid' => $user->getId(),
1826
			'returnToUrl' => $returnToUrl,
1827
			'primary' => null,
1828
			'continueRequests' => [],
1829
		];
1830
1831
		$providers = $this->getPrimaryAuthenticationProviders();
1832
		foreach ( $providers as $id => $provider ) {
1833
			if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method accountCreationType() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1834
				continue;
1835
			}
1836
1837
			$res = $provider->beginPrimaryAccountLink( $user, $reqs );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method beginPrimaryAccountLink() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1838 View Code Duplication
			switch ( $res->status ) {
1839
				case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1840
					$this->logger->info( "Account linked to {user} by $id", [
1841
						'user' => $user->getName(),
1842
					] );
1843
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1844
					return $res;
1845
1846
				case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1847
					$this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1848
						'user' => $user->getName(),
1849
					] );
1850
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1851
					return $res;
1852
1853
				case AuthenticationResponse::ABSTAIN;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1854
					// Continue loop
1855
					break;
1856
1857
				case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1858
				case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1859
					$this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1860
						'user' => $user->getName(),
1861
					] );
1862
					$this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1863
					$state['primary'] = $id;
1864
					$state['continueRequests'] = $res->neededRequests;
1865
					$session->setSecret( 'AuthManager::accountLinkState', $state );
1866
					$session->persist();
1867
					return $res;
1868
1869
					// @codeCoverageIgnoreStart
1870
				default:
1871
					throw new \DomainException(
1872
						get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1873
					);
1874
					// @codeCoverageIgnoreEnd
1875
			}
1876
		}
1877
1878
		$this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1879
			'user' => $user->getName(),
1880
		] );
1881
		$ret = AuthenticationResponse::newFail(
1882
			wfMessage( 'authmanager-link-no-primary' )
1883
		);
1884
		$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1885
		return $ret;
1886
	}
1887
1888
	/**
1889
	 * Continue an account linking flow
1890
	 * @param AuthenticationRequest[] $reqs
1891
	 * @return AuthenticationResponse
1892
	 */
1893
	public function continueAccountLink( array $reqs ) {
1894
		$session = $this->request->getSession();
1895
		try {
1896
			if ( !$this->canLinkAccounts() ) {
1897
				// Caller should have called canLinkAccounts()
1898
				$session->remove( 'AuthManager::accountLinkState' );
1899
				throw new \LogicException( 'Account linking is not possible' );
1900
			}
1901
1902
			$state = $session->getSecret( 'AuthManager::accountLinkState' );
1903
			if ( !is_array( $state ) ) {
1904
				return AuthenticationResponse::newFail(
1905
					wfMessage( 'authmanager-link-not-in-progress' )
1906
				);
1907
			}
1908
			$state['continueRequests'] = [];
1909
1910
			// Step 0: Prepare and validate the input
1911
1912
			$user = User::newFromName( $state['username'], 'usable' );
1913
			if ( !is_object( $user ) ) {
1914
				$session->remove( 'AuthManager::accountLinkState' );
1915
				return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1916
			}
1917
			if ( $user->getId() != $state['userid'] ) {
1918
				throw new \UnexpectedValueException(
1919
					"User \"{$state['username']}\" is valid, but " .
1920
						"ID {$user->getId()} != {$state['userid']}!"
1921
				);
1922
			}
1923
1924
			foreach ( $reqs as $req ) {
1925
				$req->username = $state['username'];
1926
				$req->returnToUrl = $state['returnToUrl'];
1927
			}
1928
1929
			// Step 1: Call the primary again until it succeeds
1930
1931
			$provider = $this->getAuthenticationProvider( $state['primary'] );
1932
			if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1933
				// Configuration changed? Force them to start over.
1934
				// @codeCoverageIgnoreStart
1935
				$ret = AuthenticationResponse::newFail(
1936
					wfMessage( 'authmanager-link-not-in-progress' )
1937
				);
1938
				$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1939
				$session->remove( 'AuthManager::accountLinkState' );
1940
				return $ret;
1941
				// @codeCoverageIgnoreEnd
1942
			}
1943
			$id = $provider->getUniqueId();
1944
			$res = $provider->continuePrimaryAccountLink( $user, $reqs );
1945 View Code Duplication
			switch ( $res->status ) {
1946
				case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1947
					$this->logger->info( "Account linked to {user} by $id", [
1948
						'user' => $user->getName(),
1949
					] );
1950
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1951
					$session->remove( 'AuthManager::accountLinkState' );
1952
					return $res;
1953
				case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1954
					$this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1955
						'user' => $user->getName(),
1956
					] );
1957
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1958
					$session->remove( 'AuthManager::accountLinkState' );
1959
					return $res;
1960
				case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1961
				case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1962
					$this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1963
						'user' => $user->getName(),
1964
					] );
1965
					$this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1966
					$state['continueRequests'] = $res->neededRequests;
1967
					$session->setSecret( 'AuthManager::accountLinkState', $state );
1968
					return $res;
1969
				default:
1970
					throw new \DomainException(
1971
						get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1972
					);
1973
			}
1974
		} catch ( \Exception $ex ) {
1975
			$session->remove( 'AuthManager::accountLinkState' );
1976
			throw $ex;
1977
		}
1978
	}
1979
1980
	/**@}*/
1981
1982
	/**
1983
	 * @name Information methods
1984
	 * @{
1985
	 */
1986
1987
	/**
1988
	 * Return the applicable list of AuthenticationRequests
1989
	 *
1990
	 * Possible values for $action:
1991
	 *  - ACTION_LOGIN: Valid for passing to beginAuthentication
1992
	 *  - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
1993
	 *  - ACTION_CREATE: Valid for passing to beginAccountCreation
1994
	 *  - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
1995
	 *  - ACTION_LINK: Valid for passing to beginAccountLink
1996
	 *  - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
1997
	 *  - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
1998
	 *  - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
1999
	 *  - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
2000
	 *
2001
	 * @param string $action One of the AuthManager::ACTION_* constants
2002
	 * @param User|null $user User being acted on, instead of the current user.
2003
	 * @return AuthenticationRequest[]
2004
	 */
2005
	public function getAuthenticationRequests( $action, User $user = null ) {
2006
		$options = [];
2007
		$providerAction = $action;
2008
2009
		// Figure out which providers to query
2010
		switch ( $action ) {
2011
			case self::ACTION_LOGIN:
2012
			case self::ACTION_CREATE:
2013
				$providers = $this->getPreAuthenticationProviders() +
2014
					$this->getPrimaryAuthenticationProviders() +
2015
					$this->getSecondaryAuthenticationProviders();
2016
				break;
2017
2018 View Code Duplication
			case self::ACTION_LOGIN_CONTINUE:
2019
				$state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2020
				return is_array( $state ) ? $state['continueRequests'] : [];
2021
2022 View Code Duplication
			case self::ACTION_CREATE_CONTINUE:
2023
				$state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2024
				return is_array( $state ) ? $state['continueRequests'] : [];
2025
2026 View Code Duplication
			case self::ACTION_LINK:
2027
				$providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2028
					return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2029
				} );
2030
				break;
2031
2032 View Code Duplication
			case self::ACTION_UNLINK:
2033
				$providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2034
					return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2035
				} );
2036
2037
				// To providers, unlink and remove are identical.
2038
				$providerAction = self::ACTION_REMOVE;
2039
				break;
2040
2041 View Code Duplication
			case self::ACTION_LINK_CONTINUE:
2042
				$state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2043
				return is_array( $state ) ? $state['continueRequests'] : [];
2044
2045
			case self::ACTION_CHANGE:
2046
			case self::ACTION_REMOVE:
2047
				$providers = $this->getPrimaryAuthenticationProviders() +
2048
					$this->getSecondaryAuthenticationProviders();
2049
				break;
2050
2051
			// @codeCoverageIgnoreStart
2052
			default:
2053
				throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2054
		}
2055
		// @codeCoverageIgnoreEnd
2056
2057
		return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2058
	}
2059
2060
	/**
2061
	 * Internal request lookup for self::getAuthenticationRequests
2062
	 *
2063
	 * @param string $providerAction Action to pass to providers
2064
	 * @param array $options Options to pass to providers
2065
	 * @param AuthenticationProvider[] $providers
2066
	 * @param User|null $user
2067
	 * @return AuthenticationRequest[]
2068
	 */
2069
	private function getAuthenticationRequestsInternal(
2070
		$providerAction, array $options, array $providers, User $user = null
2071
	) {
2072
		$user = $user ?: \RequestContext::getMain()->getUser();
2073
		$options['username'] = $user->isAnon() ? null : $user->getName();
2074
2075
		// Query them and merge results
2076
		$reqs = [];
2077
		foreach ( $providers as $provider ) {
2078
			$isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2079
			foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2080
				$id = $req->getUniqueId();
2081
2082
				// If a required request if from a Primary, mark it as "primary-required" instead
2083
				if ( $isPrimary ) {
2084
					if ( $req->required ) {
2085
						$req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2086
					}
2087
				}
2088
2089
				if (
2090
					!isset( $reqs[$id] )
2091
					|| $req->required === AuthenticationRequest::REQUIRED
2092
					|| $reqs[$id] === AuthenticationRequest::OPTIONAL
2093
				) {
2094
					$reqs[$id] = $req;
2095
				}
2096
			}
2097
		}
2098
2099
		// AuthManager has its own req for some actions
2100
		switch ( $providerAction ) {
2101
			case self::ACTION_LOGIN:
2102
				$reqs[] = new RememberMeAuthenticationRequest;
2103
				break;
2104
2105
			case self::ACTION_CREATE:
2106
				$reqs[] = new UsernameAuthenticationRequest;
2107
				$reqs[] = new UserDataAuthenticationRequest;
2108
				if ( $options['username'] !== null ) {
2109
					$reqs[] = new CreationReasonAuthenticationRequest;
2110
					$options['username'] = null; // Don't fill in the username below
2111
				}
2112
				break;
2113
		}
2114
2115
		// Fill in reqs data
2116
		$this->fillRequests( $reqs, $providerAction, $options['username'], true );
2117
2118
		// For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2119
		if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2120
			$reqs = array_filter( $reqs, function ( $req ) {
2121
				return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2122
			} );
2123
		}
2124
2125
		return array_values( $reqs );
2126
	}
2127
2128
	/**
2129
	 * Set values in an array of requests
2130
	 * @param AuthenticationRequest[] &$reqs
2131
	 * @param string $action
2132
	 * @param string|null $username
2133
	 * @param boolean $forceAction
2134
	 */
2135
	private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2136
		foreach ( $reqs as $req ) {
2137
			if ( !$req->action || $forceAction ) {
2138
				$req->action = $action;
2139
			}
2140
			if ( $req->username === null ) {
2141
				$req->username = $username;
2142
			}
2143
		}
2144
	}
2145
2146
	/**
2147
	 * Determine whether a username exists
2148
	 * @param string $username
2149
	 * @param int $flags Bitfield of User:READ_* constants
2150
	 * @return bool
2151
	 */
2152
	public function userExists( $username, $flags = User::READ_NORMAL ) {
2153
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2154
			if ( $provider->testUserExists( $username, $flags ) ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testUserExists() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
2155
				return true;
2156
			}
2157
		}
2158
2159
		return false;
2160
	}
2161
2162
	/**
2163
	 * Determine whether a user property should be allowed to be changed.
2164
	 *
2165
	 * Supported properties are:
2166
	 *  - emailaddress
2167
	 *  - realname
2168
	 *  - nickname
2169
	 *
2170
	 * @param string $property
2171
	 * @return bool
2172
	 */
2173
	public function allowsPropertyChange( $property ) {
2174
		$providers = $this->getPrimaryAuthenticationProviders() +
2175
			$this->getSecondaryAuthenticationProviders();
2176
		foreach ( $providers as $provider ) {
2177
			if ( !$provider->providerAllowsPropertyChange( $property ) ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method providerAllowsPropertyChange() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
2178
				return false;
2179
			}
2180
		}
2181
		return true;
2182
	}
2183
2184
	/**
2185
	 * Get a provider by ID
2186
	 * @note This is public so extensions can check whether their own provider
2187
	 *  is installed and so they can read its configuration if necessary.
2188
	 *  Other uses are not recommended.
2189
	 * @param string $id
2190
	 * @return AuthenticationProvider|null
2191
	 */
2192
	public function getAuthenticationProvider( $id ) {
2193
		// Fast version
2194
		if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2195
			return $this->allAuthenticationProviders[$id];
2196
		}
2197
2198
		// Slow version: instantiate each kind and check
2199
		$providers = $this->getPrimaryAuthenticationProviders();
2200
		if ( isset( $providers[$id] ) ) {
2201
			return $providers[$id];
2202
		}
2203
		$providers = $this->getSecondaryAuthenticationProviders();
2204
		if ( isset( $providers[$id] ) ) {
2205
			return $providers[$id];
2206
		}
2207
		$providers = $this->getPreAuthenticationProviders();
2208
		if ( isset( $providers[$id] ) ) {
2209
			return $providers[$id];
2210
		}
2211
2212
		return null;
2213
	}
2214
2215
	/**@}*/
2216
2217
	/**
2218
	 * @name Internal methods
2219
	 * @{
2220
	 */
2221
2222
	/**
2223
	 * Store authentication in the current session
2224
	 * @protected For use by AuthenticationProviders
2225
	 * @param string $key
2226
	 * @param mixed $data Must be serializable
2227
	 */
2228
	public function setAuthenticationSessionData( $key, $data ) {
2229
		$session = $this->request->getSession();
2230
		$arr = $session->getSecret( 'authData' );
2231
		if ( !is_array( $arr ) ) {
2232
			$arr = [];
2233
		}
2234
		$arr[$key] = $data;
2235
		$session->setSecret( 'authData', $arr );
2236
	}
2237
2238
	/**
2239
	 * Fetch authentication data from the current session
2240
	 * @protected For use by AuthenticationProviders
2241
	 * @param string $key
2242
	 * @param mixed $default
2243
	 * @return mixed
2244
	 */
2245
	public function getAuthenticationSessionData( $key, $default = null ) {
2246
		$arr = $this->request->getSession()->getSecret( 'authData' );
2247
		if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2248
			return $arr[$key];
2249
		} else {
2250
			return $default;
2251
		}
2252
	}
2253
2254
	/**
2255
	 * Remove authentication data
2256
	 * @protected For use by AuthenticationProviders
2257
	 * @param string|null $key If null, all data is removed
2258
	 */
2259
	public function removeAuthenticationSessionData( $key ) {
2260
		$session = $this->request->getSession();
2261
		if ( $key === null ) {
2262
			$session->remove( 'authData' );
2263
		} else {
2264
			$arr = $session->getSecret( 'authData' );
2265
			if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2266
				unset( $arr[$key] );
2267
				$session->setSecret( 'authData', $arr );
2268
			}
2269
		}
2270
	}
2271
2272
	/**
2273
	 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2274
	 * @param string $class
2275
	 * @param array[] $specs
2276
	 * @return AuthenticationProvider[]
2277
	 */
2278
	protected function providerArrayFromSpecs( $class, array $specs ) {
2279
		$i = 0;
2280
		foreach ( $specs as &$spec ) {
2281
			$spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2282
		}
2283
		unset( $spec );
2284
		usort( $specs, function ( $a, $b ) {
2285
			return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2286
				?: $a['sort2'] - $b['sort2'];
2287
		} );
2288
2289
		$ret = [];
2290
		foreach ( $specs as $spec ) {
2291
			$provider = \ObjectFactory::getObjectFromSpec( $spec );
2292
			if ( !$provider instanceof $class ) {
2293
				throw new \RuntimeException(
2294
					"Expected instance of $class, got " . get_class( $provider )
2295
				);
2296
			}
2297
			$provider->setLogger( $this->logger );
2298
			$provider->setManager( $this );
2299
			$provider->setConfig( $this->config );
2300
			$id = $provider->getUniqueId();
2301 View Code Duplication
			if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2302
				throw new \RuntimeException(
2303
					"Duplicate specifications for id $id (classes " .
2304
					get_class( $provider ) . ' and ' .
2305
					get_class( $this->allAuthenticationProviders[$id] ) . ')'
2306
				);
2307
			}
2308
			$this->allAuthenticationProviders[$id] = $provider;
2309
			$ret[$id] = $provider;
2310
		}
2311
		return $ret;
2312
	}
2313
2314
	/**
2315
	 * Get the configuration
2316
	 * @return array
2317
	 */
2318
	private function getConfiguration() {
2319
		return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2320
	}
2321
2322
	/**
2323
	 * Get the list of PreAuthenticationProviders
2324
	 * @return PreAuthenticationProvider[]
2325
	 */
2326
	protected function getPreAuthenticationProviders() {
2327
		if ( $this->preAuthenticationProviders === null ) {
2328
			$conf = $this->getConfiguration();
2329
			$this->preAuthenticationProviders = $this->providerArrayFromSpecs(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->providerArrayFrom...lass, $conf['preauth']) of type array<integer,object<Med...uthenticationProvider>> is incompatible with the declared type array<integer,object<Med...uthenticationProvider>> of property $preAuthenticationProviders.

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...
2330
				PreAuthenticationProvider::class, $conf['preauth']
2331
			);
2332
		}
2333
		return $this->preAuthenticationProviders;
2334
	}
2335
2336
	/**
2337
	 * Get the list of PrimaryAuthenticationProviders
2338
	 * @return PrimaryAuthenticationProvider[]
2339
	 */
2340
	protected function getPrimaryAuthenticationProviders() {
2341
		if ( $this->primaryAuthenticationProviders === null ) {
2342
			$conf = $this->getConfiguration();
2343
			$this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->providerArrayFrom..., $conf['primaryauth']) of type array<integer,object<Med...uthenticationProvider>> is incompatible with the declared type array<integer,object<Med...uthenticationProvider>> of property $primaryAuthenticationProviders.

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...
2344
				PrimaryAuthenticationProvider::class, $conf['primaryauth']
2345
			);
2346
		}
2347
		return $this->primaryAuthenticationProviders;
2348
	}
2349
2350
	/**
2351
	 * Get the list of SecondaryAuthenticationProviders
2352
	 * @return SecondaryAuthenticationProvider[]
2353
	 */
2354
	protected function getSecondaryAuthenticationProviders() {
2355
		if ( $this->secondaryAuthenticationProviders === null ) {
2356
			$conf = $this->getConfiguration();
2357
			$this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->providerArrayFrom...$conf['secondaryauth']) of type array<integer,object<Med...uthenticationProvider>> is incompatible with the declared type array<integer,object<Med...uthenticationProvider>> of property $secondaryAuthenticationProviders.

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...
2358
				SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2359
			);
2360
		}
2361
		return $this->secondaryAuthenticationProviders;
2362
	}
2363
2364
	/**
2365
	 * Log the user in
2366
	 * @param User $user
2367
	 * @param bool|null $remember
2368
	 */
2369
	private function setSessionDataForUser( $user, $remember = null ) {
2370
		$session = $this->request->getSession();
2371
		$delay = $session->delaySave();
2372
2373
		$session->resetId();
2374
		$session->resetAllTokens();
2375
		if ( $session->canSetUser() ) {
2376
			$session->setUser( $user );
2377
		}
2378
		if ( $remember !== null ) {
2379
			$session->setRememberUser( $remember );
2380
		}
2381
		$session->set( 'AuthManager:lastAuthId', $user->getId() );
2382
		$session->set( 'AuthManager:lastAuthTimestamp', time() );
2383
		$session->persist();
2384
2385
		\Wikimedia\ScopedCallback::consume( $delay );
2386
2387
		\Hooks::run( 'UserLoggedIn', [ $user ] );
2388
	}
2389
2390
	/**
2391
	 * @param User $user
2392
	 * @param bool $useContextLang Use 'uselang' to set the user's language
2393
	 */
2394
	private function setDefaultUserOptions( User $user, $useContextLang ) {
2395
		global $wgContLang;
2396
2397
		$user->setToken();
2398
2399
		$lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
2400
		$user->setOption( 'language', $lang->getPreferredVariant() );
2401
2402
		if ( $wgContLang->hasVariants() ) {
2403
			$user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2404
		}
2405
	}
2406
2407
	/**
2408
	 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2409
	 * @param string $method
2410
	 * @param array $args
2411
	 */
2412
	private function callMethodOnProviders( $which, $method, array $args ) {
2413
		$providers = [];
2414
		if ( $which & 1 ) {
2415
			$providers += $this->getPreAuthenticationProviders();
2416
		}
2417
		if ( $which & 2 ) {
2418
			$providers += $this->getPrimaryAuthenticationProviders();
2419
		}
2420
		if ( $which & 4 ) {
2421
			$providers += $this->getSecondaryAuthenticationProviders();
2422
		}
2423
		foreach ( $providers as $provider ) {
2424
			call_user_func_array( [ $provider, $method ], $args );
2425
		}
2426
	}
2427
2428
	/**
2429
	 * Reset the internal caching for unit testing
2430
	 * @protected Unit tests only
2431
	 */
2432
	public static function resetCache() {
2433
		if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2434
			// @codeCoverageIgnoreStart
2435
			throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2436
			// @codeCoverageIgnoreEnd
2437
		}
2438
2439
		self::$instance = null;
2440
	}
2441
2442
	/**@}*/
2443
2444
}
2445
2446
/**
2447
 * For really cool vim folding this needs to be at the end:
2448
 * vim: foldmarker=@{,@} foldmethod=marker
2449
 */
2450