Completed
Branch master (9259dd)
by
unknown
27:26
created

AuthManager::continueAuthentication()   F

Complexity

Conditions 52
Paths 3759

Size

Total Lines 293
Code Lines 197

Duplication

Lines 100
Ratio 34.13 %

Importance

Changes 0
Metric Value
cc 52
eloc 197
c 0
b 0
f 0
nc 3759
nop 1
dl 100
loc 293
rs 2

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * 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 array $options
882
	 *  - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
883
	 *  - creating: (bool) For internal use only. Never specify this.
884
	 * @return Status
885
	 */
886
	public function canCreateAccount( $username, $options = [] ) {
887
		// Back compat
888
		if ( is_int( $options ) ) {
889
			$options = [ 'flags' => $options ];
890
		}
891
		$options += [
892
			'flags' => User::READ_NORMAL,
893
			'creating' => false,
894
		];
895
		$flags = $options['flags'];
896
897
		if ( !$this->canCreateAccounts() ) {
898
			return Status::newFatal( 'authmanager-create-disabled' );
899
		}
900
901
		if ( $this->userExists( $username, $flags ) ) {
902
			return Status::newFatal( 'userexists' );
903
		}
904
905
		$user = User::newFromName( $username, 'creatable' );
906
		if ( !is_object( $user ) ) {
907
			return Status::newFatal( 'noname' );
908
		} else {
909
			$user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
910
			if ( $user->getId() !== 0 ) {
911
				return Status::newFatal( 'userexists' );
912
			}
913
		}
914
915
		// Denied by providers?
916
		$providers = $this->getPreAuthenticationProviders() +
917
			$this->getPrimaryAuthenticationProviders() +
918
			$this->getSecondaryAuthenticationProviders();
919
		foreach ( $providers as $provider ) {
920
			$status = $provider->testUserForCreation( $user, false, $options );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testUserForCreation() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractPreAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\LegacyHookPreAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider, MediaWiki\Auth\ThrottlePreAuthenticationProvider.

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
921
			if ( !$status->isGood() ) {
922
				return Status::wrap( $status );
923
			}
924
		}
925
926
		return Status::newGood();
927
	}
928
929
	/**
930
	 * Basic permissions checks on whether a user can create accounts
931
	 * @param User $creator User doing the account creation
932
	 * @return Status
933
	 */
934
	public function checkAccountCreatePermissions( User $creator ) {
935
		// Wiki is read-only?
936
		if ( wfReadOnly() ) {
937
			return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
938
		}
939
940
		// This is awful, this permission check really shouldn't go through Title.
941
		$permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
942
			->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
943
		if ( $permErrors ) {
944
			$status = Status::newGood();
945
			foreach ( $permErrors as $args ) {
946
				call_user_func_array( [ $status, 'fatal' ], $args );
947
			}
948
			return $status;
949
		}
950
951
		$block = $creator->isBlockedFromCreateAccount();
952
		if ( $block ) {
953
			$errorParams = [
954
				$block->getTarget(),
955
				$block->mReason ?: wfMessage( 'blockednoreason' )->text(),
956
				$block->getByName()
957
			];
958
959 View Code Duplication
			if ( $block->getType() === \Block::TYPE_RANGE ) {
960
				$errorMessage = 'cantcreateaccount-range-text';
961
				$errorParams[] = $this->getRequest()->getIP();
962
			} else {
963
				$errorMessage = 'cantcreateaccount-text';
964
			}
965
966
			return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
967
		}
968
969
		$ip = $this->getRequest()->getIP();
970
		if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
971
			return Status::newFatal( 'sorbs_create_account_reason' );
972
		}
973
974
		return Status::newGood();
975
	}
976
977
	/**
978
	 * Start an account creation flow
979
	 *
980
	 * In addition to the AuthenticationRequests returned by
981
	 * $this->getAuthenticationRequests(), a client might include a
982
	 * CreateFromLoginAuthenticationRequest from a previous login attempt. If
983
	 * <code>
984
	 * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
985
	 * </code>
986
	 * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
987
	 * should be omitted. If the CreateFromLoginAuthenticationRequest has a
988
	 * username set, that username must be used for all other requests.
989
	 *
990
	 * @param User $creator User doing the account creation
991
	 * @param AuthenticationRequest[] $reqs
992
	 * @param string $returnToUrl Url that REDIRECT responses should eventually
993
	 *  return to.
994
	 * @return AuthenticationResponse
995
	 */
996
	public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
997
		$session = $this->request->getSession();
998
		if ( !$this->canCreateAccounts() ) {
999
			// Caller should have called canCreateAccounts()
1000
			$session->remove( 'AuthManager::accountCreationState' );
1001
			throw new \LogicException( 'Account creation is not possible' );
1002
		}
1003
1004
		try {
1005
			$username = AuthenticationRequest::getUsernameFromRequests( $reqs );
1006
		} catch ( \UnexpectedValueException $ex ) {
1007
			$username = null;
1008
		}
1009
		if ( $username === null ) {
1010
			$this->logger->debug( __METHOD__ . ': No username provided' );
1011
			return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1012
		}
1013
1014
		// Permissions check
1015
		$status = $this->checkAccountCreatePermissions( $creator );
1016 View Code Duplication
		if ( !$status->isGood() ) {
1017
			$this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1018
				'user' => $username,
1019
				'creator' => $creator->getName(),
1020
				'reason' => $status->getWikiText( null, null, 'en' )
1021
			] );
1022
			return AuthenticationResponse::newFail( $status->getMessage() );
1023
		}
1024
1025
		$status = $this->canCreateAccount(
1026
			$username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1027
		);
1028 View Code Duplication
		if ( !$status->isGood() ) {
1029
			$this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1030
				'user' => $username,
1031
				'creator' => $creator->getName(),
1032
				'reason' => $status->getWikiText( null, null, 'en' )
1033
			] );
1034
			return AuthenticationResponse::newFail( $status->getMessage() );
1035
		}
1036
1037
		$user = User::newFromName( $username, 'creatable' );
1038
		foreach ( $reqs as $req ) {
1039
			$req->username = $username;
1040
			$req->returnToUrl = $returnToUrl;
1041
			if ( $req instanceof UserDataAuthenticationRequest ) {
1042
				$status = $req->populateUser( $user );
0 ignored issues
show
Security Bug introduced by
It seems like $user defined by \User::newFromName($username, 'creatable') on line 1037 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...
1043 View Code Duplication
				if ( !$status->isGood() ) {
1044
					$status = Status::wrap( $status );
1045
					$session->remove( 'AuthManager::accountCreationState' );
1046
					$this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1047
						'user' => $user->getName(),
1048
						'creator' => $creator->getName(),
1049
						'reason' => $status->getWikiText( null, null, 'en' ),
1050
					] );
1051
					return AuthenticationResponse::newFail( $status->getMessage() );
1052
				}
