Completed
Branch master (5f0cc1)
by
unknown
26:56
created

AuthManager::getAuthenticationRequests()   C

Complexity

Conditions 13
Paths 13

Size

Total Lines 54
Code Lines 36

Duplication

Lines 22
Ratio 40.74 %

Importance

Changes 0
Metric Value
cc 13
eloc 36
nc 13
nop 2
dl 22
loc 54
rs 6.7593
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * 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
 * @ingroup Auth
41
 * @since 1.27
42
 */
43
class AuthManager implements LoggerAwareInterface {
44
	/** Log in with an existing (not necessarily local) user */
45
	const ACTION_LOGIN = 'login';
46
	/** Continue a login process that was interrupted by the need for user input or communication
47
	 * with an external provider */
48
	const ACTION_LOGIN_CONTINUE = 'login-continue';
49
	/** Create a new user */
50
	const ACTION_CREATE = 'create';
51
	/** Continue a user creation process that was interrupted by the need for user input or
52
	 * communication with an external provider */
53
	const ACTION_CREATE_CONTINUE = 'create-continue';
54
	/** Link an existing user to a third-party account */
55
	const ACTION_LINK = 'link';
56
	/** Continue a user linking process that was interrupted by the need for user input or
57
	 * communication with an external provider */
58
	const ACTION_LINK_CONTINUE = 'link-continue';
59
	/** Change a user's credentials */
60
	const ACTION_CHANGE = 'change';
61
	/** Remove a user's credentials */
62
	const ACTION_REMOVE = 'remove';
63
	/** Like ACTION_REMOVE but for linking providers only */
64
	const ACTION_UNLINK = 'unlink';
65
66
	/** Security-sensitive operations are ok. */
67
	const SEC_OK = 'ok';
68
	/** Security-sensitive operations should re-authenticate. */
69
	const SEC_REAUTH = 'reauth';
70
	/** Security-sensitive should not be performed. */
71
	const SEC_FAIL = 'fail';
72
73
	/** Auto-creation is due to SessionManager */
74
	const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
75
76
	/** @var AuthManager|null */
77
	private static $instance = null;
78
79
	/** @var WebRequest */
80
	private $request;
81
82
	/** @var Config */
83
	private $config;
84
85
	/** @var LoggerInterface */
86
	private $logger;
87
88
	/** @var AuthenticationProvider[] */
89
	private $allAuthenticationProviders = [];
90
91
	/** @var PreAuthenticationProvider[] */
92
	private $preAuthenticationProviders = null;
93
94
	/** @var PrimaryAuthenticationProvider[] */
95
	private $primaryAuthenticationProviders = null;
96
97
	/** @var SecondaryAuthenticationProvider[] */
98
	private $secondaryAuthenticationProviders = null;
99
100
	/** @var CreatedAccountAuthenticationRequest[] */
101
	private $createdAccountAuthenticationRequests = [];
102
103
	/**
104
	 * Get the global AuthManager
105
	 * @return AuthManager
106
	 */
107
	public static function singleton() {
108
		global $wgDisableAuthManager;
109
110
		if ( $wgDisableAuthManager ) {
111
			throw new \BadMethodCallException( '$wgDisableAuthManager is set' );
112
		}
113
114
		if ( self::$instance === null ) {
115
			self::$instance = new self(
116
				\RequestContext::getMain()->getRequest(),
117
				\ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
0 ignored issues
show
Deprecated Code introduced by
The method ConfigFactory::getDefaultInstance() has been deprecated with message: since 1.27, use MediaWikiServices::getConfigFactory() instead.

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

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

Loading history...
118
			);
119
		}
120
		return self::$instance;
121
	}
122
123
	/**
124
	 * @param WebRequest $request
125
	 * @param Config $config
126
	 */
127
	public function __construct( WebRequest $request, Config $config ) {
128
		$this->request = $request;
129
		$this->config = $config;
130
		$this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
131
	}
132
133
	/**
134
	 * @param LoggerInterface $logger
135
	 */
136
	public function setLogger( LoggerInterface $logger ) {
137
		$this->logger = $logger;
138
	}
139
140
	/**
141
	 * @return WebRequest
142
	 */
143
	public function getRequest() {
144
		return $this->request;
145
	}
146
147
	/**
148
	 * Force certain PrimaryAuthenticationProviders
149
	 * @deprecated For backwards compatibility only
150
	 * @param PrimaryAuthenticationProvider[] $providers
151
	 * @param string $why
152
	 */
153
	public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
154
		$this->logger->warning( "Overriding AuthManager primary authn because $why" );
155
156
		if ( $this->primaryAuthenticationProviders !== null ) {
157
			$this->logger->warning(
158
				'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
159
			);
160
161
			$this->allAuthenticationProviders = array_diff_key(
162
				$this->allAuthenticationProviders,
163
				$this->primaryAuthenticationProviders
164
			);
165
			$session = $this->request->getSession();
166
			$session->remove( 'AuthManager::authnState' );
167
			$session->remove( 'AuthManager::accountCreationState' );
168
			$session->remove( 'AuthManager::accountLinkState' );
169
			$this->createdAccountAuthenticationRequests = [];
170
		}
171
172
		$this->primaryAuthenticationProviders = [];
173
		foreach ( $providers as $provider ) {
174
			if ( !$provider instanceof PrimaryAuthenticationProvider ) {
175
				throw new \RuntimeException(
176
					'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
177
						get_class( $provider )
178
				);
179
			}
180
			$provider->setLogger( $this->logger );
181
			$provider->setManager( $this );
182
			$provider->setConfig( $this->config );
183
			$id = $provider->getUniqueId();
184 View Code Duplication
			if ( isset( $this->allAuthenticationProviders[$id] ) ) {
185
				throw new \RuntimeException(
186
					"Duplicate specifications for id $id (classes " .
187
						get_class( $provider ) . ' and ' .
188
						get_class( $this->allAuthenticationProviders[$id] ) . ')'
189
				);
190
			}
191
			$this->allAuthenticationProviders[$id] = $provider;
192
			$this->primaryAuthenticationProviders[$id] = $provider;
193
		}
194
	}
195
196
	/**
197
	 * Call a legacy AuthPlugin method, if necessary
198
	 * @codeCoverageIgnore
199
	 * @deprecated For backwards compatibility only, should be avoided in new code
200
	 * @param string $method AuthPlugin method to call
201
	 * @param array $params Parameters to pass
202
	 * @param mixed $return Return value if AuthPlugin wasn't called
203
	 * @return mixed Return value from the AuthPlugin method, or $return
204
	 */
205
	public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
206
		global $wgAuth;
207
208
		if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
209
			return call_user_func_array( [ $wgAuth, $method ], $params );
210
		} else {
211
			return $return;
212
		}
213
	}
214
215
	/**
216
	 * @name Authentication
217
	 * @{
218
	 */
219
220
	/**
221
	 * Indicate whether user authentication is possible
222
	 *
223
	 * It may not be if the session is provided by something like OAuth
224
	 * for which each individual request includes authentication data.
225
	 *
226
	 * @return bool
227
	 */
228
	public function canAuthenticateNow() {
229
		return $this->request->getSession()->canSetUser();
230
	}
231
232
	/**
233
	 * Start an authentication flow
234
	 *
235
	 * In addition to the AuthenticationRequests returned by
236
	 * $this->getAuthenticationRequests(), a client might include a
237
	 * CreateFromLoginAuthenticationRequest from a previous login attempt to
238
	 * preserve state.
239
	 *
240
	 * Instead of the AuthenticationRequests returned by
241
	 * $this->getAuthenticationRequests(), a client might pass a
242
	 * CreatedAccountAuthenticationRequest from an account creation that just
243
	 * succeeded to log in to the just-created account.
244
	 *
245
	 * @param AuthenticationRequest[] $reqs
246
	 * @param string $returnToUrl Url that REDIRECT responses should eventually
247
	 *  return to.
248
	 * @return AuthenticationResponse See self::continueAuthentication()
249
	 */
250
	public function beginAuthentication( array $reqs, $returnToUrl ) {
251
		$session = $this->request->getSession();
252
		if ( !$session->canSetUser() ) {
253
			// Caller should have called canAuthenticateNow()
254
			$session->remove( 'AuthManager::authnState' );
255
			throw new \LogicException( 'Authentication is not possible now' );
256
		}
257
258
		$guessUserName = null;
259 View Code Duplication
		foreach ( $reqs as $req ) {
260
			$req->returnToUrl = $returnToUrl;
261
			// @codeCoverageIgnoreStart
262
			if ( $req->username !== null && $req->username !== '' ) {
263
				if ( $guessUserName === null ) {
264
					$guessUserName = $req->username;
265
				} elseif ( $guessUserName !== $req->username ) {
266
					$guessUserName = null;
267
					break;
268
				}
269
			}
270
			// @codeCoverageIgnoreEnd
271
		}
272
273
		// Check for special-case login of a just-created account
274
		$req = AuthenticationRequest::getRequestByClass(
275
			$reqs, CreatedAccountAuthenticationRequest::class
276
		);
277
		if ( $req ) {
278
			if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
279
				throw new \LogicException(
280
					'CreatedAccountAuthenticationRequests are only valid on ' .
281
						'the same AuthManager that created the account'
282
				);
283
			}
284
285
			$user = User::newFromName( $req->username );
286
			// @codeCoverageIgnoreStart
287
			if ( !$user ) {
288
				throw new \UnexpectedValueException(
289
					"CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
290
				);
291
			} 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...
292
				throw new \UnexpectedValueException(
293
					"ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
294
				);
295
			}
296
			// @codeCoverageIgnoreEnd
297
298
			$this->logger->info( 'Logging in {user} after account creation', [
299
				'user' => $user->getName(),
300
			] );
301
			$ret = AuthenticationResponse::newPass( $user->getName() );
302
			$this->setSessionDataForUser( $user );
303
			$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
304
			$session->remove( 'AuthManager::authnState' );
305
			\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
306
			return $ret;
307
		}
308
309
		$this->removeAuthenticationSessionData( null );
