Completed
Branch master (62f6c6)
by
unknown
21:31
created

AuthManager::beginAccountCreation()   D

Complexity

Conditions 15
Paths 39

Size

Total Lines 127
Code Lines 82

Duplication

Lines 26
Ratio 20.47 %

Importance

Changes 0
Metric Value
cc 15
eloc 82
nc 39
nop 3
dl 26
loc 127
rs 4.9121
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Authentication (and possibly Authorization in the future) system entry point
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Auth
22
 */
23
24
namespace MediaWiki\Auth;
25
26
use Config;
27
use Psr\Log\LoggerAwareInterface;
28
use Psr\Log\LoggerInterface;
29
use Status;
30
use StatusValue;
31
use User;
32
use WebRequest;
33
34
/**
35
 * This serves as the entry point to the authentication system.
36
 *
37
 * In the future, it may also serve as the entry point to the authorization
38
 * system.
39
 *
40
 * @ingroup Auth
41
 * @since 1.27
42
 */
43
class AuthManager implements LoggerAwareInterface {
44
	/** Log in with an existing (not necessarily local) user */
45
	const ACTION_LOGIN = 'login';
46
	/** Continue a login process that was interrupted by the need for user input or communication
47
	 * with an external provider */
48
	const ACTION_LOGIN_CONTINUE = 'login-continue';
49
	/** Create a new user */
50
	const ACTION_CREATE = 'create';
51
	/** Continue a user creation process that was interrupted by the need for user input or
52
	 * communication with an external provider */
53
	const ACTION_CREATE_CONTINUE = 'create-continue';
54
	/** Link an existing user to a third-party account */
55
	const ACTION_LINK = 'link';
56
	/** Continue a user linking process that was interrupted by the need for user input or
57
	 * communication with an external provider */
58
	const ACTION_LINK_CONTINUE = 'link-continue';
59
	/** Change a user's credentials */
60
	const ACTION_CHANGE = 'change';
61
	/** Remove a user's credentials */
62
	const ACTION_REMOVE = 'remove';
63
	/** Like ACTION_REMOVE but for linking providers only */
64
	const ACTION_UNLINK = 'unlink';
65
66
	/** Security-sensitive operations are ok. */
67
	const SEC_OK = 'ok';
68
	/** Security-sensitive operations should re-authenticate. */
69
	const SEC_REAUTH = 'reauth';
70
	/** Security-sensitive should not be performed. */
71
	const SEC_FAIL = 'fail';
72
73
	/** Auto-creation is due to SessionManager */
74
	const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
75
76
	/** @var AuthManager|null */
77
	private static $instance = null;
78
79
	/** @var WebRequest */
80
	private $request;
81
82
	/** @var Config */
83
	private $config;
84
85
	/** @var LoggerInterface */
86
	private $logger;
87
88
	/** @var AuthenticationProvider[] */
89
	private $allAuthenticationProviders = [];
90
91
	/** @var PreAuthenticationProvider[] */
92
	private $preAuthenticationProviders = null;
93
94
	/** @var PrimaryAuthenticationProvider[] */
95
	private $primaryAuthenticationProviders = null;
96
97
	/** @var SecondaryAuthenticationProvider[] */
98
	private $secondaryAuthenticationProviders = null;
99
100
	/** @var CreatedAccountAuthenticationRequest[] */
101
	private $createdAccountAuthenticationRequests = [];
102
103
	/**
104
	 * Get the global AuthManager
105
	 * @return AuthManager
106
	 */
107
	public static function singleton() {
108
		global $wgDisableAuthManager;
109
110
		if ( $wgDisableAuthManager ) {
111
			throw new \BadMethodCallException( '$wgDisableAuthManager is set' );
112
		}
113
114
		if ( self::$instance === null ) {
115
			self::$instance = new self(
116
				\RequestContext::getMain()->getRequest(),
117
				\ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
0 ignored issues
show
Deprecated Code introduced by
The method ConfigFactory::getDefaultInstance() has been deprecated with message: since 1.27, use MediaWikiServices::getConfigFactory() instead.

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

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

Loading history...
118
			);
119
		}
120
		return self::$instance;
121
	}
122
123
	/**
124
	 * @param WebRequest $request
125
	 * @param Config $config
126
	 */
127
	public function __construct( WebRequest $request, Config $config ) {
128
		$this->request = $request;
129
		$this->config = $config;
130
		$this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
131
	}
132
133
	/**
134
	 * @param LoggerInterface $logger
135
	 */
136
	public function setLogger( LoggerInterface $logger ) {
137
		$this->logger = $logger;
138
	}
139
140
	/**
141
	 * @return WebRequest
142
	 */
143
	public function getRequest() {
144
		return $this->request;
145
	}
146
147
	/**
148
	 * Force certain PrimaryAuthenticationProviders
149
	 * @deprecated For backwards compatibility only
150
	 * @param PrimaryAuthenticationProvider[] $providers
151
	 * @param string $why
152
	 */
153
	public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
154
		$this->logger->warning( "Overriding AuthManager primary authn because $why" );
155
156
		if ( $this->primaryAuthenticationProviders !== null ) {
157
			$this->logger->warning(
158
				'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
159
			);
160
161
			$this->allAuthenticationProviders = array_diff_key(
162
				$this->allAuthenticationProviders,
163
				$this->primaryAuthenticationProviders
164
			);
165
			$session = $this->request->getSession();
166
			$session->remove( 'AuthManager::authnState' );
167
			$session->remove( 'AuthManager::accountCreationState' );
168
			$session->remove( 'AuthManager::accountLinkState' );
169
			$this->createdAccountAuthenticationRequests = [];
170
		}
171
172
		$this->primaryAuthenticationProviders = [];
173
		foreach ( $providers as $provider ) {
174
			if ( !$provider instanceof PrimaryAuthenticationProvider ) {
175
				throw new \RuntimeException(
176
					'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
177
						get_class( $provider )
178
				);
179
			}
180
			$provider->setLogger( $this->logger );
181
			$provider->setManager( $this );
182
			$provider->setConfig( $this->config );
183
			$id = $provider->getUniqueId();
184 View Code Duplication
			if ( isset( $this->allAuthenticationProviders[$id] ) ) {
185
				throw new \RuntimeException(
186
					"Duplicate specifications for id $id (classes " .
187
						get_class( $provider ) . ' and ' .
188
						get_class( $this->allAuthenticationProviders[$id] ) . ')'
189
				);
190
			}
191
			$this->allAuthenticationProviders[$id] = $provider;
192
			$this->primaryAuthenticationProviders[$id] = $provider;
193
		}
194
	}
195
196
	/**
197
	 * Call a legacy AuthPlugin method, if necessary
198
	 * @codeCoverageIgnore
199
	 * @deprecated For backwards compatibility only, should be avoided in new code
200
	 * @param string $method AuthPlugin method to call
201
	 * @param array $params Parameters to pass
202
	 * @param mixed $return Return value if AuthPlugin wasn't called
203
	 * @return mixed Return value from the AuthPlugin method, or $return
204
	 */
205
	public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
206
		global $wgAuth;
207
208
		if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
209
			return call_user_func_array( [ $wgAuth, $method ], $params );
210
		} else {
211
			return $return;
212
		}
213
	}