1053
			}
1054
		}
1055
1056
		$this->removeAuthenticationSessionData( null );
1057
1058
		$state = [
1059
			'username' => $username,
1060
			'userid' => 0,
1061
			'creatorid' => $creator->getId(),
1062
			'creatorname' => $creator->getName(),
1063
			'reqs' => $reqs,
1064
			'returnToUrl' => $returnToUrl,
1065
			'primary' => null,
1066
			'primaryResponse' => null,
1067
			'secondary' => [],
1068
			'continueRequests' => [],
1069
			'maybeLink' => [],
1070
			'ranPreTests' => false,
1071
		];
1072
1073
		// Special case: converting a login to an account creation
1074
		$req = AuthenticationRequest::getRequestByClass(
1075
			$reqs, CreateFromLoginAuthenticationRequest::class
1076
		);
1077 View Code Duplication
		if ( $req ) {
1078
			$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...
1079
1080
			if ( $req->createRequest ) {
1081
				$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...
1082
				$state['reqs'][] = $req->createRequest;
1083
			}
1084
		}
1085
1086
		$session->setSecret( 'AuthManager::accountCreationState', $state );
1087
		$session->persist();
1088
1089
		return $this->continueAccountCreation( $reqs );
1090
	}
1091
1092
	/**
1093
	 * Continue an account creation flow
1094
	 * @param AuthenticationRequest[] $reqs
1095
	 * @return AuthenticationResponse
1096
	 */
1097
	public function continueAccountCreation( array $reqs ) {
1098
		$session = $this->request->getSession();
1099
		try {
1100
			if ( !$this->canCreateAccounts() ) {
1101
				// Caller should have called canCreateAccounts()
1102
				$session->remove( 'AuthManager::accountCreationState' );
1103
				throw new \LogicException( 'Account creation is not possible' );
1104
			}
1105
1106
			$state = $session->getSecret( 'AuthManager::accountCreationState' );
1107
			if ( !is_array( $state ) ) {
1108
				return AuthenticationResponse::newFail(
1109
					wfMessage( 'authmanager-create-not-in-progress' )
1110
				);
1111
			}
1112
			$state['continueRequests'] = [];
1113
1114
			// Step 0: Prepare and validate the input
1115
1116
			$user = User::newFromName( $state['username'], 'creatable' );
1117 View Code Duplication
			if ( !is_object( $user ) ) {
1118
				$session->remove( 'AuthManager::accountCreationState' );
1119
				$this->logger->debug( __METHOD__ . ': Invalid username', [
1120
					'user' => $state['username'],
1121
				] );
1122
				return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1123
			}
1124
1125
			if ( $state['creatorid'] ) {
1126
				$creator = User::newFromId( $state['creatorid'] );
1127
			} else {
1128
				$creator = new User;
1129
				$creator->setName( $state['creatorname'] );
1130
			}
1131
1132
			// Avoid account creation races on double submissions
1133
			$cache = \ObjectCache::getLocalClusterInstance();
1134
			$lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1135 View Code Duplication
			if ( !$lock ) {
1136
				// Don't clear AuthManager::accountCreationState for this code
1137
				// path because the process that won the race owns it.
1138
				$this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1139
					'user' => $user->getName(),
1140
					'creator' => $creator->getName(),
1141
				] );
1142
				return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1143
			}
1144
1145
			// Permissions check
1146
			$status = $this->checkAccountCreatePermissions( $creator );
1147 View Code Duplication
			if ( !$status->isGood() ) {
1148
				$this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1149
					'user' => $user->getName(),
1150
					'creator' => $creator->getName(),
1151
					'reason' => $status->getWikiText( null, null, 'en' )
1152
				] );
1153
				$ret = AuthenticationResponse::newFail( $status->getMessage() );
1154
				$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1155
				$session->remove( 'AuthManager::accountCreationState' );
1156
				return $ret;
1157
			}
1158
1159
			// Load from master for existence check
1160
			$user->load( User::READ_LOCKING );
1161
1162
			if ( $state['userid'] === 0 ) {
1163 View Code Duplication
				if ( $user->getId() != 0 ) {
1164
					$this->logger->debug( __METHOD__ . ': User exists locally', [
1165
						'user' => $user->getName(),
1166
						'creator' => $creator->getName(),
1167
					] );
1168
					$ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1169
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1170
					$session->remove( 'AuthManager::accountCreationState' );
1171
					return $ret;
1172
				}
1173
			} else {
1174
				if ( $user->getId() == 0 ) {
1175
					$this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1176
						'user' => $user->getName(),
1177
						'creator' => $creator->getName(),
1178
						'expected_id' => $state['userid'],
1179
					] );
1180
					throw new \UnexpectedValueException(
1181
						"User \"{$state['username']}\" should exist now, but doesn't!"
1182
					);
1183
				}
1184
				if ( $user->getId() != $state['userid'] ) {
1185
					$this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1186
						'user' => $user->getName(),
1187
						'creator' => $creator->getName(),
1188
						'expected_id' => $state['userid'],
1189
						'actual_id' => $user->getId(),
1190
					] );
1191
					throw new \UnexpectedValueException(
1192
						"User \"{$state['username']}\" exists, but " .
1193
							"ID {$user->getId()} != {$state['userid']}!"
1194
					);
1195
				}
1196
			}
1197
			foreach ( $state['reqs'] as $req ) {
1198
				if ( $req instanceof UserDataAuthenticationRequest ) {
1199
					$status = $req->populateUser( $user );
1200 View Code Duplication
					if ( !$status->isGood() ) {
1201
						// This should never happen...
1202
						$status = Status::wrap( $status );
1203
						$this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1204
							'user' => $user->getName(),
1205
							'creator' => $creator->getName(),
1206
							'reason' => $status->getWikiText( null, null, 'en' ),
1207
						] );
1208
						$ret = AuthenticationResponse::newFail( $status->getMessage() );
1209
						$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1210
						$session->remove( 'AuthManager::accountCreationState' );
1211
						return $ret;
1212
					}
1213
				}
1214
			}
1215
1216
			foreach ( $reqs as $req ) {
1217
				$req->returnToUrl = $state['returnToUrl'];
1218
				$req->username = $state['username'];
1219
			}
1220
1221
			// Run pre-creation tests, if we haven't already