310
311
		foreach ( $this->getPreAuthenticationProviders() as $provider ) {
312
			$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...
313
			if ( !$status->isGood() ) {
314
				$this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
315
				$ret = AuthenticationResponse::newFail(
316
					Status::wrap( $status )->getMessage()
317
				);
318
				$this->callMethodOnProviders( 7, 'postAuthentication',
319
					[ User::newFromName( $guessUserName ) ?: null, $ret ]
320
				);
321
				\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
322
				return $ret;
323
			}
324
		}
325
326
		$state = [
327
			'reqs' => $reqs,
328
			'returnToUrl' => $returnToUrl,
329
			'guessUserName' => $guessUserName,
330
			'primary' => null,
331
			'primaryResponse' => null,
332
			'secondary' => [],
333
			'maybeLink' => [],
334
			'continueRequests' => [],
335
		];
336
337
		// Preserve state from a previous failed login
338
		$req = AuthenticationRequest::getRequestByClass(
339
			$reqs, CreateFromLoginAuthenticationRequest::class
340
		);
341
		if ( $req ) {
342
			$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...
343
		}
344
345
		$session = $this->request->getSession();
346
		$session->setSecret( 'AuthManager::authnState', $state );
347
		$session->persist();
348
349
		return $this->continueAuthentication( $reqs );
350
	}
351
352
	/**
353
	 * Continue an authentication flow
354
	 *
355
	 * Return values are interpreted as follows:
356
	 * - status FAIL: Authentication failed. If $response->createRequest is
357
	 *   set, that may be passed to self::beginAuthentication() or to
358
	 *   self::beginAccountCreation() to preserve state.
359
	 * - status REDIRECT: The client should be redirected to the contained URL,
360
	 *   new AuthenticationRequests should be made (if any), then
361
	 *   AuthManager::continueAuthentication() should be called.
362
	 * - status UI: The client should be presented with a user interface for
363
	 *   the fields in the specified AuthenticationRequests, then new
364
	 *   AuthenticationRequests should be made, then
365
	 *   AuthManager::continueAuthentication() should be called.
366
	 * - status RESTART: The user logged in successfully with a third-party
367
	 *   service, but the third-party credentials aren't attached to any local
368
	 *   account. This could be treated as a UI or a FAIL.
369
	 * - status PASS: Authentication was successful.
370
	 *
371
	 * @param AuthenticationRequest[] $reqs
372
	 * @return AuthenticationResponse
373
	 */
374
	public function continueAuthentication( array $reqs ) {
375
		$session = $this->request->getSession();
376
		try {
377
			if ( !$session->canSetUser() ) {
378
				// Caller should have called canAuthenticateNow()
379
				// @codeCoverageIgnoreStart
380
				throw new \LogicException( 'Authentication is not possible now' );
381
				// @codeCoverageIgnoreEnd
382
			}
383
384
			$state = $session->getSecret( 'AuthManager::authnState' );
385
			if ( !is_array( $state ) ) {
386
				return AuthenticationResponse::newFail(
387
					wfMessage( 'authmanager-authn-not-in-progress' )
388
				);
389
			}
390
			$state['continueRequests'] = [];
391
392
			$guessUserName = $state['guessUserName'];
393
394
			foreach ( $reqs as $req ) {
395
				$req->returnToUrl = $state['returnToUrl'];
396
			}
397
398
			// Step 1: Choose an primary authentication provider, and call it until it succeeds.
399
400
			if ( $state['primary'] === null ) {
401
				// We haven't picked a PrimaryAuthenticationProvider yet
402
				// @codeCoverageIgnoreStart
403
				$guessUserName = null;
404 View Code Duplication
				foreach ( $reqs as $req ) {
405
					if ( $req->username !== null && $req->username !== '' ) {
406
						if ( $guessUserName === null ) {
407
							$guessUserName = $req->username;
408
						} elseif ( $guessUserName !== $req->username ) {
409
							$guessUserName = null;
410
							break;
411
						}
412
					}
413
				}
414
				$state['guessUserName'] = $guessUserName;
415
				// @codeCoverageIgnoreEnd
416
				$state['reqs'] = $reqs;
417
418
				foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
419
					$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...
420
					switch ( $res->status ) {
421
						case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
422
							$state['primary'] = $id;
423
							$state['primaryResponse'] = $res;
424
							$this->logger->debug( "Primary login with $id succeeded" );
425
							break 2;
426 View Code Duplication
						case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
427
							$this->logger->debug( "Login failed in primary authentication by $id" );
428
							if ( $res->createRequest || $state['maybeLink'] ) {
429
								$res->createRequest = new CreateFromLoginAuthenticationRequest(
430
									$res->createRequest, $state['maybeLink']
431
								);
432
							}
433
							$this->callMethodOnProviders( 7, 'postAuthentication',
434
								[ User::newFromName( $guessUserName ) ?: null, $res ]
435
							);
436
							$session->remove( 'AuthManager::authnState' );
437
							\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
438
							return $res;
439
						case AuthenticationResponse::ABSTAIN;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
440
							// Continue loop
441
							break;
442
						case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
443 View Code Duplication
						case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
444
							$this->logger->debug( "Primary login with $id returned $res->status" );
445
							$this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
446
							$state['primary'] = $id;
447
							$state['continueRequests'] = $res->neededRequests;
448
							$session->setSecret( 'AuthManager::authnState', $state );
449
							return $res;
450
451
							// @codeCoverageIgnoreStart
452
						default:
453
							throw new \DomainException(
454
								get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
455
							);
456
							// @codeCoverageIgnoreEnd
457
					}
458
				}
459
				if ( $state['primary'] === null ) {
460
					$this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
461
					$ret = AuthenticationResponse::newFail(
462
						wfMessage( 'authmanager-authn-no-primary' )
463
					);
464
					$this->callMethodOnProviders( 7, 'postAuthentication',
465
						[ User::newFromName( $guessUserName ) ?: null, $ret ]
466
					);
467
					$session->remove( 'AuthManager::authnState' );
468
					return $ret;
469
				}
470
			} elseif ( $state['primaryResponse'] === null ) {
471
				$provider = $this->getAuthenticationProvider( $state['primary'] );
472 View Code Duplication
				if ( !$provider instanceof PrimaryAuthenticationProvider ) {
473
					// Configuration changed? Force them to start over.
474
					// @codeCoverageIgnoreStart
475
					$ret = AuthenticationResponse::newFail(
476
						wfMessage( 'authmanager-authn-not-in-progress' )
477
					);
478
					$this->callMethodOnProviders( 7, 'postAuthentication',
479
						[ User::newFromName( $guessUserName ) ?: null, $ret ]
480
					);
481
					$session->remove( 'AuthManager::authnState' );
482
					return $ret;
483
					// @codeCoverageIgnoreEnd
484
				}
485
				$id = $provider->getUniqueId();
486
				$res = $provider->continuePrimaryAuthentication( $reqs );
487
				switch ( $res->status ) {
488
					case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
489
						$state['primaryResponse'] = $res;
490
						$this->logger->debug( "Primary login with $id succeeded" );
491
						break;
492 View Code Duplication
					case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
493
						$this->logger->debug( "Login failed in primary authentication by $id" );
494
						if ( $res->createRequest || $state['maybeLink'] ) {
495
							$res->createRequest = new CreateFromLoginAuthenticationRequest(
496
								$res->createRequest, $state['maybeLink']
497
							);
498
						}
499
						$this->callMethodOnProviders( 7, 'postAuthentication',
500
							[ User::newFromName( $guessUserName ) ?: null, $res ]
501
						);
502
						$session->remove( 'AuthManager::authnState' );
503
						\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
504
						return $res;
505
					case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
506
					case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
507
						$this->logger->debug( "Primary login with $id returned $res->status" );
508
						$this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
509
						$state['continueRequests'] = $res->neededRequests;
510
						$session->setSecret( 'AuthManager::authnState', $state );
511
						return $res;
512
					default:
513
						throw new \DomainException(
514
							get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
515
						);
516
				}
517
			}
518
519
			$res = $state['primaryResponse'];
520
			if ( $res->username === null ) {
521
				$provider = $this->getAuthenticationProvider( $state['primary'] );
522 View Code Duplication
				if ( !$provider instanceof PrimaryAuthenticationProvider ) {
523
					// Configuration changed? Force them to start over.
524
					// @codeCoverageIgnoreStart
525
					$ret = AuthenticationResponse::newFail(
526
						wfMessage( 'authmanager-authn-not-in-progress' )
527
					);
528
					$this->callMethodOnProviders( 7, 'postAuthentication',
529
						[ User::newFromName( $guessUserName ) ?: null, $ret ]
530
					);
531
					$session->remove( 'AuthManager::authnState' );
532
					return $ret;
533
					// @codeCoverageIgnoreEnd
534
				}
535
536
				if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
537
					$res->linkRequest &&
538
					 // don't confuse the user with an incorrect message if linking is disabled
539
					$this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
540
				) {
541
					$state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
542
					$msg = 'authmanager-authn-no-local-user-link';
543
				} else {
544
					$msg = 'authmanager-authn-no-local-user';
545
				}
546
				$this->logger->debug(
547
					"Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
548
				);
549
				$ret = AuthenticationResponse::newRestart( wfMessage( $msg ) );
550
				$ret->neededRequests = $this->getAuthenticationRequestsInternal(
551
					self::ACTION_LOGIN,
552
					[],
553
					$this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
554
				);
555 View Code Duplication
				if ( $res->createRequest || $state['maybeLink'] ) {
556
					$ret->createRequest = new CreateFromLoginAuthenticationRequest(
557
						$res->createRequest, $state['maybeLink']
558
					);
559
					$ret->neededRequests[] = $ret->createRequest;
560
				}
561
				$this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
562
				$session->setSecret( 'AuthManager::authnState', [
563
					'reqs' => [], // Will be filled in later
564
					'primary' => null,
565
					'primaryResponse' => null,
566
					'secondary' => [],
567
					'continueRequests' => $ret->neededRequests,
568
				] + $state );
569
				return $ret;
570
			}
571
572
			// Step 2: Primary authentication succeeded, create the User object
573
			// (and add the user locally if necessary)
574
575
			$user = User::newFromName( $res->username, 'usable' );
576
			if ( !$user ) {
577
				throw new \DomainException(
578
					get_class( $provider ) . " returned an invalid username: {$res->username}"
579
				);
580
			}