214
215
	/**
216
	 * @name Authentication
217
	 * @{
218
	 */
219
220
	/**
221
	 * Indicate whether user authentication is possible
222
	 *
223
	 * It may not be if the session is provided by something like OAuth
224
	 * for which each individual request includes authentication data.
225
	 *
226
	 * @return bool
227
	 */
228
	public function canAuthenticateNow() {
229
		return $this->request->getSession()->canSetUser();
230
	}
231
232
	/**
233
	 * Start an authentication flow
234
	 * @param AuthenticationRequest[] $reqs
235
	 * @param string $returnToUrl Url that REDIRECT responses should eventually
236
	 *  return to.
237
	 * @return AuthenticationResponse See self::continueAuthentication()
238
	 */
239
	public function beginAuthentication( array $reqs, $returnToUrl ) {
240
		$session = $this->request->getSession();
241
		if ( !$session->canSetUser() ) {
242
			// Caller should have called canAuthenticateNow()
243
			$session->remove( 'AuthManager::authnState' );
244
			throw new \LogicException( 'Authentication is not possible now' );
245
		}
246
247
		$guessUserName = null;
248 View Code Duplication
		foreach ( $reqs as $req ) {
249
			$req->returnToUrl = $returnToUrl;
250
			// @codeCoverageIgnoreStart
251
			if ( $req->username !== null && $req->username !== '' ) {
252
				if ( $guessUserName === null ) {
253
					$guessUserName = $req->username;
254
				} elseif ( $guessUserName !== $req->username ) {
255
					$guessUserName = null;
256
					break;
257
				}
258
			}
259
			// @codeCoverageIgnoreEnd
260
		}
261
262
		// Check for special-case login of a just-created account
263
		$req = AuthenticationRequest::getRequestByClass(
264
			$reqs, CreatedAccountAuthenticationRequest::class
265
		);
266
		if ( $req ) {
267
			if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
268
				throw new \LogicException(
269
					'CreatedAccountAuthenticationRequests are only valid on ' .
270
						'the same AuthManager that created the account'
271
				);
272
			}
273
274
			$user = User::newFromName( $req->username );
275
			// @codeCoverageIgnoreStart
276
			if ( !$user ) {
277
				throw new \UnexpectedValueException(
278
					"CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
279
				);
280
			} 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...
281
				throw new \UnexpectedValueException(
282
					"ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
283
				);
284
			}
285
			// @codeCoverageIgnoreEnd
286
287
			$this->logger->info( 'Logging in {user} after account creation', [
288
				'user' => $user->getName(),
289
			] );
290
			$ret = AuthenticationResponse::newPass( $user->getName() );
291
			$this->setSessionDataForUser( $user );
292
			$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
293
			$session->remove( 'AuthManager::authnState' );
294
			\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
295
			return $ret;
296
		}
297
298
		$this->removeAuthenticationSessionData( null );
299
300
		foreach ( $this->getPreAuthenticationProviders() as $provider ) {
301
			$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...
302
			if ( !$status->isGood() ) {
303
				$this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
304
				$ret = AuthenticationResponse::newFail(
305
					Status::wrap( $status )->getMessage()
306
				);
307
				$this->callMethodOnProviders( 7, 'postAuthentication',
308
					[ User::newFromName( $guessUserName ) ?: null, $ret ]
309
				);
310
				\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
311
				return $ret;
312
			}
313
		}
314
315
		$state = [
316
			'reqs' => $reqs,
317
			'returnToUrl' => $returnToUrl,
318
			'guessUserName' => $guessUserName,
319
			'primary' => null,
320
			'primaryResponse' => null,
321
			'secondary' => [],
322
			'maybeLink' => [],
323
			'continueRequests' => [],
324
		];
325
326
		// Preserve state from a previous failed login
327
		$req = AuthenticationRequest::getRequestByClass(
328
			$reqs, CreateFromLoginAuthenticationRequest::class
329
		);
330
		if ( $req ) {
331
			$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...
332
		}
333
334
		$session = $this->request->getSession();
335
		$session->setSecret( 'AuthManager::authnState', $state );
336
		$session->persist();
337
338
		return $this->continueAuthentication( $reqs );
339
	}
340
341
	/**
342
	 * Continue an authentication flow
343
	 *
344
	 * Return values are interpreted as follows:
345
	 * - status FAIL: Authentication failed. If $response->createRequest is
346
	 *   set, that may be passed to self::beginAuthentication() or to
347
	 *   self::beginAccountCreation() (after adding a username, if necessary)
348
	 *   to preserve state.
349
	 * - status REDIRECT: The client should be redirected to the contained URL,
350
	 *   new AuthenticationRequests should be made (if any), then
351
	 *   AuthManager::continueAuthentication() should be called.
352
	 * - status UI: The client should be presented with a user interface for
353
	 *   the fields in the specified AuthenticationRequests, then new
354
	 *   AuthenticationRequests should be made, then
355
	 *   AuthManager::continueAuthentication() should be called.
356
	 * - status RESTART: The user logged in successfully with a third-party
357
	 *   service, but the third-party credentials aren't attached to any local
358
	 *   account. This could be treated as a UI or a FAIL.
359
	 * - status PASS: Authentication was successful.
360
	 *
361
	 * @param AuthenticationRequest[] $reqs
362
	 * @return AuthenticationResponse
363
	 */