1222
			if ( !$state['ranPreTests'] ) {
1223
				$providers = $this->getPreAuthenticationProviders() +
1224
					$this->getPrimaryAuthenticationProviders() +
1225
					$this->getSecondaryAuthenticationProviders();
1226
				foreach ( $providers as $id => $provider ) {
1227
					$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...
1228
					if ( !$status->isGood() ) {
1229
						$this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1230
							'user' => $user->getName(),
1231
							'creator' => $creator->getName(),
1232
						] );
1233
						$ret = AuthenticationResponse::newFail(
1234
							Status::wrap( $status )->getMessage()
1235
						);
1236
						$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1237
						$session->remove( 'AuthManager::accountCreationState' );
1238
						return $ret;
1239
					}
1240
				}
1241
1242
				$state['ranPreTests'] = true;
1243
			}
1244
1245
			// Step 1: Choose a primary authentication provider and call it until it succeeds.
1246
1247
			if ( $state['primary'] === null ) {
1248
				// We haven't picked a PrimaryAuthenticationProvider yet
1249
				foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1250
					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...
1251
						continue;
1252
					}
1253
					$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...
1254
					switch ( $res->status ) {
1255
						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...
1256
							$this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1257
								'user' => $user->getName(),
1258
								'creator' => $creator->getName(),
1259
							] );
1260
							$state['primary'] = $id;
1261
							$state['primaryResponse'] = $res;
1262
							break 2;
1263 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...
1264
							$this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1265
								'user' => $user->getName(),
1266
								'creator' => $creator->getName(),
1267
							] );
1268
							$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1269
							$session->remove( 'AuthManager::accountCreationState' );
1270
							return $res;
1271
						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...
1272
							// Continue loop
1273
							break;
1274
						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...
1275
						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...
1276
							$this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1277
								'user' => $user->getName(),
1278
								'creator' => $creator->getName(),
1279
							] );
1280
							$this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1281
							$state['primary'] = $id;
1282
							$state['continueRequests'] = $res->neededRequests;
1283
							$session->setSecret( 'AuthManager::accountCreationState', $state );
1284
							return $res;
1285
1286
							// @codeCoverageIgnoreStart
1287
						default:
1288
							throw new \DomainException(
1289
								get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1290
							);
1291
							// @codeCoverageIgnoreEnd
1292
					}
1293
				}
1294 View Code Duplication
				if ( $state['primary'] === null ) {
1295
					$this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1296
						'user' => $user->getName(),
1297
						'creator' => $creator->getName(),
1298
					] );
1299
					$ret = AuthenticationResponse::newFail(
1300
						wfMessage( 'authmanager-create-no-primary' )
1301
					);
1302
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1303
					$session->remove( 'AuthManager::accountCreationState' );
1304
					return $ret;
1305
				}
1306
			} elseif ( $state['primaryResponse'] === null ) {
1307
				$provider = $this->getAuthenticationProvider( $state['primary'] );
1308
				if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1309
					// Configuration changed? Force them to start over.
1310
					// @codeCoverageIgnoreStart
1311
					$ret = AuthenticationResponse::newFail(
1312
						wfMessage( 'authmanager-create-not-in-progress' )
1313
					);
1314
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1315
					$session->remove( 'AuthManager::accountCreationState' );
1316
					return $ret;
1317
					// @codeCoverageIgnoreEnd
1318
				}
1319
				$id = $provider->getUniqueId();
1320
				$res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1321
				switch ( $res->status ) {
1322
					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...
1323
						$this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1324
							'user' => $user->getName(),
1325
							'creator' => $creator->getName(),
1326
						] );
1327
						$state['primaryResponse'] = $res;
1328
						break;
1329 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...
1330
						$this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1331
							'user' => $user->getName(),
1332
							'creator' => $creator->getName(),
1333
						] );
1334
						$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1335
						$session->remove( 'AuthManager::accountCreationState' );
1336
						return $res;
1337
					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...
1338
					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...
1339
						$this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1340
							'user' => $user->getName(),
1341
							'creator' => $creator->getName(),
1342
						] );
1343
						$this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1344
						$state['continueRequests'] = $res->neededRequests;
1345
						$session->setSecret( 'AuthManager::accountCreationState', $state );
1346
						return $res;
1347
					default:
1348
						throw new \DomainException(
1349
							get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1350
						);
1351
				}
1352
			}
1353
1354
			// Step 2: Primary authentication succeeded, create the User object
1355
			// and add the user locally.
1356
1357
			if ( $state['userid'] === 0 ) {
1358
				$this->logger->info( 'Creating user {user} during account creation', [
1359
					'user' => $user->getName(),
1360
					'creator' => $creator->getName(),
1361
				] );
1362
				$status = $user->addToDatabase();
1363
				if ( !$status->isOk() ) {
1364
					// @codeCoverageIgnoreStart
1365
					$ret = AuthenticationResponse::newFail( $status->getMessage() );
1366
					$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1367
					$session->remove( 'AuthManager::accountCreationState' );
1368
					return $ret;
1369
					// @codeCoverageIgnoreEnd
1370
				}
1371
				$this->setDefaultUserOptions( $user, $creator->isAnon() );
1372
				\Hooks::run( 'LocalUserCreated', [ $user, false ] );
1373
				$user->saveSettings();
1374
				$state['userid'] = $user->getId();
1375
1376
				// Update user count
1377
				\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1378
1379
				// Watch user's userpage and talk page
1380
				$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1381
1382
				// Inform the provider
1383
				$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...
1384
1385
				// Log the creation
1386
				if ( $this->config->get( 'NewUserLog' ) ) {
1387
					$isAnon = $creator->isAnon();
1388
					$logEntry = new \ManualLogEntry(
1389
						'newusers',
1390
						$logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1391
					);
1392
					$logEntry->setPerformer( $isAnon ? $user : $creator );
1393
					$logEntry->setTarget( $user->getUserPage() );
1394
					$req = AuthenticationRequest::getRequestByClass(
1395
						$state['reqs'], CreationReasonAuthenticationRequest::class
1396
					);
1397
					$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...
1398
					$logEntry->setParameters( [
1399
						'4::userid' => $user->getId(),
1400
					] );
1401
					$logid = $logEntry->insert();
1402
					$logEntry->publish( $logid );
1403
				}
1404
			}
1405
1406
			// Step 3: Iterate over all the secondary authentication providers.
1407
1408
			$beginReqs = $state['reqs'];
1409
1410
			foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1411 View Code Duplication
				if ( !isset( $state['secondary'][$id] ) ) {
1412
					// This provider isn't started yet, so we pass it the set
1413
					// of reqs from beginAuthentication instead of whatever
1414
					// might have been used by a previous provider in line.
1415
					$func = 'beginSecondaryAccountCreation';
1416
					$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...
1417
				} elseif ( !$state['secondary'][$id] ) {
1418
					$func = 'continueSecondaryAccountCreation';
1419
					$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...
1420
				} else {
1421
					continue;
1422
				}