581
			if ( $user->getId() === 0 ) {
582
				// User doesn't exist locally. Create it.
583
				$this->logger->info( 'Auto-creating {user} on login', [
584
					'user' => $user->getName(),
585
				] );
586
				$status = $this->autoCreateUser( $user, $state['primary'], false );
587
				if ( !$status->isGood() ) {
588
					$ret = AuthenticationResponse::newFail(
589
						Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
590
					);
591
					$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
592
					$session->remove( 'AuthManager::authnState' );
593
					\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
594
					return $ret;
595
				}
596
			}
597
598
			// Step 3: Iterate over all the secondary authentication providers.
599
600
			$beginReqs = $state['reqs'];
601
602
			foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
603 View Code Duplication
				if ( !isset( $state['secondary'][$id] ) ) {
604
					// This provider isn't started yet, so we pass it the set
605
					// of reqs from beginAuthentication instead of whatever
606
					// might have been used by a previous provider in line.
607
					$func = 'beginSecondaryAuthentication';
608
					$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...
609
				} elseif ( !$state['secondary'][$id] ) {
610
					$func = 'continueSecondaryAuthentication';
611
					$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...
612
				} else {
613
					continue;
614
				}
615
				switch ( $res->status ) {
616
					case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
617
						$this->logger->debug( "Secondary login with $id succeeded" );
618
						// fall through
619
					case AuthenticationResponse::ABSTAIN;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
620
						$state['secondary'][$id] = true;
621
						break;
622 View Code Duplication
					case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
623
						$this->logger->debug( "Login failed in secondary authentication by $id" );
624
						$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
625
						$session->remove( 'AuthManager::authnState' );
626
						\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
627
						return $res;
628
					case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
629 View Code Duplication
					case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
630
						$this->logger->debug( "Secondary login with $id returned " . $res->status );
631
						$this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
632
						$state['secondary'][$id] = false;
633
						$state['continueRequests'] = $res->neededRequests;
634
						$session->setSecret( 'AuthManager::authnState', $state );
635
						return $res;
636
637
						// @codeCoverageIgnoreStart
638
					default:
639
						throw new \DomainException(
640
							get_class( $provider ) . "::{$func}() returned $res->status"
641
						);
642
						// @codeCoverageIgnoreEnd
643
				}
644
			}
645
646
			// Step 4: Authentication complete! Set the user in the session and
647
			// clean up.
648
649
			$this->logger->info( 'Login for {user} succeeded', [
650
				'user' => $user->getName(),
651
			] );
652
			$req = AuthenticationRequest::getRequestByClass(
653
				$beginReqs, RememberMeAuthenticationRequest::class
654
			);
655
			$this->setSessionDataForUser( $user, $req && $req->rememberMe );
0 ignored issues
show
Bug introduced by
The property rememberMe 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...
656
			$ret = AuthenticationResponse::newPass( $user->getName() );
657
			$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
658
			$session->remove( 'AuthManager::authnState' );
659
			$this->removeAuthenticationSessionData( null );
660
			\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
661
			return $ret;
662
		} catch ( \Exception $ex ) {
663
			$session->remove( 'AuthManager::authnState' );
664
			throw $ex;
665
		}
666
	}
667
668
	/**
669
	 * Whether security-sensitive operations should proceed.
670
	 *
671
	 * A "security-sensitive operation" is something like a password or email
672
	 * change, that would normally have a "reenter your password to confirm"
673
	 * box if we only supported password-based authentication.
674
	 *
675
	 * @param string $operation Operation being checked. This should be a
676
	 *  message-key-like string such as 'change-password' or 'change-email'.
677
	 * @return string One of the SEC_* constants.
678
	 */
679
	public function securitySensitiveOperationStatus( $operation ) {
680
		$status = self::SEC_OK;
681
682
		$this->logger->debug( __METHOD__ . ": Checking $operation" );
683
684
		$session = $this->request->getSession();
685
		$aId = $session->getUser()->getId();
686
		if ( $aId === 0 ) {
687
			// User isn't authenticated. DWIM?
688
			$status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
689
			$this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
690
			return $status;
691
		}
692
693
		if ( $session->canSetUser() ) {
694
			$id = $session->get( 'AuthManager:lastAuthId' );
695
			$last = $session->get( 'AuthManager:lastAuthTimestamp' );
696
			if ( $id !== $aId || $last === null ) {
697
				$timeSinceLogin = PHP_INT_MAX; // Forever ago
698
			} else {
699
				$timeSinceLogin = max( 0, time() - $last );
700
			}
701
702
			$thresholds = $this->config->get( 'ReauthenticateTime' );
703
			if ( isset( $thresholds[$operation] ) ) {
704
				$threshold = $thresholds[$operation];
705
			} elseif ( isset( $thresholds['default'] ) ) {
706
				$threshold = $thresholds['default'];
707
			} else {
708
				throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
709
			}
710
711
			if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
712
				$status = self::SEC_REAUTH;
713
			}
714
		} else {
715
			$timeSinceLogin = -1;
716
717
			$pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
718
			if ( isset( $pass[$operation] ) ) {
719
				$status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
720
			} elseif ( isset( $pass['default'] ) ) {
721
				$status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
722
			} else {
723
				throw new \UnexpectedValueException(
724
					'$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
725
				);
726
			}
727
		}
728
729
		\Hooks::run( 'SecuritySensitiveOperationStatus', [
730
			&$status, $operation, $session, $timeSinceLogin
731
		] );
732
733
		// If authentication is not possible, downgrade from "REAUTH" to "FAIL".
734
		if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
735
			$status = self::SEC_FAIL;
736
		}
737
738
		$this->logger->info( __METHOD__ . ": $operation is $status" );
739
740
		return $status;
741
	}
742
743
	/**
744
	 * Determine whether a username can authenticate
745
	 *
746
	 * @param string $username
747
	 * @return bool
748
	 */
749
	public function userCanAuthenticate( $username ) {
750
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
751
			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...
752
				return true;
753
			}
754
		}
755
		return false;
756
	}
757
758
	/**
759
	 * Provide normalized versions of the username for security checks
760
	 *
761
	 * Since different providers can normalize the input in different ways,
762
	 * this returns an array of all the different ways the name might be
763
	 * normalized for authentication.
764
	 *
765
	 * The returned strings should not be revealed to the user, as that might
766
	 * leak private information (e.g. an email address might be normalized to a
767
	 * username).
768
	 *
769
	 * @param string $username
770
	 * @return string[]
771
	 */
772
	public function normalizeUsername( $username ) {
773
		$ret = [];
774
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
775
			$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...
776
			if ( $normalized !== null ) {
777
				$ret[$normalized] = true;
778
			}
779
		}
780
		return array_keys( $ret );
781
	}
782
783
	/**@}*/
784
785
	/**
786
	 * @name Authentication data changing
787
	 * @{
788
	 */
789
790
	/**
791
	 * Revoke any authentication credentials for a user
792
	 *
793
	 * After this, the user should no longer be able to log in.
794
	 *
795
	 * @param string $username
796
	 */
797
	public function revokeAccessForUser( $username ) {
798
		$this->logger->info( 'Revoking access for {user}', [
799
			'user' => $username,
800
		] );
801
		$this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
802
	}
803
804
	/**
805
	 * Validate a change of authentication data (e.g. passwords)
806
	 * @param AuthenticationRequest $req
807
	 * @param bool $checkData If false, $req hasn't been loaded from the
808
	 *  submission so checks on user-submitted fields should be skipped. $req->username is
809
	 *  considered user-submitted for this purpose, even if it cannot be changed via
810
	 *  $req->loadFromSubmission.
811
	 * @return Status
812
	 */
813
	public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
814
		$any = false;
815
		$providers = $this->getPrimaryAuthenticationProviders() +
816
			$this->getSecondaryAuthenticationProviders();
817
		foreach ( $providers as $provider ) {
818
			$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...
819
			if ( !$status->isGood() ) {
820
				return Status::wrap( $status );
821
			}
822
			$any = $any || $status->value !== 'ignored';
823
		}
824
		if ( !$any ) {
825
			$status = Status::newGood( 'ignored' );
826
			$status->warning( 'authmanager-change-not-supported' );
827
			return $status;
828
		}
829
		return Status::newGood();
830
	}
831
832
	/**
833
	 * Change authentication data (e.g. passwords)
834
	 *
835
	 * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
836
	 * result in a successful login in the future.
837
	 *
838
	 * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
839
	 * no longer result in a successful login.
840
	 *
841
	 * @param AuthenticationRequest $req
842
	 */
843
	public function changeAuthenticationData( AuthenticationRequest $req ) {
844
		$this->logger->info( 'Changing authentication data for {user} class {what}', [
845
			'user' => is_string( $req->username ) ? $req->username : '<no name>',
846
			'what' => get_class( $req ),
847
		] );
848
849
		$this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
850
851
		// When the main account's authentication data is changed, invalidate
852
		// all BotPasswords too.
853
		\BotPassword::invalidateAllPasswordsForUser( $req->username );
854
	}
855
856
	/**@}*/
857
858
	/**
859
	 * @name Account creation
860
	 * @{
861
	 */
862
863
	/**
864
	 * Determine whether accounts can be created
865
	 * @return bool
866
	 */
867 View Code Duplication
	public function canCreateAccounts() {
868
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
869
			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...
870
				case PrimaryAuthenticationProvider::TYPE_CREATE:
871
				case PrimaryAuthenticationProvider::TYPE_LINK:
872
					return true;
873
			}
874
		}
875
		return false;
876
	}
877
878
	/**
879
	 * Determine whether a particular account can be created
880
	 * @param string $username
881
	 * @param int $flags Bitfield of User:READ_* constants
882
	 * @return Status
883
	 */
884
	public function canCreateAccount( $username, $flags = User::READ_NORMAL ) {
885
		if ( !$this->canCreateAccounts() ) {
886
			return Status::newFatal( 'authmanager-create-disabled' );
887
		}
888
889
		if ( $this->userExists( $username, $flags ) ) {
890
			return Status::newFatal( 'userexists' );
891
		}
892
893
		$user = User::newFromName( $username, 'creatable' );
894
		if ( !is_object( $user ) ) {
895
			return Status::newFatal( 'noname' );
896
		} else {
897
			$user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
898
			if ( $user->getId() !== 0 ) {
899
				return Status::newFatal( 'userexists' );
900
			}
901
		}
902
903
		// Denied by providers?
904
		$providers = $this->getPreAuthenticationProviders() +
905
			$this->getPrimaryAuthenticationProviders() +
906
			$this->getSecondaryAuthenticationProviders();
907
		foreach ( $providers as $provider ) {
908
			$status = $provider->testUserForCreation( $user, false );
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...
909
			if ( !$status->isGood() ) {
910
				return Status::wrap( $status );
911
			}
912
		}
913
914
		return Status::newGood();
915
	}