364
	public function continueAuthentication( array $reqs ) {
365
		$session = $this->request->getSession();
366
		try {
367
			if ( !$session->canSetUser() ) {
368
				// Caller should have called canAuthenticateNow()
369
				// @codeCoverageIgnoreStart
370
				throw new \LogicException( 'Authentication is not possible now' );
371
				// @codeCoverageIgnoreEnd
372
			}
373
374
			$state = $session->getSecret( 'AuthManager::authnState' );
375
			if ( !is_array( $state ) ) {
376
				return AuthenticationResponse::newFail(
377
					wfMessage( 'authmanager-authn-not-in-progress' )
378
				);
379
			}
380
			$state['continueRequests'] = [];
381
382
			$guessUserName = $state['guessUserName'];
383
384
			foreach ( $reqs as $req ) {
385
				$req->returnToUrl = $state['returnToUrl'];
386
			}
387
388
			// Step 1: Choose an primary authentication provider, and call it until it succeeds.
389
390
			if ( $state['primary'] === null ) {
391
				// We haven't picked a PrimaryAuthenticationProvider yet
392
				// @codeCoverageIgnoreStart
393
				$guessUserName = null;
394 View Code Duplication
				foreach ( $reqs as $req ) {
395
					if ( $req->username !== null && $req->username !== '' ) {
396
						if ( $guessUserName === null ) {
397
							$guessUserName = $req->username;
398
						} elseif ( $guessUserName !== $req->username ) {
399
							$guessUserName = null;
400
							break;
401
						}
402
					}
403
				}
404
				$state['guessUserName'] = $guessUserName;
405
				// @codeCoverageIgnoreEnd
406
				$state['reqs'] = $reqs;
407
408
				foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
409
					$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...
410
					switch ( $res->status ) {
411
						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...
412
							$state['primary'] = $id;
413
							$state['primaryResponse'] = $res;
414
							$this->logger->debug( "Primary login with $id succeeded" );
415
							break 2;
416 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...
417
							$this->logger->debug( "Login failed in primary authentication by $id" );
418
							if ( $res->createRequest || $state['maybeLink'] ) {
419
								$res->createRequest = new CreateFromLoginAuthenticationRequest(
420
									$res->createRequest, $state['maybeLink']
421
								);
422
							}
423
							$this->callMethodOnProviders( 7, 'postAuthentication',
424
								[ User::newFromName( $guessUserName ) ?: null, $res ]
425
							);
426
							$session->remove( 'AuthManager::authnState' );
427
							\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
428
							return $res;
429
						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...
430
							// Continue loop
431
							break;
432
						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...
433
						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...
434
							$this->logger->debug( "Primary login with $id returned $res->status" );
435
							$state['primary'] = $id;
436
							$state['continueRequests'] = $res->neededRequests;
437
							$session->setSecret( 'AuthManager::authnState', $state );
438
							return $res;
439
440
							// @codeCoverageIgnoreStart
441
						default:
442
							throw new \DomainException(
443
								get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
444
							);
445
							// @codeCoverageIgnoreEnd
446
					}
447
				}
448
				if ( $state['primary'] === null ) {
449
					$this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
450
					$ret = AuthenticationResponse::newFail(
451
						wfMessage( 'authmanager-authn-no-primary' )
452
					);
453
					$this->callMethodOnProviders( 7, 'postAuthentication',
454
						[ User::newFromName( $guessUserName ) ?: null, $ret ]
455
					);
456
					$session->remove( 'AuthManager::authnState' );
457
					return $ret;
458
				}
459
			} elseif ( $state['primaryResponse'] === null ) {
460
				$provider = $this->getAuthenticationProvider( $state['primary'] );
461 View Code Duplication
				if ( !$provider instanceof PrimaryAuthenticationProvider ) {
462
					// Configuration changed? Force them to start over.
463
					// @codeCoverageIgnoreStart
464
					$ret = AuthenticationResponse::newFail(
465
						wfMessage( 'authmanager-authn-not-in-progress' )
466
					);
467
					$this->callMethodOnProviders( 7, 'postAuthentication',
468
						[ User::newFromName( $guessUserName ) ?: null, $ret ]
469
					);
470
					$session->remove( 'AuthManager::authnState' );
471
					return $ret;
472
					// @codeCoverageIgnoreEnd
473
				}
474
				$id = $provider->getUniqueId();
475
				$res = $provider->continuePrimaryAuthentication( $reqs );
476
				switch ( $res->status ) {
477
					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...
478
						$state['primaryResponse'] = $res;
479
						$this->logger->debug( "Primary login with $id succeeded" );
480
						break;
481 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...
482
						$this->logger->debug( "Login failed in primary authentication by $id" );
483
						if ( $res->createRequest || $state['maybeLink'] ) {
484
							$res->createRequest = new CreateFromLoginAuthenticationRequest(
485
								$res->createRequest, $state['maybeLink']
486
							);
487
						}
488
						$this->callMethodOnProviders( 7, 'postAuthentication',
489
							[ User::newFromName( $guessUserName ) ?: null, $res ]
490
						);
491
						$session->remove( 'AuthManager::authnState' );
492
						\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
493
						return $res;
494
					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...
495
					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...
496
						$this->logger->debug( "Primary login with $id returned $res->status" );
497
						$state['continueRequests'] = $res->neededRequests;
498
						$session->setSecret( 'AuthManager::authnState', $state );
499
						return $res;
500
					default:
501
						throw new \DomainException(
502
							get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
503
						);
504
				}
505
			}
506
507
			$res = $state['primaryResponse'];
508
			if ( $res->username === null ) {
509
				$provider = $this->getAuthenticationProvider( $state['primary'] );
510 View Code Duplication
				if ( !$provider instanceof PrimaryAuthenticationProvider ) {
511
					// Configuration changed? Force them to start over.
512
					// @codeCoverageIgnoreStart
513
					$ret = AuthenticationResponse::newFail(
514
						wfMessage( 'authmanager-authn-not-in-progress' )
515
					);
516
					$this->callMethodOnProviders( 7, 'postAuthentication',
517
						[ User::newFromName( $guessUserName ) ?: null, $ret ]
518
					);
519
					$session->remove( 'AuthManager::authnState' );
520
					return $ret;
521
					// @codeCoverageIgnoreEnd
522
				}
523
524
				if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
525
					$res->linkRequest &&
526
					 // don't confuse the user with an incorrect message if linking is disabled
527
					$this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
528
				) {
529
					$state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
530
					$msg = 'authmanager-authn-no-local-user-link';
531
				} else {
532
					$msg = 'authmanager-authn-no-local-user';
533
				}
534
				$this->logger->debug(
535
					"Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
536
				);
537
				$ret = AuthenticationResponse::newRestart( wfMessage( $msg ) );
538
				$ret->neededRequests = $this->getAuthenticationRequestsInternal(
539
					self::ACTION_LOGIN,
540
					[],
541
					$this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
542
				);
543
				if ( $res->createRequest || $state['maybeLink'] ) {
544
					$ret->createRequest = new CreateFromLoginAuthenticationRequest(
545
						$res->createRequest, $state['maybeLink']
546
					);
547
					$ret->neededRequests[] = $ret->createRequest;
548
				}
549
				$session->setSecret( 'AuthManager::authnState', [
550
					'reqs' => [], // Will be filled in later
551
					'primary' => null,
552
					'primaryResponse' => null,
553
					'secondary' => [],
554
					'continueRequests' => $ret->neededRequests,
555
				] + $state );
556
				return $ret;
557
			}
558
559
			// Step 2: Primary authentication succeeded, create the User object
560
			// (and add the user locally if necessary)
561
562
			$user = User::newFromName( $res->username, 'usable' );
563
			if ( !$user ) {
564
				throw new \DomainException(
565
					get_class( $provider ) . " returned an invalid username: {$res->username}"
566
				);
567
			}
568
			if ( $user->getId() === 0 ) {
569
				// User doesn't exist locally. Create it.
570
				$this->logger->info( 'Auto-creating {user} on login', [
571
					'user' => $user->getName(),
572
				] );
573
				$status = $this->autoCreateUser( $user, $state['primary'], false );
574
				if ( !$status->isGood() ) {
575
					$ret = AuthenticationResponse::newFail(
576
						Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
577
					);
578
					$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
579
					$session->remove( 'AuthManager::authnState' );
580
					\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
581
					return $ret;
582
				}
583
			}
584
585
			// Step 3: Iterate over all the secondary authentication providers.
586
587
			$beginReqs = $state['reqs'];
588
589
			foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
590 View Code Duplication
				if ( !isset( $state['secondary'][$id] ) ) {
591
					// This provider isn't started yet, so we pass it the set
592
					// of reqs from beginAuthentication instead of whatever
593
					// might have been used by a previous provider in line.
594
					$func = 'beginSecondaryAuthentication';
595
					$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...
596
				} elseif ( !$state['secondary'][$id] ) {
597
					$func = 'continueSecondaryAuthentication';
598
					$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...
599
				} else {
600
					continue;
601
				}