1423
				switch ( $res->status ) {
1424
					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...
1425
						$this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1426
							'user' => $user->getName(),
1427
							'creator' => $creator->getName(),
1428
						] );
1429
						// fall through
1430
					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...
1431
						$state['secondary'][$id] = true;
1432
						break;
1433
					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...
1434
					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...
1435
						$this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1436
							'user' => $user->getName(),
1437
							'creator' => $creator->getName(),
1438
						] );
1439
						$this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1440
						$state['secondary'][$id] = false;
1441
						$state['continueRequests'] = $res->neededRequests;
1442
						$session->setSecret( 'AuthManager::accountCreationState', $state );
1443
						return $res;
1444
					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...
1445
						throw new \DomainException(
1446
							get_class( $provider ) . "::{$func}() returned $res->status." .
1447
							' Secondary providers are not allowed to fail account creation, that' .
1448
							' should have been done via testForAccountCreation().'
1449
						);
1450
							// @codeCoverageIgnoreStart
1451
					default:
1452
						throw new \DomainException(
1453
							get_class( $provider ) . "::{$func}() returned $res->status"
1454
						);
1455
							// @codeCoverageIgnoreEnd
1456
				}
1457
			}
1458
1459
			$id = $user->getId();
1460
			$name = $user->getName();
1461
			$req = new CreatedAccountAuthenticationRequest( $id, $name );
1462
			$ret = AuthenticationResponse::newPass( $name );
1463
			$ret->loginRequest = $req;
1464
			$this->createdAccountAuthenticationRequests[] = $req;
1465
1466
			$this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1467
				'user' => $user->getName(),
1468
				'creator' => $creator->getName(),
1469
			] );
1470
1471
			$this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1472
			$session->remove( 'AuthManager::accountCreationState' );
1473
			$this->removeAuthenticationSessionData( null );
1474
			return $ret;
1475
		} catch ( \Exception $ex ) {
1476
			$session->remove( 'AuthManager::accountCreationState' );
1477
			throw $ex;
1478
		}
1479
	}
1480
1481
	/**
1482
	 * Auto-create an account, and log into that account
1483
	 * @param User $user User to auto-create
1484
	 * @param string $source What caused the auto-creation? This must be the ID
1485
	 *  of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
1486
	 * @param bool $login Whether to also log the user in
1487
	 * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1488
	 */
1489
	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...
1490
		if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1491
			!$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1492
		) {
1493
			throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1494
		}
1495
1496
		$username = $user->getName();
1497
1498
		// Try the local user from the slave DB
1499
		$localId = User::idFromName( $username );
1500
		$flags = User::READ_NORMAL;
1501
1502
		// Fetch the user ID from the master, so that we don't try to create the user
1503
		// when they already exist, due to replication lag
1504
		// @codeCoverageIgnoreStart
1505 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...
1506
			$localId = User::idFromName( $username, User::READ_LATEST );
1507
			$flags = User::READ_LATEST;
1508
		}
1509
		// @codeCoverageIgnoreEnd
1510
1511
		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...
1512
			$this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1513
				'username' => $username,
1514
			] );
1515
			$user->setId( $localId );
1516
			$user->loadFromId( $flags );
1517
			if ( $login ) {
1518
				$this->setSessionDataForUser( $user );
1519
			}
1520
			$status = Status::newGood();
1521
			$status->warning( 'userexists' );
1522
			return $status;
1523
		}
1524
1525
		// Wiki is read-only?
1526
		if ( wfReadOnly() ) {
1527
			$this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1528
				'username' => $username,
1529
				'reason' => wfReadOnlyReason(),
1530
			] );
1531
			$user->setId( 0 );
1532
			$user->loadFromId();
1533
			return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
1534
		}
1535
1536
		// Check the session, if we tried to create this user already there's
1537
		// no point in retrying.
1538
		$session = $this->request->getSession();
1539
		if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1540
			$this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1541
				'username' => $username,
1542
				'sessionid' => $session->getId(),
1543
			] );
1544
			$user->setId( 0 );
1545
			$user->loadFromId();
1546
			$reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1547
			if ( $reason instanceof StatusValue ) {
1548
				return Status::wrap( $reason );
1549
			} else {
1550
				return Status::newFatal( $reason );
1551
			}
1552
		}
1553
1554
		// Is the username creatable?
1555
		if ( !User::isCreatableName( $username ) ) {
1556
			$this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1557
				'username' => $username,
1558
			] );
1559
			$session->set( 'AuthManager::AutoCreateBlacklist', 'noname', 600 );
1560
			$user->setId( 0 );
1561
			$user->loadFromId();
1562
			return Status::newFatal( 'noname' );
1563
		}
1564
1565
		// Is the IP user able to create accounts?
1566
		$anon = new User;
1567
		if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1568
			$this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1569
				'username' => $username,
1570
				'ip' => $anon->getName(),
1571
			] );
1572
			$session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm', 600 );
1573
			$session->persist();
1574
			$user->setId( 0 );
1575
			$user->loadFromId();
1576
			return Status::newFatal( 'authmanager-autocreate-noperm' );
1577
		}
1578
1579
		// Avoid account creation races on double submissions
1580
		$cache = \ObjectCache::getLocalClusterInstance();
1581
		$lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1582 View Code Duplication
		if ( !$lock ) {
1583
			$this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1584
				'user' => $username,
1585
			] );
1586
			$user->setId( 0 );
1587
			$user->loadFromId();
1588
			return Status::newFatal( 'usernameinprogress' );
1589
		}
1590
1591
		// Denied by providers?
1592
		$options = [
1593
			'flags' => User::READ_LATEST,
1594
			'creating' => true,
1595
		];
1596
		$providers = $this->getPreAuthenticationProviders() +
1597
			$this->getPrimaryAuthenticationProviders() +
1598
			$this->getSecondaryAuthenticationProviders();
1599
		foreach ( $providers as $provider ) {
1600
			$status = $provider->testUserForCreation( $user, $source, $options );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testUserForCreation() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractPreAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\LegacyHookPreAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider, MediaWiki\Auth\ThrottlePreAuthenticationProvider.

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1601 View Code Duplication
			if ( !$status->isGood() ) {
1602
				$ret = Status::wrap( $status );
1603
				$this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1604
					'username' => $username,
1605
					'reason' => $ret->getWikiText( null, null, 'en' ),
1606
				] );
1607
				$session->set( 'AuthManager::AutoCreateBlacklist', $status, 600 );