916
917
	/**
918
	 * Basic permissions checks on whether a user can create accounts
919
	 * @param User $creator User doing the account creation
920
	 * @return Status
921
	 */
922
	public function checkAccountCreatePermissions( User $creator ) {
923
		// Wiki is read-only?
924
		if ( wfReadOnly() ) {
925
			return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
926
		}
927
928
		// This is awful, this permission check really shouldn't go through Title.
929
		$permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
930
			->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
931
		if ( $permErrors ) {
932
			$status = Status::newGood();
933
			foreach ( $permErrors as $args ) {
934
				call_user_func_array( [ $status, 'fatal' ], $args );
935
			}
936
			return $status;
937
		}
938
939
		$block = $creator->isBlockedFromCreateAccount();
940
		if ( $block ) {
941
			$errorParams = [
942
				$block->getTarget(),
943
				$block->mReason ?: wfMessage( 'blockednoreason' )->text(),
944
				$block->getByName()
945
			];
946
947 View Code Duplication
			if ( $block->getType() === \Block::TYPE_RANGE ) {
948
				$errorMessage = 'cantcreateaccount-range-text';
949
				$errorParams[] = $this->getRequest()->getIP();
950
			} else {
951
				$errorMessage = 'cantcreateaccount-text';
952
			}
953
954
			return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
955
		}
956
957
		$ip = $this->getRequest()->getIP();
958
		if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
959
			return Status::newFatal( 'sorbs_create_account_reason' );
960
		}
961
962
		return Status::newGood();
963
	}
964
965
	/**
966
	 * Start an account creation flow
967
	 *
968
	 * In addition to the AuthenticationRequests returned by
969
	 * $this->getAuthenticationRequests(), a client might include a
970
	 * CreateFromLoginAuthenticationRequest from a previous login attempt. If
971
	 * <code>
972
	 * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
973
	 * </code>
974
	 * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
975
	 * should be omitted. If the CreateFromLoginAuthenticationRequest has a
976
	 * username set, that username must be used for all other requests.
977
	 *
978
	 * @param User $creator User doing the account creation
979
	 * @param AuthenticationRequest[] $reqs
980
	 * @param string $returnToUrl Url that REDIRECT responses should eventually
981
	 *  return to.
982
	 * @return AuthenticationResponse
983
	 */
984
	public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
985
		$session = $this->request->getSession();
986
		if ( !$this->canCreateAccounts() ) {
987
			// Caller should have called canCreateAccounts()
988
			$session->remove( 'AuthManager::accountCreationState' );
989
			throw new \LogicException( 'Account creation is not possible' );
990
		}
991
992
		try {
993
			$username = AuthenticationRequest::getUsernameFromRequests( $reqs );
994
		} catch ( \UnexpectedValueException $ex ) {
995
			$username = null;
996
		}
997
		if ( $username === null ) {
998
			$this->logger->debug( __METHOD__ . ': No username provided' );
999
			return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1000
		}
1001
1002
		// Permissions check
1003
		$status = $this->checkAccountCreatePermissions( $creator );
1004 View Code Duplication
		if ( !$status->isGood() ) {
1005
			$this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1006
				'user' => $username,
1007
				'creator' => $creator->getName(),
1008
				'reason' => $status->getWikiText( null, null, 'en' )
1009
			] );
1010
			return AuthenticationResponse::newFail( $status->getMessage() );
1011
		}
1012
1013
		$status = $this->canCreateAccount( $username, User::READ_LOCKING );
1014 View Code Duplication
		if ( !$status->isGood() ) {
1015
			$this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1016
				'user' => $username,
1017
				'creator' => $creator->getName(),
1018
				'reason' => $status->getWikiText( null, null, 'en' )
1019
			] );
1020
			return AuthenticationResponse::newFail( $status->getMessage() );
1021
		}
1022
1023
		$user = User::newFromName( $username, 'creatable' );
1024
		foreach ( $reqs as $req ) {
1025
			$req->username = $username;
1026
			$req->returnToUrl = $returnToUrl;
1027
			if ( $req instanceof UserDataAuthenticationRequest ) {
1028
				$status = $req->populateUser( $user );
0 ignored issues
show
Security Bug introduced by
It seems like $user defined by \User::newFromName($username, 'creatable') on line 1023 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...
1029 View Code Duplication
				if ( !$status->isGood() ) {
1030
					$status = Status::wrap( $status );
1031
					$session->remove( 'AuthManager::accountCreationState' );
1032
					$this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1033
						'user' => $user->getName(),
1034
						'creator' => $creator->getName(),
1035
						'reason' => $status->getWikiText( null, null, 'en' ),
1036
					] );
1037
					return AuthenticationResponse::newFail( $status->getMessage() );
1038
				}
1039
			}
1040
		}
1041
1042
		$this->removeAuthenticationSessionData( null );
1043
1044
		$state = [
1045
			'username' => $username,
1046
			'userid' => 0,
1047
			'creatorid' => $creator->getId(),
1048
			'creatorname' => $creator->getName(),
1049
			'reqs' => $reqs,
1050
			'returnToUrl' => $returnToUrl,
1051
			'primary' => null,
1052
			'primaryResponse' => null,
1053
			'secondary' => [],
1054
			'continueRequests' => [],
1055
			'maybeLink' => [],
1056
			'ranPreTests' => false,
1057
		];
1058
1059
		// Special case: converting a login to an account creation
1060
		$req = AuthenticationRequest::getRequestByClass(
1061
			$reqs, CreateFromLoginAuthenticationRequest::class
1062
		);
1063 View Code Duplication
		if ( $req ) {
1064
			$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...
1065
1066
			if ( $req->createRequest ) {
1067
				$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...
1068
				$state['reqs'][] = $req->createRequest;
1069
			}
1070
		}
1071
1072
		$session->setSecret( 'AuthManager::accountCreationState', $state );
1073
		$session->persist();
1074
1075
		return $this->continueAccountCreation( $reqs );
1076
	}
1077
1078
	/**
1079
	 * Continue an account creation flow
1080
	 * @param AuthenticationRequest[] $reqs
1081
	 * @return AuthenticationResponse
1082
	 */
1083
	public function continueAccountCreation( array $reqs ) {
1084
		$session = $this->request->getSession();
1085
		try {
1086
			if ( !$this->canCreateAccounts() ) {
1087
				// Caller should have called canCreateAccounts()
1088
				$session->remove( 'AuthManager::accountCreationState' );
1089
				throw new \LogicException( 'Account creation is not possible' );
1090
			}
1091
1092
			$state = $session->getSecret( 'AuthManager::accountCreationState' );
1093
			if ( !is_array( $state ) ) {
1094
				return AuthenticationResponse::newFail(
1095
					wfMessage( 'authmanager-create-not-in-progress' )
1096
				);
1097
			}
1098
			$state['continueRequests'] = [];
1099
1100
			// Step 0: Prepare and validate the input
1101
1102
			$user = User::newFromName( $state['username'], 'creatable' );
1103 View Code Duplication
			if ( !is_object( $user ) ) {
1104
				$session->remove( 'AuthManager::accountCreationState' );
1105
				$this->logger->debug( __METHOD__ . ': Invalid username', [
1106
					'user' => $state['username'],
1107
				] );
1108
				return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1109
			}
1110
1111
			if ( $state['creatorid'] ) {
1112
				$creator = User::newFromId( $state['creatorid'] );
1113
			} else {
1114
				$creator = new User;
1115
				$creator->setName( $state['creatorname'] );
1116
			}
1117
1118
			// Avoid account creation races on double submissions
1119
			$cache = \ObjectCache::getLocalClusterInstance();
1120
			$lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1121 View Code Duplication
			if ( !$lock ) {
1122
				// Don't clear AuthManager::accountCreationState for this code
1123
				// path because the process that won the race owns it.
1124
				$this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1125
					'user' => $user->getName(),
1126
					'creator' => $creator->getName(),
1127
				] );
1128
				return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1129
			}
1130
1131
			// Permissions check
1132
			$status = $this->checkAccountCreatePermissions( $creator );
1133 View Code Duplication
			if ( !$status->isGood() ) {
1134
				$this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1135
					'user' => $user->getName(),
1136
					'creator' => $creator->getName(),
1137
					'reason' => $status->getWikiText( null, null, 'en' )
1138
				] );
1139
				$ret = AuthenticationResponse::newFail( $status->getMessage() );
1140
				$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1141
				$session->remove( 'AuthManager::accountCreationState' );
1142
				return $ret;
1143
			}
1144
1145
			// Load from master for existence check
1146
			$user->load( User::READ_LOCKING );
1147
1148
			if ( $state['userid'] === 0 ) {
1149 View Code Duplication
				if ( $user->getId() != 0 ) {
1150
					$this->logger->debug( __METHOD__ . ': User exists locally', [
1151
						'user' => $user->getName(),
1152
						'creator' => $creator->getName(),
1153
					] );
1154
					$ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1155
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1156
					$session->remove( 'AuthManager::accountCreationState' );
1157
					return $ret;
1158
				}
1159
			} else {
1160
				if ( $user->getId() == 0 ) {
1161
					$this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1162
						'user' => $user->getName(),
1163
						'creator' => $creator->getName(),
1164
						'expected_id' => $state['userid'],
1165
					] );
1166
					throw new \UnexpectedValueException(
1167
						"User \"{$state['username']}\" should exist now, but doesn't!"
1168
					);
1169
				}
1170
				if ( $user->getId() != $state['userid'] ) {
1171
					$this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1172
						'user' => $user->getName(),
1173
						'creator' => $creator->getName(),
1174
						'expected_id' => $state['userid'],
1175
						'actual_id' => $user->getId(),
1176
					] );
1177
					throw new \UnexpectedValueException(
1178
						"User \"{$state['username']}\" exists, but " .
1179
							"ID {$user->getId()} != {$state['userid']}!"
1180
					);