602
				switch ( $res->status ) {
603
					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...
604
						$this->logger->debug( "Secondary login with $id succeeded" );
605
						// fall through
606
					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...
607
						$state['secondary'][$id] = true;
608
						break;
609 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...
610
						$this->logger->debug( "Login failed in secondary authentication by $id" );
611
						$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
612
						$session->remove( 'AuthManager::authnState' );
613
						\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
614
						return $res;
615
					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...
616
					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...
617
						$this->logger->debug( "Secondary login with $id returned " . $res->status );
618
						$state['secondary'][$id] = false;
619
						$state['continueRequests'] = $res->neededRequests;
620
						$session->setSecret( 'AuthManager::authnState', $state );
621
						return $res;
622
623
						// @codeCoverageIgnoreStart
624
					default:
625
						throw new \DomainException(
626
							get_class( $provider ) . "::{$func}() returned $res->status"
627
						);
628
						// @codeCoverageIgnoreEnd
629
				}
630
			}
631
632
			// Step 4: Authentication complete! Set the user in the session and
633
			// clean up.
634
635
			$this->logger->info( 'Login for {user} succeeded', [
636
				'user' => $user->getName(),
637
			] );
638
			$req = AuthenticationRequest::getRequestByClass(
639
				$beginReqs, RememberMeAuthenticationRequest::class
640
			);
641
			$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...
642
			$ret = AuthenticationResponse::newPass( $user->getName() );
643
			$this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
644
			$session->remove( 'AuthManager::authnState' );
645
			$this->removeAuthenticationSessionData( null );
646
			\Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
647
			return $ret;
648
		} catch ( \Exception $ex ) {
649
			$session->remove( 'AuthManager::authnState' );
650
			throw $ex;
651
		}
652
	}
653
654
	/**
655
	 * Whether security-sensitive operations should proceed.
656
	 *
657
	 * A "security-sensitive operation" is something like a password or email
658
	 * change, that would normally have a "reenter your password to confirm"
659
	 * box if we only supported password-based authentication.
660
	 *
661
	 * @param string $operation Operation being checked. This should be a
662
	 *  message-key-like string such as 'change-password' or 'change-email'.
663
	 * @return string One of the SEC_* constants.
664
	 */
665
	public function securitySensitiveOperationStatus( $operation ) {
666
		$status = self::SEC_OK;
667
668
		$this->logger->debug( __METHOD__ . ": Checking $operation" );
669
670
		$session = $this->request->getSession();
671
		$aId = $session->getUser()->getId();
672
		if ( $aId === 0 ) {
673
			// User isn't authenticated. DWIM?
674
			$status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
675
			$this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
676
			return $status;
677
		}
678
679
		if ( $session->canSetUser() ) {
680
			$id = $session->get( 'AuthManager:lastAuthId' );
681
			$last = $session->get( 'AuthManager:lastAuthTimestamp' );
682
			if ( $id !== $aId || $last === null ) {
683
				$timeSinceLogin = PHP_INT_MAX; // Forever ago
684
			} else {
685
				$timeSinceLogin = max( 0, time() - $last );
686
			}
687
688
			$thresholds = $this->config->get( 'ReauthenticateTime' );
689
			if ( isset( $thresholds[$operation] ) ) {
690
				$threshold = $thresholds[$operation];
691
			} elseif ( isset( $thresholds['default'] ) ) {
692
				$threshold = $thresholds['default'];
693
			} else {
694
				throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
695
			}
696
697
			if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
698
				$status = self::SEC_REAUTH;
699
			}
700
		} else {
701
			$timeSinceLogin = -1;
702
703
			$pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
704
			if ( isset( $pass[$operation] ) ) {
705
				$status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
706
			} elseif ( isset( $pass['default'] ) ) {
707
				$status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
708
			} else {
709
				throw new \UnexpectedValueException(
710
					'$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
711
				);
712
			}
713
		}
714
715
		\Hooks::run( 'SecuritySensitiveOperationStatus', [
716
			&$status, $operation, $session, $timeSinceLogin
717
		] );
718
719
		// If authentication is not possible, downgrade from "REAUTH" to "FAIL".
720
		if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
721
			$status = self::SEC_FAIL;
722
		}
723
724
		$this->logger->info( __METHOD__ . ": $operation is $status" );
725
726
		return $status;
727
	}
728
729
	/**
730
	 * Determine whether a username can authenticate
731
	 *
732
	 * @param string $username
733
	 * @return bool
734
	 */
735
	public function userCanAuthenticate( $username ) {
736
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
737
			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...
738
				return true;
739
			}
740
		}
741
		return false;
742
	}
743
744
	/**
745
	 * Provide normalized versions of the username for security checks
746
	 *
747
	 * Since different providers can normalize the input in different ways,
748
	 * this returns an array of all the different ways the name might be
749
	 * normalized for authentication.
750
	 *
751
	 * The returned strings should not be revealed to the user, as that might
752
	 * leak private information (e.g. an email address might be normalized to a
753
	 * username).
754
	 *
755
	 * @param string $username
756
	 * @return string[]
757
	 */
758
	public function normalizeUsername( $username ) {
759
		$ret = [];
760
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
761
			$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...
762
			if ( $normalized !== null ) {
763
				$ret[$normalized] = true;
764
			}
765
		}
766
		return array_keys( $ret );
767
	}
768
769
	/**@}*/
770
771
	/**
772
	 * @name Authentication data changing
773
	 * @{
774
	 */
775
776
	/**
777
	 * Revoke any authentication credentials for a user
778
	 *
779
	 * After this, the user should no longer be able to log in.
780
	 *
781
	 * @param string $username
782
	 */
783
	public function revokeAccessForUser( $username ) {
784
		$this->logger->info( 'Revoking access for {user}', [
785
			'user' => $username,
786
		] );
787
		$this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
788
	}
789
790
	/**
791
	 * Validate a change of authentication data (e.g. passwords)
792
	 * @param AuthenticationRequest $req
793
	 * @param bool $checkData If false, $req hasn't been loaded from the
794
	 *  submission so checks on user-submitted fields should be skipped. $req->username is
795
	 *  considered user-submitted for this purpose, even if it cannot be changed via
796
	 *  $req->loadFromSubmission.
797
	 * @return Status
798
	 */
799
	public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
800
		$any = false;
801
		$providers = $this->getPrimaryAuthenticationProviders() +
802
			$this->getSecondaryAuthenticationProviders();
803
		foreach ( $providers as $provider ) {
804
			$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...
805
			if ( !$status->isGood() ) {
806
				return Status::wrap( $status );
807
			}
808
			$any = $any || $status->value !== 'ignored';
809
		}
810
		if ( !$any ) {
811
			$status = Status::newGood( 'ignored' );
812
			$status->warning( 'authmanager-change-not-supported' );
813
			return $status;
814
		}
815
		return Status::newGood();
816
	}
817
818
	/**
819
	 * Change authentication data (e.g. passwords)
820
	 *
821
	 * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
822
	 * result in a successful login in the future.
823
	 *
824
	 * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
825
	 * no longer result in a successful login.
826
	 *
827
	 * @param AuthenticationRequest $req
828
	 */
829
	public function changeAuthenticationData( AuthenticationRequest $req ) {
830
		$this->logger->info( 'Changing authentication data for {user} class {what}', [
831
			'user' => is_string( $req->username ) ? $req->username : '<no name>',
832
			'what' => get_class( $req ),
833
		] );
834
835
		$this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
836
837
		// When the main account's authentication data is changed, invalidate
838
		// all BotPasswords too.
839
		\BotPassword::invalidateAllPasswordsForUser( $req->username );
840
	}