1608
				$user->setId( 0 );
1609
				$user->loadFromId();
1610
				return $ret;
1611
			}
1612
		}
1613
1614
		// Ignore warnings about master connections/writes...hard to avoid here
1615
		\Profiler::instance()->getTransactionProfiler()->resetExpectations();
1616
1617
		$backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1618 View Code Duplication
		if ( $cache->get( $backoffKey ) ) {
1619
			$this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1620
				'username' => $username,
1621
			] );
1622
			$user->setId( 0 );
1623
			$user->loadFromId();
1624
			return Status::newFatal( 'authmanager-autocreate-exception' );
1625
		}
1626
1627
		// Checks passed, create the user...
1628
		$from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
1629
		$this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1630
			'username' => $username,
1631
			'from' => $from,
1632
		] );
1633
1634
		try {
1635
			$status = $user->addToDatabase();
1636
			if ( !$status->isOk() ) {
1637
				// double-check for a race condition (T70012)
1638
				$localId = User::idFromName( $username, User::READ_LATEST );
1639
				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...
1640
					$this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1641
						'username' => $username,
1642
					] );
1643
					$user->setId( $localId );
1644
					$user->loadFromId( User::READ_LATEST );
1645
					if ( $login ) {
1646
						$this->setSessionDataForUser( $user );
1647
					}
1648
					$status = Status::newGood();
1649
					$status->warning( 'userexists' );
1650 View Code Duplication
				} else {
1651
					$this->logger->error( __METHOD__ . ': {username} failed with message {message}', [
1652
						'username' => $username,
1653
						'message' => $status->getWikiText( null, null, 'en' )
1654
					] );
1655
					$user->setId( 0 );
1656
					$user->loadFromId();
1657
				}
1658
				return $status;
1659
			}
1660
		} catch ( \Exception $ex ) {
1661
			$this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1662
				'username' => $username,
1663
				'exception' => $ex,
1664
			] );
1665
			// Do not keep throwing errors for a while
1666
			$cache->set( $backoffKey, 1, 600 );
1667
			// Bubble up error; which should normally trigger DB rollbacks
1668
			throw $ex;
1669
		}
1670
1671
		$this->setDefaultUserOptions( $user, false );
1672
1673
		// Inform the providers
1674
		$this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1675
1676
		\Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1677
		\Hooks::run( 'LocalUserCreated', [ $user, true ] );
1678
		$user->saveSettings();
1679
1680
		// Update user count
1681
		\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1682
1683
		// Watch user's userpage and talk page
1684
		$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1685
1686
		// Log the creation
1687
		if ( $this->config->get( 'NewUserLog' ) ) {
1688
			$logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1689
			$logEntry->setPerformer( $user );
1690
			$logEntry->setTarget( $user->getUserPage() );
1691
			$logEntry->setComment( '' );
1692
			$logEntry->setParameters( [
1693
				'4::userid' => $user->getId(),
1694
			] );
1695
			$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...
1696
		}
1697
1698
		if ( $login ) {
1699
			$this->setSessionDataForUser( $user );
1700
		}
1701
1702
		return Status::newGood();
1703
	}
1704
1705
	/**@}*/
1706
1707
	/**
1708
	 * @name Account linking
1709
	 * @{
1710
	 */
1711
1712
	/**
1713
	 * Determine whether accounts can be linked
1714
	 * @return bool
1715
	 */
1716 View Code Duplication
	public function canLinkAccounts() {
1717
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1718
			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...
1719
				return true;
1720
			}
1721
		}
1722
		return false;
1723
	}
1724
1725
	/**
1726
	 * Start an account linking flow
1727
	 *
1728
	 * @param User $user User being linked
1729
	 * @param AuthenticationRequest[] $reqs
1730
	 * @param string $returnToUrl Url that REDIRECT responses should eventually
1731
	 *  return to.
1732
	 * @return AuthenticationResponse
1733
	 */
1734
	public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1735
		$session = $this->request->getSession();
1736
		$session->remove( 'AuthManager::accountLinkState' );
1737
1738
		if ( !$this->canLinkAccounts() ) {
1739
			// Caller should have called canLinkAccounts()
1740
			throw new \LogicException( 'Account linking is not possible' );
1741
		}
1742
1743
		if ( $user->getId() === 0 ) {
1744
			if ( !User::isUsableName( $user->getName() ) ) {
1745
				$msg = wfMessage( 'noname' );
1746
			} else {
1747
				$msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1748
			}
1749
			return AuthenticationResponse::newFail( $msg );
1750
		}
1751
		foreach ( $reqs as $req ) {
1752
			$req->username = $user->getName();
1753
			$req->returnToUrl = $returnToUrl;
1754
		}
1755
1756
		$this->removeAuthenticationSessionData( null );
1757
1758
		$providers = $this->getPreAuthenticationProviders();
1759
		foreach ( $providers as $id => $provider ) {
1760
			$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...
1761
			if ( !$status->isGood() ) {
1762
				$this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1763
					'user' => $user->getName(),
1764
				] );
1765
				$ret = AuthenticationResponse::newFail(
1766
					Status::wrap( $status )->getMessage()
1767
				);
1768
				$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1769
				return $ret;
1770
			}
1771
		}
1772
1773
		$state = [
1774
			'username' => $user->getName(),
1775
			'userid' => $user->getId(),
1776
			'returnToUrl' => $returnToUrl,
1777
			'primary' => null,
1778
			'continueRequests' => [],
1779
		];
1780
1781
		$providers = $this->getPrimaryAuthenticationProviders();
1782
		foreach ( $providers as $id => $provider ) {
1783
			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...
1784
				continue;
1785
			}
1786
1787
			$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...
1788 View Code Duplication
			switch ( $res->status ) {
1789
				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...
1790
					$this->logger->info( "Account linked to {user} by $id", [
1791
						'user' => $user->getName(),
1792
					] );
1793
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1794
					return $res;
1795
1796
				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...
1797
					$this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1798
						'user' => $user->getName(),
1799
					] );
1800
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1801
					return $res;
1802
1803
				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...
1804
					// Continue loop
1805
					break;
1806
1807
				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...
1808
				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...
1809
					$this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1810
						'user' => $user->getName(),
1811
					] );
1812
					$this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1813
					$state['primary'] = $id;
1814
					$state['continueRequests'] = $res->neededRequests;
1815
					$session->setSecret( 'AuthManager::accountLinkState', $state );
1816
					$session->persist();
1817
					return $res;
1818
1819
					// @codeCoverageIgnoreStart
1820
				default:
1821
					throw new \DomainException(
1822
						get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1823
					);
1824
					// @codeCoverageIgnoreEnd
1825
			}