1181
				}
1182
			}
1183
			foreach ( $state['reqs'] as $req ) {
1184
				if ( $req instanceof UserDataAuthenticationRequest ) {
1185
					$status = $req->populateUser( $user );
1186 View Code Duplication
					if ( !$status->isGood() ) {
1187
						// This should never happen...
1188
						$status = Status::wrap( $status );
1189
						$this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1190
							'user' => $user->getName(),
1191
							'creator' => $creator->getName(),
1192
							'reason' => $status->getWikiText( null, null, 'en' ),
1193
						] );
1194
						$ret = AuthenticationResponse::newFail( $status->getMessage() );
1195
						$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1196
						$session->remove( 'AuthManager::accountCreationState' );
1197
						return $ret;
1198
					}
1199
				}
1200
			}
1201
1202
			foreach ( $reqs as $req ) {
1203
				$req->returnToUrl = $state['returnToUrl'];
1204
				$req->username = $state['username'];
1205
			}
1206
1207
			// Run pre-creation tests, if we haven't already
1208
			if ( !$state['ranPreTests'] ) {
1209
				$providers = $this->getPreAuthenticationProviders() +
1210
					$this->getPrimaryAuthenticationProviders() +
1211
					$this->getSecondaryAuthenticationProviders();
1212
				foreach ( $providers as $id => $provider ) {
1213
					$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...
1214
					if ( !$status->isGood() ) {
1215
						$this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1216
							'user' => $user->getName(),
1217
							'creator' => $creator->getName(),
1218
						] );
1219
						$ret = AuthenticationResponse::newFail(
1220
							Status::wrap( $status )->getMessage()
1221
						);
1222
						$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1223
						$session->remove( 'AuthManager::accountCreationState' );
1224
						return $ret;
1225
					}
1226
				}
1227
1228
				$state['ranPreTests'] = true;
1229
			}
1230
1231
			// Step 1: Choose a primary authentication provider and call it until it succeeds.
1232
1233
			if ( $state['primary'] === null ) {
1234
				// We haven't picked a PrimaryAuthenticationProvider yet
1235
				foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1236
					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...
1237
						continue;
1238
					}
1239
					$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...
1240
					switch ( $res->status ) {
1241
						case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1242
							$this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1243
								'user' => $user->getName(),
1244
								'creator' => $creator->getName(),
1245
							] );
1246
							$state['primary'] = $id;
1247
							$state['primaryResponse'] = $res;
1248
							break 2;
1249 View Code Duplication
						case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1250
							$this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1251
								'user' => $user->getName(),
1252
								'creator' => $creator->getName(),
1253
							] );
1254
							$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1255
							$session->remove( 'AuthManager::accountCreationState' );
1256
							return $res;
1257
						case AuthenticationResponse::ABSTAIN;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1258
							// Continue loop
1259
							break;
1260
						case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1261
						case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1262
							$this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1263
								'user' => $user->getName(),
1264
								'creator' => $creator->getName(),
1265
							] );
1266
							$this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1267
							$state['primary'] = $id;
1268
							$state['continueRequests'] = $res->neededRequests;
1269
							$session->setSecret( 'AuthManager::accountCreationState', $state );
1270
							return $res;
1271
1272
							// @codeCoverageIgnoreStart
1273
						default:
1274
							throw new \DomainException(
1275
								get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1276
							);
1277
							// @codeCoverageIgnoreEnd
1278
					}
1279
				}
1280 View Code Duplication
				if ( $state['primary'] === null ) {
1281
					$this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1282
						'user' => $user->getName(),
1283
						'creator' => $creator->getName(),
1284
					] );
1285
					$ret = AuthenticationResponse::newFail(
1286
						wfMessage( 'authmanager-create-no-primary' )
1287
					);
1288
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1289
					$session->remove( 'AuthManager::accountCreationState' );
1290
					return $ret;
1291
				}
1292
			} elseif ( $state['primaryResponse'] === null ) {
1293
				$provider = $this->getAuthenticationProvider( $state['primary'] );
1294
				if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1295
					// Configuration changed? Force them to start over.
1296
					// @codeCoverageIgnoreStart
1297
					$ret = AuthenticationResponse::newFail(
1298
						wfMessage( 'authmanager-create-not-in-progress' )
1299
					);
1300
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1301
					$session->remove( 'AuthManager::accountCreationState' );
1302
					return $ret;
1303
					// @codeCoverageIgnoreEnd
1304
				}
1305
				$id = $provider->getUniqueId();
1306
				$res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1307
				switch ( $res->status ) {
1308
					case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1309
						$this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1310
							'user' => $user->getName(),
1311
							'creator' => $creator->getName(),
1312
						] );
1313
						$state['primaryResponse'] = $res;
1314
						break;
1315 View Code Duplication
					case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //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 failed by $id", [
1317
							'user' => $user->getName(),
1318
							'creator' => $creator->getName(),
1319
						] );
1320
						$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1321
						$session->remove( 'AuthManager::accountCreationState' );
1322
						return $res;
1323
					case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1324
					case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1325
						$this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1326
							'user' => $user->getName(),
1327
							'creator' => $creator->getName(),
1328
						] );
1329
						$this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1330
						$state['continueRequests'] = $res->neededRequests;
1331
						$session->setSecret( 'AuthManager::accountCreationState', $state );
1332
						return $res;
1333
					default:
1334
						throw new \DomainException(
1335
							get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1336
						);
1337
				}
1338
			}
1339
1340
			// Step 2: Primary authentication succeeded, create the User object
1341
			// and add the user locally.
1342
1343
			if ( $state['userid'] === 0 ) {
1344
				$this->logger->info( 'Creating user {user} during account creation', [
1345
					'user' => $user->getName(),
1346
					'creator' => $creator->getName(),
1347
				] );
1348
				$status = $user->addToDatabase();
1349
				if ( !$status->isOk() ) {
1350
					// @codeCoverageIgnoreStart
1351
					$ret = AuthenticationResponse::newFail( $status->getMessage() );
1352
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1353
					$session->remove( 'AuthManager::accountCreationState' );
1354
					return $ret;
1355
					// @codeCoverageIgnoreEnd
1356
				}
1357
				$this->setDefaultUserOptions( $user, $creator->isAnon() );
1358
				\Hooks::run( 'LocalUserCreated', [ $user, false ] );
1359
				$user->saveSettings();
1360
				$state['userid'] = $user->getId();
1361
1362
				// Update user count
1363
				\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1364
1365
				// Watch user's userpage and talk page
1366
				$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1367
1368
				// Inform the provider
1369
				$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...
1370
1371
				// Log the creation
1372
				if ( $this->config->get( 'NewUserLog' ) ) {
1373
					$isAnon = $creator->isAnon();
1374
					$logEntry = new \ManualLogEntry(
1375
						'newusers',
1376
						$logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1377
					);
1378
					$logEntry->setPerformer( $isAnon ? $user : $creator );
1379
					$logEntry->setTarget( $user->getUserPage() );
1380
					$req = AuthenticationRequest::getRequestByClass(
1381
						$state['reqs'], CreationReasonAuthenticationRequest::class
1382
					);
1383
					$logEntry->setComment( $req ? $req->reason : '' );
0 ignored issues
show
Bug introduced by
The property reason 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...
1384
					$logEntry->setParameters( [
1385
						'4::userid' => $user->getId(),
1386
					] );
1387
					$logid = $logEntry->insert();
1388
					$logEntry->publish( $logid );
1389
				}
1390
			}
1391
1392
			// Step 3: Iterate over all the secondary authentication providers.
1393
1394
			$beginReqs = $state['reqs'];
1395
1396
			foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1397 View Code Duplication
				if ( !isset( $state['secondary'][$id] ) ) {
1398
					// This provider isn't started yet, so we pass it the set
1399
					// of reqs from beginAuthentication instead of whatever
1400
					// might have been used by a previous provider in line.
1401
					$func = 'beginSecondaryAccountCreation';
1402
					$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...
1403
				} elseif ( !$state['secondary'][$id] ) {
1404
					$func = 'continueSecondaryAccountCreation';
1405
					$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...
1406
				} else {
1407
					continue;
1408
				}
1409
				switch ( $res->status ) {
1410
					case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1411
						$this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1412
							'user' => $user->getName(),
1413
							'creator' => $creator->getName(),
1414
						] );
1415
						// fall through
1416
					case AuthenticationResponse::ABSTAIN;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1417
						$state['secondary'][$id] = true;
1418
						break;
1419
					case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1420
					case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1421
						$this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1422
							'user' => $user->getName(),
1423
							'creator' => $creator->getName(),
1424
						] );
1425
						$this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1426
						$state['secondary'][$id] = false;
1427
						$state['continueRequests'] = $res->neededRequests;
1428
						$session->setSecret( 'AuthManager::accountCreationState', $state );
1429
						return $res;
1430
					case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1431
						throw new \DomainException(
1432
							get_class( $provider ) . "::{$func}() returned $res->status." .
1433
							' Secondary providers are not allowed to fail account creation, that' .
1434
							' should have been done via testForAccountCreation().'
1435
						);
1436
							// @codeCoverageIgnoreStart
1437
					default:
1438
						throw new \DomainException(
1439
							get_class( $provider ) . "::{$func}() returned $res->status"
1440
						);
1441
							// @codeCoverageIgnoreEnd
1442
				}
1443
			}
1444
1445
			$id = $user->getId();
1446
			$name = $user->getName();
1447
			$req = new CreatedAccountAuthenticationRequest( $id, $name );
1448
			$ret = AuthenticationResponse::newPass( $name );
1449
			$ret->loginRequest = $req;
1450
			$this->createdAccountAuthenticationRequests[] = $req;
1451
1452
			$this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1453
				'user' => $user->getName(),
1454
				'creator' => $creator->getName(),
1455
			] );
1456
1457
			$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1458
			$session->remove( 'AuthManager::accountCreationState' );
1459
			$this->removeAuthenticationSessionData( null );
1460
			return $ret;
1461
		} catch ( \Exception $ex ) {
1462
			$session->remove( 'AuthManager::accountCreationState' );
1463
			throw $ex;
1464
		}
1465
	}