841
842
	/**@}*/
843
844
	/**
845
	 * @name Account creation
846
	 * @{
847
	 */
848
849
	/**
850
	 * Determine whether accounts can be created
851
	 * @return bool
852
	 */
853 View Code Duplication
	public function canCreateAccounts() {
854
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
855
			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...
856
				case PrimaryAuthenticationProvider::TYPE_CREATE:
857
				case PrimaryAuthenticationProvider::TYPE_LINK:
858
					return true;
859
			}
860
		}
861
		return false;
862
	}
863
864
	/**
865
	 * Determine whether a particular account can be created
866
	 * @param string $username
867
	 * @param int $flags Bitfield of User:READ_* constants
868
	 * @return Status
869
	 */
870
	public function canCreateAccount( $username, $flags = User::READ_NORMAL ) {
871
		if ( !$this->canCreateAccounts() ) {
872
			return Status::newFatal( 'authmanager-create-disabled' );
873
		}
874
875
		if ( $this->userExists( $username, $flags ) ) {
876
			return Status::newFatal( 'userexists' );
877
		}
878
879
		$user = User::newFromName( $username, 'creatable' );
880
		if ( !is_object( $user ) ) {
881
			return Status::newFatal( 'noname' );
882
		} else {
883
			$user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
884
			if ( $user->getId() !== 0 ) {
885
				return Status::newFatal( 'userexists' );
886
			}
887
		}
888
889
		// Denied by providers?
890
		$providers = $this->getPreAuthenticationProviders() +
891
			$this->getPrimaryAuthenticationProviders() +
892
			$this->getSecondaryAuthenticationProviders();
893
		foreach ( $providers as $provider ) {
894
			$status = $provider->testUserForCreation( $user, false );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testUserForCreation() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractPreAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\LegacyHookPreAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider, MediaWiki\Auth\ThrottlePreAuthenticationProvider.

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

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

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1598 View Code Duplication
			if ( !$status->isGood() ) {
1599
				$ret = Status::wrap( $status );
1600
				$this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1601
					'username' => $username,
1602
					'reason' => $ret->getWikiText( null, null, 'en' ),
1603
				] );
1604
				$session->set( 'AuthManager::AutoCreateBlacklist', $status, 600 );
1605
				$user->setId( 0 );
1606
				$user->loadFromId();
1607
				return $ret;
1608
			}
1609
		}
1610
1611
		// Ignore warnings about master connections/writes...hard to avoid here
1612
		\Profiler::instance()->getTransactionProfiler()->resetExpectations();
1613
1614
		$backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1615 View Code Duplication
		if ( $cache->get( $backoffKey ) ) {
1616
			$this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1617
				'username' => $username,
1618
			] );
1619
			$user->setId( 0 );
1620
			$user->loadFromId();
1621
			return Status::newFatal( 'authmanager-autocreate-exception' );
1622
		}
1623
1624
		// Checks passed, create the user...
1625
		$from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
1626
		$this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1627
			'username' => $username,
1628
			'from' => $from,
1629
		] );
1630
1631
		try {
1632
			$status = $user->addToDatabase();
1633
			if ( !$status->isOk() ) {
1634
				// double-check for a race condition (T70012)
1635
				$localId = User::idFromName( $username, User::READ_LATEST );
1636
				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...
1637
					$this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1638
						'username' => $username,
1639
					] );
1640
					$user->setId( $localId );
1641
					$user->loadFromId( User::READ_LATEST );
1642
					if ( $login ) {
1643
						$this->setSessionDataForUser( $user );
1644
					}
1645
					$status = Status::newGood();
1646
					$status->warning( 'userexists' );
1647 View Code Duplication
				} else {
1648
					$this->logger->error( __METHOD__ . ': {username} failed with message {message}', [
1649
						'username' => $username,
1650
						'message' => $status->getWikiText( null, null, 'en' )
1651
					] );
1652
					$user->setId( 0 );
1653
					$user->loadFromId();
1654
				}
1655
				return $status;
1656
			}
1657
		} catch ( \Exception $ex ) {
1658
			$this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1659
				'username' => $username,
1660
				'exception' => $ex,
1661
			] );
1662
			// Do not keep throwing errors for a while
1663
			$cache->set( $backoffKey, 1, 600 );
1664
			// Bubble up error; which should normally trigger DB rollbacks
1665
			throw $ex;
1666
		}
1667
1668
		$this->setDefaultUserOptions( $user, true );
1669
1670
		// Inform the providers
1671
		$this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1672
1673
		\Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1674
		\Hooks::run( 'LocalUserCreated', [ $user, true ] );
1675
		$user->saveSettings();
1676
1677
		// Update user count
1678
		\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1679
1680
		// Watch user's userpage and talk page
1681
		$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1682
1683
		// Log the creation
1684
		if ( $this->config->get( 'NewUserLog' ) ) {
1685
			$logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1686
			$logEntry->setPerformer( $user );
1687
			$logEntry->setTarget( $user->getUserPage() );
1688
			$logEntry->setComment( '' );
1689
			$logEntry->setParameters( [
1690
				'4::userid' => $user->getId(),
1691
			] );
1692
			$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...
1693
		}
1694
1695
		if ( $login ) {
1696
			$this->setSessionDataForUser( $user );
1697
		}
1698
1699
		return Status::newGood();
1700
	}
1701
1702
	/**@}*/
1703
1704
	/**
1705
	 * @name Account linking
1706
	 * @{
1707
	 */
1708
1709
	/**
1710
	 * Determine whether accounts can be linked
1711
	 * @return bool
1712
	 */
1713 View Code Duplication
	public function canLinkAccounts() {
1714
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1715
			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...
1716
				return true;
1717
			}
1718
		}
1719
		return false;
1720
	}
1721
1722
	/**
1723
	 * Start an account linking flow
1724
	 *
1725
	 * @param User $user User being linked
1726
	 * @param AuthenticationRequest[] $reqs
1727
	 * @param string $returnToUrl Url that REDIRECT responses should eventually
1728
	 *  return to.
1729
	 * @return AuthenticationResponse
1730
	 */
1731
	public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1732
		$session = $this->request->getSession();
1733
		$session->remove( 'AuthManager::accountLinkState' );
1734
1735
		if ( !$this->canLinkAccounts() ) {
1736
			// Caller should have called canLinkAccounts()
1737
			throw new \LogicException( 'Account linking is not possible' );
1738
		}
1739
1740
		if ( $user->getId() === 0 ) {
1741
			if ( !User::isUsableName( $user->getName() ) ) {
1742
				$msg = wfMessage( 'noname' );
1743
			} else {
1744
				$msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1745
			}
1746
			return AuthenticationResponse::newFail( $msg );
1747
		}
1748
		foreach ( $reqs as $req ) {
1749
			$req->username = $user->getName();
1750
			$req->returnToUrl = $returnToUrl;
1751
		}
1752
1753
		$this->removeAuthenticationSessionData( null );
1754
1755
		$providers = $this->getPreAuthenticationProviders();