1826
		}
1827
1828
		$this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1829
			'user' => $user->getName(),
1830
		] );
1831
		$ret = AuthenticationResponse::newFail(
1832
			wfMessage( 'authmanager-link-no-primary' )
1833
		);
1834
		$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1835
		return $ret;
1836
	}
1837
1838
	/**
1839
	 * Continue an account linking flow
1840
	 * @param AuthenticationRequest[] $reqs
1841
	 * @return AuthenticationResponse
1842
	 */
1843
	public function continueAccountLink( array $reqs ) {
1844
		$session = $this->request->getSession();
1845
		try {
1846
			if ( !$this->canLinkAccounts() ) {
1847
				// Caller should have called canLinkAccounts()
1848
				$session->remove( 'AuthManager::accountLinkState' );
1849
				throw new \LogicException( 'Account linking is not possible' );
1850
			}
1851
1852
			$state = $session->getSecret( 'AuthManager::accountLinkState' );
1853
			if ( !is_array( $state ) ) {
1854
				return AuthenticationResponse::newFail(
1855
					wfMessage( 'authmanager-link-not-in-progress' )
1856
				);
1857
			}
1858
			$state['continueRequests'] = [];
1859
1860
			// Step 0: Prepare and validate the input
1861
1862
			$user = User::newFromName( $state['username'], 'usable' );
1863
			if ( !is_object( $user ) ) {
1864
				$session->remove( 'AuthManager::accountLinkState' );
1865
				return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1866
			}
1867
			if ( $user->getId() != $state['userid'] ) {
1868
				throw new \UnexpectedValueException(
1869
					"User \"{$state['username']}\" is valid, but " .
1870
						"ID {$user->getId()} != {$state['userid']}!"
1871
				);
1872
			}
1873
1874
			foreach ( $reqs as $req ) {
1875
				$req->username = $state['username'];
1876
				$req->returnToUrl = $state['returnToUrl'];
1877
			}
1878
1879
			// Step 1: Call the primary again until it succeeds
1880
1881
			$provider = $this->getAuthenticationProvider( $state['primary'] );
1882
			if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1883
				// Configuration changed? Force them to start over.
1884
				// @codeCoverageIgnoreStart
1885
				$ret = AuthenticationResponse::newFail(
1886
					wfMessage( 'authmanager-link-not-in-progress' )
1887
				);
1888
				$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1889
				$session->remove( 'AuthManager::accountLinkState' );
1890
				return $ret;
1891
				// @codeCoverageIgnoreEnd
1892
			}
1893
			$id = $provider->getUniqueId();
1894
			$res = $provider->continuePrimaryAccountLink( $user, $reqs );
1895 View Code Duplication
			switch ( $res->status ) {
1896
				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...
1897
					$this->logger->info( "Account linked to {user} by $id", [
1898
						'user' => $user->getName(),
1899
					] );
1900
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1901
					$session->remove( 'AuthManager::accountLinkState' );
1902
					return $res;
1903
				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...
1904
					$this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1905
						'user' => $user->getName(),
1906
					] );
1907
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1908
					$session->remove( 'AuthManager::accountLinkState' );
1909
					return $res;
1910
				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...
1911
				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...
1912
					$this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1913
						'user' => $user->getName(),
1914
					] );
1915
					$this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1916
					$state['continueRequests'] = $res->neededRequests;
1917
					$session->setSecret( 'AuthManager::accountLinkState', $state );
1918
					return $res;
1919
				default:
1920
					throw new \DomainException(
1921
						get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1922
					);
1923
			}
1924
		} catch ( \Exception $ex ) {
1925
			$session->remove( 'AuthManager::accountLinkState' );
1926
			throw $ex;
1927
		}
1928
	}
1929
1930
	/**@}*/
1931
1932
	/**
1933
	 * @name Information methods
1934
	 * @{
1935
	 */
1936
1937
	/**
1938
	 * Return the applicable list of AuthenticationRequests
1939
	 *
1940
	 * Possible values for $action:
1941
	 *  - ACTION_LOGIN: Valid for passing to beginAuthentication
1942
	 *  - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
1943
	 *  - ACTION_CREATE: Valid for passing to beginAccountCreation
1944
	 *  - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
1945
	 *  - ACTION_LINK: Valid for passing to beginAccountLink
1946
	 *  - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
1947
	 *  - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
1948
	 *  - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
1949
	 *  - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
1950
	 *
1951
	 * @param string $action One of the AuthManager::ACTION_* constants
1952
	 * @param User|null $user User being acted on, instead of the current user.
1953
	 * @return AuthenticationRequest[]
1954
	 */
1955
	public function getAuthenticationRequests( $action, User $user = null ) {
1956
		$options = [];
1957
		$providerAction = $action;
1958
1959
		// Figure out which providers to query
1960
		switch ( $action ) {
1961
			case self::ACTION_LOGIN:
1962
			case self::ACTION_CREATE:
1963
				$providers = $this->getPreAuthenticationProviders() +
1964
					$this->getPrimaryAuthenticationProviders() +
1965
					$this->getSecondaryAuthenticationProviders();
1966
				break;
1967
1968 View Code Duplication
			case self::ACTION_LOGIN_CONTINUE:
1969
				$state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
1970
				return is_array( $state ) ? $state['continueRequests'] : [];
1971
1972 View Code Duplication
			case self::ACTION_CREATE_CONTINUE:
1973
				$state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
1974
				return is_array( $state ) ? $state['continueRequests'] : [];
1975
1976 View Code Duplication
			case self::ACTION_LINK:
1977
				$providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
1978
					return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
1979
				} );
1980
				break;
1981
1982 View Code Duplication
			case self::ACTION_UNLINK:
1983
				$providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
1984
					return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
1985
				} );
1986
1987
				// To providers, unlink and remove are identical.
1988
				$providerAction = self::ACTION_REMOVE;
1989
				break;
1990
1991 View Code Duplication
			case self::ACTION_LINK_CONTINUE:
1992
				$state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
1993
				return is_array( $state ) ? $state['continueRequests'] : [];
1994
1995
			case self::ACTION_CHANGE:
1996
			case self::ACTION_REMOVE:
1997
				$providers = $this->getPrimaryAuthenticationProviders() +
1998
					$this->getSecondaryAuthenticationProviders();
1999
				break;
2000
2001
			// @codeCoverageIgnoreStart
2002
			default:
2003
				throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2004
		}
2005
		// @codeCoverageIgnoreEnd
2006
2007
		return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2008
	}