1466
1467
	/**
1468
	 * Auto-create an account, and log into that account
1469
	 * @param User $user User to auto-create
1470
	 * @param string $source What caused the auto-creation? This must be the ID
1471
	 *  of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
1472
	 * @param bool $login Whether to also log the user in
1473
	 * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1474
	 */
1475
	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...
1476
		if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1477
			!$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1478
		) {
1479
			throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1480
		}
1481
1482
		$username = $user->getName();
1483
1484
		// Try the local user from the slave DB
1485
		$localId = User::idFromName( $username );
1486
		$flags = User::READ_NORMAL;
1487
1488
		// Fetch the user ID from the master, so that we don't try to create the user
1489
		// when they already exist, due to replication lag
1490
		// @codeCoverageIgnoreStart
1491 View Code Duplication
		if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $localId of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

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

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

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

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

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

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

Loading history...
1492
			$localId = User::idFromName( $username, User::READ_LATEST );
1493
			$flags = User::READ_LATEST;
1494
		}
1495
		// @codeCoverageIgnoreEnd
1496
1497
		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...
1498
			$this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1499
				'username' => $username,
1500
			] );
1501
			$user->setId( $localId );
1502
			$user->loadFromId( $flags );
1503
			if ( $login ) {
1504
				$this->setSessionDataForUser( $user );
1505
			}
1506
			$status = Status::newGood();
1507
			$status->warning( 'userexists' );
1508
			return $status;
1509
		}
1510
1511
		// Wiki is read-only?
1512
		if ( wfReadOnly() ) {
1513
			$this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1514
				'username' => $username,
1515
				'reason' => wfReadOnlyReason(),
1516
			] );
1517
			$user->setId( 0 );
1518
			$user->loadFromId();
1519
			return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
1520
		}
1521
1522
		// Check the session, if we tried to create this user already there's
1523
		// no point in retrying.
1524
		$session = $this->request->getSession();
1525
		if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1526
			$this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1527
				'username' => $username,
1528
				'sessionid' => $session->getId(),
1529
			] );
1530
			$user->setId( 0 );
1531
			$user->loadFromId();
1532
			$reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1533
			if ( $reason instanceof StatusValue ) {
1534
				return Status::wrap( $reason );
1535
			} else {
1536
				return Status::newFatal( $reason );
1537
			}
1538
		}
1539
1540
		// Is the username creatable?
1541
		if ( !User::isCreatableName( $username ) ) {
1542
			$this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1543
				'username' => $username,
1544
			] );
1545
			$session->set( 'AuthManager::AutoCreateBlacklist', 'noname', 600 );
1546
			$user->setId( 0 );
1547
			$user->loadFromId();
1548
			return Status::newFatal( 'noname' );
1549
		}
1550
1551
		// Is the IP user able to create accounts?
1552
		$anon = new User;
1553
		if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1554
			$this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1555
				'username' => $username,
1556
				'ip' => $anon->getName(),
1557
			] );
1558
			$session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm', 600 );
1559
			$session->persist();
1560
			$user->setId( 0 );
1561
			$user->loadFromId();
1562
			return Status::newFatal( 'authmanager-autocreate-noperm' );
1563
		}
1564
1565
		// Avoid account creation races on double submissions
1566
		$cache = \ObjectCache::getLocalClusterInstance();
1567
		$lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1568 View Code Duplication
		if ( !$lock ) {
1569
			$this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1570
				'user' => $username,
1571
			] );
1572
			$user->setId( 0 );
1573
			$user->loadFromId();
1574
			return Status::newFatal( 'usernameinprogress' );
1575
		}
1576
1577
		// Denied by providers?
1578
		$providers = $this->getPreAuthenticationProviders() +
1579
			$this->getPrimaryAuthenticationProviders() +
1580
			$this->getSecondaryAuthenticationProviders();
1581
		foreach ( $providers as $provider ) {
1582
			$status = $provider->testUserForCreation( $user, $source );
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...
1583 View Code Duplication
			if ( !$status->isGood() ) {
1584
				$ret = Status::wrap( $status );
1585
				$this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1586
					'username' => $username,
1587
					'reason' => $ret->getWikiText( null, null, 'en' ),
1588
				] );
1589
				$session->set( 'AuthManager::AutoCreateBlacklist', $status, 600 );
1590
				$user->setId( 0 );
1591
				$user->loadFromId();
1592
				return $ret;
1593
			}
1594
		}
1595
1596
		// Ignore warnings about master connections/writes...hard to avoid here
1597
		\Profiler::instance()->getTransactionProfiler()->resetExpectations();
1598
1599
		$backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1600 View Code Duplication
		if ( $cache->get( $backoffKey ) ) {
1601
			$this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1602
				'username' => $username,
1603
			] );
1604
			$user->setId( 0 );
1605
			$user->loadFromId();
1606
			return Status::newFatal( 'authmanager-autocreate-exception' );
1607
		}
1608
1609
		// Checks passed, create the user...
1610
		$from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
1611
		$this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1612
			'username' => $username,
1613
			'from' => $from,
1614
		] );
1615
1616
		try {
1617
			$status = $user->addToDatabase();
1618
			if ( !$status->isOk() ) {
1619
				// double-check for a race condition (T70012)
1620
				$localId = User::idFromName( $username, User::READ_LATEST );
1621
				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...
1622
					$this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1623
						'username' => $username,
1624
					] );
1625
					$user->setId( $localId );
1626
					$user->loadFromId( User::READ_LATEST );
1627
					if ( $login ) {
1628
						$this->setSessionDataForUser( $user );
1629
					}
1630
					$status = Status::newGood();
1631
					$status->warning( 'userexists' );
1632 View Code Duplication
				} else {
1633
					$this->logger->error( __METHOD__ . ': {username} failed with message {message}', [
1634
						'username' => $username,
1635
						'message' => $status->getWikiText( null, null, 'en' )
1636
					] );
1637
					$user->setId( 0 );
1638
					$user->loadFromId();
1639
				}
1640
				return $status;
1641
			}
1642
		} catch ( \Exception $ex ) {
1643
			$this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1644
				'username' => $username,
1645
				'exception' => $ex,
1646
			] );
1647
			// Do not keep throwing errors for a while
1648
			$cache->set( $backoffKey, 1, 600 );
1649
			// Bubble up error; which should normally trigger DB rollbacks
1650
			throw $ex;
1651
		}
1652
1653
		$this->setDefaultUserOptions( $user, true );
1654
1655
		// Inform the providers
1656
		$this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1657
1658
		\Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1659
		\Hooks::run( 'LocalUserCreated', [ $user, true ] );
1660
		$user->saveSettings();
1661
1662
		// Update user count
1663
		\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1664
1665
		// Watch user's userpage and talk page
1666
		$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1667
1668
		// Log the creation
1669
		if ( $this->config->get( 'NewUserLog' ) ) {
1670
			$logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1671
			$logEntry->setPerformer( $user );
1672
			$logEntry->setTarget( $user->getUserPage() );
1673
			$logEntry->setComment( '' );
1674
			$logEntry->setParameters( [
1675
				'4::userid' => $user->getId(),
1676
			] );
1677
			$logid = $logEntry->insert();
0 ignored issues
show
Unused Code introduced by
$logid is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1678
		}
1679
1680
		if ( $login ) {
1681
			$this->setSessionDataForUser( $user );
1682
		}
1683
1684
		return Status::newGood();
1685
	}
1686
1687
	/**@}*/
1688
1689
	/**
1690
	 * @name Account linking
1691
	 * @{
1692
	 */
1693
1694
	/**
1695
	 * Determine whether accounts can be linked
1696
	 * @return bool
1697
	 */
1698 View Code Duplication
	public function canLinkAccounts() {
1699
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1700
			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...
1701
				return true;
1702
			}
1703
		}
1704
		return false;
1705
	}
1706
1707
	/**
1708
	 * Start an account linking flow
1709
	 *
1710
	 * @param User $user User being linked
1711
	 * @param AuthenticationRequest[] $reqs
1712
	 * @param string $returnToUrl Url that REDIRECT responses should eventually
1713
	 *  return to.
1714
	 * @return AuthenticationResponse
1715
	 */
1716
	public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1717
		$session = $this->request->getSession();
1718
		$session->remove( 'AuthManager::accountLinkState' );
1719
1720
		if ( !$this->canLinkAccounts() ) {
1721
			// Caller should have called canLinkAccounts()
1722
			throw new \LogicException( 'Account linking is not possible' );
1723
		}
1724
1725
		if ( $user->getId() === 0 ) {
1726
			if ( !User::isUsableName( $user->getName() ) ) {
1727
				$msg = wfMessage( 'noname' );
1728
			} else {
1729
				$msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1730
			}
1731
			return AuthenticationResponse::newFail( $msg );
1732
		}
1733
		foreach ( $reqs as $req ) {
1734
			$req->username = $user->getName();
1735
			$req->returnToUrl = $returnToUrl;
1736
		}
1737
1738
		$this->removeAuthenticationSessionData( null );
1739
1740
		$providers = $this->getPreAuthenticationProviders();
1741
		foreach ( $providers as $id => $provider ) {
1742
			$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...
1743
			if ( !$status->isGood() ) {
1744
				$this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1745
					'user' => $user->getName(),
1746
				] );
1747
				$ret = AuthenticationResponse::newFail(
1748
					Status::wrap( $status )->getMessage()
1749
				);
1750
				$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1751
				return $ret;
1752
			}
1753
		}
1754
1755
		$state = [
1756
			'username' => $user->getName(),
1757
			'userid' => $user->getId(),
1758
			'returnToUrl' => $returnToUrl,
1759
			'primary' => null,
1760
			'continueRequests' => [],
1761
		];
1762
1763
		$providers = $this->getPrimaryAuthenticationProviders();
1764
		foreach ( $providers as $id => $provider ) {
1765
			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...
1766
				continue;
1767
			}
1768
1769
			$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...
1770 View Code Duplication
			switch ( $res->status ) {
1771
				case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1772
					$this->logger->info( "Account linked to {user} by $id", [
1773
						'user' => $user->getName(),
1774
					] );
1775
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1776
					return $res;
1777
1778
				case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1779
					$this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1780
						'user' => $user->getName(),
1781
					] );
1782
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1783
					return $res;
1784
1785
				case AuthenticationResponse::ABSTAIN;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1786
					// Continue loop