1756
		foreach ( $providers as $id => $provider ) {
1757
			$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...
1758
			if ( !$status->isGood() ) {
1759
				$this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1760
					'user' => $user->getName(),
1761
				] );
1762
				$ret = AuthenticationResponse::newFail(
1763
					Status::wrap( $status )->getMessage()
1764
				);
1765
				$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1766
				return $ret;
1767
			}
1768
		}
1769
1770
		$state = [
1771
			'username' => $user->getName(),
1772
			'userid' => $user->getId(),
1773
			'returnToUrl' => $returnToUrl,
1774
			'primary' => null,
1775
			'continueRequests' => [],
1776
		];
1777
1778
		$providers = $this->getPrimaryAuthenticationProviders();
1779
		foreach ( $providers as $id => $provider ) {
1780
			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...
1781
				continue;
1782
			}
1783
1784
			$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...
1785 View Code Duplication
			switch ( $res->status ) {
1786
				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...
1787
					$this->logger->info( "Account linked to {user} by $id", [
1788
						'user' => $user->getName(),
1789
					] );
1790
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1791
					return $res;
1792
1793
				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...
1794
					$this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1795
						'user' => $user->getName(),
1796
					] );
1797
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1798
					return $res;
1799
1800
				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...
1801
					// Continue loop
1802
					break;
1803
1804
				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...
1805
				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...
1806
					$this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1807
						'user' => $user->getName(),
1808
					] );
1809
					$state['primary'] = $id;
1810
					$state['continueRequests'] = $res->neededRequests;
1811
					$session->setSecret( 'AuthManager::accountLinkState', $state );
1812
					$session->persist();
1813
					return $res;
1814
1815
					// @codeCoverageIgnoreStart
1816
				default:
1817
					throw new \DomainException(
1818
						get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1819
					);
1820
					// @codeCoverageIgnoreEnd
1821
			}
1822
		}
1823
1824
		$this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1825
			'user' => $user->getName(),
1826
		] );
1827
		$ret = AuthenticationResponse::newFail(
1828
			wfMessage( 'authmanager-link-no-primary' )
1829
		);
1830
		$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1831
		return $ret;
1832
	}
1833
1834
	/**
1835
	 * Continue an account linking flow
1836
	 * @param AuthenticationRequest[] $reqs
1837
	 * @return AuthenticationResponse
1838
	 */
1839
	public function continueAccountLink( array $reqs ) {
1840
		$session = $this->request->getSession();
1841
		try {
1842
			if ( !$this->canLinkAccounts() ) {
1843
				// Caller should have called canLinkAccounts()
1844
				$session->remove( 'AuthManager::accountLinkState' );
1845
				throw new \LogicException( 'Account linking is not possible' );
1846
			}
1847
1848
			$state = $session->getSecret( 'AuthManager::accountLinkState' );
1849
			if ( !is_array( $state ) ) {
1850
				return AuthenticationResponse::newFail(
1851
					wfMessage( 'authmanager-link-not-in-progress' )
1852
				);
1853
			}
1854
			$state['continueRequests'] = [];
1855
1856
			// Step 0: Prepare and validate the input
1857
1858
			$user = User::newFromName( $state['username'], 'usable' );
1859
			if ( !is_object( $user ) ) {
1860
				$session->remove( 'AuthManager::accountLinkState' );
1861
				return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1862
			}
1863
			if ( $user->getId() != $state['userid'] ) {
1864
				throw new \UnexpectedValueException(
1865
					"User \"{$state['username']}\" is valid, but " .
1866
						"ID {$user->getId()} != {$state['userid']}!"
1867
				);
1868
			}
1869
1870
			foreach ( $reqs as $req ) {
1871
				$req->username = $state['username'];
1872
				$req->returnToUrl = $state['returnToUrl'];
1873
			}
1874
1875
			// Step 1: Call the primary again until it succeeds
1876
1877
			$provider = $this->getAuthenticationProvider( $state['primary'] );
1878
			if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1879
				// Configuration changed? Force them to start over.
1880
				// @codeCoverageIgnoreStart
1881
				$ret = AuthenticationResponse::newFail(
1882
					wfMessage( 'authmanager-link-not-in-progress' )
1883
				);
1884
				$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1885
				$session->remove( 'AuthManager::accountLinkState' );
1886
				return $ret;
1887
				// @codeCoverageIgnoreEnd
1888
			}
1889
			$id = $provider->getUniqueId();
1890
			$res = $provider->continuePrimaryAccountLink( $user, $reqs );
1891 View Code Duplication
			switch ( $res->status ) {
1892
				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...
1893
					$this->logger->info( "Account linked to {user} by $id", [
1894
						'user' => $user->getName(),
1895
					] );
1896
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1897
					$session->remove( 'AuthManager::accountLinkState' );
1898
					return $res;
1899
				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...
1900
					$this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1901
						'user' => $user->getName(),
1902
					] );
1903
					$this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1904
					$session->remove( 'AuthManager::accountLinkState' );
1905
					return $res;
1906
				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...
1907
				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...
1908
					$this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1909
						'user' => $user->getName(),
1910
					] );
1911
					$state['continueRequests'] = $res->neededRequests;
1912
					$session->setSecret( 'AuthManager::accountLinkState', $state );
1913
					return $res;
1914
				default:
1915
					throw new \DomainException(
1916
						get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1917
					);
1918
			}
1919
		} catch ( \Exception $ex ) {
1920
			$session->remove( 'AuthManager::accountLinkState' );
1921
			throw $ex;
1922
		}
1923
	}
1924
1925
	/**@}*/
1926
1927
	/**
1928
	 * @name Information methods
1929
	 * @{
1930
	 */
1931
1932
	/**
1933
	 * Return the applicable list of AuthenticationRequests
1934
	 *
1935
	 * Possible values for $action:
1936
	 *  - ACTION_LOGIN: Valid for passing to beginAuthentication
1937
	 *  - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
1938
	 *  - ACTION_CREATE: Valid for passing to beginAccountCreation
1939
	 *  - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
1940
	 *  - ACTION_LINK: Valid for passing to beginAccountLink
1941
	 *  - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
1942
	 *  - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
1943
	 *  - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
1944
	 *  - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
1945
	 *
1946
	 * @param string $action One of the AuthManager::ACTION_* constants
1947
	 * @param User|null $user User being acted on, instead of the current user.
1948
	 * @return AuthenticationRequest[]
1949
	 */
1950
	public function getAuthenticationRequests( $action, User $user = null ) {
1951
		$options = [];
1952
		$providerAction = $action;
1953
1954
		// Figure out which providers to query
1955
		switch ( $action ) {
1956
			case self::ACTION_LOGIN:
1957
			case self::ACTION_CREATE:
1958
				$providers = $this->getPreAuthenticationProviders() +
1959
					$this->getPrimaryAuthenticationProviders() +
1960
					$this->getSecondaryAuthenticationProviders();
1961
				break;
1962
1963 View Code Duplication
			case self::ACTION_LOGIN_CONTINUE:
1964
				$state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
1965
				return is_array( $state ) ? $state['continueRequests'] : [];
1966
1967 View Code Duplication
			case self::ACTION_CREATE_CONTINUE:
1968
				$state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
1969
				return is_array( $state ) ? $state['continueRequests'] : [];
1970
1971 View Code Duplication
			case self::ACTION_LINK:
1972
				$providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
1973
					return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
1974
				} );
