]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/auth/AuthManager.php
MediaWiki 1.30.2
[autoinstalls/mediawiki.git] / includes / auth / AuthManager.php
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 MediaWiki\MediaWikiServices;
28 use Psr\Log\LoggerAwareInterface;
29 use Psr\Log\LoggerInterface;
30 use Status;
31 use StatusValue;
32 use User;
33 use WebRequest;
34
35 /**
36  * This serves as the entry point to the authentication system.
37  *
38  * In the future, it may also serve as the entry point to the authorization
39  * system.
40  *
41  * If you are looking at this because you are working on an extension that creates its own
42  * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
43  * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
44  * or the createaccount API. Trying to call this class directly will very likely end up in
45  * security vulnerabilities or broken UX in edge cases.
46  *
47  * If you are working on an extension that needs to integrate with the authentication system
48  * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
49  * need to write an AuthenticationProvider.
50  *
51  * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
52  * you are looking for. If you want to change user data, use User::changeAuthenticationData().
53  * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
54  * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
55  * responsibility to ensure that the user can authenticate somehow (see especially
56  * PrimaryAuthenticationProvider::autoCreatedAccount()).
57  * If you are writing code that is not associated with such a provider and needs to create accounts
58  * programmatically for real users, you should rethink your architecture. There is no good way to
59  * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
60  * cannot provide any means for users to access the accounts it would create.
61  *
62  * The two main control flows when using this class are as follows:
63  * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
64  *   the requests with data (by using them to build a HTMLForm and have the user fill it, or by
65  *   exposing a form specification via the API, so that the client can build it), and pass them to
66  *   the appropriate begin* method. That will return either a success/failure response, or more
67  *   requests to fill (either by building a form or by redirecting the user to some external
68  *   provider which will send the data back), in which case they need to be submitted to the
69  *   appropriate continue* method and that step has to be repeated until the response is a success
70  *   or failure response. AuthManager will use the session to maintain internal state during the
71  *   process.
72  * * Code doing an authentication data change will call getAuthenticationRequests(), select
73  *   a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
74  *   changeAuthenticationData(). If the data change is user-initiated, the whole process needs
75  *   to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
76  *   a non-OK status.
77  *
78  * @ingroup Auth
79  * @since 1.27
80  * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
81  */
82 class AuthManager implements LoggerAwareInterface {
83         /** Log in with an existing (not necessarily local) user */
84         const ACTION_LOGIN = 'login';
85         /** Continue a login process that was interrupted by the need for user input or communication
86          * with an external provider */
87         const ACTION_LOGIN_CONTINUE = 'login-continue';
88         /** Create a new user */
89         const ACTION_CREATE = 'create';
90         /** Continue a user creation process that was interrupted by the need for user input or
91          * communication with an external provider */
92         const ACTION_CREATE_CONTINUE = 'create-continue';
93         /** Link an existing user to a third-party account */
94         const ACTION_LINK = 'link';
95         /** Continue a user linking process that was interrupted by the need for user input or
96          * communication with an external provider */
97         const ACTION_LINK_CONTINUE = 'link-continue';
98         /** Change a user's credentials */
99         const ACTION_CHANGE = 'change';
100         /** Remove a user's credentials */
101         const ACTION_REMOVE = 'remove';
102         /** Like ACTION_REMOVE but for linking providers only */
103         const ACTION_UNLINK = 'unlink';
104
105         /** Security-sensitive operations are ok. */
106         const SEC_OK = 'ok';
107         /** Security-sensitive operations should re-authenticate. */
108         const SEC_REAUTH = 'reauth';
109         /** Security-sensitive should not be performed. */
110         const SEC_FAIL = 'fail';
111
112         /** Auto-creation is due to SessionManager */
113         const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
114
115         /** @var AuthManager|null */
116         private static $instance = null;
117
118         /** @var WebRequest */
119         private $request;
120
121         /** @var Config */
122         private $config;
123
124         /** @var LoggerInterface */
125         private $logger;
126
127         /** @var AuthenticationProvider[] */
128         private $allAuthenticationProviders = [];
129
130         /** @var PreAuthenticationProvider[] */
131         private $preAuthenticationProviders = null;
132
133         /** @var PrimaryAuthenticationProvider[] */
134         private $primaryAuthenticationProviders = null;
135
136         /** @var SecondaryAuthenticationProvider[] */
137         private $secondaryAuthenticationProviders = null;
138
139         /** @var CreatedAccountAuthenticationRequest[] */
140         private $createdAccountAuthenticationRequests = [];
141
142         /**
143          * Get the global AuthManager
144          * @return AuthManager
145          */
146         public static function singleton() {
147                 if ( self::$instance === null ) {
148                         self::$instance = new self(
149                                 \RequestContext::getMain()->getRequest(),
150                                 MediaWikiServices::getInstance()->getMainConfig()
151                         );
152                 }
153                 return self::$instance;
154         }
155
156         /**
157          * @param WebRequest $request
158          * @param Config $config
159          */
160         public function __construct( WebRequest $request, Config $config ) {
161                 $this->request = $request;
162                 $this->config = $config;
163                 $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
164         }
165
166         /**
167          * @param LoggerInterface $logger
168          */
169         public function setLogger( LoggerInterface $logger ) {
170                 $this->logger = $logger;
171         }
172
173         /**
174          * @return WebRequest
175          */
176         public function getRequest() {
177                 return $this->request;
178         }
179
180         /**
181          * Force certain PrimaryAuthenticationProviders
182          * @deprecated For backwards compatibility only
183          * @param PrimaryAuthenticationProvider[] $providers
184          * @param string $why
185          */
186         public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
187                 $this->logger->warning( "Overriding AuthManager primary authn because $why" );
188
189                 if ( $this->primaryAuthenticationProviders !== null ) {
190                         $this->logger->warning(
191                                 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
192                         );
193
194                         $this->allAuthenticationProviders = array_diff_key(
195                                 $this->allAuthenticationProviders,
196                                 $this->primaryAuthenticationProviders
197                         );
198                         $session = $this->request->getSession();
199                         $session->remove( 'AuthManager::authnState' );
200                         $session->remove( 'AuthManager::accountCreationState' );
201                         $session->remove( 'AuthManager::accountLinkState' );
202                         $this->createdAccountAuthenticationRequests = [];
203                 }
204
205                 $this->primaryAuthenticationProviders = [];
206                 foreach ( $providers as $provider ) {
207                         if ( !$provider instanceof PrimaryAuthenticationProvider ) {
208                                 throw new \RuntimeException(
209                                         'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
210                                                 get_class( $provider )
211                                 );
212                         }
213                         $provider->setLogger( $this->logger );
214                         $provider->setManager( $this );
215                         $provider->setConfig( $this->config );
216                         $id = $provider->getUniqueId();
217                         if ( isset( $this->allAuthenticationProviders[$id] ) ) {
218                                 throw new \RuntimeException(
219                                         "Duplicate specifications for id $id (classes " .
220                                                 get_class( $provider ) . ' and ' .
221                                                 get_class( $this->allAuthenticationProviders[$id] ) . ')'
222                                 );
223                         }
224                         $this->allAuthenticationProviders[$id] = $provider;
225                         $this->primaryAuthenticationProviders[$id] = $provider;
226                 }
227         }
228
229         /**
230          * Call a legacy AuthPlugin method, if necessary
231          * @codeCoverageIgnore
232          * @deprecated For backwards compatibility only, should be avoided in new code
233          * @param string $method AuthPlugin method to call
234          * @param array $params Parameters to pass
235          * @param mixed $return Return value if AuthPlugin wasn't called
236          * @return mixed Return value from the AuthPlugin method, or $return
237          */
238         public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
239                 global $wgAuth;
240
241                 if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
242                         return call_user_func_array( [ $wgAuth, $method ], $params );
243                 } else {
244                         return $return;
245                 }
246         }
247
248         /**
249          * @name Authentication
250          * @{
251          */
252
253         /**
254          * Indicate whether user authentication is possible
255          *
256          * It may not be if the session is provided by something like OAuth
257          * for which each individual request includes authentication data.
258          *
259          * @return bool
260          */
261         public function canAuthenticateNow() {
262                 return $this->request->getSession()->canSetUser();
263         }
264
265         /**
266          * Start an authentication flow
267          *
268          * In addition to the AuthenticationRequests returned by
269          * $this->getAuthenticationRequests(), a client might include a
270          * CreateFromLoginAuthenticationRequest from a previous login attempt to
271          * preserve state.
272          *
273          * Instead of the AuthenticationRequests returned by
274          * $this->getAuthenticationRequests(), a client might pass a
275          * CreatedAccountAuthenticationRequest from an account creation that just
276          * succeeded to log in to the just-created account.
277          *
278          * @param AuthenticationRequest[] $reqs
279          * @param string $returnToUrl Url that REDIRECT responses should eventually
280          *  return to.
281          * @return AuthenticationResponse See self::continueAuthentication()
282          */
283         public function beginAuthentication( array $reqs, $returnToUrl ) {
284                 $session = $this->request->getSession();
285                 if ( !$session->canSetUser() ) {
286                         // Caller should have called canAuthenticateNow()
287                         $session->remove( 'AuthManager::authnState' );
288                         throw new \LogicException( 'Authentication is not possible now' );
289                 }
290
291                 $guessUserName = null;
292                 foreach ( $reqs as $req ) {
293                         $req->returnToUrl = $returnToUrl;
294                         // @codeCoverageIgnoreStart
295                         if ( $req->username !== null && $req->username !== '' ) {
296                                 if ( $guessUserName === null ) {
297                                         $guessUserName = $req->username;
298                                 } elseif ( $guessUserName !== $req->username ) {
299                                         $guessUserName = null;
300                                         break;
301                                 }
302                         }
303                         // @codeCoverageIgnoreEnd
304                 }
305
306                 // Check for special-case login of a just-created account
307                 $req = AuthenticationRequest::getRequestByClass(
308                         $reqs, CreatedAccountAuthenticationRequest::class
309                 );
310                 if ( $req ) {
311                         if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
312                                 throw new \LogicException(
313                                         'CreatedAccountAuthenticationRequests are only valid on ' .
314                                                 'the same AuthManager that created the account'
315                                 );
316                         }
317
318                         $user = User::newFromName( $req->username );
319                         // @codeCoverageIgnoreStart
320                         if ( !$user ) {
321                                 throw new \UnexpectedValueException(
322                                         "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
323                                 );
324                         } elseif ( $user->getId() != $req->id ) {
325                                 throw new \UnexpectedValueException(
326                                         "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
327                                 );
328                         }
329                         // @codeCoverageIgnoreEnd
330
331                         $this->logger->info( 'Logging in {user} after account creation', [
332                                 'user' => $user->getName(),
333                         ] );
334                         $ret = AuthenticationResponse::newPass( $user->getName() );
335                         $this->setSessionDataForUser( $user );
336                         $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
337                         $session->remove( 'AuthManager::authnState' );
338                         \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
339                         return $ret;
340                 }
341
342                 $this->removeAuthenticationSessionData( null );
343
344                 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
345                         $status = $provider->testForAuthentication( $reqs );
346                         if ( !$status->isGood() ) {
347                                 $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
348                                 $ret = AuthenticationResponse::newFail(
349                                         Status::wrap( $status )->getMessage()
350                                 );
351                                 $this->callMethodOnProviders( 7, 'postAuthentication',
352                                         [ User::newFromName( $guessUserName ) ?: null, $ret ]
353                                 );
354                                 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
355                                 return $ret;
356                         }
357                 }
358
359                 $state = [
360                         'reqs' => $reqs,
361                         'returnToUrl' => $returnToUrl,
362                         'guessUserName' => $guessUserName,
363                         'primary' => null,
364                         'primaryResponse' => null,
365                         'secondary' => [],
366                         'maybeLink' => [],
367                         'continueRequests' => [],
368                 ];
369
370                 // Preserve state from a previous failed login
371                 $req = AuthenticationRequest::getRequestByClass(
372                         $reqs, CreateFromLoginAuthenticationRequest::class
373                 );
374                 if ( $req ) {
375                         $state['maybeLink'] = $req->maybeLink;
376                 }
377
378                 $session = $this->request->getSession();
379                 $session->setSecret( 'AuthManager::authnState', $state );
380                 $session->persist();
381
382                 return $this->continueAuthentication( $reqs );
383         }
384
385         /**
386          * Continue an authentication flow
387          *
388          * Return values are interpreted as follows:
389          * - status FAIL: Authentication failed. If $response->createRequest is
390          *   set, that may be passed to self::beginAuthentication() or to
391          *   self::beginAccountCreation() to preserve state.
392          * - status REDIRECT: The client should be redirected to the contained URL,
393          *   new AuthenticationRequests should be made (if any), then
394          *   AuthManager::continueAuthentication() should be called.
395          * - status UI: The client should be presented with a user interface for
396          *   the fields in the specified AuthenticationRequests, then new
397          *   AuthenticationRequests should be made, then
398          *   AuthManager::continueAuthentication() should be called.
399          * - status RESTART: The user logged in successfully with a third-party
400          *   service, but the third-party credentials aren't attached to any local
401          *   account. This could be treated as a UI or a FAIL.
402          * - status PASS: Authentication was successful.
403          *
404          * @param AuthenticationRequest[] $reqs
405          * @return AuthenticationResponse
406          */
407         public function continueAuthentication( array $reqs ) {
408                 $session = $this->request->getSession();
409                 try {
410                         if ( !$session->canSetUser() ) {
411                                 // Caller should have called canAuthenticateNow()
412                                 // @codeCoverageIgnoreStart
413                                 throw new \LogicException( 'Authentication is not possible now' );
414                                 // @codeCoverageIgnoreEnd
415                         }
416
417                         $state = $session->getSecret( 'AuthManager::authnState' );
418                         if ( !is_array( $state ) ) {
419                                 return AuthenticationResponse::newFail(
420                                         wfMessage( 'authmanager-authn-not-in-progress' )
421                                 );
422                         }
423                         $state['continueRequests'] = [];
424
425                         $guessUserName = $state['guessUserName'];
426
427                         foreach ( $reqs as $req ) {
428                                 $req->returnToUrl = $state['returnToUrl'];
429                         }
430
431                         // Step 1: Choose an primary authentication provider, and call it until it succeeds.
432
433                         if ( $state['primary'] === null ) {
434                                 // We haven't picked a PrimaryAuthenticationProvider yet
435                                 // @codeCoverageIgnoreStart
436                                 $guessUserName = null;
437                                 foreach ( $reqs as $req ) {
438                                         if ( $req->username !== null && $req->username !== '' ) {
439                                                 if ( $guessUserName === null ) {
440                                                         $guessUserName = $req->username;
441                                                 } elseif ( $guessUserName !== $req->username ) {
442                                                         $guessUserName = null;
443                                                         break;
444                                                 }
445                                         }
446                                 }
447                                 $state['guessUserName'] = $guessUserName;
448                                 // @codeCoverageIgnoreEnd
449                                 $state['reqs'] = $reqs;
450
451                                 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
452                                         $res = $provider->beginPrimaryAuthentication( $reqs );
453                                         switch ( $res->status ) {
454                                                 case AuthenticationResponse::PASS;
455                                                         $state['primary'] = $id;
456                                                         $state['primaryResponse'] = $res;
457                                                         $this->logger->debug( "Primary login with $id succeeded" );
458                                                         break 2;
459                                                 case AuthenticationResponse::FAIL;
460                                                         $this->logger->debug( "Login failed in primary authentication by $id" );
461                                                         if ( $res->createRequest || $state['maybeLink'] ) {
462                                                                 $res->createRequest = new CreateFromLoginAuthenticationRequest(
463                                                                         $res->createRequest, $state['maybeLink']
464                                                                 );
465                                                         }
466                                                         $this->callMethodOnProviders( 7, 'postAuthentication',
467                                                                 [ User::newFromName( $guessUserName ) ?: null, $res ]
468                                                         );
469                                                         $session->remove( 'AuthManager::authnState' );
470                                                         \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
471                                                         return $res;
472                                                 case AuthenticationResponse::ABSTAIN;
473                                                         // Continue loop
474                                                         break;
475                                                 case AuthenticationResponse::REDIRECT;
476                                                 case AuthenticationResponse::UI;
477                                                         $this->logger->debug( "Primary login with $id returned $res->status" );
478                                                         $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
479                                                         $state['primary'] = $id;
480                                                         $state['continueRequests'] = $res->neededRequests;
481                                                         $session->setSecret( 'AuthManager::authnState', $state );
482                                                         return $res;
483
484                                                         // @codeCoverageIgnoreStart
485                                                 default:
486                                                         throw new \DomainException(
487                                                                 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
488                                                         );
489                                                         // @codeCoverageIgnoreEnd
490                                         }
491                                 }
492                                 if ( $state['primary'] === null ) {
493                                         $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
494                                         $ret = AuthenticationResponse::newFail(
495                                                 wfMessage( 'authmanager-authn-no-primary' )
496                                         );
497                                         $this->callMethodOnProviders( 7, 'postAuthentication',
498                                                 [ User::newFromName( $guessUserName ) ?: null, $ret ]
499                                         );
500                                         $session->remove( 'AuthManager::authnState' );
501                                         return $ret;
502                                 }
503                         } elseif ( $state['primaryResponse'] === null ) {
504                                 $provider = $this->getAuthenticationProvider( $state['primary'] );
505                                 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
506                                         // Configuration changed? Force them to start over.
507                                         // @codeCoverageIgnoreStart
508                                         $ret = AuthenticationResponse::newFail(
509                                                 wfMessage( 'authmanager-authn-not-in-progress' )
510                                         );
511                                         $this->callMethodOnProviders( 7, 'postAuthentication',
512                                                 [ User::newFromName( $guessUserName ) ?: null, $ret ]
513                                         );
514                                         $session->remove( 'AuthManager::authnState' );
515                                         return $ret;
516                                         // @codeCoverageIgnoreEnd
517                                 }
518                                 $id = $provider->getUniqueId();
519                                 $res = $provider->continuePrimaryAuthentication( $reqs );
520                                 switch ( $res->status ) {
521                                         case AuthenticationResponse::PASS;
522                                                 $state['primaryResponse'] = $res;
523                                                 $this->logger->debug( "Primary login with $id succeeded" );
524                                                 break;
525                                         case AuthenticationResponse::FAIL;
526                                                 $this->logger->debug( "Login failed in primary authentication by $id" );
527                                                 if ( $res->createRequest || $state['maybeLink'] ) {
528                                                         $res->createRequest = new CreateFromLoginAuthenticationRequest(
529                                                                 $res->createRequest, $state['maybeLink']
530                                                         );
531                                                 }
532                                                 $this->callMethodOnProviders( 7, 'postAuthentication',
533                                                         [ User::newFromName( $guessUserName ) ?: null, $res ]
534                                                 );
535                                                 $session->remove( 'AuthManager::authnState' );
536                                                 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
537                                                 return $res;
538                                         case AuthenticationResponse::REDIRECT;
539                                         case AuthenticationResponse::UI;
540                                                 $this->logger->debug( "Primary login with $id returned $res->status" );
541                                                 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
542                                                 $state['continueRequests'] = $res->neededRequests;
543                                                 $session->setSecret( 'AuthManager::authnState', $state );
544                                                 return $res;
545                                         default:
546                                                 throw new \DomainException(
547                                                         get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
548                                                 );
549                                 }
550                         }
551
552                         $res = $state['primaryResponse'];
553                         if ( $res->username === null ) {
554                                 $provider = $this->getAuthenticationProvider( $state['primary'] );
555                                 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
556                                         // Configuration changed? Force them to start over.
557                                         // @codeCoverageIgnoreStart
558                                         $ret = AuthenticationResponse::newFail(
559                                                 wfMessage( 'authmanager-authn-not-in-progress' )
560                                         );
561                                         $this->callMethodOnProviders( 7, 'postAuthentication',
562                                                 [ User::newFromName( $guessUserName ) ?: null, $ret ]
563                                         );
564                                         $session->remove( 'AuthManager::authnState' );
565                                         return $ret;
566                                         // @codeCoverageIgnoreEnd
567                                 }
568
569                                 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
570                                         $res->linkRequest &&
571                                          // don't confuse the user with an incorrect message if linking is disabled
572                                         $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
573                                 ) {
574                                         $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
575                                         $msg = 'authmanager-authn-no-local-user-link';
576                                 } else {
577                                         $msg = 'authmanager-authn-no-local-user';
578                                 }
579                                 $this->logger->debug(
580                                         "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
581                                 );
582                                 $ret = AuthenticationResponse::newRestart( wfMessage( $msg ) );
583                                 $ret->neededRequests = $this->getAuthenticationRequestsInternal(
584                                         self::ACTION_LOGIN,
585                                         [],
586                                         $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
587                                 );
588                                 if ( $res->createRequest || $state['maybeLink'] ) {
589                                         $ret->createRequest = new CreateFromLoginAuthenticationRequest(
590                                                 $res->createRequest, $state['maybeLink']
591                                         );
592                                         $ret->neededRequests[] = $ret->createRequest;
593                                 }
594                                 $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
595                                 $session->setSecret( 'AuthManager::authnState', [
596                                         'reqs' => [], // Will be filled in later
597                                         'primary' => null,
598                                         'primaryResponse' => null,
599                                         'secondary' => [],
600                                         'continueRequests' => $ret->neededRequests,
601                                 ] + $state );
602                                 return $ret;
603                         }
604
605                         // Step 2: Primary authentication succeeded, create the User object
606                         // (and add the user locally if necessary)
607
608                         $user = User::newFromName( $res->username, 'usable' );
609                         if ( !$user ) {
610                                 $provider = $this->getAuthenticationProvider( $state['primary'] );
611                                 throw new \DomainException(
612                                         get_class( $provider ) . " returned an invalid username: {$res->username}"
613                                 );
614                         }
615                         if ( $user->getId() === 0 ) {
616                                 // User doesn't exist locally. Create it.
617                                 $this->logger->info( 'Auto-creating {user} on login', [
618                                         'user' => $user->getName(),
619                                 ] );
620                                 $status = $this->autoCreateUser( $user, $state['primary'], false );
621                                 if ( !$status->isGood() ) {
622                                         $ret = AuthenticationResponse::newFail(
623                                                 Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
624                                         );
625                                         $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
626                                         $session->remove( 'AuthManager::authnState' );
627                                         \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
628                                         return $ret;
629                                 }
630                         }
631
632                         // Step 3: Iterate over all the secondary authentication providers.
633
634                         $beginReqs = $state['reqs'];
635
636                         foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
637                                 if ( !isset( $state['secondary'][$id] ) ) {
638                                         // This provider isn't started yet, so we pass it the set
639                                         // of reqs from beginAuthentication instead of whatever
640                                         // might have been used by a previous provider in line.
641                                         $func = 'beginSecondaryAuthentication';
642                                         $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
643                                 } elseif ( !$state['secondary'][$id] ) {
644                                         $func = 'continueSecondaryAuthentication';
645                                         $res = $provider->continueSecondaryAuthentication( $user, $reqs );
646                                 } else {
647                                         continue;
648                                 }
649                                 switch ( $res->status ) {
650                                         case AuthenticationResponse::PASS;
651                                                 $this->logger->debug( "Secondary login with $id succeeded" );
652                                                 // fall through
653                                         case AuthenticationResponse::ABSTAIN;
654                                                 $state['secondary'][$id] = true;
655                                                 break;
656                                         case AuthenticationResponse::FAIL;
657                                                 $this->logger->debug( "Login failed in secondary authentication by $id" );
658                                                 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
659                                                 $session->remove( 'AuthManager::authnState' );
660                                                 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
661                                                 return $res;
662                                         case AuthenticationResponse::REDIRECT;
663                                         case AuthenticationResponse::UI;
664                                                 $this->logger->debug( "Secondary login with $id returned " . $res->status );
665                                                 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
666                                                 $state['secondary'][$id] = false;
667                                                 $state['continueRequests'] = $res->neededRequests;
668                                                 $session->setSecret( 'AuthManager::authnState', $state );
669                                                 return $res;
670
671                                                 // @codeCoverageIgnoreStart
672                                         default:
673                                                 throw new \DomainException(
674                                                         get_class( $provider ) . "::{$func}() returned $res->status"
675                                                 );
676                                                 // @codeCoverageIgnoreEnd
677                                 }
678                         }
679
680                         // Step 4: Authentication complete! Set the user in the session and
681                         // clean up.
682
683                         $this->logger->info( 'Login for {user} succeeded from {clientip}', [
684                                 'user' => $user->getName(),
685                                 'clientip' => $this->request->getIP(),
686                         ] );
687                         /** @var RememberMeAuthenticationRequest $req */
688                         $req = AuthenticationRequest::getRequestByClass(
689                                 $beginReqs, RememberMeAuthenticationRequest::class
690                         );
691                         $this->setSessionDataForUser( $user, $req && $req->rememberMe );
692                         $ret = AuthenticationResponse::newPass( $user->getName() );
693                         $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
694                         $session->remove( 'AuthManager::authnState' );
695                         $this->removeAuthenticationSessionData( null );
696                         \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
697                         return $ret;
698                 } catch ( \Exception $ex ) {
699                         $session->remove( 'AuthManager::authnState' );
700                         throw $ex;
701                 }
702         }
703
704         /**
705          * Whether security-sensitive operations should proceed.
706          *
707          * A "security-sensitive operation" is something like a password or email
708          * change, that would normally have a "reenter your password to confirm"
709          * box if we only supported password-based authentication.
710          *
711          * @param string $operation Operation being checked. This should be a
712          *  message-key-like string such as 'change-password' or 'change-email'.
713          * @return string One of the SEC_* constants.
714          */
715         public function securitySensitiveOperationStatus( $operation ) {
716                 $status = self::SEC_OK;
717
718                 $this->logger->debug( __METHOD__ . ": Checking $operation" );
719
720                 $session = $this->request->getSession();
721                 $aId = $session->getUser()->getId();
722                 if ( $aId === 0 ) {
723                         // User isn't authenticated. DWIM?
724                         $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
725                         $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
726                         return $status;
727                 }
728
729                 if ( $session->canSetUser() ) {
730                         $id = $session->get( 'AuthManager:lastAuthId' );
731                         $last = $session->get( 'AuthManager:lastAuthTimestamp' );
732                         if ( $id !== $aId || $last === null ) {
733                                 $timeSinceLogin = PHP_INT_MAX; // Forever ago
734                         } else {
735                                 $timeSinceLogin = max( 0, time() - $last );
736                         }
737
738                         $thresholds = $this->config->get( 'ReauthenticateTime' );
739                         if ( isset( $thresholds[$operation] ) ) {
740                                 $threshold = $thresholds[$operation];
741                         } elseif ( isset( $thresholds['default'] ) ) {
742                                 $threshold = $thresholds['default'];
743                         } else {
744                                 throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
745                         }
746
747                         if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
748                                 $status = self::SEC_REAUTH;
749                         }
750                 } else {
751                         $timeSinceLogin = -1;
752
753                         $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
754                         if ( isset( $pass[$operation] ) ) {
755                                 $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
756                         } elseif ( isset( $pass['default'] ) ) {
757                                 $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
758                         } else {
759                                 throw new \UnexpectedValueException(
760                                         '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
761                                 );
762                         }
763                 }
764
765                 \Hooks::run( 'SecuritySensitiveOperationStatus', [
766                         &$status, $operation, $session, $timeSinceLogin
767                 ] );
768
769                 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
770                 if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
771                         $status = self::SEC_FAIL;
772                 }
773
774                 $this->logger->info( __METHOD__ . ": $operation is $status" );
775
776                 return $status;
777         }
778
779         /**
780          * Determine whether a username can authenticate
781          *
782          * This is mainly for internal purposes and only takes authentication data into account,
783          * not things like blocks that can change without the authentication system being aware.
784          *
785          * @param string $username MediaWiki username
786          * @return bool
787          */
788         public function userCanAuthenticate( $username ) {
789                 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
790                         if ( $provider->testUserCanAuthenticate( $username ) ) {
791                                 return true;
792                         }
793                 }
794                 return false;
795         }
796
797         /**
798          * Provide normalized versions of the username for security checks
799          *
800          * Since different providers can normalize the input in different ways,
801          * this returns an array of all the different ways the name might be
802          * normalized for authentication.
803          *
804          * The returned strings should not be revealed to the user, as that might
805          * leak private information (e.g. an email address might be normalized to a
806          * username).
807          *
808          * @param string $username
809          * @return string[]
810          */
811         public function normalizeUsername( $username ) {
812                 $ret = [];
813                 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
814                         $normalized = $provider->providerNormalizeUsername( $username );
815                         if ( $normalized !== null ) {
816                                 $ret[$normalized] = true;
817                         }
818                 }
819                 return array_keys( $ret );
820         }
821
822         /**@}*/
823
824         /**
825          * @name Authentication data changing
826          * @{
827          */
828
829         /**
830          * Revoke any authentication credentials for a user
831          *
832          * After this, the user should no longer be able to log in.
833          *
834          * @param string $username
835          */
836         public function revokeAccessForUser( $username ) {
837                 $this->logger->info( 'Revoking access for {user}', [
838                         'user' => $username,
839                 ] );
840                 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
841         }
842
843         /**
844          * Validate a change of authentication data (e.g. passwords)
845          * @param AuthenticationRequest $req
846          * @param bool $checkData If false, $req hasn't been loaded from the
847          *  submission so checks on user-submitted fields should be skipped. $req->username is
848          *  considered user-submitted for this purpose, even if it cannot be changed via
849          *  $req->loadFromSubmission.
850          * @return Status
851          */
852         public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
853                 $any = false;
854                 $providers = $this->getPrimaryAuthenticationProviders() +
855                         $this->getSecondaryAuthenticationProviders();
856                 foreach ( $providers as $provider ) {
857                         $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
858                         if ( !$status->isGood() ) {
859                                 return Status::wrap( $status );
860                         }
861                         $any = $any || $status->value !== 'ignored';
862                 }
863                 if ( !$any ) {
864                         $status = Status::newGood( 'ignored' );
865                         $status->warning( 'authmanager-change-not-supported' );
866                         return $status;
867                 }
868                 return Status::newGood();
869         }
870
871         /**
872          * Change authentication data (e.g. passwords)
873          *
874          * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
875          * result in a successful login in the future.
876          *
877          * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
878          * no longer result in a successful login.
879          *
880          * This method should only be called if allowsAuthenticationDataChange( $req, true )
881          * returned success.
882          *
883          * @param AuthenticationRequest $req
884          */
885         public function changeAuthenticationData( AuthenticationRequest $req ) {
886                 $this->logger->info( 'Changing authentication data for {user} class {what}', [
887                         'user' => is_string( $req->username ) ? $req->username : '<no name>',
888                         'what' => get_class( $req ),
889                 ] );
890
891                 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
892
893                 // When the main account's authentication data is changed, invalidate
894                 // all BotPasswords too.
895                 \BotPassword::invalidateAllPasswordsForUser( $req->username );
896         }
897
898         /**@}*/
899
900         /**
901          * @name Account creation
902          * @{
903          */
904
905         /**
906          * Determine whether accounts can be created
907          * @return bool
908          */
909         public function canCreateAccounts() {
910                 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
911                         switch ( $provider->accountCreationType() ) {
912                                 case PrimaryAuthenticationProvider::TYPE_CREATE:
913                                 case PrimaryAuthenticationProvider::TYPE_LINK:
914                                         return true;
915                         }
916                 }
917                 return false;
918         }
919
920         /**
921          * Determine whether a particular account can be created
922          * @param string $username MediaWiki username
923          * @param array $options
924          *  - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
925          *  - creating: (bool) For internal use only. Never specify this.
926          * @return Status
927          */
928         public function canCreateAccount( $username, $options = [] ) {
929                 // Back compat
930                 if ( is_int( $options ) ) {
931                         $options = [ 'flags' => $options ];
932                 }
933                 $options += [
934                         'flags' => User::READ_NORMAL,
935                         'creating' => false,
936                 ];
937                 $flags = $options['flags'];
938
939                 if ( !$this->canCreateAccounts() ) {
940                         return Status::newFatal( 'authmanager-create-disabled' );
941                 }
942
943                 if ( $this->userExists( $username, $flags ) ) {
944                         return Status::newFatal( 'userexists' );
945                 }
946
947                 $user = User::newFromName( $username, 'creatable' );
948                 if ( !is_object( $user ) ) {
949                         return Status::newFatal( 'noname' );
950                 } else {
951                         $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
952                         if ( $user->getId() !== 0 ) {
953                                 return Status::newFatal( 'userexists' );
954                         }
955                 }
956
957                 // Denied by providers?
958                 $providers = $this->getPreAuthenticationProviders() +
959                         $this->getPrimaryAuthenticationProviders() +
960                         $this->getSecondaryAuthenticationProviders();
961                 foreach ( $providers as $provider ) {
962                         $status = $provider->testUserForCreation( $user, false, $options );
963                         if ( !$status->isGood() ) {
964                                 return Status::wrap( $status );
965                         }
966                 }
967
968                 return Status::newGood();
969         }
970
971         /**
972          * Basic permissions checks on whether a user can create accounts
973          * @param User $creator User doing the account creation
974          * @return Status
975          */
976         public function checkAccountCreatePermissions( User $creator ) {
977                 // Wiki is read-only?
978                 if ( wfReadOnly() ) {
979                         return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
980                 }
981
982                 // This is awful, this permission check really shouldn't go through Title.
983                 $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
984                         ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
985                 if ( $permErrors ) {
986                         $status = Status::newGood();
987                         foreach ( $permErrors as $args ) {
988                                 call_user_func_array( [ $status, 'fatal' ], $args );
989                         }
990                         return $status;
991                 }
992
993                 $block = $creator->isBlockedFromCreateAccount();
994                 if ( $block ) {
995                         $errorParams = [
996                                 $block->getTarget(),
997                                 $block->mReason ?: wfMessage( 'blockednoreason' )->text(),
998                                 $block->getByName()
999                         ];
1000
1001                         if ( $block->getType() === \Block::TYPE_RANGE ) {
1002                                 $errorMessage = 'cantcreateaccount-range-text';
1003                                 $errorParams[] = $this->getRequest()->getIP();
1004                         } else {
1005                                 $errorMessage = 'cantcreateaccount-text';
1006                         }
1007
1008                         return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1009                 }
1010
1011                 $ip = $this->getRequest()->getIP();
1012                 if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1013                         return Status::newFatal( 'sorbs_create_account_reason' );
1014                 }
1015
1016                 return Status::newGood();
1017         }
1018
1019         /**
1020          * Start an account creation flow
1021          *
1022          * In addition to the AuthenticationRequests returned by
1023          * $this->getAuthenticationRequests(), a client might include a
1024          * CreateFromLoginAuthenticationRequest from a previous login attempt. If
1025          * <code>
1026          * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
1027          * </code>
1028          * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
1029          * should be omitted. If the CreateFromLoginAuthenticationRequest has a
1030          * username set, that username must be used for all other requests.
1031          *
1032          * @param User $creator User doing the account creation
1033          * @param AuthenticationRequest[] $reqs
1034          * @param string $returnToUrl Url that REDIRECT responses should eventually
1035          *  return to.
1036          * @return AuthenticationResponse
1037          */
1038         public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1039                 $session = $this->request->getSession();
1040                 if ( !$this->canCreateAccounts() ) {
1041                         // Caller should have called canCreateAccounts()
1042                         $session->remove( 'AuthManager::accountCreationState' );
1043                         throw new \LogicException( 'Account creation is not possible' );
1044                 }
1045
1046                 try {
1047                         $username = AuthenticationRequest::getUsernameFromRequests( $reqs );
1048                 } catch ( \UnexpectedValueException $ex ) {
1049                         $username = null;
1050                 }
1051                 if ( $username === null ) {
1052                         $this->logger->debug( __METHOD__ . ': No username provided' );
1053                         return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1054                 }
1055
1056                 // Permissions check
1057                 $status = $this->checkAccountCreatePermissions( $creator );
1058                 if ( !$status->isGood() ) {
1059                         $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1060                                 'user' => $username,
1061                                 'creator' => $creator->getName(),
1062                                 'reason' => $status->getWikiText( null, null, 'en' )
1063                         ] );
1064                         return AuthenticationResponse::newFail( $status->getMessage() );
1065                 }
1066
1067                 $status = $this->canCreateAccount(
1068                         $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1069                 );
1070                 if ( !$status->isGood() ) {
1071                         $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1072                                 'user' => $username,
1073                                 'creator' => $creator->getName(),
1074                                 'reason' => $status->getWikiText( null, null, 'en' )
1075                         ] );
1076                         return AuthenticationResponse::newFail( $status->getMessage() );
1077                 }
1078
1079                 $user = User::newFromName( $username, 'creatable' );
1080                 foreach ( $reqs as $req ) {
1081                         $req->username = $username;
1082                         $req->returnToUrl = $returnToUrl;
1083                         if ( $req instanceof UserDataAuthenticationRequest ) {
1084                                 $status = $req->populateUser( $user );
1085                                 if ( !$status->isGood() ) {
1086                                         $status = Status::wrap( $status );
1087                                         $session->remove( 'AuthManager::accountCreationState' );
1088                                         $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1089                                                 'user' => $user->getName(),
1090                                                 'creator' => $creator->getName(),
1091                                                 'reason' => $status->getWikiText( null, null, 'en' ),
1092                                         ] );
1093                                         return AuthenticationResponse::newFail( $status->getMessage() );
1094                                 }
1095                         }
1096                 }
1097
1098                 $this->removeAuthenticationSessionData( null );
1099
1100                 $state = [
1101                         'username' => $username,
1102                         'userid' => 0,
1103                         'creatorid' => $creator->getId(),
1104                         'creatorname' => $creator->getName(),
1105                         'reqs' => $reqs,
1106                         'returnToUrl' => $returnToUrl,
1107                         'primary' => null,
1108                         'primaryResponse' => null,
1109                         'secondary' => [],
1110                         'continueRequests' => [],
1111                         'maybeLink' => [],
1112                         'ranPreTests' => false,
1113                 ];
1114
1115                 // Special case: converting a login to an account creation
1116                 $req = AuthenticationRequest::getRequestByClass(
1117                         $reqs, CreateFromLoginAuthenticationRequest::class
1118                 );
1119                 if ( $req ) {
1120                         $state['maybeLink'] = $req->maybeLink;
1121
1122                         if ( $req->createRequest ) {
1123                                 $reqs[] = $req->createRequest;
1124                                 $state['reqs'][] = $req->createRequest;
1125                         }
1126                 }
1127
1128                 $session->setSecret( 'AuthManager::accountCreationState', $state );
1129                 $session->persist();
1130
1131                 return $this->continueAccountCreation( $reqs );
1132         }
1133
1134         /**
1135          * Continue an account creation flow
1136          * @param AuthenticationRequest[] $reqs
1137          * @return AuthenticationResponse
1138          */
1139         public function continueAccountCreation( array $reqs ) {
1140                 $session = $this->request->getSession();
1141                 try {
1142                         if ( !$this->canCreateAccounts() ) {
1143                                 // Caller should have called canCreateAccounts()
1144                                 $session->remove( 'AuthManager::accountCreationState' );
1145                                 throw new \LogicException( 'Account creation is not possible' );
1146                         }
1147
1148                         $state = $session->getSecret( 'AuthManager::accountCreationState' );
1149                         if ( !is_array( $state ) ) {
1150                                 return AuthenticationResponse::newFail(
1151                                         wfMessage( 'authmanager-create-not-in-progress' )
1152                                 );
1153                         }
1154                         $state['continueRequests'] = [];
1155
1156                         // Step 0: Prepare and validate the input
1157
1158                         $user = User::newFromName( $state['username'], 'creatable' );
1159                         if ( !is_object( $user ) ) {
1160                                 $session->remove( 'AuthManager::accountCreationState' );
1161                                 $this->logger->debug( __METHOD__ . ': Invalid username', [
1162                                         'user' => $state['username'],
1163                                 ] );
1164                                 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1165                         }
1166
1167                         if ( $state['creatorid'] ) {
1168                                 $creator = User::newFromId( $state['creatorid'] );
1169                         } else {
1170                                 $creator = new User;
1171                                 $creator->setName( $state['creatorname'] );
1172                         }
1173
1174                         // Avoid account creation races on double submissions
1175                         $cache = \ObjectCache::getLocalClusterInstance();
1176                         $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1177                         if ( !$lock ) {
1178                                 // Don't clear AuthManager::accountCreationState for this code
1179                                 // path because the process that won the race owns it.
1180                                 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1181                                         'user' => $user->getName(),
1182                                         'creator' => $creator->getName(),
1183                                 ] );
1184                                 return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1185                         }
1186
1187                         // Permissions check
1188                         $status = $this->checkAccountCreatePermissions( $creator );
1189                         if ( !$status->isGood() ) {
1190                                 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1191                                         'user' => $user->getName(),
1192                                         'creator' => $creator->getName(),
1193                                         'reason' => $status->getWikiText( null, null, 'en' )
1194                                 ] );
1195                                 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1196                                 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1197                                 $session->remove( 'AuthManager::accountCreationState' );
1198                                 return $ret;
1199                         }
1200
1201                         // Load from master for existence check
1202                         $user->load( User::READ_LOCKING );
1203
1204                         if ( $state['userid'] === 0 ) {
1205                                 if ( $user->getId() != 0 ) {
1206                                         $this->logger->debug( __METHOD__ . ': User exists locally', [
1207                                                 'user' => $user->getName(),
1208                                                 'creator' => $creator->getName(),
1209                                         ] );
1210                                         $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1211                                         $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1212                                         $session->remove( 'AuthManager::accountCreationState' );
1213                                         return $ret;
1214                                 }
1215                         } else {
1216                                 if ( $user->getId() == 0 ) {
1217                                         $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1218                                                 'user' => $user->getName(),
1219                                                 'creator' => $creator->getName(),
1220                                                 'expected_id' => $state['userid'],
1221                                         ] );
1222                                         throw new \UnexpectedValueException(
1223                                                 "User \"{$state['username']}\" should exist now, but doesn't!"
1224                                         );
1225                                 }
1226                                 if ( $user->getId() != $state['userid'] ) {
1227                                         $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1228                                                 'user' => $user->getName(),
1229                                                 'creator' => $creator->getName(),
1230                                                 'expected_id' => $state['userid'],
1231                                                 'actual_id' => $user->getId(),
1232                                         ] );
1233                                         throw new \UnexpectedValueException(
1234                                                 "User \"{$state['username']}\" exists, but " .
1235                                                         "ID {$user->getId()} != {$state['userid']}!"
1236                                         );
1237                                 }
1238                         }
1239                         foreach ( $state['reqs'] as $req ) {
1240                                 if ( $req instanceof UserDataAuthenticationRequest ) {
1241                                         $status = $req->populateUser( $user );
1242                                         if ( !$status->isGood() ) {
1243                                                 // This should never happen...
1244                                                 $status = Status::wrap( $status );
1245                                                 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1246                                                         'user' => $user->getName(),
1247                                                         'creator' => $creator->getName(),
1248                                                         'reason' => $status->getWikiText( null, null, 'en' ),
1249                                                 ] );
1250                                                 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1251                                                 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1252                                                 $session->remove( 'AuthManager::accountCreationState' );
1253                                                 return $ret;
1254                                         }
1255                                 }
1256                         }
1257
1258                         foreach ( $reqs as $req ) {
1259                                 $req->returnToUrl = $state['returnToUrl'];
1260                                 $req->username = $state['username'];
1261                         }
1262
1263                         // Run pre-creation tests, if we haven't already
1264                         if ( !$state['ranPreTests'] ) {
1265                                 $providers = $this->getPreAuthenticationProviders() +
1266                                         $this->getPrimaryAuthenticationProviders() +
1267                                         $this->getSecondaryAuthenticationProviders();
1268                                 foreach ( $providers as $id => $provider ) {
1269                                         $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1270                                         if ( !$status->isGood() ) {
1271                                                 $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1272                                                         'user' => $user->getName(),
1273                                                         'creator' => $creator->getName(),
1274                                                 ] );
1275                                                 $ret = AuthenticationResponse::newFail(
1276                                                         Status::wrap( $status )->getMessage()
1277                                                 );
1278                                                 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1279                                                 $session->remove( 'AuthManager::accountCreationState' );
1280                                                 return $ret;
1281                                         }
1282                                 }
1283
1284                                 $state['ranPreTests'] = true;
1285                         }
1286
1287                         // Step 1: Choose a primary authentication provider and call it until it succeeds.
1288
1289                         if ( $state['primary'] === null ) {
1290                                 // We haven't picked a PrimaryAuthenticationProvider yet
1291                                 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1292                                         if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1293                                                 continue;
1294                                         }
1295                                         $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1296                                         switch ( $res->status ) {
1297                                                 case AuthenticationResponse::PASS;
1298                                                         $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1299                                                                 'user' => $user->getName(),
1300                                                                 'creator' => $creator->getName(),
1301                                                         ] );
1302                                                         $state['primary'] = $id;
1303                                                         $state['primaryResponse'] = $res;
1304                                                         break 2;
1305                                                 case AuthenticationResponse::FAIL;
1306                                                         $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1307                                                                 'user' => $user->getName(),
1308                                                                 'creator' => $creator->getName(),
1309                                                         ] );
1310                                                         $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1311                                                         $session->remove( 'AuthManager::accountCreationState' );
1312                                                         return $res;
1313                                                 case AuthenticationResponse::ABSTAIN;
1314                                                         // Continue loop
1315                                                         break;
1316                                                 case AuthenticationResponse::REDIRECT;
1317                                                 case AuthenticationResponse::UI;
1318                                                         $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1319                                                                 'user' => $user->getName(),
1320                                                                 'creator' => $creator->getName(),
1321                                                         ] );
1322                                                         $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1323                                                         $state['primary'] = $id;
1324                                                         $state['continueRequests'] = $res->neededRequests;
1325                                                         $session->setSecret( 'AuthManager::accountCreationState', $state );
1326                                                         return $res;
1327
1328                                                         // @codeCoverageIgnoreStart
1329                                                 default:
1330                                                         throw new \DomainException(
1331                                                                 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1332                                                         );
1333                                                         // @codeCoverageIgnoreEnd
1334                                         }
1335                                 }
1336                                 if ( $state['primary'] === null ) {
1337                                         $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1338                                                 'user' => $user->getName(),
1339                                                 'creator' => $creator->getName(),
1340                                         ] );
1341                                         $ret = AuthenticationResponse::newFail(
1342                                                 wfMessage( 'authmanager-create-no-primary' )
1343                                         );
1344                                         $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1345                                         $session->remove( 'AuthManager::accountCreationState' );
1346                                         return $ret;
1347                                 }
1348                         } elseif ( $state['primaryResponse'] === null ) {
1349                                 $provider = $this->getAuthenticationProvider( $state['primary'] );
1350                                 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1351                                         // Configuration changed? Force them to start over.
1352                                         // @codeCoverageIgnoreStart
1353                                         $ret = AuthenticationResponse::newFail(
1354                                                 wfMessage( 'authmanager-create-not-in-progress' )
1355                                         );
1356                                         $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1357                                         $session->remove( 'AuthManager::accountCreationState' );
1358                                         return $ret;
1359                                         // @codeCoverageIgnoreEnd
1360                                 }
1361                                 $id = $provider->getUniqueId();
1362                                 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1363                                 switch ( $res->status ) {
1364                                         case AuthenticationResponse::PASS;
1365                                                 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1366                                                         'user' => $user->getName(),
1367                                                         'creator' => $creator->getName(),
1368                                                 ] );
1369                                                 $state['primaryResponse'] = $res;
1370                                                 break;
1371                                         case AuthenticationResponse::FAIL;
1372                                                 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1373                                                         'user' => $user->getName(),
1374                                                         'creator' => $creator->getName(),
1375                                                 ] );
1376                                                 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1377                                                 $session->remove( 'AuthManager::accountCreationState' );
1378                                                 return $res;
1379                                         case AuthenticationResponse::REDIRECT;
1380                                         case AuthenticationResponse::UI;
1381                                                 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1382                                                         'user' => $user->getName(),
1383                                                         'creator' => $creator->getName(),
1384                                                 ] );
1385                                                 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1386                                                 $state['continueRequests'] = $res->neededRequests;
1387                                                 $session->setSecret( 'AuthManager::accountCreationState', $state );
1388                                                 return $res;
1389                                         default:
1390                                                 throw new \DomainException(
1391                                                         get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1392                                                 );
1393                                 }
1394                         }
1395
1396                         // Step 2: Primary authentication succeeded, create the User object
1397                         // and add the user locally.
1398
1399                         if ( $state['userid'] === 0 ) {
1400                                 $this->logger->info( 'Creating user {user} during account creation', [
1401                                         'user' => $user->getName(),
1402                                         'creator' => $creator->getName(),
1403                                 ] );
1404                                 $status = $user->addToDatabase();
1405                                 if ( !$status->isOK() ) {
1406                                         // @codeCoverageIgnoreStart
1407                                         $ret = AuthenticationResponse::newFail( $status->getMessage() );
1408                                         $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1409                                         $session->remove( 'AuthManager::accountCreationState' );
1410                                         return $ret;
1411                                         // @codeCoverageIgnoreEnd
1412                                 }
1413                                 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1414                                 \Hooks::run( 'LocalUserCreated', [ $user, false ] );
1415                                 $user->saveSettings();
1416                                 $state['userid'] = $user->getId();
1417
1418                                 // Update user count
1419                                 \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1420
1421                                 // Watch user's userpage and talk page
1422                                 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1423
1424                                 // Inform the provider
1425                                 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1426
1427                                 // Log the creation
1428                                 if ( $this->config->get( 'NewUserLog' ) ) {
1429                                         $isAnon = $creator->isAnon();
1430                                         $logEntry = new \ManualLogEntry(
1431                                                 'newusers',
1432                                                 $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1433                                         );
1434                                         $logEntry->setPerformer( $isAnon ? $user : $creator );
1435                                         $logEntry->setTarget( $user->getUserPage() );
1436                                         /** @var CreationReasonAuthenticationRequest $req */
1437                                         $req = AuthenticationRequest::getRequestByClass(
1438                                                 $state['reqs'], CreationReasonAuthenticationRequest::class
1439                                         );
1440                                         $logEntry->setComment( $req ? $req->reason : '' );
1441                                         $logEntry->setParameters( [
1442                                                 '4::userid' => $user->getId(),
1443                                         ] );
1444                                         $logid = $logEntry->insert();
1445                                         $logEntry->publish( $logid );
1446                                 }
1447                         }
1448
1449                         // Step 3: Iterate over all the secondary authentication providers.
1450
1451                         $beginReqs = $state['reqs'];
1452
1453                         foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1454                                 if ( !isset( $state['secondary'][$id] ) ) {
1455                                         // This provider isn't started yet, so we pass it the set
1456                                         // of reqs from beginAuthentication instead of whatever
1457                                         // might have been used by a previous provider in line.
1458                                         $func = 'beginSecondaryAccountCreation';
1459                                         $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1460                                 } elseif ( !$state['secondary'][$id] ) {
1461                                         $func = 'continueSecondaryAccountCreation';
1462                                         $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1463                                 } else {
1464                                         continue;
1465                                 }
1466                                 switch ( $res->status ) {
1467                                         case AuthenticationResponse::PASS;
1468                                                 $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1469                                                         'user' => $user->getName(),
1470                                                         'creator' => $creator->getName(),
1471                                                 ] );
1472                                                 // fall through
1473                                         case AuthenticationResponse::ABSTAIN;
1474                                                 $state['secondary'][$id] = true;
1475                                                 break;
1476                                         case AuthenticationResponse::REDIRECT;
1477                                         case AuthenticationResponse::UI;
1478                                                 $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1479                                                         'user' => $user->getName(),
1480                                                         'creator' => $creator->getName(),
1481                                                 ] );
1482                                                 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1483                                                 $state['secondary'][$id] = false;
1484                                                 $state['continueRequests'] = $res->neededRequests;
1485                                                 $session->setSecret( 'AuthManager::accountCreationState', $state );
1486                                                 return $res;
1487                                         case AuthenticationResponse::FAIL;
1488                                                 throw new \DomainException(
1489                                                         get_class( $provider ) . "::{$func}() returned $res->status." .
1490                                                         ' Secondary providers are not allowed to fail account creation, that' .
1491                                                         ' should have been done via testForAccountCreation().'
1492                                                 );
1493                                                         // @codeCoverageIgnoreStart
1494                                         default:
1495                                                 throw new \DomainException(
1496                                                         get_class( $provider ) . "::{$func}() returned $res->status"
1497                                                 );
1498                                                         // @codeCoverageIgnoreEnd
1499                                 }
1500                         }
1501
1502                         $id = $user->getId();
1503                         $name = $user->getName();
1504                         $req = new CreatedAccountAuthenticationRequest( $id, $name );
1505                         $ret = AuthenticationResponse::newPass( $name );
1506                         $ret->loginRequest = $req;
1507                         $this->createdAccountAuthenticationRequests[] = $req;
1508
1509                         $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1510                                 'user' => $user->getName(),
1511                                 'creator' => $creator->getName(),
1512                         ] );
1513
1514                         $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1515                         $session->remove( 'AuthManager::accountCreationState' );
1516                         $this->removeAuthenticationSessionData( null );
1517                         return $ret;
1518                 } catch ( \Exception $ex ) {
1519                         $session->remove( 'AuthManager::accountCreationState' );
1520                         throw $ex;
1521                 }
1522         }
1523
1524         /**
1525          * Auto-create an account, and log into that account
1526          *
1527          * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
1528          * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
1529          * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
1530          * the username of a non-existing user from provideSessionInfo(). Calling this method
1531          * explicitly (e.g. from a maintenance script) is also fine.
1532          *
1533          * @param User $user User to auto-create
1534          * @param string $source What caused the auto-creation? This must be the ID
1535          *  of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
1536          * @param bool $login Whether to also log the user in
1537          * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1538          */
1539         public function autoCreateUser( User $user, $source, $login = true ) {
1540                 if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1541                         !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1542                 ) {
1543                         throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1544                 }
1545
1546                 $username = $user->getName();
1547
1548                 // Try the local user from the replica DB
1549                 $localId = User::idFromName( $username );
1550                 $flags = User::READ_NORMAL;
1551
1552                 // Fetch the user ID from the master, so that we don't try to create the user
1553                 // when they already exist, due to replication lag
1554                 // @codeCoverageIgnoreStart
1555                 if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
1556                         $localId = User::idFromName( $username, User::READ_LATEST );
1557                         $flags = User::READ_LATEST;
1558                 }
1559                 // @codeCoverageIgnoreEnd
1560
1561                 if ( $localId ) {
1562                         $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1563                                 'username' => $username,
1564                         ] );
1565                         $user->setId( $localId );
1566                         $user->loadFromId( $flags );
1567                         if ( $login ) {
1568                                 $this->setSessionDataForUser( $user );
1569                         }
1570                         $status = Status::newGood();
1571                         $status->warning( 'userexists' );
1572                         return $status;
1573                 }
1574
1575                 // Wiki is read-only?
1576                 if ( wfReadOnly() ) {
1577                         $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1578                                 'username' => $username,
1579                                 'reason' => wfReadOnlyReason(),
1580                         ] );
1581                         $user->setId( 0 );
1582                         $user->loadFromId();
1583                         return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1584                 }
1585
1586                 // Check the session, if we tried to create this user already there's
1587                 // no point in retrying.
1588                 $session = $this->request->getSession();
1589                 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1590                         $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1591                                 'username' => $username,
1592                                 'sessionid' => $session->getId(),
1593                         ] );
1594                         $user->setId( 0 );
1595                         $user->loadFromId();
1596                         $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1597                         if ( $reason instanceof StatusValue ) {
1598                                 return Status::wrap( $reason );
1599                         } else {
1600                                 return Status::newFatal( $reason );
1601                         }
1602                 }
1603
1604                 // Is the username creatable?
1605                 if ( !User::isCreatableName( $username ) ) {
1606                         $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1607                                 'username' => $username,
1608                         ] );
1609                         $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1610                         $user->setId( 0 );
1611                         $user->loadFromId();
1612                         return Status::newFatal( 'noname' );
1613                 }
1614
1615                 // Is the IP user able to create accounts?
1616                 $anon = new User;
1617                 if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1618                         $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1619                                 'username' => $username,
1620                                 'ip' => $anon->getName(),
1621                         ] );
1622                         $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1623                         $session->persist();
1624                         $user->setId( 0 );
1625                         $user->loadFromId();
1626                         return Status::newFatal( 'authmanager-autocreate-noperm' );
1627                 }
1628
1629                 // Avoid account creation races on double submissions
1630                 $cache = \ObjectCache::getLocalClusterInstance();
1631                 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1632                 if ( !$lock ) {
1633                         $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1634                                 'user' => $username,
1635                         ] );
1636                         $user->setId( 0 );
1637                         $user->loadFromId();
1638                         return Status::newFatal( 'usernameinprogress' );
1639                 }
1640
1641                 // Denied by providers?
1642                 $options = [
1643                         'flags' => User::READ_LATEST,
1644                         'creating' => true,
1645                 ];
1646                 $providers = $this->getPreAuthenticationProviders() +
1647                         $this->getPrimaryAuthenticationProviders() +
1648                         $this->getSecondaryAuthenticationProviders();
1649                 foreach ( $providers as $provider ) {
1650                         $status = $provider->testUserForCreation( $user, $source, $options );
1651                         if ( !$status->isGood() ) {
1652                                 $ret = Status::wrap( $status );
1653                                 $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1654                                         'username' => $username,
1655                                         'reason' => $ret->getWikiText( null, null, 'en' ),
1656                                 ] );
1657                                 $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1658                                 $user->setId( 0 );
1659                                 $user->loadFromId();
1660                                 return $ret;
1661                         }
1662                 }
1663
1664                 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1665                 if ( $cache->get( $backoffKey ) ) {
1666                         $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1667                                 'username' => $username,
1668                         ] );
1669                         $user->setId( 0 );
1670                         $user->loadFromId();
1671                         return Status::newFatal( 'authmanager-autocreate-exception' );
1672                 }
1673
1674                 // Checks passed, create the user...
1675                 $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
1676                 $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1677                         'username' => $username,
1678                         'from' => $from,
1679                 ] );
1680
1681                 // Ignore warnings about master connections/writes...hard to avoid here
1682                 $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1683                 $old = $trxProfiler->setSilenced( true );
1684                 try {
1685                         $status = $user->addToDatabase();
1686                         if ( !$status->isOK() ) {
1687                                 // Double-check for a race condition (T70012). We make use of the fact that when
1688                                 // addToDatabase fails due to the user already existing, the user object gets loaded.
1689                                 if ( $user->getId() ) {
1690                                         $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1691                                                 'username' => $username,
1692                                         ] );
1693                                         if ( $login ) {
1694                                                 $this->setSessionDataForUser( $user );
1695                                         }
1696                                         $status = Status::newGood();
1697                                         $status->warning( 'userexists' );
1698                                 } else {
1699                                         $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1700                                                 'username' => $username,
1701                                                 'msg' => $status->getWikiText( null, null, 'en' )
1702                                         ] );
1703                                         $user->setId( 0 );
1704                                         $user->loadFromId();
1705                                 }
1706                                 return $status;
1707                         }
1708                 } catch ( \Exception $ex ) {
1709                         $trxProfiler->setSilenced( $old );
1710                         $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1711                                 'username' => $username,
1712                                 'exception' => $ex,
1713                         ] );
1714                         // Do not keep throwing errors for a while
1715                         $cache->set( $backoffKey, 1, 600 );
1716                         // Bubble up error; which should normally trigger DB rollbacks
1717                         throw $ex;
1718                 }
1719
1720                 $this->setDefaultUserOptions( $user, false );
1721
1722                 // Inform the providers
1723                 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1724
1725                 \Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1726                 \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1727                 $user->saveSettings();
1728
1729                 // Update user count
1730                 \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1731                 // Watch user's userpage and talk page
1732                 \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1733                         $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1734                 } );
1735
1736                 // Log the creation
1737                 if ( $this->config->get( 'NewUserLog' ) ) {
1738                         $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1739                         $logEntry->setPerformer( $user );
1740                         $logEntry->setTarget( $user->getUserPage() );
1741                         $logEntry->setComment( '' );
1742                         $logEntry->setParameters( [
1743                                 '4::userid' => $user->getId(),
1744                         ] );
1745                         $logEntry->insert();
1746                 }
1747
1748                 $trxProfiler->setSilenced( $old );
1749
1750                 if ( $login ) {
1751                         $this->setSessionDataForUser( $user );
1752                 }
1753
1754                 return Status::newGood();
1755         }
1756
1757         /**@}*/
1758
1759         /**
1760          * @name Account linking
1761          * @{
1762          */
1763
1764         /**
1765          * Determine whether accounts can be linked
1766          * @return bool
1767          */
1768         public function canLinkAccounts() {
1769                 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1770                         if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1771                                 return true;
1772                         }
1773                 }
1774                 return false;
1775         }
1776
1777         /**
1778          * Start an account linking flow
1779          *
1780          * @param User $user User being linked
1781          * @param AuthenticationRequest[] $reqs
1782          * @param string $returnToUrl Url that REDIRECT responses should eventually
1783          *  return to.
1784          * @return AuthenticationResponse
1785          */
1786         public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1787                 $session = $this->request->getSession();
1788                 $session->remove( 'AuthManager::accountLinkState' );
1789
1790                 if ( !$this->canLinkAccounts() ) {
1791                         // Caller should have called canLinkAccounts()
1792                         throw new \LogicException( 'Account linking is not possible' );
1793                 }
1794
1795                 if ( $user->getId() === 0 ) {
1796                         if ( !User::isUsableName( $user->getName() ) ) {
1797                                 $msg = wfMessage( 'noname' );
1798                         } else {
1799                                 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1800                         }
1801                         return AuthenticationResponse::newFail( $msg );
1802                 }
1803                 foreach ( $reqs as $req ) {
1804                         $req->username = $user->getName();
1805                         $req->returnToUrl = $returnToUrl;
1806                 }
1807
1808                 $this->removeAuthenticationSessionData( null );
1809
1810                 $providers = $this->getPreAuthenticationProviders();
1811                 foreach ( $providers as $id => $provider ) {
1812                         $status = $provider->testForAccountLink( $user );
1813                         if ( !$status->isGood() ) {
1814                                 $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1815                                         'user' => $user->getName(),
1816                                 ] );
1817                                 $ret = AuthenticationResponse::newFail(
1818                                         Status::wrap( $status )->getMessage()
1819                                 );
1820                                 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1821                                 return $ret;
1822                         }
1823                 }
1824
1825                 $state = [
1826                         'username' => $user->getName(),
1827                         'userid' => $user->getId(),
1828                         'returnToUrl' => $returnToUrl,
1829                         'primary' => null,
1830                         'continueRequests' => [],
1831                 ];
1832
1833                 $providers = $this->getPrimaryAuthenticationProviders();
1834                 foreach ( $providers as $id => $provider ) {
1835                         if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1836                                 continue;
1837                         }
1838
1839                         $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1840                         switch ( $res->status ) {
1841                                 case AuthenticationResponse::PASS;
1842                                         $this->logger->info( "Account linked to {user} by $id", [
1843                                                 'user' => $user->getName(),
1844                                         ] );
1845                                         $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1846                                         return $res;
1847
1848                                 case AuthenticationResponse::FAIL;
1849                                         $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1850                                                 'user' => $user->getName(),
1851                                         ] );
1852                                         $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1853                                         return $res;
1854
1855                                 case AuthenticationResponse::ABSTAIN;
1856                                         // Continue loop
1857                                         break;
1858
1859                                 case AuthenticationResponse::REDIRECT;
1860                                 case AuthenticationResponse::UI;
1861                                         $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1862                                                 'user' => $user->getName(),
1863                                         ] );
1864                                         $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1865                                         $state['primary'] = $id;
1866                                         $state['continueRequests'] = $res->neededRequests;
1867                                         $session->setSecret( 'AuthManager::accountLinkState', $state );
1868                                         $session->persist();
1869                                         return $res;
1870
1871                                         // @codeCoverageIgnoreStart
1872                                 default:
1873                                         throw new \DomainException(
1874                                                 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1875                                         );
1876                                         // @codeCoverageIgnoreEnd
1877                         }
1878                 }
1879
1880                 $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1881                         'user' => $user->getName(),
1882                 ] );
1883                 $ret = AuthenticationResponse::newFail(
1884                         wfMessage( 'authmanager-link-no-primary' )
1885                 );
1886                 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1887                 return $ret;
1888         }
1889
1890         /**
1891          * Continue an account linking flow
1892          * @param AuthenticationRequest[] $reqs
1893          * @return AuthenticationResponse
1894          */
1895         public function continueAccountLink( array $reqs ) {
1896                 $session = $this->request->getSession();
1897                 try {
1898                         if ( !$this->canLinkAccounts() ) {
1899                                 // Caller should have called canLinkAccounts()
1900                                 $session->remove( 'AuthManager::accountLinkState' );
1901                                 throw new \LogicException( 'Account linking is not possible' );
1902                         }
1903
1904                         $state = $session->getSecret( 'AuthManager::accountLinkState' );
1905                         if ( !is_array( $state ) ) {
1906                                 return AuthenticationResponse::newFail(
1907                                         wfMessage( 'authmanager-link-not-in-progress' )
1908                                 );
1909                         }
1910                         $state['continueRequests'] = [];
1911
1912                         // Step 0: Prepare and validate the input
1913
1914                         $user = User::newFromName( $state['username'], 'usable' );
1915                         if ( !is_object( $user ) ) {
1916                                 $session->remove( 'AuthManager::accountLinkState' );
1917                                 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1918                         }
1919                         if ( $user->getId() != $state['userid'] ) {
1920                                 throw new \UnexpectedValueException(
1921                                         "User \"{$state['username']}\" is valid, but " .
1922                                                 "ID {$user->getId()} != {$state['userid']}!"
1923                                 );
1924                         }
1925
1926                         foreach ( $reqs as $req ) {
1927                                 $req->username = $state['username'];
1928                                 $req->returnToUrl = $state['returnToUrl'];
1929                         }
1930
1931                         // Step 1: Call the primary again until it succeeds
1932
1933                         $provider = $this->getAuthenticationProvider( $state['primary'] );
1934                         if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1935                                 // Configuration changed? Force them to start over.
1936                                 // @codeCoverageIgnoreStart
1937                                 $ret = AuthenticationResponse::newFail(
1938                                         wfMessage( 'authmanager-link-not-in-progress' )
1939                                 );
1940                                 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1941                                 $session->remove( 'AuthManager::accountLinkState' );
1942                                 return $ret;
1943                                 // @codeCoverageIgnoreEnd
1944                         }
1945                         $id = $provider->getUniqueId();
1946                         $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1947                         switch ( $res->status ) {
1948                                 case AuthenticationResponse::PASS;
1949                                         $this->logger->info( "Account linked to {user} by $id", [
1950                                                 'user' => $user->getName(),
1951                                         ] );
1952                                         $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1953                                         $session->remove( 'AuthManager::accountLinkState' );
1954                                         return $res;
1955                                 case AuthenticationResponse::FAIL;
1956                                         $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1957                                                 'user' => $user->getName(),
1958                                         ] );
1959                                         $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1960                                         $session->remove( 'AuthManager::accountLinkState' );
1961                                         return $res;
1962                                 case AuthenticationResponse::REDIRECT;
1963                                 case AuthenticationResponse::UI;
1964                                         $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1965                                                 'user' => $user->getName(),
1966                                         ] );
1967                                         $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1968                                         $state['continueRequests'] = $res->neededRequests;
1969                                         $session->setSecret( 'AuthManager::accountLinkState', $state );
1970                                         return $res;
1971                                 default:
1972                                         throw new \DomainException(
1973                                                 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1974                                         );
1975                         }
1976                 } catch ( \Exception $ex ) {
1977                         $session->remove( 'AuthManager::accountLinkState' );
1978                         throw $ex;
1979                 }
1980         }
1981
1982         /**@}*/
1983
1984         /**
1985          * @name Information methods
1986          * @{
1987          */
1988
1989         /**
1990          * Return the applicable list of AuthenticationRequests
1991          *
1992          * Possible values for $action:
1993          *  - ACTION_LOGIN: Valid for passing to beginAuthentication
1994          *  - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
1995          *  - ACTION_CREATE: Valid for passing to beginAccountCreation
1996          *  - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
1997          *  - ACTION_LINK: Valid for passing to beginAccountLink
1998          *  - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
1999          *  - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
2000          *  - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
2001          *  - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
2002          *
2003          * @param string $action One of the AuthManager::ACTION_* constants
2004          * @param User|null $user User being acted on, instead of the current user.
2005          * @return AuthenticationRequest[]
2006          */
2007         public function getAuthenticationRequests( $action, User $user = null ) {
2008                 $options = [];
2009                 $providerAction = $action;
2010
2011                 // Figure out which providers to query
2012                 switch ( $action ) {
2013                         case self::ACTION_LOGIN:
2014                         case self::ACTION_CREATE:
2015                                 $providers = $this->getPreAuthenticationProviders() +
2016                                         $this->getPrimaryAuthenticationProviders() +
2017                                         $this->getSecondaryAuthenticationProviders();
2018                                 break;
2019
2020                         case self::ACTION_LOGIN_CONTINUE:
2021                                 $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2022                                 return is_array( $state ) ? $state['continueRequests'] : [];
2023
2024                         case self::ACTION_CREATE_CONTINUE:
2025                                 $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2026                                 return is_array( $state ) ? $state['continueRequests'] : [];
2027
2028                         case self::ACTION_LINK:
2029                                 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2030                                         return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2031                                 } );
2032                                 break;
2033
2034                         case self::ACTION_UNLINK:
2035                                 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2036                                         return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2037                                 } );
2038
2039                                 // To providers, unlink and remove are identical.
2040                                 $providerAction = self::ACTION_REMOVE;
2041                                 break;
2042
2043                         case self::ACTION_LINK_CONTINUE:
2044                                 $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2045                                 return is_array( $state ) ? $state['continueRequests'] : [];
2046
2047                         case self::ACTION_CHANGE:
2048                         case self::ACTION_REMOVE:
2049                                 $providers = $this->getPrimaryAuthenticationProviders() +
2050                                         $this->getSecondaryAuthenticationProviders();
2051                                 break;
2052
2053                         // @codeCoverageIgnoreStart
2054                         default:
2055                                 throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2056                 }
2057                 // @codeCoverageIgnoreEnd
2058
2059                 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2060         }
2061
2062         /**
2063          * Internal request lookup for self::getAuthenticationRequests
2064          *
2065          * @param string $providerAction Action to pass to providers
2066          * @param array $options Options to pass to providers
2067          * @param AuthenticationProvider[] $providers
2068          * @param User|null $user
2069          * @return AuthenticationRequest[]
2070          */
2071         private function getAuthenticationRequestsInternal(
2072                 $providerAction, array $options, array $providers, User $user = null
2073         ) {
2074                 $user = $user ?: \RequestContext::getMain()->getUser();
2075                 $options['username'] = $user->isAnon() ? null : $user->getName();
2076
2077                 // Query them and merge results
2078                 $reqs = [];
2079                 foreach ( $providers as $provider ) {
2080                         $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2081                         foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2082                                 $id = $req->getUniqueId();
2083
2084                                 // If a required request if from a Primary, mark it as "primary-required" instead
2085                                 if ( $isPrimary ) {
2086                                         if ( $req->required ) {
2087                                                 $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2088                                         }
2089                                 }
2090
2091                                 if (
2092                                         !isset( $reqs[$id] )
2093                                         || $req->required === AuthenticationRequest::REQUIRED
2094                                         || $reqs[$id] === AuthenticationRequest::OPTIONAL
2095                                 ) {
2096                                         $reqs[$id] = $req;
2097                                 }
2098                         }
2099                 }
2100
2101                 // AuthManager has its own req for some actions
2102                 switch ( $providerAction ) {
2103                         case self::ACTION_LOGIN:
2104                                 $reqs[] = new RememberMeAuthenticationRequest;
2105                                 break;
2106
2107                         case self::ACTION_CREATE:
2108                                 $reqs[] = new UsernameAuthenticationRequest;
2109                                 $reqs[] = new UserDataAuthenticationRequest;
2110                                 if ( $options['username'] !== null ) {
2111                                         $reqs[] = new CreationReasonAuthenticationRequest;
2112                                         $options['username'] = null; // Don't fill in the username below
2113                                 }
2114                                 break;
2115                 }
2116
2117                 // Fill in reqs data
2118                 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2119
2120                 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2121                 if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2122                         $reqs = array_filter( $reqs, function ( $req ) {
2123                                 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2124                         } );
2125                 }
2126
2127                 return array_values( $reqs );
2128         }
2129
2130         /**
2131          * Set values in an array of requests
2132          * @param AuthenticationRequest[] &$reqs
2133          * @param string $action
2134          * @param string|null $username
2135          * @param bool $forceAction
2136          */
2137         private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2138                 foreach ( $reqs as $req ) {
2139                         if ( !$req->action || $forceAction ) {
2140                                 $req->action = $action;
2141                         }
2142                         if ( $req->username === null ) {
2143                                 $req->username = $username;
2144                         }
2145                 }
2146         }
2147
2148         /**
2149          * Determine whether a username exists
2150          * @param string $username
2151          * @param int $flags Bitfield of User:READ_* constants
2152          * @return bool
2153          */
2154         public function userExists( $username, $flags = User::READ_NORMAL ) {
2155                 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2156                         if ( $provider->testUserExists( $username, $flags ) ) {
2157                                 return true;
2158                         }
2159                 }
2160
2161                 return false;
2162         }
2163
2164         /**
2165          * Determine whether a user property should be allowed to be changed.
2166          *
2167          * Supported properties are:
2168          *  - emailaddress
2169          *  - realname
2170          *  - nickname
2171          *
2172          * @param string $property
2173          * @return bool
2174          */
2175         public function allowsPropertyChange( $property ) {
2176                 $providers = $this->getPrimaryAuthenticationProviders() +
2177                         $this->getSecondaryAuthenticationProviders();
2178                 foreach ( $providers as $provider ) {
2179                         if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2180                                 return false;
2181                         }
2182                 }
2183                 return true;
2184         }
2185
2186         /**
2187          * Get a provider by ID
2188          * @note This is public so extensions can check whether their own provider
2189          *  is installed and so they can read its configuration if necessary.
2190          *  Other uses are not recommended.
2191          * @param string $id
2192          * @return AuthenticationProvider|null
2193          */
2194         public function getAuthenticationProvider( $id ) {
2195                 // Fast version
2196                 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2197                         return $this->allAuthenticationProviders[$id];
2198                 }
2199
2200                 // Slow version: instantiate each kind and check
2201                 $providers = $this->getPrimaryAuthenticationProviders();
2202                 if ( isset( $providers[$id] ) ) {
2203                         return $providers[$id];
2204                 }
2205                 $providers = $this->getSecondaryAuthenticationProviders();
2206                 if ( isset( $providers[$id] ) ) {
2207                         return $providers[$id];
2208                 }
2209                 $providers = $this->getPreAuthenticationProviders();
2210                 if ( isset( $providers[$id] ) ) {
2211                         return $providers[$id];
2212                 }
2213
2214                 return null;
2215         }
2216
2217         /**@}*/
2218
2219         /**
2220          * @name Internal methods
2221          * @{
2222          */
2223
2224         /**
2225          * Store authentication in the current session
2226          * @protected For use by AuthenticationProviders
2227          * @param string $key
2228          * @param mixed $data Must be serializable
2229          */
2230         public function setAuthenticationSessionData( $key, $data ) {
2231                 $session = $this->request->getSession();
2232                 $arr = $session->getSecret( 'authData' );
2233                 if ( !is_array( $arr ) ) {
2234                         $arr = [];
2235                 }
2236                 $arr[$key] = $data;
2237                 $session->setSecret( 'authData', $arr );
2238         }
2239
2240         /**
2241          * Fetch authentication data from the current session
2242          * @protected For use by AuthenticationProviders
2243          * @param string $key
2244          * @param mixed $default
2245          * @return mixed
2246          */
2247         public function getAuthenticationSessionData( $key, $default = null ) {
2248                 $arr = $this->request->getSession()->getSecret( 'authData' );
2249                 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2250                         return $arr[$key];
2251                 } else {
2252                         return $default;
2253                 }
2254         }
2255
2256         /**
2257          * Remove authentication data
2258          * @protected For use by AuthenticationProviders
2259          * @param string|null $key If null, all data is removed
2260          */
2261         public function removeAuthenticationSessionData( $key ) {
2262                 $session = $this->request->getSession();
2263                 if ( $key === null ) {
2264                         $session->remove( 'authData' );
2265                 } else {
2266                         $arr = $session->getSecret( 'authData' );
2267                         if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2268                                 unset( $arr[$key] );
2269                                 $session->setSecret( 'authData', $arr );
2270                         }
2271                 }
2272         }
2273
2274         /**
2275          * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2276          * @param string $class
2277          * @param array[] $specs
2278          * @return AuthenticationProvider[]
2279          */
2280         protected function providerArrayFromSpecs( $class, array $specs ) {
2281                 $i = 0;
2282                 foreach ( $specs as &$spec ) {
2283                         $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2284                 }
2285                 unset( $spec );
2286                 usort( $specs, function ( $a, $b ) {
2287                         return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2288                                 ?: $a['sort2'] - $b['sort2'];
2289                 } );
2290
2291                 $ret = [];
2292                 foreach ( $specs as $spec ) {
2293                         $provider = \ObjectFactory::getObjectFromSpec( $spec );
2294                         if ( !$provider instanceof $class ) {
2295                                 throw new \RuntimeException(
2296                                         "Expected instance of $class, got " . get_class( $provider )
2297                                 );
2298                         }
2299                         $provider->setLogger( $this->logger );
2300                         $provider->setManager( $this );
2301                         $provider->setConfig( $this->config );
2302                         $id = $provider->getUniqueId();
2303                         if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2304                                 throw new \RuntimeException(
2305                                         "Duplicate specifications for id $id (classes " .
2306                                         get_class( $provider ) . ' and ' .
2307                                         get_class( $this->allAuthenticationProviders[$id] ) . ')'
2308                                 );
2309                         }
2310                         $this->allAuthenticationProviders[$id] = $provider;
2311                         $ret[$id] = $provider;
2312                 }
2313                 return $ret;
2314         }
2315
2316         /**
2317          * Get the configuration
2318          * @return array
2319          */
2320         private function getConfiguration() {
2321                 return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2322         }
2323
2324         /**
2325          * Get the list of PreAuthenticationProviders
2326          * @return PreAuthenticationProvider[]
2327          */
2328         protected function getPreAuthenticationProviders() {
2329                 if ( $this->preAuthenticationProviders === null ) {
2330                         $conf = $this->getConfiguration();
2331                         $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2332                                 PreAuthenticationProvider::class, $conf['preauth']
2333                         );
2334                 }
2335                 return $this->preAuthenticationProviders;
2336         }
2337
2338         /**
2339          * Get the list of PrimaryAuthenticationProviders
2340          * @return PrimaryAuthenticationProvider[]
2341          */
2342         protected function getPrimaryAuthenticationProviders() {
2343                 if ( $this->primaryAuthenticationProviders === null ) {
2344                         $conf = $this->getConfiguration();
2345                         $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2346                                 PrimaryAuthenticationProvider::class, $conf['primaryauth']
2347                         );
2348                 }
2349                 return $this->primaryAuthenticationProviders;
2350         }
2351
2352         /**
2353          * Get the list of SecondaryAuthenticationProviders
2354          * @return SecondaryAuthenticationProvider[]
2355          */
2356         protected function getSecondaryAuthenticationProviders() {
2357                 if ( $this->secondaryAuthenticationProviders === null ) {
2358                         $conf = $this->getConfiguration();
2359                         $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2360                                 SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2361                         );
2362                 }
2363                 return $this->secondaryAuthenticationProviders;
2364         }
2365
2366         /**
2367          * Log the user in
2368          * @param User $user
2369          * @param bool|null $remember
2370          */
2371         private function setSessionDataForUser( $user, $remember = null ) {
2372                 $session = $this->request->getSession();
2373                 $delay = $session->delaySave();
2374
2375                 $session->resetId();
2376                 $session->resetAllTokens();
2377                 if ( $session->canSetUser() ) {
2378                         $session->setUser( $user );
2379                 }
2380                 if ( $remember !== null ) {
2381                         $session->setRememberUser( $remember );
2382                 }
2383                 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2384                 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2385                 $session->persist();
2386
2387                 \Wikimedia\ScopedCallback::consume( $delay );
2388
2389                 \Hooks::run( 'UserLoggedIn', [ $user ] );
2390         }
2391
2392         /**
2393          * @param User $user
2394          * @param bool $useContextLang Use 'uselang' to set the user's language
2395          */
2396         private function setDefaultUserOptions( User $user, $useContextLang ) {
2397                 global $wgContLang;
2398
2399                 $user->setToken();
2400
2401                 $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
2402                 $user->setOption( 'language', $lang->getPreferredVariant() );
2403
2404                 if ( $wgContLang->hasVariants() ) {
2405                         $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2406                 }
2407         }
2408
2409         /**
2410          * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2411          * @param string $method
2412          * @param array $args
2413          */
2414         private function callMethodOnProviders( $which, $method, array $args ) {
2415                 $providers = [];
2416                 if ( $which & 1 ) {
2417                         $providers += $this->getPreAuthenticationProviders();
2418                 }
2419                 if ( $which & 2 ) {
2420                         $providers += $this->getPrimaryAuthenticationProviders();
2421                 }
2422                 if ( $which & 4 ) {
2423                         $providers += $this->getSecondaryAuthenticationProviders();
2424                 }
2425                 foreach ( $providers as $provider ) {
2426                         call_user_func_array( [ $provider, $method ], $args );
2427                 }
2428         }
2429
2430         /**
2431          * Reset the internal caching for unit testing
2432          * @protected Unit tests only
2433          */
2434         public static function resetCache() {
2435                 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2436                         // @codeCoverageIgnoreStart
2437                         throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2438                         // @codeCoverageIgnoreEnd
2439                 }
2440
2441                 self::$instance = null;
2442         }
2443
2444         /**@}*/
2445
2446 }
2447
2448 /**
2449  * For really cool vim folding this needs to be at the end:
2450  * vim: foldmarker=@{,@} foldmethod=marker
2451  */