1787
					break;
1788
1789
				case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1790
				case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1791
					$this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1792
						'user' => $user->getName(),
1793
					] );
1794
					$this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1795
					$state['primary'] = $id;
1796
					$state['continueRequests'] = $res->neededRequests;
1797
					$session->setSecret( 'AuthManager::accountLinkState', $state );
1798
					$session->persist();
1799
					return $res;
1800
1801
					// @codeCoverageIgnoreStart
1802
				default:
1803
					throw new \DomainException(
1804
						get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1805
					);
1806
					// @codeCoverageIgnoreEnd
1807
			}
1808
		}
1809
1810
		$this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1811
			'user' => $user->getName(),
1812
		] );
1813
		$ret = AuthenticationResponse::newFail(
1814
			wfMessage( 'authmanager-link-no-primary' )
1815
		);
1816
		$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1817
		return $ret;
1818
	}
1819
1820
	/**
1821
	 * Continue an account linking flow
1822
	 * @param AuthenticationRequest[] $reqs
1823
	 * @return AuthenticationResponse
1824
	 */
1825
	public function continueAccountLink( array $reqs ) {
1826
		$session = $this->request->getSession();
1827
		try {
1828
			if ( !$this->canLinkAccounts() ) {
1829
				// Caller should have called canLinkAccounts()
1830
				$session->remove( 'AuthManager::accountLinkState' );
1831
				throw new \LogicException( 'Account linking is not possible' );
1832
			}
1833
1834
			$state = $session->getSecret( 'AuthManager::accountLinkState' );
1835
			if ( !is_array( $state ) ) {
1836
				return AuthenticationResponse::newFail(
1837
					wfMessage( 'authmanager-link-not-in-progress' )
1838
				);
1839
			}
1840
			$state['continueRequests'] = [];
1841
1842
			// Step 0: Prepare and validate the input
1843
1844
			$user = User::newFromName( $state['username'], 'usable' );
1845
			if ( !is_object( $user ) ) {
1846
				$session->remove( 'AuthManager::accountLinkState' );
1847
				return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1848
			}
1849
			if ( $user->getId() != $state['userid'] ) {
1850
				throw new \UnexpectedValueException(
1851
					"User \"{$state['username']}\" is valid, but " .
1852
						"ID {$user->getId()} != {$state['userid']}!"
1853
				);
1854
			}
1855
1856
			foreach ( $reqs as $req ) {
1857
				$req->username = $state['username'];
1858
				$req->returnToUrl = $state['returnToUrl'];
1859
			}
1860
1861
			// Step 1: Call the primary again until it succeeds
1862
1863
			$provider = $this->getAuthenticationProvider( $state['primary'] );
1864
			if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1865
				// Configuration changed? Force them to start over.
1866
				// @codeCoverageIgnoreStart
1867
				$ret = AuthenticationResponse::newFail(
1868
					wfMessage( 'authmanager-link-not-in-progress' )
1869
				);
1870
				$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1871
				$session->remove( 'AuthManager::accountLinkState' );
1872
				return $ret;
1873
				// @codeCoverageIgnoreEnd
1874
			}
1875
			$id = $provider->getUniqueId();
1876
			$res = $provider->continuePrimaryAccountLink( $user, $reqs );
1877 View Code Duplication
			switch ( $res->status ) {
1878
				case AuthenticationResponse::PASS;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1879
					$this->logger->info( "Account linked to {user} by $id", [
1880
						'user' => $user->getName(),
1881
					] );
1882
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1883
					$session->remove( 'AuthManager::accountLinkState' );
1884
					return $res;
1885
				case AuthenticationResponse::FAIL;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1886
					$this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1887
						'user' => $user->getName(),
1888
					] );
1889
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1890
					$session->remove( 'AuthManager::accountLinkState' );
1891
					return $res;
1892
				case AuthenticationResponse::REDIRECT;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1893
				case AuthenticationResponse::UI;
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

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

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

Loading history...
1894
					$this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1895
						'user' => $user->getName(),
1896
					] );
1897
					$this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1898
					$state['continueRequests'] = $res->neededRequests;
1899
					$session->setSecret( 'AuthManager::accountLinkState', $state );
1900
					return $res;
1901
				default:
1902
					throw new \DomainException(
1903
						get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1904
					);
1905
			}
1906
		} catch ( \Exception $ex ) {
1907
			$session->remove( 'AuthManager::accountLinkState' );
1908
			throw $ex;
1909
		}
1910
	}
1911
1912
	/**@}*/
1913
1914
	/**
1915
	 * @name Information methods
1916
	 * @{
1917
	 */
1918
1919
	/**
1920
	 * Return the applicable list of AuthenticationRequests
1921
	 *
1922
	 * Possible values for $action:
1923
	 *  - ACTION_LOGIN: Valid for passing to beginAuthentication
1924
	 *  - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
1925
	 *  - ACTION_CREATE: Valid for passing to beginAccountCreation
1926
	 *  - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
1927
	 *  - ACTION_LINK: Valid for passing to beginAccountLink
1928
	 *  - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
1929
	 *  - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
1930
	 *  - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
1931
	 *  - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
1932
	 *
1933
	 * @param string $action One of the AuthManager::ACTION_* constants
1934
	 * @param User|null $user User being acted on, instead of the current user.
1935
	 * @return AuthenticationRequest[]
1936
	 */
1937
	public function getAuthenticationRequests( $action, User $user = null ) {
1938
		$options = [];
1939
		$providerAction = $action;
1940
1941
		// Figure out which providers to query
1942
		switch ( $action ) {
1943
			case self::ACTION_LOGIN:
1944
			case self::ACTION_CREATE:
1945
				$providers = $this->getPreAuthenticationProviders() +
1946
					$this->getPrimaryAuthenticationProviders() +
1947
					$this->getSecondaryAuthenticationProviders();
1948
				break;
1949
1950 View Code Duplication
			case self::ACTION_LOGIN_CONTINUE:
1951
				$state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
1952
				return is_array( $state ) ? $state['continueRequests'] : [];
1953
1954 View Code Duplication
			case self::ACTION_CREATE_CONTINUE:
1955
				$state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
1956
				return is_array( $state ) ? $state['continueRequests'] : [];
1957
1958 View Code Duplication
			case self::ACTION_LINK:
1959
				$providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
1960
					return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
1961
				} );
1962
				break;
1963
1964 View Code Duplication
			case self::ACTION_UNLINK:
1965
				$providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
1966
					return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
1967
				} );
1968
1969
				// To providers, unlink and remove are identical.
1970
				$providerAction = self::ACTION_REMOVE;
1971
				break;
1972
1973 View Code Duplication
			case self::ACTION_LINK_CONTINUE:
1974
				$state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
1975
				return is_array( $state ) ? $state['continueRequests'] : [];
1976
1977
			case self::ACTION_CHANGE:
1978
			case self::ACTION_REMOVE:
1979
				$providers = $this->getPrimaryAuthenticationProviders() +
1980
					$this->getSecondaryAuthenticationProviders();
1981
				break;
1982
1983
			// @codeCoverageIgnoreStart
1984
			default:
1985
				throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
1986
		}
1987
		// @codeCoverageIgnoreEnd
1988
1989
		return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
1990
	}
1991
1992
	/**
1993
	 * Internal request lookup for self::getAuthenticationRequests
1994
	 *
1995
	 * @param string $providerAction Action to pass to providers
1996
	 * @param array $options Options to pass to providers
1997
	 * @param AuthenticationProvider[] $providers
1998
	 * @param User|null $user
1999
	 * @return AuthenticationRequest[]
2000
	 */
2001
	private function getAuthenticationRequestsInternal(
2002
		$providerAction, array $options, array $providers, User $user = null
2003
	) {
2004
		$user = $user ?: \RequestContext::getMain()->getUser();
2005
		$options['username'] = $user->isAnon() ? null : $user->getName();
2006
2007
		// Query them and merge results
2008
		$reqs = [];
2009
		$allPrimaryRequired = null;
2010
		foreach ( $providers as $provider ) {
2011
			$isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2012
			$thisRequired = [];
2013
			foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2014
				$id = $req->getUniqueId();
2015
2016
				// If it's from a Primary, mark it as "primary-required" but
2017
				// track it for later.
2018
				if ( $isPrimary ) {
2019
					if ( $req->required ) {
2020
						$thisRequired[$id] = true;
2021
						$req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2022
					}
2023
				}
2024
2025
				if ( !isset( $reqs[$id] ) || $req->required === AuthenticationRequest::REQUIRED ) {
2026
					$reqs[$id] = $req;
2027
				}
2028
			}
2029
2030
			// Track which requests are required by all primaries
2031
			if ( $isPrimary ) {
2032
				$allPrimaryRequired = $allPrimaryRequired === null
2033
					? $thisRequired
2034
					: array_intersect_key( $allPrimaryRequired, $thisRequired );
2035
			}
2036
		}
2037
		// Any requests that were required by all primaries are required.
2038
		foreach ( (array)$allPrimaryRequired as $id => $dummy ) {
2039
			$reqs[$id]->required = AuthenticationRequest::REQUIRED;
2040
		}
2041
2042
		// AuthManager has its own req for some actions
2043
		switch ( $providerAction ) {
2044
			case self::ACTION_LOGIN:
2045
				$reqs[] = new RememberMeAuthenticationRequest;
2046
				break;
2047
2048
			case self::ACTION_CREATE:
2049
				$reqs[] = new UsernameAuthenticationRequest;
2050
				$reqs[] = new UserDataAuthenticationRequest;
2051
				if ( $options['username'] !== null ) {
2052
					$reqs[] = new CreationReasonAuthenticationRequest;
2053
					$options['username'] = null; // Don't fill in the username below
2054
				}
2055
				break;
2056
		}
2057
2058
		// Fill in reqs data
2059
		$this->fillRequests( $reqs, $providerAction, $options['username'], true );
2060
2061
		// For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2062
		if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2063
			$reqs = array_filter( $reqs, function ( $req ) {
2064
				return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2065
			} );
2066
		}
2067
2068
		return array_values( $reqs );
2069
	}
2070
2071
	/**
2072
	 * Set values in an array of requests
2073
	 * @param AuthenticationRequest[] &$reqs
2074
	 * @param string $action
2075
	 * @param string|null $username
2076
	 * @param boolean $forceAction
2077
	 */
