Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.65% covered (success)
99.65%
1421 / 1426
93.75% covered (success)
93.75%
45 / 48
CRAP
0.00% covered (danger)
0.00%
0 / 1
AuthManager
99.65% covered (success)
99.65%
1421 / 1426
93.75% covered (success)
93.75%
45 / 48
363
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 setLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 forcePrimaryAuthenticationProviders
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
5
 canAuthenticateNow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 beginAuthentication
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
1 / 1
14
 continueAuthentication
99.13% covered (success)
99.13%
228 / 230
0.00% covered (danger)
0.00%
0 / 1
56
 securitySensitiveOperationStatus
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
16
 userCanAuthenticate
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 normalizeUsername
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 setRequestContextUserFromSessionUser
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 revokeAccessForUser
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 allowsAuthenticationDataChange
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 changeAuthenticationData
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 canCreateAccounts
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 canCreateAccount
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
8
 authorizeInternal
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 probablyCanCreateAccount
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 authorizeCreateAccount
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 beginAccountCreation
100.00% covered (success)
100.00%
70 / 70
100.00% covered (success)
100.00%
1 / 1
11
 continueAccountCreation
100.00% covered (success)
100.00%
288 / 288
100.00% covered (success)
100.00%
1 / 1
54
 logAutocreationAttempt
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 autoCreateUser
100.00% covered (success)
100.00%
178 / 178
100.00% covered (success)
100.00%
1 / 1
37
 authorizeAutoCreateAccount
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 canLinkAccounts
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 beginAccountLink
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
1 / 1
15
 continueAccountLink
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
1 / 1
14
 getAuthenticationRequests
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
18
 getAuthenticationRequestsInternal
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
16
 fillRequests
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 userExists
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 allowsPropertyChange
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getAuthenticationProvider
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
5.12
 setAuthenticationSessionData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getAuthenticationSessionData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 removeAuthenticationSessionData
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 providerArrayFromSpecs
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
5
 getPreAuthenticationProviders
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPrimaryAuthenticationProviders
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getSecondaryAuthenticationProviders
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getProviderIds
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 initializeAuthenticationProviders
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 setSessionDataForUser
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 setDefaultUserOptions
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 runVerifyHook
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
8
 callMethodOnProviders
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getHookContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHookRunner
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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
24namespace MediaWiki\Auth;
25
26use InvalidArgumentException;
27use LogicException;
28use MediaWiki\Auth\Hook\AuthManagerVerifyAuthenticationHook;
29use MediaWiki\Block\BlockManager;
30use MediaWiki\Config\Config;
31use MediaWiki\Context\RequestContext;
32use MediaWiki\Deferred\DeferredUpdates;
33use MediaWiki\Deferred\SiteStatsUpdate;
34use MediaWiki\HookContainer\HookContainer;
35use MediaWiki\HookContainer\HookRunner;
36use MediaWiki\Language\Language;
37use MediaWiki\Languages\LanguageConverterFactory;
38use MediaWiki\MainConfigNames;
39use MediaWiki\MediaWikiServices;
40use MediaWiki\Page\PageIdentity;
41use MediaWiki\Permissions\Authority;
42use MediaWiki\Permissions\PermissionStatus;
43use MediaWiki\Request\WebRequest;
44use MediaWiki\SpecialPage\SpecialPage;
45use MediaWiki\Status\Status;
46use MediaWiki\StubObject\StubGlobalUser;
47use MediaWiki\User\BotPasswordStore;
48use MediaWiki\User\Options\UserOptionsManager;
49use MediaWiki\User\TempUser\TempUserCreator;
50use MediaWiki\User\User;
51use MediaWiki\User\UserFactory;
52use MediaWiki\User\UserIdentity;
53use MediaWiki\User\UserIdentityLookup;
54use MediaWiki\User\UserNameUtils;
55use MediaWiki\User\UserRigorOptions;
56use MediaWiki\Watchlist\WatchlistManager;
57use MWExceptionHandler;
58use Psr\Log\LoggerAwareInterface;
59use Psr\Log\LoggerInterface;
60use Psr\Log\NullLogger;
61use StatusValue;
62use Wikimedia\NormalizedException\NormalizedException;
63use Wikimedia\ObjectFactory\ObjectFactory;
64use Wikimedia\Rdbms\IDBAccessObject;
65use Wikimedia\Rdbms\ILoadBalancer;
66use Wikimedia\Rdbms\ReadOnlyMode;
67use Wikimedia\ScopedCallback;
68
69/**
70 * This serves as the entry point to the authentication system.
71 *
72 * In the future, it may also serve as the entry point to the authorization
73 * system.
74 *
75 * If you are looking at this because you are working on an extension that creates its own
76 * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
77 * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
78 * or the createaccount API. Trying to call this class directly will very likely end up in
79 * security vulnerabilities or broken UX in edge cases.
80 *
81 * If you are working on an extension that needs to integrate with the authentication system
82 * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
83 * need to write an AuthenticationProvider.
84 *
85 * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
86 * you are looking for. If you want to change user data, use User::changeAuthenticationData().
87 * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
88 * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
89 * responsibility to ensure that the user can authenticate somehow (see especially
90 * PrimaryAuthenticationProvider::autoCreatedAccount()). The same functionality can also be used
91 * from Maintenance scripts such as createAndPromote.php.
92 * If you are writing code that is not associated with such a provider and needs to create accounts
93 * programmatically for real users, you should rethink your architecture. There is no good way to
94 * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
95 * cannot provide any means for users to access the accounts it would create.
96 *
97 * The two main control flows when using this class are as follows:
98 * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
99 *   the requests with data (by using them to build a HTMLForm and have the user fill it, or by
100 *   exposing a form specification via the API, so that the client can build it), and pass them to
101 *   the appropriate begin* method. That will return either a success/failure response, or more
102 *   requests to fill (either by building a form or by redirecting the user to some external
103 *   provider which will send the data back), in which case they need to be submitted to the
104 *   appropriate continue* method and that step has to be repeated until the response is a success
105 *   or failure response. AuthManager will use the session to maintain internal state during the
106 *   process.
107 * * Code doing an authentication data change will call getAuthenticationRequests(), select
108 *   a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
109 *   changeAuthenticationData(). If the data change is user-initiated, the whole process needs
110 *   to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
111 *   a non-OK status.
112 *
113 * @ingroup Auth
114 * @since 1.27
115 * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
116 */
117class AuthManager implements LoggerAwareInterface {
118    /**
119     * @internal
120     * Key in the user's session data for storing login state.
121     */
122    public const AUTHN_STATE = 'AuthManager::authnState';
123
124    /**
125     * @internal
126     * Key in the user's session data for storing account creation state.
127     */
128    public const ACCOUNT_CREATION_STATE = 'AuthManager::accountCreationState';
129
130    /**
131     * @internal
132     * Key in the user's session data for storing account linking state.
133     */
134    public const ACCOUNT_LINK_STATE = 'AuthManager::accountLinkState';
135
136    /**
137     * @internal
138     * Key in the user's session data for storing autocreation failures,
139     * to avoid re-attempting expensive autocreation checks on every request.
140     */
141    public const AUTOCREATE_BLOCKLIST = 'AuthManager::AutoCreateBlacklist';
142
143    /** Log in with an existing (not necessarily local) user */
144    public const ACTION_LOGIN = 'login';
145    /** Continue a login process that was interrupted by the need for user input or communication
146     * with an external provider
147     */
148    public const ACTION_LOGIN_CONTINUE = 'login-continue';
149    /** Create a new user */
150    public const ACTION_CREATE = 'create';
151    /** Continue a user creation process that was interrupted by the need for user input or
152     * communication with an external provider
153     */
154    public const ACTION_CREATE_CONTINUE = 'create-continue';
155    /** Link an existing user to a third-party account */
156    public const ACTION_LINK = 'link';
157    /** Continue a user linking process that was interrupted by the need for user input or
158     * communication with an external provider
159     */
160    public const ACTION_LINK_CONTINUE = 'link-continue';
161    /** Change a user's credentials */
162    public const ACTION_CHANGE = 'change';
163    /** Remove a user's credentials */
164    public const ACTION_REMOVE = 'remove';
165    /** Like ACTION_REMOVE but for linking providers only */
166    public const ACTION_UNLINK = 'unlink';
167
168    /** Security-sensitive operations are ok. */
169    public const SEC_OK = 'ok';
170    /** Security-sensitive operations should re-authenticate. */
171    public const SEC_REAUTH = 'reauth';
172    /** Security-sensitive should not be performed. */
173    public const SEC_FAIL = 'fail';
174
175    /** Auto-creation is due to SessionManager */
176    public const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
177
178    /** Auto-creation is due to a Maintenance script */
179    public const AUTOCREATE_SOURCE_MAINT = '::Maintenance::';
180
181    /** Auto-creation is due to temporary account creation on page save */
182    public const AUTOCREATE_SOURCE_TEMP = TempUserCreator::class;
183
184    /**
185     * @internal To be used by primary authentication providers only.
186     * @var string "Remember me" status flag shared between auth providers
187     */
188    public const REMEMBER_ME = 'rememberMe';
189
190    /** Call pre-authentication providers */
191    private const CALL_PRE = 1;
192
193    /** Call primary authentication providers */
194    private const CALL_PRIMARY = 2;
195
196    /** Call secondary authentication providers */
197    private const CALL_SECONDARY = 4;
198
199    /** Call all authentication providers */
200    private const CALL_ALL = self::CALL_PRE | self::CALL_PRIMARY | self::CALL_SECONDARY;
201
202    /** @var AuthenticationProvider[] */
203    private $allAuthenticationProviders = [];
204
205    /** @var PreAuthenticationProvider[] */
206    private $preAuthenticationProviders = null;
207
208    /** @var PrimaryAuthenticationProvider[] */
209    private $primaryAuthenticationProviders = null;
210
211    /** @var SecondaryAuthenticationProvider[] */
212    private $secondaryAuthenticationProviders = null;
213
214    /** @var CreatedAccountAuthenticationRequest[] */
215    private $createdAccountAuthenticationRequests = [];
216
217    private WebRequest $request;
218    private Config $config;
219    private ObjectFactory $objectFactory;
220    private LoggerInterface $logger;
221    private UserNameUtils $userNameUtils;
222    private HookContainer $hookContainer;
223    private HookRunner $hookRunner;
224    private ReadOnlyMode $readOnlyMode;
225    private BlockManager $blockManager;
226    private WatchlistManager $watchlistManager;
227    private ILoadBalancer $loadBalancer;
228    private Language $contentLanguage;
229    private LanguageConverterFactory $languageConverterFactory;
230    private BotPasswordStore $botPasswordStore;
231    private UserFactory $userFactory;
232    private UserIdentityLookup $userIdentityLookup;
233    private UserOptionsManager $userOptionsManager;
234
235    public function __construct(
236        WebRequest $request,
237        Config $config,
238        ObjectFactory $objectFactory,
239        HookContainer $hookContainer,
240        ReadOnlyMode $readOnlyMode,
241        UserNameUtils $userNameUtils,
242        BlockManager $blockManager,
243        WatchlistManager $watchlistManager,
244        ILoadBalancer $loadBalancer,
245        Language $contentLanguage,
246        LanguageConverterFactory $languageConverterFactory,
247        BotPasswordStore $botPasswordStore,
248        UserFactory $userFactory,
249        UserIdentityLookup $userIdentityLookup,
250        UserOptionsManager $userOptionsManager
251    ) {
252        $this->request = $request;
253        $this->config = $config;
254        $this->objectFactory = $objectFactory;
255        $this->hookContainer = $hookContainer;
256        $this->hookRunner = new HookRunner( $hookContainer );
257        $this->setLogger( new NullLogger() );
258        $this->readOnlyMode = $readOnlyMode;
259        $this->userNameUtils = $userNameUtils;
260        $this->blockManager = $blockManager;
261        $this->watchlistManager = $watchlistManager;
262        $this->loadBalancer = $loadBalancer;
263        $this->contentLanguage = $contentLanguage;
264        $this->languageConverterFactory = $languageConverterFactory;
265        $this->botPasswordStore = $botPasswordStore;
266        $this->userFactory = $userFactory;
267        $this->userIdentityLookup = $userIdentityLookup;
268        $this->userOptionsManager = $userOptionsManager;
269    }
270
271    public function setLogger( LoggerInterface $logger ) {
272        $this->logger = $logger;
273    }
274
275    /**
276     * @return WebRequest
277     */
278    public function getRequest() {
279        return $this->request;
280    }
281
282    /**
283     * Force certain PrimaryAuthenticationProviders
284     *
285     * @deprecated since 1.43; for backwards compatibility only
286     * @param PrimaryAuthenticationProvider[] $providers
287     * @param string $why
288     */
289    public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
290        wfDeprecated( __METHOD__, '1.43' );
291
292        $this->logger->warning( "Overriding AuthManager primary authn because $why" );
293
294        if ( $this->primaryAuthenticationProviders !== null ) {
295            $this->logger->warning(
296                'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
297            );
298
299            $this->allAuthenticationProviders = array_diff_key(
300                $this->allAuthenticationProviders,
301                $this->primaryAuthenticationProviders
302            );
303            $session = $this->request->getSession();
304            $session->remove( self::AUTHN_STATE );
305            $session->remove( self::ACCOUNT_CREATION_STATE );
306            $session->remove( self::ACCOUNT_LINK_STATE );
307            $this->createdAccountAuthenticationRequests = [];
308        }
309
310        $this->primaryAuthenticationProviders = [];
311        foreach ( $providers as $provider ) {
312            if ( !$provider instanceof AbstractPrimaryAuthenticationProvider ) {
313                throw new \RuntimeException(
314                    'Expected instance of MediaWiki\\Auth\\AbstractPrimaryAuthenticationProvider, got ' .
315                        get_class( $provider )
316                );
317            }
318            $provider->init( $this->logger, $this, $this->hookContainer, $this->config, $this->userNameUtils );
319            $id = $provider->getUniqueId();
320            if ( isset( $this->allAuthenticationProviders[$id] ) ) {
321                throw new \RuntimeException(
322                    "Duplicate specifications for id $id (classes " .
323                        get_class( $provider ) . ' and ' .
324                        get_class( $this->allAuthenticationProviders[$id] ) . ')'
325                );
326            }
327            $this->allAuthenticationProviders[$id] = $provider;
328            $this->primaryAuthenticationProviders[$id] = $provider;
329        }
330    }
331
332    /***************************************************************************/
333    // region   Authentication
334    /** @name   Authentication */
335
336    /**
337     * Indicate whether user authentication is possible
338     *
339     * It may not be if the session is provided by something like OAuth
340     * for which each individual request includes authentication data.
341     *
342     * @return bool
343     */
344    public function canAuthenticateNow() {
345        return $this->request->getSession()->canSetUser();
346    }
347
348    /**
349     * Start an authentication flow
350     *
351     * In addition to the AuthenticationRequests returned by
352     * $this->getAuthenticationRequests(), a client might include a
353     * CreateFromLoginAuthenticationRequest from a previous login attempt to
354     * preserve state.
355     *
356     * Instead of the AuthenticationRequests returned by
357     * $this->getAuthenticationRequests(), a client might pass a
358     * CreatedAccountAuthenticationRequest from an account creation that just
359     * succeeded to log in to the just-created account.
360     *
361     * @param AuthenticationRequest[] $reqs
362     * @param string $returnToUrl Url that REDIRECT responses should eventually
363     *  return to.
364     * @return AuthenticationResponse See self::continueAuthentication()
365     */
366    public function beginAuthentication( array $reqs, $returnToUrl ) {
367        $session = $this->request->getSession();
368        if ( !$session->canSetUser() ) {
369            // Caller should have called canAuthenticateNow()
370            $session->remove( self::AUTHN_STATE );
371            throw new LogicException( 'Authentication is not possible now' );
372        }
373
374        $guessUserName = null;
375        foreach ( $reqs as $req ) {
376            $req->returnToUrl = $returnToUrl;
377            // @codeCoverageIgnoreStart
378            if ( $req->username !== null && $req->username !== '' ) {
379                if ( $guessUserName === null ) {
380                    $guessUserName = $req->username;
381                } elseif ( $guessUserName !== $req->username ) {
382                    $guessUserName = null;
383                    break;
384                }
385            }
386            // @codeCoverageIgnoreEnd
387        }
388
389        // Check for special-case login of a just-created account
390        $req = AuthenticationRequest::getRequestByClass(
391            $reqs, CreatedAccountAuthenticationRequest::class
392        );
393        if ( $req ) {
394            if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
395                throw new LogicException(
396                    'CreatedAccountAuthenticationRequests are only valid on ' .
397                        'the same AuthManager that created the account'
398                );
399            }
400
401            $user = $this->userFactory->newFromName( (string)$req->username );
402            // @codeCoverageIgnoreStart
403            if ( !$user ) {
404                throw new \UnexpectedValueException(
405                    "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
406                );
407            } elseif ( $user->getId() != $req->id ) {
408                throw new \UnexpectedValueException(
409                    "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
410                );
411            }
412            // @codeCoverageIgnoreEnd
413
414            $this->logger->info( 'Logging in {user} after account creation', [
415                'user' => $user->getName(),
416            ] );
417            $ret = AuthenticationResponse::newPass( $user->getName() );
418            $performer = $session->getUser();
419            $this->setSessionDataForUser( $user );
420            $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $ret ] );
421            $session->remove( self::AUTHN_STATE );
422            $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
423                $ret, $user, $user->getName(), [
424                    'performer' => $performer
425                ] );
426            return $ret;
427        }
428
429        $this->removeAuthenticationSessionData( null );
430
431        foreach ( $this->getPreAuthenticationProviders() as $provider ) {
432            $status = $provider->testForAuthentication( $reqs );
433            if ( !$status->isGood() ) {
434                $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
435                $ret = AuthenticationResponse::newFail(
436                    Status::wrap( $status )->getMessage()
437                );
438                $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
439                    [ $this->userFactory->newFromName( (string)$guessUserName ), $ret ]
440                );
441                $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit( $ret, null, $guessUserName, [
442                    'performer' => $session->getUser()
443                ] );
444                return $ret;
445            }
446        }
447
448        $state = [
449            'reqs' => $reqs,
450            'returnToUrl' => $returnToUrl,
451            'guessUserName' => $guessUserName,
452            'providerIds' => $this->getProviderIds(),
453            'primary' => null,
454            'primaryResponse' => null,
455            'secondary' => [],
456            'maybeLink' => [],
457            'continueRequests' => [],
458        ];
459
460        // Preserve state from a previous failed login
461        $req = AuthenticationRequest::getRequestByClass(
462            $reqs, CreateFromLoginAuthenticationRequest::class
463        );
464        if ( $req ) {
465            $state['maybeLink'] = $req->maybeLink;
466        }
467
468        $session = $this->request->getSession();
469        $session->setSecret( self::AUTHN_STATE, $state );
470        $session->persist();
471
472        return $this->continueAuthentication( $reqs );
473    }
474
475    /**
476     * Continue an authentication flow
477     *
478     * Return values are interpreted as follows:
479     * - status FAIL: Authentication failed. If $response->createRequest is
480     *   set, that may be passed to self::beginAuthentication() or to
481     *   self::beginAccountCreation() to preserve state.
482     * - status REDIRECT: The client should be redirected to the contained URL,
483     *   new AuthenticationRequests should be made (if any), then
484     *   AuthManager::continueAuthentication() should be called.
485     * - status UI: The client should be presented with a user interface for
486     *   the fields in the specified AuthenticationRequests, then new
487     *   AuthenticationRequests should be made, then
488     *   AuthManager::continueAuthentication() should be called.
489     * - status RESTART: The user logged in successfully with a third-party
490     *   service, but the third-party credentials aren't attached to any local
491     *   account. This could be treated as a UI or a FAIL.
492     * - status PASS: Authentication was successful.
493     *
494     * @param AuthenticationRequest[] $reqs
495     * @return AuthenticationResponse
496     */
497    public function continueAuthentication( array $reqs ) {
498        $session = $this->request->getSession();
499        try {
500            if ( !$session->canSetUser() ) {
501                // Caller should have called canAuthenticateNow()
502                // @codeCoverageIgnoreStart
503                throw new LogicException( 'Authentication is not possible now' );
504                // @codeCoverageIgnoreEnd
505            }
506
507            $state = $session->getSecret( self::AUTHN_STATE );
508            if ( !is_array( $state ) ) {
509                return AuthenticationResponse::newFail(
510                    wfMessage( 'authmanager-authn-not-in-progress' )
511                );
512            }
513            if ( $state['providerIds'] !== $this->getProviderIds() ) {
514                // An inconsistent AuthManagerFilterProviders hook, or site configuration changed
515                // while the user was in the middle of authentication. The first is a bug, the
516                // second is rare but expected when deploying a config change. Try handle in a way
517                // that's useful for both cases.
518                // @codeCoverageIgnoreStart
519                MWExceptionHandler::logException( new NormalizedException(
520                    'Authentication failed because of inconsistent provider array',
521                    [ 'old' => json_encode( $state['providerIds'] ), 'new' => json_encode( $this->getProviderIds() ) ]
522                ) );
523                $response = AuthenticationResponse::newFail(
524                    wfMessage( 'authmanager-authn-not-in-progress' )
525                );
526                $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
527                    [ $this->userFactory->newFromName( (string)$state['guessUserName'] ), $response ]
528                );
529                $session->remove( self::AUTHN_STATE );
530                return $response;
531                // @codeCoverageIgnoreEnd
532            }
533            $state['continueRequests'] = [];
534
535            $guessUserName = $state['guessUserName'];
536
537            foreach ( $reqs as $req ) {
538                $req->returnToUrl = $state['returnToUrl'];
539            }
540
541            // Step 1: Choose a primary authentication provider, and call it until it succeeds.
542
543            if ( $state['primary'] === null ) {
544                // We haven't picked a PrimaryAuthenticationProvider yet
545                // @codeCoverageIgnoreStart
546                $guessUserName = null;
547                foreach ( $reqs as $req ) {
548                    if ( $req->username !== null && $req->username !== '' ) {
549                        if ( $guessUserName === null ) {
550                            $guessUserName = $req->username;
551                        } elseif ( $guessUserName !== $req->username ) {
552                            $guessUserName = null;
553                            break;
554                        }
555                    }
556                }
557                $state['guessUserName'] = $guessUserName;
558                // @codeCoverageIgnoreEnd
559                $state['reqs'] = $reqs;
560
561                foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
562                    $res = $provider->beginPrimaryAuthentication( $reqs );
563                    switch ( $res->status ) {
564                        case AuthenticationResponse::PASS:
565                            $state['primary'] = $id;
566                            $state['primaryResponse'] = $res;
567                            $this->logger->debug( "Primary login with $id succeeded" );
568                            break 2;
569                        case AuthenticationResponse::FAIL:
570                            $this->logger->debug( "Login failed in primary authentication by $id" );
571                            if ( $res->createRequest || $state['maybeLink'] ) {
572                                $res->createRequest = new CreateFromLoginAuthenticationRequest(
573                                    $res->createRequest, $state['maybeLink']
574                                );
575                            }
576                            $this->callMethodOnProviders(
577                                self::CALL_ALL,
578                                'postAuthentication',
579                                [
580                                    $this->userFactory->newFromName( (string)$guessUserName ),
581                                    $res
582                                ]
583                            );
584                            $session->remove( self::AUTHN_STATE );
585                            $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
586                                $res, null, $guessUserName, [
587                                    'performer' => $session->getUser()
588                                ] );
589                            return $res;
590                        case AuthenticationResponse::ABSTAIN:
591                            // Continue loop
592                            break;
593                        case AuthenticationResponse::REDIRECT:
594                        case AuthenticationResponse::UI:
595                            $this->logger->debug( "Primary login with $id returned $res->status" );
596                            $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
597                            $state['primary'] = $id;
598                            $state['continueRequests'] = $res->neededRequests;
599                            $session->setSecret( self::AUTHN_STATE, $state );
600                            return $res;
601
602                            // @codeCoverageIgnoreStart
603                        default:
604                            throw new \DomainException(
605                                get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
606                            );
607                            // @codeCoverageIgnoreEnd
608                    }
609                }
610                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Always set in loop before, if passed
611                if ( $state['primary'] === null ) {
612                    $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
613                    $response = AuthenticationResponse::newFail(
614                        wfMessage( 'authmanager-authn-no-primary' )
615                    );
616                    $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
617                        [ $this->userFactory->newFromName( (string)$guessUserName ), $response ]
618                    );
619                    $session->remove( self::AUTHN_STATE );
620                    return $response;
621                }
622            } elseif ( $state['primaryResponse'] === null ) {
623                $provider = $this->getAuthenticationProvider( $state['primary'] );
624                if ( !$provider instanceof PrimaryAuthenticationProvider ) {
625                    // Configuration changed? Force them to start over.
626                    // @codeCoverageIgnoreStart
627                    $response = AuthenticationResponse::newFail(
628                        wfMessage( 'authmanager-authn-not-in-progress' )
629                    );
630                    $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
631                        [ $this->userFactory->newFromName( (string)$guessUserName ), $response ]
632                    );
633                    $session->remove( self::AUTHN_STATE );
634                    return $response;
635                    // @codeCoverageIgnoreEnd
636                }
637                $id = $provider->getUniqueId();
638                $res = $provider->continuePrimaryAuthentication( $reqs );
639                switch ( $res->status ) {
640                    case AuthenticationResponse::PASS:
641                        $state['primaryResponse'] = $res;
642                        $this->logger->debug( "Primary login with $id succeeded" );
643                        break;
644                    case AuthenticationResponse::FAIL:
645                        $this->logger->debug( "Login failed in primary authentication by $id" );
646                        if ( $res->createRequest || $state['maybeLink'] ) {
647                            $res->createRequest = new CreateFromLoginAuthenticationRequest(
648                                $res->createRequest, $state['maybeLink']
649                            );
650                        }
651                        $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
652                            [ $this->userFactory->newFromName( (string)$guessUserName ), $res ]
653                        );
654                        $session->remove( self::AUTHN_STATE );
655                        $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
656                            $res, null, $guessUserName, [
657                                'performer' => $session->getUser()
658                            ] );
659                        return $res;
660                    case AuthenticationResponse::REDIRECT:
661                    case AuthenticationResponse::UI:
662                        $this->logger->debug( "Primary login with $id returned $res->status" );
663                        $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
664                        $state['continueRequests'] = $res->neededRequests;
665                        $session->setSecret( self::AUTHN_STATE, $state );
666                        return $res;
667                    default:
668                        throw new \DomainException(
669                            get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
670                        );
671                }
672            }
673
674            // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Always set in loop before, if passed
675            $res = $state['primaryResponse'];
676            if ( $res->username === null ) {
677                $provider = $this->getAuthenticationProvider( $state['primary'] );
678                if ( !$provider instanceof PrimaryAuthenticationProvider ) {
679                    // Configuration changed? Force them to start over.
680                    // @codeCoverageIgnoreStart
681                    $response = AuthenticationResponse::newFail(
682                        wfMessage( 'authmanager-authn-not-in-progress' )
683                    );
684                    $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
685                        [ $this->userFactory->newFromName( (string)$guessUserName ), $response ]
686                    );
687                    $session->remove( self::AUTHN_STATE );
688                    return $response;
689                    // @codeCoverageIgnoreEnd
690                }
691
692                if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
693                    $res->linkRequest &&
694                    // don't confuse the user with an incorrect message if linking is disabled
695                    $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
696                ) {
697                    $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
698                    $msg = 'authmanager-authn-no-local-user-link';
699                } else {
700                    $msg = 'authmanager-authn-no-local-user';
701                }
702                $this->logger->debug(
703                    "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
704                );
705                $response = AuthenticationResponse::newRestart( wfMessage( $msg ) );
706                $response->neededRequests = $this->getAuthenticationRequestsInternal(
707                    self::ACTION_LOGIN,
708                    [],
709                    $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
710                );
711                if ( $res->createRequest || $state['maybeLink'] ) {
712                    $response->createRequest = new CreateFromLoginAuthenticationRequest(
713                        $res->createRequest, $state['maybeLink']
714                    );
715                    $response->neededRequests[] = $response->createRequest;
716                }
717                $this->fillRequests( $response->neededRequests, self::ACTION_LOGIN, null, true );
718                $session->setSecret( self::AUTHN_STATE, [
719                    'reqs' => [], // Will be filled in later
720                    'primary' => null,
721                    'primaryResponse' => null,
722                    'secondary' => [],
723                    'continueRequests' => $response->neededRequests,
724                ] + $state );
725
726                // Give the AuthManagerVerifyAuthentication hook a chance to interrupt - even though
727                // RESTART does not immediately result in a successful login, the response and session
728                // state can hold information identifying a (remote) user, and that could be turned
729                // into access to that user's account in a follow-up request.
730                if ( !$this->runVerifyHook( self::ACTION_LOGIN, null, $response, $state['primary'] ) ) {
731                    $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ null, $response ] );
732                    $session->remove( self::AUTHN_STATE );
733                    $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
734                        $response, null, null, [ 'performer' => $session->getUser() ]
735                    );
736                    return $response;
737                }
738
739                return $response;
740            }
741
742            // Step 2: Primary authentication succeeded, create the User object
743            // (and add the user locally if necessary)
744
745            $user = $this->userFactory->newFromName(
746                (string)$res->username,
747                UserRigorOptions::RIGOR_USABLE
748            );
749            if ( !$user ) {
750                $provider = $this->getAuthenticationProvider( $state['primary'] );
751                throw new \DomainException(
752                    get_class( $provider ) . " returned an invalid username: {$res->username}"
753                );
754            }
755            if ( !$user->isRegistered() ) {
756                // User doesn't exist locally. Create it.
757                $this->logger->info( 'Auto-creating {user} on login', [
758                    'user' => $user->getName(),
759                ] );
760                // Also use $user as performer, because the performer will be used for permission
761                // checks and global rights extensions might add rights based on the username,
762                // even if the user doesn't exist at this point.
763                $status = $this->autoCreateUser( $user, $state['primary'], false, true, $user );
764                if ( !$status->isGood() ) {
765                    $response = AuthenticationResponse::newFail(
766                        Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
767                    );
768                    $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $response ] );
769                    $session->remove( self::AUTHN_STATE );
770                    $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
771                        $response, $user, $user->getName(), [
772                            'performer' => $session->getUser()
773                        ] );
774                    return $response;
775                }
776            }
777
778            // Step 3: Iterate over all the secondary authentication providers.
779
780            $beginReqs = $state['reqs'];
781
782            foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
783                if ( !isset( $state['secondary'][$id] ) ) {
784                    // This provider isn't started yet, so we pass it the set
785                    // of reqs from beginAuthentication instead of whatever
786                    // might have been used by a previous provider in line.
787                    $func = 'beginSecondaryAuthentication';
788                    $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
789                } elseif ( !$state['secondary'][$id] ) {
790                    $func = 'continueSecondaryAuthentication';
791                    $res = $provider->continueSecondaryAuthentication( $user, $reqs );
792                } else {
793                    continue;
794                }
795                switch ( $res->status ) {
796                    case AuthenticationResponse::PASS:
797                        $this->logger->debug( "Secondary login with $id succeeded" );
798                        // fall through
799                    case AuthenticationResponse::ABSTAIN:
800                        $state['secondary'][$id] = true;
801                        break;
802                    case AuthenticationResponse::FAIL:
803                        $this->logger->debug( "Login failed in secondary authentication by $id" );
804                        $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $res ] );
805                        $session->remove( self::AUTHN_STATE );
806                        $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
807                            $res, $user, $user->getName(), [
808                                'performer' => $session->getUser()
809                            ] );
810                        return $res;
811                    case AuthenticationResponse::REDIRECT:
812                    case AuthenticationResponse::UI:
813                        $this->logger->debug( "Secondary login with $id returned " . $res->status );
814                        $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
815                        $state['secondary'][$id] = false;
816                        $state['continueRequests'] = $res->neededRequests;
817                        $session->setSecret( self::AUTHN_STATE, $state );
818                        return $res;
819
820                        // @codeCoverageIgnoreStart
821                    default:
822                        throw new \DomainException(
823                            get_class( $provider ) . "::{$func}() returned $res->status"
824                        );
825                        // @codeCoverageIgnoreEnd
826                }
827            }
828
829            // Step 4: Authentication complete! Give hook handlers a chance to interrupt, then
830            // set the user in the session and clean up.
831
832            $response = AuthenticationResponse::newPass( $user->getName() );
833            if ( !$this->runVerifyHook( self::ACTION_LOGIN, $user, $response, $state['primary'] ) ) {
834                $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $response ] );
835                $session->remove( self::AUTHN_STATE );
836                $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
837                    $response, $user, $user->getName(), [
838                        'performer' => $session->getUser(),
839                    ] );
840                return $response;
841            }
842            $this->logger->info( 'Login for {user} succeeded from {clientip}', [
843                'user' => $user->getName(),
844                'clientip' => $this->request->getIP(),
845            ] );
846            $rememberMeConfig = $this->config->get( MainConfigNames::RememberMe );
847            if ( $rememberMeConfig === RememberMeAuthenticationRequest::ALWAYS_REMEMBER ) {
848                $rememberMe = true;
849            } elseif ( $rememberMeConfig === RememberMeAuthenticationRequest::NEVER_REMEMBER ) {
850                $rememberMe = false;
851            } else {
852                /** @var RememberMeAuthenticationRequest $req */
853                $req = AuthenticationRequest::getRequestByClass(
854                    $beginReqs, RememberMeAuthenticationRequest::class
855                );
856
857                // T369668: Before we conclude, let's make sure the user hasn't specified
858                // that they want their login remembered elsewhere like in the central domain.
859                // If the user clicked "remember me" in the central domain, then we should
860                // prioritise that when we call continuePrimaryAuthentication() in the provider
861                // that makes calls continuePrimaryAuthentication(). NOTE: It is the responsibility
862                // of the provider to refresh the "remember me" state that will be applied to
863                // the local wiki.
864                $rememberMe = ( $req && $req->rememberMe ) ||
865                    $this->getAuthenticationSessionData( self::REMEMBER_ME );
866            }
867            $this->setSessionDataForUser( $user, $rememberMe );
868            $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $response ] );
869            $performer = $session->getUser();
870            $session->remove( self::AUTHN_STATE );
871            $this->removeAuthenticationSessionData( null );
872            $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
873                $response, $user, $user->getName(), [
874                    'performer' => $performer
875                ] );
876            return $response;
877        } catch ( \Exception $ex ) {
878            $session->remove( self::AUTHN_STATE );
879            throw $ex;
880        }
881    }
882
883    /**
884     * Whether security-sensitive operations should proceed.
885     *
886     * A "security-sensitive operation" is something like a password or email
887     * change, that would normally have a "reenter your password to confirm"
888     * box if we only supported password-based authentication.
889     *
890     * @param string $operation Operation being checked. This should be a
891     *  message-key-like string such as 'change-password' or 'change-email'.
892     * @return string One of the SEC_* constants.
893     */
894    public function securitySensitiveOperationStatus( $operation ) {
895        $status = self::SEC_OK;
896
897        $this->logger->debug( __METHOD__ . ": Checking $operation" );
898
899        $session = $this->request->getSession();
900        $aId = $session->getUser()->getId();
901        if ( $aId === 0 ) {
902            // User isn't authenticated. DWIM?
903            $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
904            $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
905            return $status;
906        }
907
908        if ( $session->canSetUser() ) {
909            $id = $session->get( 'AuthManager:lastAuthId' );
910            $last = $session->get( 'AuthManager:lastAuthTimestamp' );
911            if ( $id !== $aId || $last === null ) {
912                $timeSinceLogin = PHP_INT_MAX; // Forever ago
913            } else {
914                $timeSinceLogin = max( 0, time() - $last );
915            }
916
917            $thresholds = $this->config->get( MainConfigNames::ReauthenticateTime );
918            if ( isset( $thresholds[$operation] ) ) {
919                $threshold = $thresholds[$operation];
920            } elseif ( isset( $thresholds['default'] ) ) {
921                $threshold = $thresholds['default'];
922            } else {
923                throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
924            }
925
926            if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
927                $status = self::SEC_REAUTH;
928            }
929        } else {
930            $timeSinceLogin = -1;
931
932            $pass = $this->config->get(
933                MainConfigNames::AllowSecuritySensitiveOperationIfCannotReauthenticate );
934            if ( isset( $pass[$operation] ) ) {
935                $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
936            } elseif ( isset( $pass['default'] ) ) {
937                $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
938            } else {
939                throw new \UnexpectedValueException(
940                    '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
941                );
942            }
943        }
944
945        $this->getHookRunner()->onSecuritySensitiveOperationStatus(
946            $status, $operation, $session, $timeSinceLogin );
947
948        // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
949        if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
950            $status = self::SEC_FAIL;
951        }
952
953        $this->logger->info( __METHOD__ . "$operation is $status for '{user}'",
954            [
955                'user' => $session->getUser()->getName(),
956                'clientip' => $this->getRequest()->getIP(),
957            ]
958        );
959
960        return $status;
961    }
962
963    /**
964     * Determine whether a username can authenticate
965     *
966     * This is mainly for internal purposes and only takes authentication data into account,
967     * not things like blocks that can change without the authentication system being aware.
968     *
969     * @param string $username MediaWiki username
970     * @return bool
971     */
972    public function userCanAuthenticate( $username ) {
973        foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
974            if ( $provider->testUserCanAuthenticate( $username ) ) {
975                return true;
976            }
977        }
978        return false;
979    }
980
981    /**
982     * Provide normalized versions of the username for security checks
983     *
984     * Since different providers can normalize the input in different ways,
985     * this returns an array of all the different ways the name might be
986     * normalized for authentication.
987     *
988     * The returned strings should not be revealed to the user, as that might
989     * leak private information (e.g. an email address might be normalized to a
990     * username).
991     *
992     * @param string $username
993     * @return string[]
994     */
995    public function normalizeUsername( $username ) {
996        $ret = [];
997        foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
998            $normalized = $provider->providerNormalizeUsername( $username );
999            if ( $normalized !== null ) {
1000                $ret[$normalized] = true;
1001            }
1002        }
1003        return array_keys( $ret );
1004    }
1005
1006    /**
1007     * Call this method to set the request context user for the current request
1008     * from the context session user.
1009     *
1010     * Useful in cases where we need to make sure that a MediaWiki request outputs
1011     * correct context data for a user who has just been logged-in.
1012     *
1013     * The method will also update the global language variable based on the
1014     * session's user's context language.
1015     *
1016     * This won't affect objects which already made a copy of the user or the
1017     * context, so it shouldn't be relied on too heavily, but can help to make the
1018     * UI more consistent after changing the user. Typically used after a successful
1019     * AuthManager action that changed the session user (e.g.
1020     * AuthManager::autoCreateUser() with the login flag set).
1021     */
1022    public function setRequestContextUserFromSessionUser(): void {
1023        $context = RequestContext::getMain();
1024        $user = $context->getRequest()->getSession()->getUser();
1025
1026        StubGlobalUser::setUser( $user );
1027        $context->setUser( $user );
1028
1029        // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
1030        global $wgLang;
1031        // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
1032        $wgLang = $context->getLanguage();
1033    }
1034
1035    // endregion -- end of Authentication
1036
1037    /***************************************************************************/
1038    // region   Authentication data changing
1039    /** @name   Authentication data changing */
1040
1041    /**
1042     * Revoke any authentication credentials for a user
1043     *
1044     * After this, the user should no longer be able to log in.
1045     *
1046     * @param string $username
1047     */
1048    public function revokeAccessForUser( $username ) {
1049        $this->logger->info( 'Revoking access for {user}', [
1050            'user' => $username,
1051        ] );
1052        $this->callMethodOnProviders( self::CALL_PRIMARY | self::CALL_SECONDARY, 'providerRevokeAccessForUser',
1053            [ $username ]
1054        );
1055    }
1056
1057    /**
1058     * Validate a change of authentication data (e.g. passwords)
1059     * @param AuthenticationRequest $req
1060     * @param bool $checkData If false, $req hasn't been loaded from the
1061     *  submission so checks on user-submitted fields should be skipped. $req->username is
1062     *  considered user-submitted for this purpose, even if it cannot be changed via
1063     *  $req->loadFromSubmission.
1064     * @return Status
1065     */
1066    public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
1067        $any = false;
1068        $providers = $this->getPrimaryAuthenticationProviders() +
1069            $this->getSecondaryAuthenticationProviders();
1070
1071        foreach ( $providers as $provider ) {
1072            $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
1073            if ( !$status->isGood() ) {
1074                // If status is not good because reset email password last attempt was within
1075                // $wgPasswordReminderResendTime then return good status with throttled-mailpassword value;
1076                // otherwise, return the $status wrapped.
1077                return $status->hasMessage( 'throttled-mailpassword' )
1078                    ? Status::newGood( 'throttled-mailpassword' )
1079                    : Status::wrap( $status );
1080            }
1081            $any = $any || $status->value !== 'ignored';
1082        }
1083        if ( !$any ) {
1084            return Status::newGood( 'ignored' )
1085                ->warning( 'authmanager-change-not-supported' );
1086        }
1087        return Status::newGood();
1088    }
1089
1090    /**
1091     * Change authentication data (e.g. passwords)
1092     *
1093     * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
1094     * result in a successful login in the future.
1095     *
1096     * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
1097     * no longer result in a successful login.
1098     *
1099     * This method should only be called if allowsAuthenticationDataChange( $req, true )
1100     * returned success.
1101     *
1102     * @param AuthenticationRequest $req
1103     * @param bool $isAddition Set true if this represents an addition of
1104     *  credentials rather than a change. The main difference is that additions
1105     *  should not invalidate BotPasswords. If you're not sure, leave it false.
1106     */
1107    public function changeAuthenticationData( AuthenticationRequest $req, $isAddition = false ) {
1108        $this->logger->info( 'Changing authentication data for {user} class {what}', [
1109            'user' => is_string( $req->username ) ? $req->username : '<no name>',
1110            'what' => get_class( $req ),
1111        ] );
1112
1113        $this->callMethodOnProviders( self::CALL_PRIMARY | self::CALL_SECONDARY, 'providerChangeAuthenticationData',
1114            [ $req ]
1115        );
1116
1117        // When the main account's authentication data is changed, invalidate
1118        // all BotPasswords too.
1119        if ( !$isAddition ) {
1120            $this->botPasswordStore->invalidateUserPasswords( (string)$req->username );
1121        }
1122    }
1123
1124    // endregion -- end of Authentication data changing
1125
1126    /***************************************************************************/
1127    // region   Account creation
1128    /** @name   Account creation */
1129
1130    /**
1131     * Determine whether accounts can be created
1132     * @return bool
1133     */
1134    public function canCreateAccounts() {
1135        foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1136            switch ( $provider->accountCreationType() ) {
1137                case PrimaryAuthenticationProvider::TYPE_CREATE:
1138                case PrimaryAuthenticationProvider::TYPE_LINK:
1139                    return true;
1140            }
1141        }
1142        return false;
1143    }
1144
1145    /**
1146     * Determine whether a particular account can be created
1147     * @param string $username MediaWiki username
1148     * @param array $options
1149     *  - flags: (int) Bitfield of IDBAccessObject::READ_* constants, default IDBAccessObject::READ_NORMAL
1150     *  - creating: (bool) For internal use only. Never specify this.
1151     * @return Status
1152     */
1153    public function canCreateAccount( $username, $options = [] ) {
1154        // Back compat
1155        if ( is_int( $options ) ) {
1156            $options = [ 'flags' => $options ];
1157        }
1158        $options += [
1159            'flags' => IDBAccessObject::READ_NORMAL,
1160            'creating' => false,
1161        ];
1162        // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
1163        $flags = $options['flags'];
1164
1165        if ( !$this->canCreateAccounts() ) {
1166            return Status::newFatal( 'authmanager-create-disabled' );
1167        }
1168
1169        if ( $this->userExists( $username, $flags ) ) {
1170            return Status::newFatal( 'userexists' );
1171        }
1172
1173        $user = $this->userFactory->newFromName( (string)$username, UserRigorOptions::RIGOR_CREATABLE );
1174        if ( !is_object( $user ) ) {
1175            return Status::newFatal( 'noname' );
1176        } else {
1177            $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
1178            if ( $user->isRegistered() ) {
1179                return Status::newFatal( 'userexists' );
1180            }
1181        }
1182
1183        // Denied by providers?
1184        $providers = $this->getPreAuthenticationProviders() +
1185            $this->getPrimaryAuthenticationProviders() +
1186            $this->getSecondaryAuthenticationProviders();
1187        foreach ( $providers as $provider ) {
1188            $status = $provider->testUserForCreation( $user, false, $options );
1189            if ( !$status->isGood() ) {
1190                return Status::wrap( $status );
1191            }
1192        }
1193
1194        return Status::newGood();
1195    }
1196
1197    /**
1198     * @param callable $authorizer ( string $action, PageIdentity $target, PermissionStatus $status )
1199     * @param string $action
1200     * @return StatusValue
1201     */
1202    private function authorizeInternal(
1203        callable $authorizer,
1204        string $action
1205    ): StatusValue {
1206        // Wiki is read-only?
1207        if ( $this->readOnlyMode->isReadOnly() ) {
1208            return StatusValue::newFatal( wfMessage( 'readonlytext', $this->readOnlyMode->getReason() ) );
1209        }
1210
1211        $permStatus = new PermissionStatus();
1212        if ( !$authorizer(
1213            $action,
1214            SpecialPage::getTitleFor( 'CreateAccount' ),
1215            $permStatus
1216        ) ) {
1217            return $permStatus;
1218        }
1219
1220        $ip = $this->getRequest()->getIP();
1221        if ( $this->blockManager->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1222            return StatusValue::newFatal( 'sorbs_create_account_reason' );
1223        }
1224
1225        return StatusValue::newGood();
1226    }
1227
1228    /**
1229     * Check whether $creator can create accounts.
1230     *
1231     * @note this method does not guarantee full permissions check, so it should only
1232     * be used to to decide whether to show a form. To authorize the account creation
1233     * action use {@link self::authorizeCreateAccount} instead.
1234     *
1235     * @since 1.39
1236     * @param Authority $creator
1237     * @return StatusValue
1238     */
1239    public function probablyCanCreateAccount( Authority $creator ): StatusValue {
1240        return $this->authorizeInternal(
1241            static function (
1242                string $action,
1243                PageIdentity $target,
1244                PermissionStatus $status
1245            ) use ( $creator ) {
1246                return $creator->probablyCan( $action, $target, $status );
1247            },
1248            'createaccount'
1249        );
1250    }
1251
1252    /**
1253     * Authorize the account creation by $creator
1254     *
1255     * @note this method should be used right before the account is created.
1256     * To check whether a current performer has the potential to create accounts,
1257     * use {@link self::probablyCanCreateAccount} instead.
1258     *
1259     * @since 1.39
1260     * @param Authority $creator
1261     * @return StatusValue
1262     */
1263    public function authorizeCreateAccount( Authority $creator ): StatusValue {
1264        return $this->authorizeInternal(
1265            static function (
1266                string $action,
1267                PageIdentity $target,
1268                PermissionStatus $status
1269            ) use ( $creator ) {
1270                return $creator->authorizeWrite( $action, $target, $status );
1271            },
1272            'createaccount'
1273        );
1274    }
1275
1276    /**
1277     * Start an account creation flow
1278     *
1279     * In addition to the AuthenticationRequests returned by
1280     * $this->getAuthenticationRequests(), a client might include a
1281     * CreateFromLoginAuthenticationRequest from a previous login attempt. If
1282     * <code>
1283     * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
1284     * </code>
1285     * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
1286     * should be omitted. If the CreateFromLoginAuthenticationRequest has a
1287     * username set, that username must be used for all other requests.
1288     *
1289     * @param Authority $creator User doing the account creation
1290     * @param AuthenticationRequest[] $reqs
1291     * @param string $returnToUrl Url that REDIRECT responses should eventually
1292     *  return to.
1293     * @return AuthenticationResponse
1294     */
1295    public function beginAccountCreation( Authority $creator, array $reqs, $returnToUrl ) {
1296        $session = $this->request->getSession();
1297        if ( !$this->canCreateAccounts() ) {
1298            // Caller should have called canCreateAccounts()
1299            $session->remove( self::ACCOUNT_CREATION_STATE );
1300            throw new LogicException( 'Account creation is not possible' );
1301        }
1302
1303        try {
1304            $username = AuthenticationRequest::getUsernameFromRequests( $reqs );
1305        } catch ( \UnexpectedValueException $ex ) {
1306            $username = null;
1307        }
1308        if ( $username === null ) {
1309            $this->logger->debug( __METHOD__ . ': No username provided' );
1310            return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1311        }
1312
1313        // Permissions check
1314        $status = Status::wrap( $this->authorizeCreateAccount( $creator ) );
1315        if ( !$status->isGood() ) {
1316            $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1317                'user' => $username,
1318                'creator' => $creator->getUser()->getName(),
1319                'reason' => $status->getWikiText( false, false, 'en' )
1320            ] );
1321            return AuthenticationResponse::newFail( $status->getMessage() );
1322        }
1323
1324        // Avoid deadlocks by placing no shared or exclusive gap locks (T199393)
1325        // As defense in-depth, PrimaryAuthenticationProvider::testUserExists only
1326        // supports READ_NORMAL/READ_LATEST (no support for recency query flags).
1327        $status = $this->canCreateAccount(
1328            $username, [ 'flags' => IDBAccessObject::READ_LATEST, 'creating' => true ]
1329        );
1330        if ( !$status->isGood() ) {
1331            $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1332                'user' => $username,
1333                'creator' => $creator->getUser()->getName(),
1334                'reason' => $status->getWikiText( false, false, 'en' )
1335            ] );
1336            return AuthenticationResponse::newFail( $status->getMessage() );
1337        }
1338
1339        $user = $this->userFactory->newFromName( (string)$username, UserRigorOptions::RIGOR_CREATABLE );
1340        foreach ( $reqs as $req ) {
1341            $req->username = $username;
1342            $req->returnToUrl = $returnToUrl;
1343            if ( $req instanceof UserDataAuthenticationRequest ) {
1344                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable user should be checked and valid here
1345                $status = $req->populateUser( $user );
1346                if ( !$status->isGood() ) {
1347                    $status = Status::wrap( $status );
1348                    $session->remove( self::ACCOUNT_CREATION_STATE );
1349                    $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1350                        'user' => $user->getName(),
1351                        'creator' => $creator->getUser()->getName(),
1352                        'reason' => $status->getWikiText( false, false, 'en' ),
1353                    ] );
1354                    return AuthenticationResponse::newFail( $status->getMessage() );
1355                }
1356            }
1357        }
1358
1359        $this->removeAuthenticationSessionData( null );
1360
1361        $state = [
1362            'username' => $username,
1363            'userid' => 0,
1364            'creatorid' => $creator->getUser()->getId(),
1365            'creatorname' => $creator->getUser()->getName(),
1366            'reqs' => $reqs,
1367            'returnToUrl' => $returnToUrl,
1368            'providerIds' => $this->getProviderIds(),
1369            'primary' => null,
1370            'primaryResponse' => null,
1371            'secondary' => [],
1372            'continueRequests' => [],
1373            'maybeLink' => [],
1374            'ranPreTests' => false,
1375        ];
1376
1377        // Special case: converting a login to an account creation
1378        $req = AuthenticationRequest::getRequestByClass(
1379            $reqs, CreateFromLoginAuthenticationRequest::class
1380        );
1381        if ( $req ) {
1382            $state['maybeLink'] = $req->maybeLink;
1383
1384            if ( $req->createRequest ) {
1385                $reqs[] = $req->createRequest;
1386                $state['reqs'][] = $req->createRequest;
1387            }
1388        }
1389
1390        $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1391        $session->persist();
1392
1393        return $this->continueAccountCreation( $reqs );
1394    }
1395
1396    /**
1397     * Continue an account creation flow
1398     * @param AuthenticationRequest[] $reqs
1399     * @return AuthenticationResponse
1400     */
1401    public function continueAccountCreation( array $reqs ) {
1402        $session = $this->request->getSession();
1403        try {
1404            if ( !$this->canCreateAccounts() ) {
1405                // Caller should have called canCreateAccounts()
1406                $session->remove( self::ACCOUNT_CREATION_STATE );
1407                throw new LogicException( 'Account creation is not possible' );
1408            }
1409
1410            $state = $session->getSecret( self::ACCOUNT_CREATION_STATE );
1411            if ( !is_array( $state ) ) {
1412                return AuthenticationResponse::newFail(
1413                    wfMessage( 'authmanager-create-not-in-progress' )
1414                );
1415            }
1416            $state['continueRequests'] = [];
1417
1418            // Step 0: Prepare and validate the input
1419
1420            $user = $this->userFactory->newFromName(
1421                (string)$state['username'],
1422                UserRigorOptions::RIGOR_CREATABLE
1423            );
1424            if ( !is_object( $user ) ) {
1425                $session->remove( self::ACCOUNT_CREATION_STATE );
1426                $this->logger->debug( __METHOD__ . ': Invalid username', [
1427                    'user' => $state['username'],
1428                ] );
1429                return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1430            }
1431
1432            if ( $state['creatorid'] ) {
1433                $creator = $this->userFactory->newFromId( (int)$state['creatorid'] );
1434            } else {
1435                $creator = $this->userFactory->newAnonymous();
1436                $creator->setName( $state['creatorname'] );
1437            }
1438
1439            if ( $state['providerIds'] !== $this->getProviderIds() ) {
1440                // An inconsistent AuthManagerFilterProviders hook, or site configuration changed
1441                // while the user was in the middle of authentication. The first is a bug, the
1442                // second is rare but expected when deploying a config change. Try handle in a way
1443                // that's useful for both cases.
1444                // @codeCoverageIgnoreStart
1445                MWExceptionHandler::logException( new NormalizedException(
1446                    'Authentication failed because of inconsistent provider array',
1447                    [ 'old' => json_encode( $state['providerIds'] ), 'new' => json_encode( $this->getProviderIds() ) ]
1448                ) );
1449                $ret = AuthenticationResponse::newFail(
1450                    wfMessage( 'authmanager-create-not-in-progress' )
1451                );
1452                $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1453                $session->remove( self::ACCOUNT_CREATION_STATE );
1454                return $ret;
1455                // @codeCoverageIgnoreEnd
1456            }
1457
1458            // Avoid account creation races on double submissions
1459            $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getLocalClusterInstance();
1460            $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1461            if ( !$lock ) {
1462                // Don't clear AuthManager::accountCreationState for this code
1463                // path because the process that won the race owns it.
1464                $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1465                    'user' => $user->getName(),
1466                    'creator' => $creator->getName(),
1467                ] );
1468                return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1469            }
1470
1471            // Permissions check
1472            $status = Status::wrap( $this->authorizeCreateAccount( $creator ) );
1473            if ( !$status->isGood() ) {
1474                $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1475                    'user' => $user->getName(),
1476                    'creator' => $creator->getName(),
1477                    'reason' => $status->getWikiText( false, false, 'en' )
1478                ] );
1479                $ret = AuthenticationResponse::newFail( $status->getMessage() );
1480                $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1481                $session->remove( self::ACCOUNT_CREATION_STATE );
1482                return $ret;
1483            }
1484
1485            // Load from primary DB for existence check
1486            $user->load( IDBAccessObject::READ_LATEST );
1487
1488            if ( $state['userid'] === 0 ) {
1489                if ( $user->isRegistered() ) {
1490                    $this->logger->debug( __METHOD__ . ': User exists locally', [
1491                        'user' => $user->getName(),
1492                        'creator' => $creator->getName(),
1493                    ] );
1494                    $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1495                    $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1496                    $session->remove( self::ACCOUNT_CREATION_STATE );
1497                    return $ret;
1498                }
1499            } else {
1500                if ( !$user->isRegistered() ) {
1501                    $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1502                        'user' => $user->getName(),
1503                        'creator' => $creator->getName(),
1504                        'expected_id' => $state['userid'],
1505                    ] );
1506                    throw new \UnexpectedValueException(
1507                        "User \"{$state['username']}\" should exist now, but doesn't!"
1508                    );
1509                }
1510                if ( $user->getId() !== $state['userid'] ) {
1511                    $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1512                        'user' => $user->getName(),
1513                        'creator' => $creator->getName(),
1514                        'expected_id' => $state['userid'],
1515                        'actual_id' => $user->getId(),
1516                    ] );
1517                    throw new \UnexpectedValueException(
1518                        "User \"{$state['username']}\" exists, but " .
1519                            "ID {$user->getId()} !== {$state['userid']}!"
1520                    );
1521                }
1522            }
1523            foreach ( $state['reqs'] as $req ) {
1524                if ( $req instanceof UserDataAuthenticationRequest ) {
1525                    $status = $req->populateUser( $user );
1526                    if ( !$status->isGood() ) {
1527                        // This should never happen...
1528                        $status = Status::wrap( $status );
1529                        $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1530                            'user' => $user->getName(),
1531                            'creator' => $creator->getName(),
1532                            'reason' => $status->getWikiText( false, false, 'en' ),
1533                        ] );
1534                        $ret = AuthenticationResponse::newFail( $status->getMessage() );
1535                        $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1536                            [ $user, $creator, $ret ]
1537                        );
1538                        $session->remove( self::ACCOUNT_CREATION_STATE );
1539                        return $ret;
1540                    }
1541                }
1542            }
1543
1544            foreach ( $reqs as $req ) {
1545                $req->returnToUrl = $state['returnToUrl'];
1546                $req->username = $state['username'];
1547            }
1548
1549            // Run pre-creation tests, if we haven't already
1550            if ( !$state['ranPreTests'] ) {
1551                $providers = $this->getPreAuthenticationProviders() +
1552                    $this->getPrimaryAuthenticationProviders() +
1553                    $this->getSecondaryAuthenticationProviders();
1554                foreach ( $providers as $id => $provider ) {
1555                    $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1556                    if ( !$status->isGood() ) {
1557                        $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1558                            'user' => $user->getName(),
1559                            'creator' => $creator->getName(),
1560                        ] );
1561                        $ret = AuthenticationResponse::newFail(
1562                            Status::wrap( $status )->getMessage()
1563                        );
1564                        $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1565                            [ $user, $creator, $ret ]
1566                        );
1567                        $session->remove( self::ACCOUNT_CREATION_STATE );
1568                        return $ret;
1569                    }
1570                }
1571
1572                $state['ranPreTests'] = true;
1573            }
1574
1575            // Step 1: Choose a primary authentication provider and call it until it succeeds.
1576
1577            if ( $state['primary'] === null ) {
1578                // We haven't picked a PrimaryAuthenticationProvider yet
1579                foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1580                    if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1581                        continue;
1582                    }
1583                    $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1584                    switch ( $res->status ) {
1585                        case AuthenticationResponse::PASS:
1586                            $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1587                                'user' => $user->getName(),
1588                                'creator' => $creator->getName(),
1589                            ] );
1590                            $state['primary'] = $id;
1591                            $state['primaryResponse'] = $res;
1592                            break 2;
1593                        case AuthenticationResponse::FAIL:
1594                            $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1595                                'user' => $user->getName(),
1596                                'creator' => $creator->getName(),
1597                            ] );
1598                            $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1599                                [ $user, $creator, $res ]
1600                            );
1601                            $session->remove( self::ACCOUNT_CREATION_STATE );
1602                            return $res;
1603                        case AuthenticationResponse::ABSTAIN:
1604                            // Continue loop
1605                            break;
1606                        case AuthenticationResponse::REDIRECT:
1607                        case AuthenticationResponse::UI:
1608                            $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1609                                'user' => $user->getName(),
1610                                'creator' => $creator->getName(),
1611                            ] );
1612                            $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1613                            $state['primary'] = $id;
1614                            $state['continueRequests'] = $res->neededRequests;
1615                            $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1616                            return $res;
1617
1618                            // @codeCoverageIgnoreStart
1619                        default:
1620                            throw new \DomainException(
1621                                get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1622                            );
1623                            // @codeCoverageIgnoreEnd
1624                    }
1625                }
1626                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Always set in loop before, if passed
1627                if ( $state['primary'] === null ) {
1628                    $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1629                        'user' => $user->getName(),
1630                        'creator' => $creator->getName(),
1631                    ] );
1632                    $ret = AuthenticationResponse::newFail(
1633                        wfMessage( 'authmanager-create-no-primary' )
1634                    );
1635                    $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1636                    $session->remove( self::ACCOUNT_CREATION_STATE );
1637                    return $ret;
1638                }
1639            } elseif ( $state['primaryResponse'] === null ) {
1640                $provider = $this->getAuthenticationProvider( $state['primary'] );
1641                if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1642                    // Configuration changed? Force them to start over.
1643                    // @codeCoverageIgnoreStart
1644                    $ret = AuthenticationResponse::newFail(
1645                        wfMessage( 'authmanager-create-not-in-progress' )
1646                    );
1647                    $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1648                    $session->remove( self::ACCOUNT_CREATION_STATE );
1649                    return $ret;
1650                    // @codeCoverageIgnoreEnd
1651                }
1652                $id = $provider->getUniqueId();
1653                $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1654                switch ( $res->status ) {
1655                    case AuthenticationResponse::PASS:
1656                        $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1657                            'user' => $user->getName(),
1658                            'creator' => $creator->getName(),
1659                        ] );
1660                        $state['primaryResponse'] = $res;
1661                        break;
1662                    case AuthenticationResponse::FAIL:
1663                        $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1664                            'user' => $user->getName(),
1665                            'creator' => $creator->getName(),
1666                        ] );
1667                        $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1668                            [ $user, $creator, $res ]
1669                        );
1670                        $session->remove( self::ACCOUNT_CREATION_STATE );
1671                        return $res;
1672                    case AuthenticationResponse::REDIRECT:
1673                    case AuthenticationResponse::UI:
1674                        $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1675                            'user' => $user->getName(),
1676                            'creator' => $creator->getName(),
1677                        ] );
1678                        $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1679                        $state['continueRequests'] = $res->neededRequests;
1680                        $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1681                        return $res;
1682                    default:
1683                        throw new \DomainException(
1684                            get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1685                        );
1686                }
1687            }
1688
1689            // Step 2: Primary authentication succeeded. Give hook handlers a chance to interrupt,
1690            // then create the User object and add the user locally.
1691
1692            if ( $state['userid'] === 0 ) {
1693                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Always set if we passed step 1
1694                $response = $state['primaryResponse'];
1695                if ( !$this->runVerifyHook( self::ACTION_CREATE, $user, $response, $state['primary'] ) ) {
1696                    $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1697                        [ $user, $creator, $response ]
1698                    );
1699                    $session->remove( self::ACCOUNT_CREATION_STATE );
1700                    return $response;
1701                }
1702                $this->logger->info( 'Creating user {user} during account creation', [
1703                    'user' => $user->getName(),
1704                    'creator' => $creator->getName(),
1705                ] );
1706                $status = $user->addToDatabase();
1707                if ( !$status->isOK() ) {
1708                    // @codeCoverageIgnoreStart
1709                    $ret = AuthenticationResponse::newFail( $status->getMessage() );
1710                    $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1711                    $session->remove( self::ACCOUNT_CREATION_STATE );
1712                    return $ret;
1713                    // @codeCoverageIgnoreEnd
1714                }
1715                $this->setDefaultUserOptions( $user, $creator->isAnon() );
1716                $this->getHookRunner()->onLocalUserCreated( $user, false );
1717                $user->saveSettings();
1718                $state['userid'] = $user->getId();
1719
1720                // Update user count
1721                DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1722
1723                // Watch user's userpage and talk page
1724                $this->watchlistManager->addWatchIgnoringRights( $user, $user->getUserPage() );
1725
1726                // Inform the provider
1727                // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
1728                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Always set in loop before, if passed
1729                $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1730
1731                // Log the creation
1732                if ( $this->config->get( MainConfigNames::NewUserLog ) ) {
1733                    $isNamed = $creator->isNamed();
1734                    $logEntry = new \ManualLogEntry(
1735                        'newusers',
1736                        $logSubtype ?: ( $isNamed ? 'create2' : 'create' )
1737                    );
1738                    $logEntry->setPerformer( $isNamed ? $creator : $user );
1739                    $logEntry->setTarget( $user->getUserPage() );
1740                    /** @var CreationReasonAuthenticationRequest $req */
1741                    $req = AuthenticationRequest::getRequestByClass(
1742                        $state['reqs'], CreationReasonAuthenticationRequest::class
1743                    );
1744                    $logEntry->setComment( $req ? $req->reason : '' );
1745                    $logEntry->setParameters( [
1746                        '4::userid' => $user->getId(),
1747                    ] );
1748                    $logid = $logEntry->insert();
1749                    $logEntry->publish( $logid );
1750                }
1751            }
1752
1753            // Step 3: Iterate over all the secondary authentication providers.
1754
1755            $beginReqs = $state['reqs'];
1756
1757            foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1758                if ( !isset( $state['secondary'][$id] ) ) {
1759                    // This provider isn't started yet, so we pass it the set
1760                    // of reqs from beginAuthentication instead of whatever
1761                    // might have been used by a previous provider in line.
1762                    $func = 'beginSecondaryAccountCreation';
1763                    $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1764                } elseif ( !$state['secondary'][$id] ) {
1765                    $func = 'continueSecondaryAccountCreation';
1766                    $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1767                } else {
1768                    continue;
1769                }
1770                switch ( $res->status ) {
1771                    case AuthenticationResponse::PASS:
1772                        $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1773                            'user' => $user->getName(),
1774                            'creator' => $creator->getName(),
1775                        ] );
1776                        // fall through
1777                    case AuthenticationResponse::ABSTAIN:
1778                        $state['secondary'][$id] = true;
1779                        break;
1780                    case AuthenticationResponse::REDIRECT:
1781                    case AuthenticationResponse::UI:
1782                        $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1783                            'user' => $user->getName(),
1784                            'creator' => $creator->getName(),
1785                        ] );
1786                        $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1787                        $state['secondary'][$id] = false;
1788                        $state['continueRequests'] = $res->neededRequests;
1789                        $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1790                        return $res;
1791                    case AuthenticationResponse::FAIL:
1792                        throw new \DomainException(
1793                            get_class( $provider ) . "::{$func}() returned $res->status." .
1794                            ' Secondary providers are not allowed to fail account creation, that' .
1795                            ' should have been done via testForAccountCreation().'
1796                        );
1797                            // @codeCoverageIgnoreStart
1798                    default:
1799                        throw new \DomainException(
1800                            get_class( $provider ) . "::{$func}() returned $res->status"
1801                        );
1802                            // @codeCoverageIgnoreEnd
1803                }
1804            }
1805
1806            $id = $user->getId();
1807            $name = $user->getName();
1808            $req = new CreatedAccountAuthenticationRequest( $id, $name );
1809            $ret = AuthenticationResponse::newPass( $name );
1810            $ret->loginRequest = $req;
1811            $this->createdAccountAuthenticationRequests[] = $req;
1812
1813            $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1814                'user' => $user->getName(),
1815                'creator' => $creator->getName(),
1816            ] );
1817
1818            $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1819            $session->remove( self::ACCOUNT_CREATION_STATE );
1820            $this->removeAuthenticationSessionData( null );
1821            return $ret;
1822        } catch ( \Exception $ex ) {
1823            $session->remove( self::ACCOUNT_CREATION_STATE );
1824            throw $ex;
1825        }
1826    }
1827
1828    /**
1829     * @param Status $status
1830     * @param User $targetUser
1831     * @param string $source What caused the auto-creation @see ::autoCreateUser
1832     * @param bool $login Whether to also log the user in
1833     * @return void
1834     * @todo Inject both identityUtils and logger
1835     */
1836    private function logAutocreationAttempt( Status $status, User $targetUser, $source, $login ) {
1837        if ( $status->isOK() && !$status->isGood() ) {
1838            return; // user already existed, no need to log
1839        }
1840
1841        $firstMessage = $status->getMessages( 'error' )[0] ?? $status->getMessages( 'warning' )[0] ?? null;
1842        $identityUtils = MediaWikiServices::getInstance()->getUserIdentityUtils();
1843
1844        \MediaWiki\Logger\LoggerFactory::getInstance( 'authevents' )->info( 'Autocreation attempt', [
1845            'event' => 'autocreate',
1846            'successful' => $status->isGood(),
1847            'status' => $firstMessage ? $firstMessage->getKey() : '-',
1848            'accountType' => $identityUtils->getShortUserTypeInternal( $targetUser ),
1849            'source' => $source,
1850            'login' => $login,
1851        ] );
1852    }
1853
1854    /**
1855     * Auto-create an account, and optionally log into that account
1856     *
1857     * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
1858     * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
1859     * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
1860     * the username of a non-existing user from provideSessionInfo(). Calling this method
1861     * explicitly (e.g. from a maintenance script) is also fine.
1862     *
1863     * @param User $user User to auto-create
1864     * @param string $source What caused the auto-creation? This must be one of:
1865     *  - the ID of a PrimaryAuthenticationProvider,
1866     *  - one of the self::AUTOCREATE_SOURCE_* constants
1867     * @param bool $login Whether to also log the user in
1868     * @param bool $log Whether to generate a user creation log entry (since 1.36)
1869     * @param Authority|null $performer The performer of the action to use for user rights
1870     *   checking. Normally null to indicate an anonymous performer. Added in 1.42 for
1871     *   Special:CreateLocalAccount (T234371).
1872     *
1873     * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1874     */
1875    public function autoCreateUser(
1876        User $user,
1877        $source,
1878        $login = true,
1879        $log = true,
1880        ?Authority $performer = null
1881    ) {
1882        $validSources = [
1883            self::AUTOCREATE_SOURCE_SESSION,
1884            self::AUTOCREATE_SOURCE_MAINT,
1885            self::AUTOCREATE_SOURCE_TEMP
1886        ];
1887        if ( !in_array( $source, $validSources, true )
1888            && !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1889        ) {
1890            throw new InvalidArgumentException( "Unknown auto-creation source: $source" );
1891        }
1892
1893        $username = $user->getName();
1894
1895        // Try the local user from the replica DB, then fall back to the primary.
1896        $localUserIdentity = $this->userIdentityLookup->getUserIdentityByName( $username );
1897        // @codeCoverageIgnoreStart
1898        if ( ( !$localUserIdentity || !$localUserIdentity->isRegistered() )
1899            && $this->loadBalancer->getReaderIndex() !== 0
1900        ) {
1901            $localUserIdentity = $this->userIdentityLookup->getUserIdentityByName(
1902                $username, IDBAccessObject::READ_LATEST
1903            );
1904        }
1905        // @codeCoverageIgnoreEnd
1906        $localId = ( $localUserIdentity && $localUserIdentity->isRegistered() )
1907            ? $localUserIdentity->getId()
1908            : null;
1909
1910        if ( $localId ) {
1911            $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1912                'username' => $username,
1913            ] );
1914            $user->setId( $localId );
1915
1916            // Can't rely on a replica read, not even when getUserIdentityByName() used
1917            // READ_NORMAL, because that method has an in-process cache not shared
1918            // with loadFromId.
1919            $user->loadFromId( IDBAccessObject::READ_LATEST );
1920            if ( $login ) {
1921                $remember = $source === self::AUTOCREATE_SOURCE_TEMP;
1922                $this->setSessionDataForUser( $user, $remember );
1923            }
1924            return Status::newGood()->warning( 'userexists' );
1925        }
1926
1927        // Wiki is read-only?
1928        if ( $this->readOnlyMode->isReadOnly() ) {
1929            $reason = $this->readOnlyMode->getReason();
1930            $this->logger->debug( __METHOD__ . ': denied because of read only mode: {reason}', [
1931                'username' => $username,
1932                'reason' => $reason,
1933            ] );
1934            $user->setId( 0 );
1935            $user->loadFromId();
1936            $fatalStatus = Status::newFatal( wfMessage( 'readonlytext', $reason ) );
1937            $this->logAutocreationAttempt( $fatalStatus, $user, $source, $login );
1938            return $fatalStatus;
1939        }
1940
1941        // If there is a non-anonymous performer, don't use their session
1942        $session = null;
1943        if ( !$performer || $performer->getUser()->equals( $user ) ) {
1944            $session = $this->request->getSession();
1945        }
1946
1947        // Check the session, if we tried to create this user already there's
1948        // no point in retrying.
1949        if ( $session && $session->get( self::AUTOCREATE_BLOCKLIST ) ) {
1950            $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1951                'username' => $username,
1952                'sessionid' => $session->getId(),
1953            ] );
1954            $user->setId( 0 );
1955            $user->loadFromId();
1956            $reason = $session->get( self::AUTOCREATE_BLOCKLIST );
1957
1958            $status = $reason instanceof StatusValue ? Status::wrap( $reason ) : Status::newFatal( $reason );
1959            $this->logAutocreationAttempt( $status, $user, $source, $login );
1960            return $status;
1961        }
1962
1963        // Is the username usable? (Previously isCreatable() was checked here but
1964        // that doesn't work with auto-creation of TempUser accounts by CentralAuth)
1965        if ( !$this->userNameUtils->isUsable( $username ) ) {
1966            $this->logger->debug( __METHOD__ . ': name "{username}" is not usable', [
1967                'username' => $username,
1968            ] );
1969            if ( $session ) {
1970                $session->set( self::AUTOCREATE_BLOCKLIST, 'noname' );
1971            }
1972            $user->setId( 0 );
1973            $user->loadFromId();
1974            $fatalStatus = Status::newFatal( 'noname' );
1975            $this->logAutocreationAttempt( $fatalStatus, $user, $source, $login );
1976            return $fatalStatus;
1977        }
1978
1979        // Is the IP user able to create accounts?
1980        $performer ??= $this->userFactory->newAnonymous();
1981        $bypassAuthorization = $session ? $session->getProvider()->canAlwaysAutocreate() : false;
1982        if ( $source !== self::AUTOCREATE_SOURCE_MAINT && !$bypassAuthorization ) {
1983            $status = $this->authorizeAutoCreateAccount( $performer );
1984            if ( !$status->isOK() ) {
1985                $this->logger->debug( __METHOD__ . ': cannot create or autocreate accounts', [
1986                    'username' => $username,
1987                    'creator' => $performer->getUser()->getName(),
1988                ] );
1989                if ( $session ) {
1990                    $session->set( self::AUTOCREATE_BLOCKLIST, $status );
1991                    $session->persist();
1992                }
1993                $user->setId( 0 );
1994                $user->loadFromId();
1995                $statusWrapped = Status::wrap( $status );
1996                $this->logAutocreationAttempt( $statusWrapped, $user, $source, $login );
1997                return $statusWrapped;
1998            }
1999        }
2000
2001        // Avoid account creation races on double submissions
2002        $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getLocalClusterInstance();
2003        $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2004        if ( !$lock ) {
2005            $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
2006                'user' => $username,
2007            ] );
2008            $user->setId( 0 );
2009            $user->loadFromId();
2010            $status = Status::newFatal( 'usernameinprogress' );
2011            $this->logAutocreationAttempt( $status, $user, $source, $login );
2012            return $status;
2013        }
2014
2015        // Denied by providers?
2016        $options = [
2017            'flags' => IDBAccessObject::READ_LATEST,
2018            'creating' => true,
2019            'canAlwaysAutocreate' => $session && $session->getProvider()->canAlwaysAutocreate(),
2020        ];
2021        $providers = $this->getPreAuthenticationProviders() +
2022            $this->getPrimaryAuthenticationProviders() +
2023            $this->getSecondaryAuthenticationProviders();
2024        foreach ( $providers as $provider ) {
2025            $status = $provider->testUserForCreation( $user, $source, $options );
2026            if ( !$status->isGood() ) {
2027                $ret = Status::wrap( $status );
2028                $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
2029                    'username' => $username,
2030                    'reason' => $ret->getWikiText( false, false, 'en' ),
2031                ] );
2032                if ( $session ) {
2033                    $session->set( self::AUTOCREATE_BLOCKLIST, $status );
2034                }
2035                $user->setId( 0 );
2036                $user->loadFromId();
2037                $this->logAutocreationAttempt( $ret, $user, $source, $login );
2038                return $ret;
2039            }
2040        }
2041
2042        $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2043        if ( $cache->get( $backoffKey ) ) {
2044            $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
2045                'username' => $username,
2046            ] );
2047            $user->setId( 0 );
2048            $user->loadFromId();
2049            $status = Status::newFatal( 'authmanager-autocreate-exception' );
2050            $this->logAutocreationAttempt( $status, $user, $source, $login );
2051            return $status;
2052
2053        }
2054
2055        // Checks passed, create the user...
2056        $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
2057        $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
2058            'username' => $username,
2059            'from' => $from,
2060        ] );
2061
2062        // Ignore warnings about primary connections/writes...hard to avoid here
2063        $trxProfiler = \Profiler::instance()->getTransactionProfiler();
2064        $scope = $trxProfiler->silenceForScope( $trxProfiler::EXPECTATION_REPLICAS_ONLY );
2065        try {
2066            $status = $user->addToDatabase();
2067            if ( !$status->isOK() ) {
2068                // Double-check for a race condition (T70012). We make use of the fact that when
2069                // addToDatabase fails due to the user already existing, the user object gets loaded.
2070                if ( $user->getId() ) {
2071                    $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
2072                        'username' => $username,
2073                    ] );
2074                    if ( $login ) {
2075                        $remember = $source === self::AUTOCREATE_SOURCE_TEMP;
2076                        $this->setSessionDataForUser( $user, $remember );
2077                    }
2078                    $status = Status::newGood()->warning( 'userexists' );
2079                } else {
2080                    $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
2081                        'username' => $username,
2082                        'msg' => $status->getWikiText( false, false, 'en' )
2083                    ] );
2084                    $user->setId( 0 );
2085                    $user->loadFromId();
2086                }
2087                $this->logAutocreationAttempt( $status, $user, $source, $login );
2088                return $status;
2089            }
2090        } catch ( \Exception $ex ) {
2091            $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
2092                'username' => $username,
2093                'exception' => $ex,
2094            ] );
2095            // Do not keep throwing errors for a while
2096            $cache->set( $backoffKey, 1, 600 );
2097            // Bubble up error; which should normally trigger DB rollbacks
2098            throw $ex;
2099        }
2100
2101        $this->setDefaultUserOptions( $user, false );
2102
2103        // Inform the providers
2104        $this->callMethodOnProviders( self::CALL_PRIMARY | self::CALL_SECONDARY, 'autoCreatedAccount',
2105            [ $user, $source ]
2106        );
2107
2108        $this->getHookRunner()->onLocalUserCreated( $user, true );
2109        $user->saveSettings();
2110
2111        // Update user count
2112        DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
2113        // Watch user's userpage and talk page (except temp users)
2114        if ( $source !== self::AUTOCREATE_SOURCE_TEMP ) {
2115            DeferredUpdates::addCallableUpdate( function () use ( $user ) {
2116                $this->watchlistManager->addWatchIgnoringRights( $user, $user->getUserPage() );
2117            } );
2118        }
2119
2120        // Log the creation
2121        if ( $this->config->get( MainConfigNames::NewUserLog ) && $log ) {
2122            $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
2123            $logEntry->setPerformer( $user );
2124            $logEntry->setTarget( $user->getUserPage() );
2125            $logEntry->setComment( '' );
2126            $logEntry->setParameters( [
2127                '4::userid' => $user->getId(),
2128            ] );
2129            $logEntry->insert();
2130        }
2131
2132        ScopedCallback::consume( $scope );
2133
2134        if ( $login ) {
2135            $remember = $source === self::AUTOCREATE_SOURCE_TEMP;
2136            $this->setSessionDataForUser( $user, $remember );
2137        }
2138        $retStatus = Status::newGood();
2139        $this->logAutocreationAttempt( $retStatus, $user, $source, $login );
2140        return $retStatus;
2141    }
2142
2143    /**
2144     * Authorize automatic account creation. This is like account creation but
2145     * checks the autocreateaccount right instead of the createaccount right.
2146     *
2147     * @param Authority $creator
2148     * @return StatusValue
2149     */
2150    private function authorizeAutoCreateAccount( Authority $creator ) {
2151        return $this->authorizeInternal(
2152            static function (
2153                string $action,
2154                PageIdentity $target,
2155                PermissionStatus $status
2156            ) use ( $creator ) {
2157                return $creator->authorizeWrite( $action, $target, $status );
2158            },
2159            'autocreateaccount'
2160        );
2161    }
2162
2163    // endregion -- end of Account creation
2164
2165    /***************************************************************************/
2166    // region   Account linking
2167    /** @name   Account linking */
2168
2169    /**
2170     * Determine whether accounts can be linked
2171     * @return bool
2172     */
2173    public function canLinkAccounts() {
2174        foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2175            if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2176                return true;
2177            }
2178        }
2179        return false;
2180    }
2181
2182    /**
2183     * Start an account linking flow
2184     *
2185     * @param User $user User being linked
2186     * @param AuthenticationRequest[] $reqs
2187     * @param string $returnToUrl Url that REDIRECT responses should eventually
2188     *  return to.
2189     * @return AuthenticationResponse
2190     */
2191    public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
2192        $session = $this->request->getSession();
2193        $session->remove( self::ACCOUNT_LINK_STATE );
2194
2195        if ( !$this->canLinkAccounts() ) {
2196            // Caller should have called canLinkAccounts()
2197            throw new LogicException( 'Account linking is not possible' );
2198        }
2199
2200        if ( !$user->isRegistered() ) {
2201            if ( !$this->userNameUtils->isUsable( $user->getName() ) ) {
2202                $msg = wfMessage( 'noname' );
2203            } else {
2204                $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
2205            }
2206            return AuthenticationResponse::newFail( $msg );
2207        }
2208        foreach ( $reqs as $req ) {
2209            $req->username = $user->getName();
2210            $req->returnToUrl = $returnToUrl;
2211        }
2212
2213        $this->removeAuthenticationSessionData( null );
2214
2215        $providers = $this->getPreAuthenticationProviders();
2216        foreach ( $providers as $id => $provider ) {
2217            $status = $provider->testForAccountLink( $user );
2218            if ( !$status->isGood() ) {
2219                $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
2220                    'user' => $user->getName(),
2221                ] );
2222                $ret = AuthenticationResponse::newFail(
2223                    Status::wrap( $status )->getMessage()
2224                );
2225                $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2226                return $ret;
2227            }
2228        }
2229
2230        $state = [
2231            'username' => $user->getName(),
2232            'userid' => $user->getId(),
2233            'returnToUrl' => $returnToUrl,
2234            'providerIds' => $this->getProviderIds(),
2235            'primary' => null,
2236            'continueRequests' => [],
2237        ];
2238
2239        $providers = $this->getPrimaryAuthenticationProviders();
2240        foreach ( $providers as $id => $provider ) {
2241            if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
2242                continue;
2243            }
2244
2245            $res = $provider->beginPrimaryAccountLink( $user, $reqs );
2246            switch ( $res->status ) {
2247                case AuthenticationResponse::PASS:
2248                    $this->logger->info( "Account linked to {user} by $id", [
2249                        'user' => $user->getName(),
2250                    ] );
2251                    $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2252                        [ $user, $res ]
2253                    );
2254                    return $res;
2255
2256                case AuthenticationResponse::FAIL:
2257                    $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
2258                        'user' => $user->getName(),
2259                    ] );
2260                    $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2261                        [ $user, $res ]
2262                    );
2263                    return $res;
2264
2265                case AuthenticationResponse::ABSTAIN:
2266                    // Continue loop
2267                    break;
2268
2269                case AuthenticationResponse::REDIRECT:
2270                case AuthenticationResponse::UI:
2271                    $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
2272                        'user' => $user->getName(),
2273                    ] );
2274                    $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
2275                    $state['primary'] = $id;
2276                    $state['continueRequests'] = $res->neededRequests;
2277                    $session->setSecret( self::ACCOUNT_LINK_STATE, $state );
2278                    $session->persist();
2279                    return $res;
2280
2281                    // @codeCoverageIgnoreStart
2282                default:
2283                    throw new \DomainException(
2284                        get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
2285                    );
2286                    // @codeCoverageIgnoreEnd
2287            }
2288        }
2289
2290        $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
2291            'user' => $user->getName(),
2292        ] );
2293        $ret = AuthenticationResponse::newFail(
2294            wfMessage( 'authmanager-link-no-primary' )
2295        );
2296        $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2297        return $ret;
2298    }
2299
2300    /**
2301     * Continue an account linking flow
2302     * @param AuthenticationRequest[] $reqs
2303     * @return AuthenticationResponse
2304     */
2305    public function continueAccountLink( array $reqs ) {
2306        $session = $this->request->getSession();
2307        try {
2308            if ( !$this->canLinkAccounts() ) {
2309                // Caller should have called canLinkAccounts()
2310                $session->remove( self::ACCOUNT_LINK_STATE );
2311                throw new LogicException( 'Account linking is not possible' );
2312            }
2313
2314            $state = $session->getSecret( self::ACCOUNT_LINK_STATE );
2315            if ( !is_array( $state ) ) {
2316                return AuthenticationResponse::newFail(
2317                    wfMessage( 'authmanager-link-not-in-progress' )
2318                );
2319            }
2320            $state['continueRequests'] = [];
2321
2322            // Step 0: Prepare and validate the input
2323
2324            $user = $this->userFactory->newFromName(
2325                (string)$state['username'],
2326                UserRigorOptions::RIGOR_USABLE
2327            );
2328            if ( !is_object( $user ) ) {
2329                $session->remove( self::ACCOUNT_LINK_STATE );
2330                return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
2331            }
2332            if ( $user->getId() !== $state['userid'] ) {
2333                throw new \UnexpectedValueException(
2334                    "User \"{$state['username']}\" is valid, but " .
2335                        "ID {$user->getId()} !== {$state['userid']}!"
2336                );
2337            }
2338
2339            if ( $state['providerIds'] !== $this->getProviderIds() ) {
2340                // An inconsistent AuthManagerFilterProviders hook, or site configuration changed
2341                // while the user was in the middle of authentication. The first is a bug, the
2342                // second is rare but expected when deploying a config change. Try handle in a way
2343                // that's useful for both cases.
2344                // @codeCoverageIgnoreStart
2345                MWExceptionHandler::logException( new NormalizedException(
2346                    'Authentication failed because of inconsistent provider array',
2347                    [ 'old' => json_encode( $state['providerIds'] ), 'new' => json_encode( $this->getProviderIds() ) ]
2348                ) );
2349                $ret = AuthenticationResponse::newFail(
2350                    wfMessage( 'authmanager-link-not-in-progress' )
2351                );
2352                $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $ret ] );
2353                $session->remove( self::ACCOUNT_LINK_STATE );
2354                return $ret;
2355                // @codeCoverageIgnoreEnd
2356            }
2357
2358            foreach ( $reqs as $req ) {
2359                $req->username = $state['username'];
2360                $req->returnToUrl = $state['returnToUrl'];
2361            }
2362
2363            // Step 1: Call the primary again until it succeeds
2364
2365            $provider = $this->getAuthenticationProvider( $state['primary'] );
2366            if ( !$provider instanceof PrimaryAuthenticationProvider ) {
2367                // Configuration changed? Force them to start over.
2368                // @codeCoverageIgnoreStart
2369                $ret = AuthenticationResponse::newFail(
2370                    wfMessage( 'authmanager-link-not-in-progress' )
2371                );
2372                $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2373                $session->remove( self::ACCOUNT_LINK_STATE );
2374                return $ret;
2375                // @codeCoverageIgnoreEnd
2376            }
2377            $id = $provider->getUniqueId();
2378            $res = $provider->continuePrimaryAccountLink( $user, $reqs );
2379            switch ( $res->status ) {
2380                case AuthenticationResponse::PASS:
2381                    $this->logger->info( "Account linked to {user} by $id", [
2382                        'user' => $user->getName(),
2383                    ] );
2384                    $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2385                        [ $user, $res ]
2386                    );
2387                    $session->remove( self::ACCOUNT_LINK_STATE );
2388                    return $res;
2389                case AuthenticationResponse::FAIL:
2390                    $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
2391                        'user' => $user->getName(),
2392                    ] );
2393                    $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2394                        [ $user, $res ]
2395                    );
2396                    $session->remove( self::ACCOUNT_LINK_STATE );
2397                    return $res;
2398                case AuthenticationResponse::REDIRECT:
2399                case AuthenticationResponse::UI:
2400                    $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
2401                        'user' => $user->getName(),
2402                    ] );
2403                    $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
2404                    $state['continueRequests'] = $res->neededRequests;
2405                    $session->setSecret( self::ACCOUNT_LINK_STATE, $state );
2406                    return $res;
2407                default:
2408                    throw new \DomainException(
2409                        get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
2410                    );
2411            }
2412        } catch ( \Exception $ex ) {
2413            $session->remove( self::ACCOUNT_LINK_STATE );
2414            throw $ex;
2415        }
2416    }
2417
2418    // endregion -- end of Account linking
2419
2420    /***************************************************************************/
2421    // region   Information methods
2422    /** @name   Information methods */
2423
2424    /**
2425     * Return the applicable list of AuthenticationRequests
2426     *
2427     * Possible values for $action:
2428     *  - ACTION_LOGIN: Valid for passing to beginAuthentication
2429     *  - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
2430     *  - ACTION_CREATE: Valid for passing to beginAccountCreation
2431     *  - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
2432     *  - ACTION_LINK: Valid for passing to beginAccountLink
2433     *  - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
2434     *  - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
2435     *  - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
2436     *  - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
2437     *
2438     * @param string $action One of the AuthManager::ACTION_* constants
2439     * @param UserIdentity|null $user User being acted on, instead of the current user.
2440     * @return AuthenticationRequest[]
2441     */
2442    public function getAuthenticationRequests( $action, ?UserIdentity $user = null ) {
2443        $options = [];
2444        $providerAction = $action;
2445
2446        // Figure out which providers to query
2447        switch ( $action ) {
2448            case self::ACTION_LOGIN:
2449            case self::ACTION_CREATE:
2450                $providers = $this->getPreAuthenticationProviders() +
2451                    $this->getPrimaryAuthenticationProviders() +
2452                    $this->getSecondaryAuthenticationProviders();
2453                break;
2454
2455            case self::ACTION_LOGIN_CONTINUE:
2456                $state = $this->request->getSession()->getSecret( self::AUTHN_STATE );
2457                return is_array( $state ) ? $state['continueRequests'] : [];
2458
2459            case self::ACTION_CREATE_CONTINUE:
2460                $state = $this->request->getSession()->getSecret( self::ACCOUNT_CREATION_STATE );
2461                return is_array( $state ) ? $state['continueRequests'] : [];
2462
2463            case self::ACTION_LINK:
2464                $providers = [];
2465                foreach ( $this->getPrimaryAuthenticationProviders() as $p ) {
2466                    if ( $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2467                        $providers[] = $p;
2468                    }
2469                }
2470                break;
2471
2472            case self::ACTION_UNLINK:
2473                $providers = [];
2474                foreach ( $this->getPrimaryAuthenticationProviders() as $p ) {
2475                    if ( $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2476                        $providers[] = $p;
2477                    }
2478                }
2479
2480                // To providers, unlink and remove are identical.
2481                $providerAction = self::ACTION_REMOVE;
2482                break;
2483
2484            case self::ACTION_LINK_CONTINUE:
2485                $state = $this->request->getSession()->getSecret( self::ACCOUNT_LINK_STATE );
2486                return is_array( $state ) ? $state['continueRequests'] : [];
2487
2488            case self::ACTION_CHANGE:
2489            case self::ACTION_REMOVE:
2490                $providers = $this->getPrimaryAuthenticationProviders() +
2491                    $this->getSecondaryAuthenticationProviders();
2492                break;
2493
2494            // @codeCoverageIgnoreStart
2495            default:
2496                throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2497        }
2498        // @codeCoverageIgnoreEnd
2499
2500        return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2501    }
2502
2503    /**
2504     * Internal request lookup for self::getAuthenticationRequests
2505     *
2506     * @param string $providerAction Action to pass to providers
2507     * @param array $options Options to pass to providers
2508     * @param AuthenticationProvider[] $providers
2509     * @param UserIdentity|null $user being acted on
2510     * @return AuthenticationRequest[]
2511     */
2512    private function getAuthenticationRequestsInternal(
2513        $providerAction, array $options, array $providers, ?UserIdentity $user = null
2514    ) {
2515        $user = $user ?: RequestContext::getMain()->getUser();
2516        $options['username'] = $user->isRegistered() ? $user->getName() : null;
2517
2518        // Query them and merge results
2519        $reqs = [];
2520        foreach ( $providers as $provider ) {
2521            $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2522            foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2523                $id = $req->getUniqueId();
2524
2525                // If a required request if from a Primary, mark it as "primary-required" instead
2526                if ( $isPrimary && $req->required ) {
2527                    $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2528                }
2529
2530                if (
2531                    !isset( $reqs[$id] )
2532                    || $req->required === AuthenticationRequest::REQUIRED
2533                    || $reqs[$id] === AuthenticationRequest::OPTIONAL
2534                ) {
2535                    $reqs[$id] = $req;
2536                }
2537            }
2538        }
2539
2540        // AuthManager has its own req for some actions
2541        switch ( $providerAction ) {
2542            case self::ACTION_LOGIN:
2543                $reqs[] = new RememberMeAuthenticationRequest(
2544                    $this->config->get( MainConfigNames::RememberMe ) );
2545                $options['username'] = null; // Don't fill in the username below
2546                break;
2547
2548            case self::ACTION_CREATE:
2549                $reqs[] = new UsernameAuthenticationRequest;
2550                $reqs[] = new UserDataAuthenticationRequest;
2551
2552                // Registered users should be prompted to provide a rationale for account creations,
2553                // except for the case of a temporary user registering a full account (T328718).
2554                if (
2555                    $options['username'] !== null &&
2556                    !$this->userNameUtils->isTemp( $options['username'] )
2557                ) {
2558                    $reqs[] = new CreationReasonAuthenticationRequest;
2559                    $options['username'] = null; // Don't fill in the username below
2560                }
2561                break;
2562        }
2563
2564        // Fill in reqs data
2565        $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2566
2567        // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2568        if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2569            $reqs = array_filter( $reqs, function ( $req ) {
2570                return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2571            } );
2572        }
2573
2574        return array_values( $reqs );
2575    }
2576
2577    /**
2578     * Set values in an array of requests
2579     * @param AuthenticationRequest[] &$reqs
2580     * @param string $action
2581     * @param string|null $username
2582     * @param bool $forceAction
2583     */
2584    private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2585        foreach ( $reqs as $req ) {
2586            if ( !$req->action || $forceAction ) {
2587                $req->action = $action;
2588            }
2589            $req->username ??= $username;
2590        }
2591    }
2592
2593    /**
2594     * Determine whether a username exists
2595     * @param string $username
2596     * @param int $flags Bitfield of IDBAccessObject::READ_* constants
2597     * @return bool
2598     */
2599    public function userExists( $username, $flags = IDBAccessObject::READ_NORMAL ) {
2600        foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2601            if ( $provider->testUserExists( $username, $flags ) ) {
2602                return true;
2603            }
2604        }
2605
2606        return false;
2607    }
2608
2609    /**
2610     * Determine whether a user property should be allowed to be changed.
2611     *
2612     * Supported properties are:
2613     *  - emailaddress
2614     *  - realname
2615     *  - nickname
2616     *
2617     * @param string $property
2618     * @return bool
2619     */
2620    public function allowsPropertyChange( $property ) {
2621        $providers = $this->getPrimaryAuthenticationProviders() +
2622            $this->getSecondaryAuthenticationProviders();
2623        foreach ( $providers as $provider ) {
2624            if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2625                return false;
2626            }
2627        }
2628        return true;
2629    }
2630
2631    /**
2632     * Get a provider by ID
2633     * @note This is public so extensions can check whether their own provider
2634     *  is installed and so they can read its configuration if necessary.
2635     *  Other uses are not recommended.
2636     * @param string $id
2637     * @return AuthenticationProvider|null
2638     */
2639    public function getAuthenticationProvider( $id ) {
2640        // Fast version
2641        if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2642            return $this->allAuthenticationProviders[$id];
2643        }
2644
2645        // Slow version: instantiate each kind and check
2646        $providers = $this->getPrimaryAuthenticationProviders();
2647        if ( isset( $providers[$id] ) ) {
2648            return $providers[$id];
2649        }
2650        $providers = $this->getSecondaryAuthenticationProviders();
2651        if ( isset( $providers[$id] ) ) {
2652            return $providers[$id];
2653        }
2654        $providers = $this->getPreAuthenticationProviders();
2655        if ( isset( $providers[$id] ) ) {
2656            return $providers[$id];
2657        }
2658
2659        return null;
2660    }
2661
2662    // endregion -- end of Information methods
2663
2664    /***************************************************************************/
2665    // region   Internal methods
2666    /** @name   Internal methods */
2667
2668    /**
2669     * Store authentication in the current session
2670     * @note For use by AuthenticationProviders only
2671     * @param string $key
2672     * @param mixed $data Must be serializable
2673     */
2674    public function setAuthenticationSessionData( $key, $data ) {
2675        $session = $this->request->getSession();
2676        $arr = $session->getSecret( 'authData' );
2677        if ( !is_array( $arr ) ) {
2678            $arr = [];
2679        }
2680        $arr[$key] = $data;
2681        $session->setSecret( 'authData', $arr );
2682    }
2683
2684    /**
2685     * Fetch authentication data from the current session
2686     * @note For use by AuthenticationProviders only
2687     * @param string $key
2688     * @param mixed|null $default
2689     * @return mixed
2690     */
2691    public function getAuthenticationSessionData( $key, $default = null ) {
2692        $arr = $this->request->getSession()->getSecret( 'authData' );
2693        if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2694            return $arr[$key];
2695        } else {
2696            return $default;
2697        }
2698    }
2699
2700    /**
2701     * Remove authentication data
2702     * @note For use by AuthenticationProviders
2703     * @param string|null $key If null, all data is removed
2704     */
2705    public function removeAuthenticationSessionData( $key ) {
2706        $session = $this->request->getSession();
2707        if ( $key === null ) {
2708            $session->remove( 'authData' );
2709        } else {
2710            $arr = $session->getSecret( 'authData' );
2711            if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2712                unset( $arr[$key] );
2713                $session->setSecret( 'authData', $arr );
2714            }
2715        }
2716    }
2717
2718    /**
2719     * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2720     * @param string $class
2721     * @param array[] $specs
2722     * @return AuthenticationProvider[]
2723     */
2724    protected function providerArrayFromSpecs( $class, array $specs ) {
2725        $i = 0;
2726        foreach ( $specs as &$spec ) {
2727            $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2728        }
2729        unset( $spec );
2730        // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2731        usort( $specs, static function ( $a, $b ) {
2732            return $a['sort'] <=> $b['sort']
2733                ?: $a['sort2'] <=> $b['sort2'];
2734        } );
2735
2736        $ret = [];
2737        foreach ( $specs as $spec ) {
2738            /** @var AbstractAuthenticationProvider $provider */
2739            $provider = $this->objectFactory->createObject( $spec, [ 'assertClass' => $class ] );
2740            $provider->init( $this->logger, $this, $this->getHookContainer(), $this->config, $this->userNameUtils );
2741            $id = $provider->getUniqueId();
2742            if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2743                throw new \RuntimeException(
2744                    "Duplicate specifications for id $id (classes " .
2745                    get_class( $provider ) . ' and ' .
2746                    get_class( $this->allAuthenticationProviders[$id] ) . ')'
2747                );
2748            }
2749            $this->allAuthenticationProviders[$id] = $provider;
2750            $ret[$id] = $provider;
2751        }
2752        return $ret;
2753    }
2754
2755    /**
2756     * Get the list of PreAuthenticationProviders
2757     * @return PreAuthenticationProvider[]
2758     */
2759    protected function getPreAuthenticationProviders() {
2760        if ( $this->preAuthenticationProviders === null ) {
2761            $this->initializeAuthenticationProviders();
2762        }
2763        return $this->preAuthenticationProviders;
2764    }
2765
2766    /**
2767     * Get the list of PrimaryAuthenticationProviders
2768     * @return PrimaryAuthenticationProvider[]
2769     */
2770    protected function getPrimaryAuthenticationProviders() {
2771        if ( $this->primaryAuthenticationProviders === null ) {
2772            $this->initializeAuthenticationProviders();
2773        }
2774        return $this->primaryAuthenticationProviders;
2775    }
2776
2777    /**
2778     * Get the list of SecondaryAuthenticationProviders
2779     * @return SecondaryAuthenticationProvider[]
2780     */
2781    protected function getSecondaryAuthenticationProviders() {
2782        if ( $this->secondaryAuthenticationProviders === null ) {
2783            $this->initializeAuthenticationProviders();
2784        }
2785        return $this->secondaryAuthenticationProviders;
2786    }
2787
2788    private function getProviderIds(): array {
2789        return [
2790            'preauth' => array_keys( $this->getPreAuthenticationProviders() ),
2791            'primaryauth' => array_keys( $this->getPrimaryAuthenticationProviders() ),
2792            'secondaryauth' => array_keys( $this->getSecondaryAuthenticationProviders() ),
2793        ];
2794    }
2795
2796    private function initializeAuthenticationProviders() {
2797        $conf = $this->config->get( MainConfigNames::AuthManagerConfig )
2798            ?: $this->config->get( MainConfigNames::AuthManagerAutoConfig );
2799
2800        $providers = array_map( fn ( $stepConf ) => array_fill_keys( array_keys( $stepConf ), true ), $conf );
2801        $this->getHookRunner()->onAuthManagerFilterProviders( $providers );
2802        foreach ( $conf as $step => $stepConf ) {
2803            $conf[$step] = array_intersect_key( $stepConf, array_filter( $providers[$step] ) );
2804        }
2805
2806        $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2807            PreAuthenticationProvider::class, $conf['preauth']
2808        );
2809        $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2810            PrimaryAuthenticationProvider::class, $conf['primaryauth']
2811        );
2812        $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2813            SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2814        );
2815    }
2816
2817    /**
2818     * Log the user in
2819     * @param User $user
2820     * @param bool|null $remember
2821     */
2822    private function setSessionDataForUser( $user, $remember = null ) {
2823        $session = $this->request->getSession();
2824        $delay = $session->delaySave();
2825
2826        $session->resetId();
2827        $session->resetAllTokens();
2828        if ( $session->canSetUser() ) {
2829            $session->setUser( $user );
2830        }
2831        if ( $remember !== null ) {
2832            $session->setRememberUser( $remember );
2833        }
2834        $session->set( 'AuthManager:lastAuthId', $user->getId() );
2835        $session->set( 'AuthManager:lastAuthTimestamp', time() );
2836        $session->persist();
2837
2838        \Wikimedia\ScopedCallback::consume( $delay );
2839
2840        $this->getHookRunner()->onUserLoggedIn( $user );
2841    }
2842
2843    /**
2844     * @param User $user
2845     * @param bool $useContextLang Use 'uselang' to set the user's language
2846     */
2847    private function setDefaultUserOptions( User $user, $useContextLang ) {
2848        $user->setToken();
2849
2850        $lang = $useContextLang ? RequestContext::getMain()->getLanguage() : $this->contentLanguage;
2851        $this->userOptionsManager->setOption(
2852            $user,
2853            'language',
2854            $this->languageConverterFactory->getLanguageConverter( $lang )->getPreferredVariant()
2855        );
2856
2857        $contLangConverter = $this->languageConverterFactory->getLanguageConverter( $this->contentLanguage );
2858        if ( $contLangConverter->hasVariants() ) {
2859            $this->userOptionsManager->setOption(
2860                $user,
2861                'variant',
2862                $contLangConverter->getPreferredVariant()
2863            );
2864        }
2865    }
2866
2867    /**
2868     * @see AuthManagerVerifyAuthenticationHook::onAuthManagerVerifyAuthentication()
2869     */
2870    private function runVerifyHook(
2871        string $action,
2872        ?UserIdentity $user,
2873        AuthenticationResponse &$response,
2874        string $primaryId
2875    ): bool {
2876        $oldResponse = $response;
2877        $info = [
2878            'action' => $action,
2879            'primaryId' => $primaryId,
2880        ];
2881        $proceed = $this->getHookRunner()->onAuthManagerVerifyAuthentication( $user, $response, $this, $info );
2882        if ( !( $response instanceof AuthenticationResponse ) ) {
2883            throw new LogicException( '$response must be an AuthenticationResponse' );
2884        } elseif ( $proceed && $response !== $oldResponse ) {
2885            throw new LogicException(
2886                'AuthManagerVerifyAuthenticationHook must not modify the response unless it returns false' );
2887        } elseif ( !$proceed && $response->status !== AuthenticationResponse::FAIL ) {
2888            throw new LogicException(
2889                'AuthManagerVerifyAuthenticationHook must set the response to FAIL if it returns false' );
2890        }
2891        if ( !$proceed ) {
2892            $this->logger->info(
2893                $action . ' action for {user} from {clientip} prevented by '
2894                    . 'AuthManagerVerifyAuthentication hook: {reason}',
2895                [
2896                    'user' => $user ? $user->getName() : '<null>',
2897                    'clientip' => $this->request->getIP(),
2898                    'reason' => $response->message->getKey(),
2899                    'primaryId' => $primaryId,
2900                ]
2901            );
2902        }
2903        return $proceed;
2904    }
2905
2906    /**
2907     * @param int $which Bitmask of values of the self::CALL_* constants
2908     * @param string $method
2909     * @param array $args
2910     */
2911    private function callMethodOnProviders( $which, $method, array $args ) {
2912        $providers = [];
2913        if ( $which & self::CALL_PRE ) {
2914            $providers += $this->getPreAuthenticationProviders();
2915        }
2916        if ( $which & self::CALL_PRIMARY ) {
2917            $providers += $this->getPrimaryAuthenticationProviders();
2918        }
2919        if ( $which & self::CALL_SECONDARY ) {
2920            $providers += $this->getSecondaryAuthenticationProviders();
2921        }
2922        foreach ( $providers as $provider ) {
2923            $provider->$method( ...$args );
2924        }
2925    }
2926
2927    /**
2928     * @return HookContainer
2929     */
2930    private function getHookContainer() {
2931        return $this->hookContainer;
2932    }
2933
2934    /**
2935     * @return HookRunner
2936     */
2937    private function getHookRunner() {
2938        return $this->hookRunner;
2939    }
2940
2941    // endregion -- end of Internal methods
2942
2943}
2944
2945/*
2946 * This file uses VisualStudio style region/endregion fold markers which are
2947 * recognised by PHPStorm. If modelines are enabled, the following editor
2948 * configuration will also enable folding in vim, if it is in the last 5 lines
2949 * of the file. We also use "@name" which creates sections in Doxygen.
2950 *
2951 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
2952 */