2009
2010
	/**
2011
	 * Internal request lookup for self::getAuthenticationRequests
2012
	 *
2013
	 * @param string $providerAction Action to pass to providers
2014
	 * @param array $options Options to pass to providers
2015
	 * @param AuthenticationProvider[] $providers
2016
	 * @param User|null $user
2017
	 * @return AuthenticationRequest[]
2018
	 */
2019
	private function getAuthenticationRequestsInternal(
2020
		$providerAction, array $options, array $providers, User $user = null
2021
	) {
2022
		$user = $user ?: \RequestContext::getMain()->getUser();
2023
		$options['username'] = $user->isAnon() ? null : $user->getName();
2024
2025
		// Query them and merge results
2026
		$reqs = [];
2027
		$allPrimaryRequired = null;
2028
		foreach ( $providers as $provider ) {
2029
			$isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2030
			$thisRequired = [];
2031
			foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2032
				$id = $req->getUniqueId();
2033
2034
				// If it's from a Primary, mark it as "primary-required" but
2035
				// track it for later.
2036
				if ( $isPrimary ) {
2037
					if ( $req->required ) {
2038
						$thisRequired[$id] = true;
2039
						$req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2040
					}
2041
				}
2042
2043
				if ( !isset( $reqs[$id] ) || $req->required === AuthenticationRequest::REQUIRED ) {
2044
					$reqs[$id] = $req;
2045
				}
2046
			}
2047
2048
			// Track which requests are required by all primaries
2049
			if ( $isPrimary ) {
2050
				$allPrimaryRequired = $allPrimaryRequired === null
2051
					? $thisRequired
2052
					: array_intersect_key( $allPrimaryRequired, $thisRequired );
2053
			}
2054
		}
2055
		// Any requests that were required by all primaries are required.
2056
		foreach ( (array)$allPrimaryRequired as $id => $dummy ) {
2057
			$reqs[$id]->required = AuthenticationRequest::REQUIRED;
2058
		}
2059
2060
		// AuthManager has its own req for some actions
2061
		switch ( $providerAction ) {
2062
			case self::ACTION_LOGIN:
2063
				$reqs[] = new RememberMeAuthenticationRequest;
2064
				break;
2065
2066
			case self::ACTION_CREATE:
2067
				$reqs[] = new UsernameAuthenticationRequest;
2068
				$reqs[] = new UserDataAuthenticationRequest;
2069
				if ( $options['username'] !== null ) {
2070
					$reqs[] = new CreationReasonAuthenticationRequest;
2071
					$options['username'] = null; // Don't fill in the username below
2072
				}
2073
				break;
2074
		}
2075
2076
		// Fill in reqs data
2077
		$this->fillRequests( $reqs, $providerAction, $options['username'], true );
2078
2079
		// For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2080
		if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2081
			$reqs = array_filter( $reqs, function ( $req ) {
2082
				return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2083
			} );
2084
		}
2085
2086
		return array_values( $reqs );
2087
	}
2088
2089
	/**
2090
	 * Set values in an array of requests
2091
	 * @param AuthenticationRequest[] &$reqs
2092
	 * @param string $action
2093
	 * @param string|null $username
2094
	 * @param boolean $forceAction
2095
	 */
2096
	private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2097
		foreach ( $reqs as $req ) {
2098
			if ( !$req->action || $forceAction ) {
2099
				$req->action = $action;
2100
			}
2101
			if ( $req->username === null ) {
2102
				$req->username = $username;
2103
			}
2104
		}
2105
	}
2106
2107
	/**
2108
	 * Determine whether a username exists
2109
	 * @param string $username
2110
	 * @param int $flags Bitfield of User:READ_* constants
2111
	 * @return bool
2112
	 */
2113
	public function userExists( $username, $flags = User::READ_NORMAL ) {
2114
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2115
			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...
2116
				return true;
2117
			}
2118
		}
2119
2120
		return false;
2121
	}
2122
2123
	/**
2124
	 * Determine whether a user property should be allowed to be changed.
2125
	 *
2126
	 * Supported properties are:
2127
	 *  - emailaddress
2128
	 *  - realname
2129
	 *  - nickname
2130
	 *
2131
	 * @param string $property
2132
	 * @return bool
2133
	 */
2134
	public function allowsPropertyChange( $property ) {
2135
		$providers = $this->getPrimaryAuthenticationProviders() +
2136
			$this->getSecondaryAuthenticationProviders();
2137
		foreach ( $providers as $provider ) {
2138
			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...
2139
				return false;
2140
			}
2141
		}
2142
		return true;
2143
	}
2144
2145
	/**
2146
	 * Get a provider by ID
2147
	 * @note This is public so extensions can check whether their own provider
2148
	 *  is installed and so they can read its configuration if necessary.
2149
	 *  Other uses are not recommended.
2150
	 * @param string $id
2151
	 * @return AuthenticationProvider|null
2152
	 */
2153
	public function getAuthenticationProvider( $id ) {
2154
		// Fast version
2155
		if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2156
			return $this->allAuthenticationProviders[$id];
2157
		}
2158
2159
		// Slow version: instantiate each kind and check
2160
		$providers = $this->getPrimaryAuthenticationProviders();
2161
		if ( isset( $providers[$id] ) ) {
2162
			return $providers[$id];
2163
		}
2164
		$providers = $this->getSecondaryAuthenticationProviders();
2165
		if ( isset( $providers[$id] ) ) {
2166
			return $providers[$id];
2167
		}
2168
		$providers = $this->getPreAuthenticationProviders();
2169
		if ( isset( $providers[$id] ) ) {
2170
			return $providers[$id];
2171
		}
2172
2173
		return null;
2174
	}
2175
2176
	/**@}*/
2177
2178
	/**
2179
	 * @name Internal methods
2180
	 * @{
2181
	 */
2182
2183
	/**
2184
	 * Store authentication in the current session
2185
	 * @protected For use by AuthenticationProviders
2186
	 * @param string $key
2187
	 * @param mixed $data Must be serializable
2188
	 */
2189
	public function setAuthenticationSessionData( $key, $data ) {
2190
		$session = $this->request->getSession();
2191
		$arr = $session->getSecret( 'authData' );
2192
		if ( !is_array( $arr ) ) {
2193
			$arr = [];
2194
		}
2195
		$arr[$key] = $data;
2196
		$session->setSecret( 'authData', $arr );
2197
	}
2198
2199
	/**
2200
	 * Fetch authentication data from the current session
2201
	 * @protected For use by AuthenticationProviders
2202
	 * @param string $key
2203
	 * @param mixed $default
2204
	 * @return mixed
2205
	 */
2206
	public function getAuthenticationSessionData( $key, $default = null ) {
2207
		$arr = $this->request->getSession()->getSecret( 'authData' );
2208
		if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2209
			return $arr[$key];
2210
		} else {
2211
			return $default;
2212
		}