2078
	private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2079
		foreach ( $reqs as $req ) {
2080
			if ( !$req->action || $forceAction ) {
2081
				$req->action = $action;
2082
			}
2083
			if ( $req->username === null ) {
2084
				$req->username = $username;
2085
			}
2086
		}
2087
	}
2088
2089
	/**
2090
	 * Determine whether a username exists
2091
	 * @param string $username
2092
	 * @param int $flags Bitfield of User:READ_* constants
2093
	 * @return bool
2094
	 */
2095
	public function userExists( $username, $flags = User::READ_NORMAL ) {
2096
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2097
			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...
2098
				return true;
2099
			}
2100
		}
2101
2102
		return false;
2103
	}
2104
2105
	/**
2106
	 * Determine whether a user property should be allowed to be changed.
2107
	 *
2108
	 * Supported properties are:
2109
	 *  - emailaddress
2110
	 *  - realname
2111
	 *  - nickname
2112
	 *
2113
	 * @param string $property
2114
	 * @return bool
2115
	 */
2116
	public function allowsPropertyChange( $property ) {
2117
		$providers = $this->getPrimaryAuthenticationProviders() +
2118
			$this->getSecondaryAuthenticationProviders();
2119
		foreach ( $providers as $provider ) {
2120
			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...
2121
				return false;
2122
			}
2123
		}
2124
		return true;
2125
	}
2126
2127
	/**
2128
	 * Get a provider by ID
2129
	 * @note This is public so extensions can check whether their own provider
2130
	 *  is installed and so they can read its configuration if necessary.
2131
	 *  Other uses are not recommended.
2132
	 * @param string $id
2133
	 * @return AuthenticationProvider|null
2134
	 */
2135
	public function getAuthenticationProvider( $id ) {
2136
		// Fast version
2137
		if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2138
			return $this->allAuthenticationProviders[$id];
2139
		}
2140
2141
		// Slow version: instantiate each kind and check
2142
		$providers = $this->getPrimaryAuthenticationProviders();
2143
		if ( isset( $providers[$id] ) ) {
2144
			return $providers[$id];
2145
		}
2146
		$providers = $this->getSecondaryAuthenticationProviders();
2147
		if ( isset( $providers[$id] ) ) {
2148
			return $providers[$id];
2149
		}
2150
		$providers = $this->getPreAuthenticationProviders();
2151
		if ( isset( $providers[$id] ) ) {
2152
			return $providers[$id];
2153
		}
2154
2155
		return null;
2156
	}
2157
2158
	/**@}*/
2159
2160
	/**
2161
	 * @name Internal methods
2162
	 * @{
2163
	 */
2164
2165
	/**
2166
	 * Store authentication in the current session
2167
	 * @protected For use by AuthenticationProviders
2168
	 * @param string $key
2169
	 * @param mixed $data Must be serializable
2170
	 */
2171
	public function setAuthenticationSessionData( $key, $data ) {
2172
		$session = $this->request->getSession();
2173
		$arr = $session->getSecret( 'authData' );
2174
		if ( !is_array( $arr ) ) {
2175
			$arr = [];
2176
		}
2177
		$arr[$key] = $data;
2178
		$session->setSecret( 'authData', $arr );
2179
	}
2180
2181
	/**
2182
	 * Fetch authentication data from the current session
2183
	 * @protected For use by AuthenticationProviders
2184
	 * @param string $key
2185
	 * @param mixed $default
2186
	 * @return mixed
2187
	 */
2188
	public function getAuthenticationSessionData( $key, $default = null ) {
2189
		$arr = $this->request->getSession()->getSecret( 'authData' );
2190
		if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2191
			return $arr[$key];
2192
		} else {
2193
			return $default;
2194
		}
2195
	}
2196
2197
	/**
2198
	 * Remove authentication data
2199
	 * @protected For use by AuthenticationProviders
2200
	 * @param string|null $key If null, all data is removed
2201
	 */
2202
	public function removeAuthenticationSessionData( $key ) {
2203
		$session = $this->request->getSession();
2204
		if ( $key === null ) {
2205
			$session->remove( 'authData' );
2206
		} else {
2207
			$arr = $session->getSecret( 'authData' );
2208
			if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2209
				unset( $arr[$key] );
2210
				$session->setSecret( 'authData', $arr );
2211
			}
2212
		}
2213
	}
2214
2215
	/**
2216
	 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2217
	 * @param string $class
2218
	 * @param array[] $specs
2219
	 * @return AuthenticationProvider[]
2220
	 */
2221
	protected function providerArrayFromSpecs( $class, array $specs ) {
2222
		$i = 0;
2223
		foreach ( $specs as &$spec ) {
2224
			$spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2225
		}
2226
		unset( $spec );
2227
		usort( $specs, function ( $a, $b ) {
2228
			return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2229
				?: $a['sort2'] - $b['sort2'];
2230
		} );
2231
2232
		$ret = [];
2233
		foreach ( $specs as $spec ) {
2234
			$provider = \ObjectFactory::getObjectFromSpec( $spec );
2235
			if ( !$provider instanceof $class ) {
2236
				throw new \RuntimeException(
2237
					"Expected instance of $class, got " . get_class( $provider )
2238
				);
2239
			}
2240
			$provider->setLogger( $this->logger );
2241
			$provider->setManager( $this );
2242
			$provider->setConfig( $this->config );
2243
			$id = $provider->getUniqueId();
2244 View Code Duplication
			if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2245
				throw new \RuntimeException(
2246
					"Duplicate specifications for id $id (classes " .
2247
					get_class( $provider ) . ' and ' .
2248
					get_class( $this->allAuthenticationProviders[$id] ) . ')'
2249
				);
2250
			}
2251
			$this->allAuthenticationProviders[$id] = $provider;
2252
			$ret[$id] = $provider;
2253
		}
2254
		return $ret;
2255
	}
2256
2257
	/**
2258
	 * Get the configuration
2259
	 * @return array
2260
	 */
2261
	private function getConfiguration() {
2262
		return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2263
	}
2264
2265
	/**
2266
	 * Get the list of PreAuthenticationProviders
2267
	 * @return PreAuthenticationProvider[]
2268
	 */
2269
	protected function getPreAuthenticationProviders() {
2270
		if ( $this->preAuthenticationProviders === null ) {
2271
			$conf = $this->getConfiguration();
2272
			$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...
2273
				PreAuthenticationProvider::class, $conf['preauth']
2274
			);
2275
		}
2276
		return $this->preAuthenticationProviders;
2277
	}
2278
2279
	/**
2280
	 * Get the list of PrimaryAuthenticationProviders
2281
	 * @return PrimaryAuthenticationProvider[]
2282
	 */
2283
	protected function getPrimaryAuthenticationProviders() {
2284
		if ( $this->primaryAuthenticationProviders === null ) {
2285
			$conf = $this->getConfiguration();
2286
			$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...
2287
				PrimaryAuthenticationProvider::class, $conf['primaryauth']
2288
			);
2289
		}
2290
		return $this->primaryAuthenticationProviders;
2291
	}
2292
2293
	/**
2294
	 * Get the list of SecondaryAuthenticationProviders
2295
	 * @return SecondaryAuthenticationProvider[]
2296
	 */
2297
	protected function getSecondaryAuthenticationProviders() {
2298
		if ( $this->secondaryAuthenticationProviders === null ) {
2299
			$conf = $this->getConfiguration();
2300
			$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...
2301
				SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2302
			);
2303
		}
2304
		return $this->secondaryAuthenticationProviders;
2305
	}
2306
2307
	/**
2308
	 * @param User $user
2309
	 * @param bool|null $remember
2310
	 */
2311
	private function setSessionDataForUser( $user, $remember = null ) {
2312
		$session = $this->request->getSession();
2313
		$delay = $session->delaySave();
2314
2315
		$session->resetId();
2316
		$session->resetAllTokens();
2317
		if ( $session->canSetUser() ) {
2318
			$session->setUser( $user );
2319
		}
2320
		if ( $remember !== null ) {
2321
			$session->setRememberUser( $remember );
2322
		}
2323
		$session->set( 'AuthManager:lastAuthId', $user->getId() );
2324
		$session->set( 'AuthManager:lastAuthTimestamp', time() );
2325
		$session->persist();
2326
2327
		\ScopedCallback::consume( $delay );
2328
2329
		\Hooks::run( 'UserLoggedIn', [ $user ] );
2330
	}
2331
2332
	/**
2333
	 * @param User $user
2334
	 * @param bool $useContextLang Use 'uselang' to set the user's language
2335
	 */
2336
	private function setDefaultUserOptions( User $user, $useContextLang ) {
2337
		global $wgContLang;
2338
2339
		$user->setToken();
2340
2341
		$lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
2342
		$user->setOption( 'language', $lang->getPreferredVariant() );
2343
2344
		if ( $wgContLang->hasVariants() ) {
2345
			$user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2346
		}
2347
	}
2348
2349
	/**
2350
	 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2351
	 * @param string $method
2352
	 * @param array $args
2353
	 */
2354
	private function callMethodOnProviders( $which, $method, array $args ) {
2355
		$providers = [];
2356
		if ( $which & 1 ) {
2357
			$providers += $this->getPreAuthenticationProviders();
2358
		}
2359
		if ( $which & 2 ) {
2360
			$providers += $this->getPrimaryAuthenticationProviders();
2361
		}
2362
		if ( $which & 4 ) {
2363
			$providers += $this->getSecondaryAuthenticationProviders();
2364
		}
2365
		foreach ( $providers as $provider ) {
2366
			call_user_func_array( [ $provider, $method ], $args );
2367
		}
2368
	}
2369
2370
	/**
2371
	 * Reset the internal caching for unit testing
2372
	 */
2373
	public static function resetCache() {
2374
		if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2375
			// @codeCoverageIgnoreStart
2376
			throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2377
			// @codeCoverageIgnoreEnd
2378
		}
2379
2380
		self::$instance = null;
2381
	}
2382
2383
	/**@}*/
2384
2385
}
2386
2387
/**
2388
 * For really cool vim folding this needs to be at the end:
2389
 * vim: foldmarker=@{,@} foldmethod=marker
2390
 */
2391