1975
				break;
1976
1977 View Code Duplication
			case self::ACTION_UNLINK:
1978
				$providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
1979
					return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
1980
				} );
1981
1982
				// To providers, unlink and remove are identical.
1983
				$providerAction = self::ACTION_REMOVE;
1984
				break;
1985
1986 View Code Duplication
			case self::ACTION_LINK_CONTINUE:
1987
				$state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
1988
				return is_array( $state ) ? $state['continueRequests'] : [];
1989
1990
			case self::ACTION_CHANGE:
1991
			case self::ACTION_REMOVE:
1992
				$providers = $this->getPrimaryAuthenticationProviders() +
1993
					$this->getSecondaryAuthenticationProviders();
1994
				break;
1995
1996
			// @codeCoverageIgnoreStart
1997
			default:
1998
				throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
1999
		}
2000
		// @codeCoverageIgnoreEnd
2001
2002
		return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2003
	}
2004
2005
	/**
2006
	 * Internal request lookup for self::getAuthenticationRequests
2007
	 *
2008
	 * @param string $providerAction Action to pass to providers
2009
	 * @param array $options Options to pass to providers
2010
	 * @param AuthenticationProvider[] $providers
2011
	 * @param User|null $user
2012
	 * @return AuthenticationRequest[]
2013
	 */
2014
	private function getAuthenticationRequestsInternal(
2015
		$providerAction, array $options, array $providers, User $user = null
2016
	) {
2017
		$user = $user ?: \RequestContext::getMain()->getUser();
2018
		$options['username'] = $user->isAnon() ? null : $user->getName();
2019
2020
		// Query them and merge results
2021
		$reqs = [];
2022
		$allPrimaryRequired = null;
2023
		foreach ( $providers as $provider ) {
2024
			$isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2025
			$thisRequired = [];
2026
			foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2027
				$id = $req->getUniqueId();
2028
2029
				// If it's from a Primary, mark it as "primary-required" but
2030
				// track it for later.
2031
				if ( $isPrimary ) {
2032
					if ( $req->required ) {
2033
						$thisRequired[$id] = true;
2034
						$req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2035
					}
2036
				}
2037
2038
				if ( !isset( $reqs[$id] ) || $req->required === AuthenticationRequest::REQUIRED ) {
2039
					$reqs[$id] = $req;
2040
				}
2041
			}
2042
2043
			// Track which requests are required by all primaries
2044
			if ( $isPrimary ) {
2045
				$allPrimaryRequired = $allPrimaryRequired === null
2046
					? $thisRequired
2047
					: array_intersect_key( $allPrimaryRequired, $thisRequired );
2048
			}
2049
		}
2050
		// Any requests that were required by all primaries are required.
2051
		foreach ( (array)$allPrimaryRequired as $id => $dummy ) {
2052
			$reqs[$id]->required = AuthenticationRequest::REQUIRED;
2053
		}
2054
2055
		// AuthManager has its own req for some actions
2056
		switch ( $providerAction ) {
2057
			case self::ACTION_LOGIN:
2058
				$reqs[] = new RememberMeAuthenticationRequest;
2059
				break;
2060
2061
			case self::ACTION_CREATE:
2062
				$reqs[] = new UsernameAuthenticationRequest;
2063
				$reqs[] = new UserDataAuthenticationRequest;
2064
				if ( $options['username'] !== null ) {
2065
					$reqs[] = new CreationReasonAuthenticationRequest;
2066
					$options['username'] = null; // Don't fill in the username below
2067
				}
2068
				break;
2069
		}
2070
2071
		// Fill in reqs data
2072
		foreach ( $reqs as $req ) {
2073
			$req->action = $providerAction;
2074
			if ( $req->username === null ) {
2075
				$req->username = $options['username'];
2076
			}
2077
		}
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
	 * Determine whether a username exists
2091
	 * @param string $username
2092
	 * @param int $flags Bitfield of User:READ_* constants
2093
	 * @return bool
2094
	 */
2095
	public function userExists( $username, $flags = User::READ_NORMAL ) {
2096
		foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2097
			if ( $provider->testUserExists( $username, $flags ) ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method testUserExists() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
2098
				return true;
2099
			}
2100
		}
2101
2102
		return false;
2103
	}
2104
2105
	/**
2106
	 * Determine whether a user property should be allowed to be changed.
2107
	 *
2108
	 * Supported properties are:
2109
	 *  - emailaddress
2110
	 *  - realname
2111
	 *  - nickname
2112
	 *
2113
	 * @param string $property
2114
	 * @return bool
2115
	 */
2116
	public function allowsPropertyChange( $property ) {
2117
		$providers = $this->getPrimaryAuthenticationProviders() +
2118
			$this->getSecondaryAuthenticationProviders();
2119
		foreach ( $providers as $provider ) {
2120
			if ( !$provider->providerAllowsPropertyChange( $property ) ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface MediaWiki\Auth\AuthenticationProvider as the method providerAllowsPropertyChange() does only exist in the following implementations of said interface: MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractP...yAuthenticationProvider, MediaWiki\Auth\AbstractS...yAuthenticationProvider, MediaWiki\Auth\AuthPlugi...yAuthenticationProvider, MediaWiki\Auth\CheckBloc...yAuthenticationProvider, MediaWiki\Auth\ConfirmLi...yAuthenticationProvider, MediaWiki\Auth\EmailNoti...yAuthenticationProvider, MediaWiki\Auth\LocalPass...yAuthenticationProvider, MediaWiki\Auth\ResetPass...yAuthenticationProvider, MediaWiki\Auth\Temporary...yAuthenticationProvider.

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
2121
				return false;
2122
			}
2123
		}
2124
		return true;
2125
	}
2126
2127
	/**@}*/
2128
2129
	/**
2130
	 * @name Internal methods
2131
	 * @{
2132
	 */
2133
2134
	/**
2135
	 * Store authentication in the current session
2136
	 * @protected For use by AuthenticationProviders
2137
	 * @param string $key
2138
	 * @param mixed $data Must be serializable
2139
	 */
2140
	public function setAuthenticationSessionData( $key, $data ) {
2141
		$session = $this->request->getSession();
2142
		$arr = $session->getSecret( 'authData' );
2143
		if ( !is_array( $arr ) ) {
2144
			$arr = [];
2145
		}
2146
		$arr[$key] = $data;
2147
		$session->setSecret( 'authData', $arr );
2148
	}
2149
2150
	/**
2151
	 * Fetch authentication data from the current session
2152
	 * @protected For use by AuthenticationProviders
2153
	 * @param string $key
2154
	 * @param mixed $default
2155
	 * @return mixed
2156
	 */
2157
	public function getAuthenticationSessionData( $key, $default = null ) {
2158
		$arr = $this->request->getSession()->getSecret( 'authData' );
2159
		if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2160
			return $arr[$key];
2161
		} else {
2162
			return $default;
2163
		}
2164
	}
2165
2166
	/**
2167
	 * Remove authentication data
2168
	 * @protected For use by AuthenticationProviders
2169
	 * @param string|null $key If null, all data is removed
2170
	 */