2213
	}
2214
2215
	/**
2216
	 * Remove authentication data
2217
	 * @protected For use by AuthenticationProviders
2218
	 * @param string|null $key If null, all data is removed
2219
	 */
2220
	public function removeAuthenticationSessionData( $key ) {
2221
		$session = $this->request->getSession();
2222
		if ( $key === null ) {
2223
			$session->remove( 'authData' );
2224
		} else {
2225
			$arr = $session->getSecret( 'authData' );
2226
			if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2227
				unset( $arr[$key] );
2228
				$session->setSecret( 'authData', $arr );
2229
			}
2230
		}
2231
	}
2232
2233
	/**
2234
	 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2235
	 * @param string $class
2236
	 * @param array[] $specs
2237
	 * @return AuthenticationProvider[]
2238
	 */
2239
	protected function providerArrayFromSpecs( $class, array $specs ) {
2240
		$i = 0;
2241
		foreach ( $specs as &$spec ) {
2242
			$spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2243
		}
2244
		unset( $spec );
2245
		usort( $specs, function ( $a, $b ) {
2246
			return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2247
				?: $a['sort2'] - $b['sort2'];
2248
		} );
2249
2250
		$ret = [];
2251
		foreach ( $specs as $spec ) {
2252
			$provider = \ObjectFactory::getObjectFromSpec( $spec );
2253
			if ( !$provider instanceof $class ) {
2254
				throw new \RuntimeException(
2255
					"Expected instance of $class, got " . get_class( $provider )
2256
				);
2257
			}
2258
			$provider->setLogger( $this->logger );
2259
			$provider->setManager( $this );
2260
			$provider->setConfig( $this->config );
2261
			$id = $provider->getUniqueId();
2262 View Code Duplication
			if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2263
				throw new \RuntimeException(
2264
					"Duplicate specifications for id $id (classes " .
2265
					get_class( $provider ) . ' and ' .
2266
					get_class( $this->allAuthenticationProviders[$id] ) . ')'
2267
				);
2268
			}
2269
			$this->allAuthenticationProviders[$id] = $provider;
2270
			$ret[$id] = $provider;
2271
		}
2272
		return $ret;
2273
	}
2274
2275
	/**
2276
	 * Get the configuration
2277
	 * @return array
2278
	 */
2279
	private function getConfiguration() {
2280
		return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2281
	}
2282
2283
	/**
2284
	 * Get the list of PreAuthenticationProviders
2285
	 * @return PreAuthenticationProvider[]
2286
	 */
2287
	protected function getPreAuthenticationProviders() {
2288
		if ( $this->preAuthenticationProviders === null ) {
2289
			$conf = $this->getConfiguration();
2290
			$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...
2291
				PreAuthenticationProvider::class, $conf['preauth']
2292
			);
2293
		}
2294
		return $this->preAuthenticationProviders;
2295
	}
2296
2297
	/**
2298
	 * Get the list of PrimaryAuthenticationProviders
2299
	 * @return PrimaryAuthenticationProvider[]
2300
	 */
2301
	protected function getPrimaryAuthenticationProviders() {
2302
		if ( $this->primaryAuthenticationProviders === null ) {
2303
			$conf = $this->getConfiguration();
2304
			$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...
2305
				PrimaryAuthenticationProvider::class, $conf['primaryauth']
2306
			);
2307
		}
2308
		return $this->primaryAuthenticationProviders;
2309
	}
2310
2311
	/**
2312
	 * Get the list of SecondaryAuthenticationProviders
2313
	 * @return SecondaryAuthenticationProvider[]
2314
	 */
2315
	protected function getSecondaryAuthenticationProviders() {
2316
		if ( $this->secondaryAuthenticationProviders === null ) {
2317
			$conf = $this->getConfiguration();
2318
			$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...
2319
				SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2320
			);
2321
		}
2322
		return $this->secondaryAuthenticationProviders;
2323
	}
2324
2325
	/**
2326
	 * @param User $user
2327
	 * @param bool|null $remember
2328
	 */
2329
	private function setSessionDataForUser( $user, $remember = null ) {
2330
		$session = $this->request->getSession();
2331
		$delay = $session->delaySave();
2332
2333
		$session->resetId();
2334
		$session->resetAllTokens();
2335
		if ( $session->canSetUser() ) {
2336
			$session->setUser( $user );
2337
		}
2338
		if ( $remember !== null ) {
2339
			$session->setRememberUser( $remember );
2340
		}
2341
		$session->set( 'AuthManager:lastAuthId', $user->getId() );
2342
		$session->set( 'AuthManager:lastAuthTimestamp', time() );
2343
		$session->persist();
2344
2345
		\ScopedCallback::consume( $delay );
2346
2347
		\Hooks::run( 'UserLoggedIn', [ $user ] );
2348
	}
2349
2350
	/**
2351
	 * @param User $user
2352
	 * @param bool $useContextLang Use 'uselang' to set the user's language
2353
	 */
2354
	private function setDefaultUserOptions( User $user, $useContextLang ) {
2355
		global $wgContLang;
2356
2357
		$user->setToken();
2358
2359
		$lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
2360
		$user->setOption( 'language', $lang->getPreferredVariant() );
2361
2362
		if ( $wgContLang->hasVariants() ) {
2363
			$user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2364
		}
2365
	}
2366
2367
	/**
2368
	 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2369
	 * @param string $method
2370
	 * @param array $args
2371
	 */
2372
	private function callMethodOnProviders( $which, $method, array $args ) {
2373
		$providers = [];
2374
		if ( $which & 1 ) {
2375
			$providers += $this->getPreAuthenticationProviders();
2376
		}
2377
		if ( $which & 2 ) {
2378
			$providers += $this->getPrimaryAuthenticationProviders();
2379
		}
2380
		if ( $which & 4 ) {
2381
			$providers += $this->getSecondaryAuthenticationProviders();
2382
		}
2383
		foreach ( $providers as $provider ) {
2384
			call_user_func_array( [ $provider, $method ], $args );
2385
		}
2386
	}
2387
2388
	/**
2389
	 * Reset the internal caching for unit testing
2390
	 */
2391
	public static function resetCache() {
2392
		if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2393
			// @codeCoverageIgnoreStart
2394
			throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2395
			// @codeCoverageIgnoreEnd
2396
		}
2397
2398
		self::$instance = null;
2399
	}
2400
2401
	/**@}*/
2402
2403
}
2404
2405
/**
2406
 * For really cool vim folding this needs to be at the end:
2407
 * vim: foldmarker=@{,@} foldmethod=marker
2408
 */
2409