2171
	public function removeAuthenticationSessionData( $key ) {
2172
		$session = $this->request->getSession();
2173
		if ( $key === null ) {
2174
			$session->remove( 'authData' );
2175
		} else {
2176
			$arr = $session->getSecret( 'authData' );
2177
			if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2178
				unset( $arr[$key] );
2179
				$session->setSecret( 'authData', $arr );
2180
			}
2181
		}
2182
	}
2183
2184
	/**
2185
	 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2186
	 * @param string $class
2187
	 * @param array[] $specs
2188
	 * @return AuthenticationProvider[]
2189
	 */
2190
	protected function providerArrayFromSpecs( $class, array $specs ) {
2191
		$i = 0;
2192
		foreach ( $specs as &$spec ) {
2193
			$spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2194
		}
2195
		unset( $spec );
2196
		usort( $specs, function ( $a, $b ) {
2197
			return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2198
				?: $a['sort2'] - $b['sort2'];
2199
		} );
2200
2201
		$ret = [];
2202
		foreach ( $specs as $spec ) {
2203
			$provider = \ObjectFactory::getObjectFromSpec( $spec );
2204
			if ( !$provider instanceof $class ) {
2205
				throw new \RuntimeException(
2206
					"Expected instance of $class, got " . get_class( $provider )
2207
				);
2208
			}
2209
			$provider->setLogger( $this->logger );
2210
			$provider->setManager( $this );
2211
			$provider->setConfig( $this->config );
2212
			$id = $provider->getUniqueId();
2213 View Code Duplication
			if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2214
				throw new \RuntimeException(
2215
					"Duplicate specifications for id $id (classes " .
2216
					get_class( $provider ) . ' and ' .
2217
					get_class( $this->allAuthenticationProviders[$id] ) . ')'
2218
				);
2219
			}
2220
			$this->allAuthenticationProviders[$id] = $provider;
2221
			$ret[$id] = $provider;
2222
		}
2223
		return $ret;
2224
	}
2225
2226
	/**
2227
	 * Get the configuration
2228
	 * @return array
2229
	 */
2230
	private function getConfiguration() {
2231
		return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2232
	}
2233
2234
	/**
2235
	 * Get the list of PreAuthenticationProviders
2236
	 * @return PreAuthenticationProvider[]
2237
	 */
2238
	protected function getPreAuthenticationProviders() {
2239
		if ( $this->preAuthenticationProviders === null ) {
2240
			$conf = $this->getConfiguration();
2241
			$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...
2242
				PreAuthenticationProvider::class, $conf['preauth']
2243
			);
2244
		}
2245
		return $this->preAuthenticationProviders;
2246
	}
2247
2248
	/**
2249
	 * Get the list of PrimaryAuthenticationProviders
2250
	 * @return PrimaryAuthenticationProvider[]
2251
	 */
2252
	protected function getPrimaryAuthenticationProviders() {
2253
		if ( $this->primaryAuthenticationProviders === null ) {
2254
			$conf = $this->getConfiguration();
2255
			$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...
2256
				PrimaryAuthenticationProvider::class, $conf['primaryauth']
2257
			);
2258
		}
2259
		return $this->primaryAuthenticationProviders;
2260
	}
2261
2262
	/**
2263
	 * Get the list of SecondaryAuthenticationProviders
2264
	 * @return SecondaryAuthenticationProvider[]
2265
	 */
2266
	protected function getSecondaryAuthenticationProviders() {
2267
		if ( $this->secondaryAuthenticationProviders === null ) {
2268
			$conf = $this->getConfiguration();
2269
			$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...
2270
				SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2271
			);
2272
		}
2273
		return $this->secondaryAuthenticationProviders;
2274
	}
2275
2276
	/**
2277
	 * Get a provider by ID
2278
	 * @param string $id
2279
	 * @return AuthenticationProvider|null
2280
	 */
2281
	protected function getAuthenticationProvider( $id ) {
2282
		// Fast version
2283
		if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2284
			return $this->allAuthenticationProviders[$id];
2285
		}
2286
2287
		// Slow version: instantiate each kind and check
2288
		$providers = $this->getPrimaryAuthenticationProviders();
2289
		if ( isset( $providers[$id] ) ) {
2290
			return $providers[$id];
2291
		}
2292
		$providers = $this->getSecondaryAuthenticationProviders();
2293
		if ( isset( $providers[$id] ) ) {
2294
			return $providers[$id];
2295
		}
2296
		$providers = $this->getPreAuthenticationProviders();
2297
		if ( isset( $providers[$id] ) ) {
2298
			return $providers[$id];
2299
		}
2300
2301
		return null;
2302
	}
2303
2304
	/**
2305
	 * @param User $user
2306
	 * @param bool|null $remember
2307
	 */
2308
	private function setSessionDataForUser( $user, $remember = null ) {
2309
		$session = $this->request->getSession();
2310
		$delay = $session->delaySave();
2311
2312
		$session->resetId();
2313
		if ( $session->canSetUser() ) {
2314
			$session->setUser( $user );
2315
		}
2316
		if ( $remember !== null ) {
2317
			$session->setRememberUser( $remember );
2318
		}
2319
		$session->set( 'AuthManager:lastAuthId', $user->getId() );
2320
		$session->set( 'AuthManager:lastAuthTimestamp', time() );
2321
		$session->persist();
2322
2323
		\ScopedCallback::consume( $delay );
2324
2325
		\Hooks::run( 'UserLoggedIn', [ $user ] );
2326
	}
2327
2328
	/**
2329
	 * @param User $user
2330
	 * @param bool $useContextLang Use 'uselang' to set the user's language
2331
	 */
2332
	private function setDefaultUserOptions( User $user, $useContextLang ) {
2333
		global $wgContLang;
2334
2335
		\MediaWiki\Session\SessionManager::singleton()->invalidateSessionsForUser( $user );
2336
2337
		$lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
2338
		$user->setOption( 'language', $lang->getPreferredVariant() );
2339
2340
		if ( $wgContLang->hasVariants() ) {
2341
			$user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2342
		}
2343
	}
2344
2345
	/**
2346
	 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2347
	 * @param string $method
2348
	 * @param array $args
2349
	 */
2350
	private function callMethodOnProviders( $which, $method, array $args ) {
2351
		$providers = [];
2352
		if ( $which & 1 ) {
2353
			$providers += $this->getPreAuthenticationProviders();
2354
		}
2355
		if ( $which & 2 ) {
2356
			$providers += $this->getPrimaryAuthenticationProviders();
2357
		}
2358
		if ( $which & 4 ) {
2359
			$providers += $this->getSecondaryAuthenticationProviders();
2360
		}
2361
		foreach ( $providers as $provider ) {
2362
			call_user_func_array( [ $provider, $method ], $args );
2363
		}
2364
	}
2365
2366
	/**
2367
	 * Reset the internal caching for unit testing
2368
	 */
2369
	public static function resetCache() {
2370
		if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2371
			// @codeCoverageIgnoreStart
2372
			throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2373
			// @codeCoverageIgnoreEnd
2374
		}
2375
2376
		self::$instance = null;
2377
	}
2378
2379
	/**@}*/
2380
2381
}
2382
2383
/**
2384
 * For really cool vim folding this needs to be at the end:
2385
 * vim: foldmarker=@{,@} foldmethod=marker
2386
 */
2387