Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 772
0.00% covered (danger)
0.00%
0 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
LoginSignupSpecialPage
0.00% covered (danger)
0.00%
0 / 771
0.00% covered (danger)
0.00%
0 / 23
48180
0.00% covered (danger)
0.00%
0 / 1
 isSignup
n/a
0 / 0
n/a
0 / 0
0
 successfulAction
n/a
0 / 0
n/a
0 / 0
0
 logAuthResult
n/a
0 / 0
n/a
0 / 0
0
 setRequest
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getLoginHelper
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 loadRequestParameters
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 load
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
182
 getPreservedParams
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
 beforeExecute
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 1
1892
 canBypassForm
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 showSuccessPage
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 mainLoginForm
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
342
 getPageHtml
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
182
 getBenefitsContainerHtml
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
42
 getAuthForm
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
72
 onAuthChangeFormFields
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 showExtraInformation
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 getFieldDefinitions
0.00% covered (danger)
0.00%
0 / 285
0.00% covered (danger)
0.00%
0 / 1
3080
 showCreateAccountLink
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getTokenName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 makeLanguageSelector
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 makeLanguageSelectorLink
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 postProcessFormDescriptor
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
272
 getNoticeHtml
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Holds shared logic for login and account creation pages.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup SpecialPage
8 */
9
10namespace MediaWiki\SpecialPage;
11
12use Exception;
13use LogicException;
14use MediaWiki\Auth\AuthenticationRequest;
15use MediaWiki\Auth\AuthenticationResponse;
16use MediaWiki\Auth\AuthManager;
17use MediaWiki\Auth\PasswordAuthenticationRequest;
18use MediaWiki\Auth\UsernameAuthenticationRequest;
19use MediaWiki\Context\DerivativeContext;
20use MediaWiki\Context\RequestContext;
21use MediaWiki\Exception\ErrorPageError;
22use MediaWiki\Exception\FatalError;
23use MediaWiki\Exception\PermissionsError;
24use MediaWiki\Exception\ReadOnlyError;
25use MediaWiki\Html\Html;
26use MediaWiki\HTMLForm\HTMLForm;
27use MediaWiki\Logger\LoggerFactory;
28use MediaWiki\MainConfigNames;
29use MediaWiki\MediaWikiServices;
30use MediaWiki\Message\Message;
31use MediaWiki\Parser\Sanitizer;
32use MediaWiki\Skin\Skin;
33use MediaWiki\Specials\Helpers\LoginHelper;
34use MediaWiki\Status\Status;
35use MediaWiki\Title\Title;
36use MediaWiki\User\User;
37use MediaWiki\User\UserIdentity;
38use StatusValue;
39use Wikimedia\ScopedCallback;
40
41/**
42 * Holds shared logic for login and account creation pages.
43 *
44 * @ingroup SpecialPage
45 * @ingroup Auth
46 */
47abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
48
49    /**
50     * The title of the page to return to after authentication finishes, or the empty string
51     * when there is no return target.
52     * Typically comes from the 'returnto' URL parameter. Validating and normalizing is the
53     * caller's responsibility.
54     * @var string
55     */
56    protected string $mReturnTo;
57    /**
58     * The query string part of the URL to return to after authentication finishes.
59     * Typically comes from the 'returntoquery' URL parameter.
60     * @var string
61     */
62    protected string $mReturnToQuery;
63    /**
64     * The fragment part of the URL to return to after authentication finishes.
65     * When not empty, should include the '#' character.
66     * Typically comes from the 'returntoanchor' URL parameter.
67     * @var string
68     */
69    protected string $mReturnToAnchor;
70    /**
71     * Value of the 'alwaysShowLogin' URL parameter. When true, the login form will be shown
72     * even if the user is already logged in. When false, the user might be redirected if we
73     * know (from the 'returnto' parameter) where to redirect them to (although there are
74     * other conditions which can also prevent that).
75     */
76    protected bool $mAlwaysShowLogin;
77
78    /** @var bool */
79    protected $mPosted;
80    /** @var string|null */
81    protected $mAction;
82    /** @var string */
83    protected $mToken;
84    /** @var bool */
85    protected $mStickHTTPS;
86    /** @var bool */
87    protected $mFromHTTP;
88    /** @var string */
89    protected $mEntryError = '';
90    /** @var string */
91    protected $mEntryErrorType = 'error';
92    /** @var LoginHelper */
93    protected $loginHelper = null;
94
95    /** @var bool */
96    protected $mLoaded = false;
97    /** @var bool */
98    protected $mLoadedRequest = false;
99    /** @var string|null */
100    protected $mSecureLoginUrl;
101    /** @var Message|true|null */
102    private $reasonValidatorResult = null;
103
104    /** @var string */
105    protected $securityLevel;
106
107    /** @var bool True if the user if creating an account for someone else. Flag used for internal
108     * communication, only set at the very end.
109     */
110    protected $proxyAccountCreation;
111    /** @var User FIXME another flag for passing data. */
112    protected $targetUser;
113
114    /** @var HTMLForm|null */
115    protected $authForm;
116
117    /**
118     * @return bool
119     */
120    abstract protected function isSignup();
121
122    /**
123     * @param bool $direct True if the action was successful just now; false if that happened
124     *    pre-redirection (so this handler was called already)
125     * @param StatusValue|null $extraMessages
126     * @return void
127     */
128    abstract protected function successfulAction( $direct = false, $extraMessages = null );
129
130    /**
131     * Logs to the authmanager-stats channel.
132     * @param bool $success
133     * @param UserIdentity $performer The performer
134     * @param string|null $status Error message key
135     */
136    abstract protected function logAuthResult( $success, UserIdentity $performer, $status = null );
137
138    /** @inheritDoc */
139    protected function setRequest( array $data, $wasPosted = null ) {
140        parent::setRequest( $data, $wasPosted );
141        $this->mLoadedRequest = false;
142    }
143
144    /**
145     * Get the login helper.
146     * @return LoginHelper
147     */
148    private function getLoginHelper(): LoginHelper {
149        if ( $this->loginHelper === null ) {
150            $this->loginHelper = new LoginHelper( $this->getContext() );
151        }
152        return $this->loginHelper;
153    }
154
155    /**
156     * Load basic request parameters for this Special page.
157     */
158    private function loadRequestParameters() {
159        if ( $this->mLoadedRequest ) {
160            return;
161        }
162        $this->mLoadedRequest = true;
163        $request = $this->getRequest();
164
165        $this->mPosted = $request->wasPosted();
166        $this->mAction = $request->getRawVal( 'action' );
167        $this->mFromHTTP = $request->getBool( 'fromhttp', false )
168            || $request->getBool( 'wpFromhttp', false );
169        $this->mStickHTTPS = $this->getConfig()->get( MainConfigNames::ForceHTTPS )
170            || ( !$this->mFromHTTP && $request->getProtocol() === 'https' )
171            || $request->getBool( 'wpForceHttps', false );
172        $this->mReturnTo = $request->getVal( 'returnto', '' );
173        $this->mReturnToQuery = $request->getVal( 'returntoquery', '' );
174        $this->mReturnToAnchor = $request->getVal( 'returntoanchor', '' );
175        $this->mAlwaysShowLogin = $request->getBool( 'alwaysShowLogin' );
176    }
177
178    /**
179     * Load data from request.
180     * @internal
181     * @param string $subPage Subpage of Special:Userlogin
182     */
183    protected function load( $subPage ) {
184        $this->loadRequestParameters();
185        if ( $this->mLoaded ) {
186            return;
187        }
188        $this->mLoaded = true;
189        $request = $this->getRequest();
190
191        $securityLevel = $this->getRequest()->getText( 'force' );
192        if (
193            $securityLevel &&
194                MediaWikiServices::getInstance()->getAuthManager()->securitySensitiveOperationStatus(
195                    $securityLevel ) === AuthManager::SEC_REAUTH
196        ) {
197            $this->securityLevel = $securityLevel;
198        }
199
200        $this->loadAuth( $subPage );
201
202        $this->mToken = $request->getVal( $this->getTokenName() );
203
204        // Show an error or warning or a notice passed on from a previous page
205        $entryError = $this->msg( $request->getVal( 'error', '' ) );
206        $entryWarning = $this->msg( $request->getVal( 'warning', '' ) );
207        $entryNotice = $this->msg( $request->getVal( 'notice', '' ) );
208        // bc: provide login link as a parameter for messages where the translation
209        // was not updated
210        $loginreqlink = $this->getLinkRenderer()->makeKnownLink(
211            $this->getPageTitle(),
212            $this->msg( 'loginreqlink' )->text(),
213            [],
214            $this->getPreservedParams( [ 'reset' => true ] )
215        );
216
217        // Only show valid error or warning messages.
218        $validErrorMessages = LoginHelper::getValidErrorMessages();
219        if ( $entryError->exists()
220            && in_array( $entryError->getKey(), $validErrorMessages, true )
221        ) {
222            $this->mEntryErrorType = 'error';
223            $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse();
224
225        } elseif ( $entryWarning->exists()
226            && in_array( $entryWarning->getKey(), $validErrorMessages, true )
227        ) {
228            $this->mEntryErrorType = 'warning';
229            $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse();
230        } elseif ( $entryNotice->exists()
231            && in_array( $entryNotice->getKey(), $validErrorMessages, true )
232        ) {
233            $this->mEntryErrorType = 'notice';
234            $this->mEntryError = $entryNotice->parse();
235        }
236
237        # 1. When switching accounts, it sucks to get automatically logged out
238        # 2. Do not return to PasswordReset after a successful password change
239        #    but goto Wiki start page (Main_Page) instead ( T35997 )
240        $returnToTitle = Title::newFromText( $this->mReturnTo );
241        if ( is_object( $returnToTitle )
242            && ( $returnToTitle->isSpecial( 'Userlogout' )
243                || $returnToTitle->isSpecial( 'PasswordReset' ) )
244        ) {
245            $this->mReturnTo = '';
246            $this->mReturnToQuery = '';
247        }
248    }
249
250    /** @inheritDoc */
251    protected function getPreservedParams( $options = [] ) {
252        $params = parent::getPreservedParams( $options );
253
254        // Override returnto* with their property-based values, to account for the
255        // special-casing in load().
256        $this->loadRequestParameters();
257        $properties = [
258            'returnto' => 'mReturnTo',
259            'returntoquery' => 'mReturnToQuery',
260            'returntoanchor' => 'mReturnToAnchor',
261        ];
262        foreach ( $properties as $key => $prop ) {
263            $value = $this->$prop;
264            if ( $value !== '' ) {
265                $params[$key] = $value;
266            } else {
267                unset( $params[$key] );
268            }
269        }
270
271        if ( $this->mAlwaysShowLogin ) {
272            $params['alwaysShowLogin'] = '1';
273        }
274        if ( $this->getConfig()->get( MainConfigNames::SecureLogin ) && !$this->isSignup() ) {
275            $params['fromhttp'] = $this->mFromHTTP ? '1' : null;
276        }
277
278        $displayMode = $this->getLoginHelper()->getDisplayMode();
279        if ( $displayMode !== LoginHelper::DISPLAY_MODE_DEFAULT ) {
280            $params[ LoginHelper::DISPLAY_MODE_PARAM_NAME ] = $displayMode;
281        }
282
283        return array_filter( $params, static fn ( $val ) => $val !== null );
284    }
285
286    /** @inheritDoc */
287    protected function beforeExecute( $subPage ) {
288        // finish initializing the class before processing the request - T135924
289        $this->loadRequestParameters();
290        return parent::beforeExecute( $subPage );
291    }
292
293    /**
294     * @param string|null $subPage
295     */
296    public function execute( $subPage ) {
297        if ( $this->mPosted ) {
298            $timer = MediaWikiServices::getInstance()->getStatsFactory()
299                ->getTiming( 'auth_specialpage_executeTiming_seconds' )
300                ->start();
301            $profilingScope = new ScopedCallback( function () use ( $timer ) {
302                $timer
303                    ->setLabel( 'action', $this->authAction )
304                    ->stop();
305            } );
306        }
307
308        $authManager = MediaWikiServices::getInstance()->getAuthManager();
309        $session = $this->getRequest()->getSession();
310
311        // Before persisting, set the login token to avoid double writes
312        $this->getToken();
313
314        // Session data is used for various things in the authentication process, so we must make
315        // sure a session cookie or some equivalent mechanism is set.
316        $session->persist();
317        // Explicitly disable cache to ensure cookie blocks may be set (T152462).
318        // (Technically redundant with sessions persisting from this page.)
319        $this->getOutput()->disableClientCache();
320
321        $this->load( $subPage );
322
323        // Do this early, so that it affects how error pages are rendered too
324        if ( $this->getLoginHelper()->isDisplayModePopup() ) {
325            // Replace the default skin with a "micro-skin" that omits most of the interface. (T362706)
326            // In the future, we might allow normal skins to serve this mode too, if they advise that
327            // they support it by setting a skin option, so that colors and fonts could stay consistent.
328            $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
329            $this->getContext()->setSkin( $skinFactory->makeSkin( 'authentication-popup' ) );
330        }
331
332        $this->setHeaders();
333        $this->checkPermissions();
334
335        // Make sure the system configuration allows log in / sign up
336        if ( !$this->isSignup() && !$authManager->canAuthenticateNow() ) {
337            if ( !$session->canSetUser() ) {
338                throw new ErrorPageError( 'cannotloginnow-title', 'cannotloginnow-text', [
339                    $session->getProvider()->describe( $this->getLanguage() )
340                ] );
341            }
342            throw new ErrorPageError( 'cannotlogin-title', 'cannotlogin-text' );
343        } elseif ( $this->isSignup() && !$authManager->canCreateAccounts() ) {
344            throw new ErrorPageError( 'cannotcreateaccount-title', 'cannotcreateaccount-text' );
345        }
346
347        /*
348         * In the case where the user is already logged in, and was redirected to
349         * the login form from a page that requires login, do not show the login
350         * page. The use case scenario for this is when a user opens a large number
351         * of tabs, is redirected to the login page on all of them, and then logs
352         * in on one, expecting all the others to work properly.
353         *
354         * However, do show the form if it was visited intentionally (no 'returnto'
355         * is present). People who often switch between several accounts have grown
356         * accustomed to this behavior.
357         *
358         * For temporary users, the form is always shown, since the UI presents
359         * temporary users as not logged in and offers to discard their temporary
360         * account by logging in.
361         *
362         * Also make an exception when force=<level> is set in the URL, which means the user must
363         * reauthenticate for security reasons; for POST (although it shouldn't really happen);
364         * and allow an explicit URL parameter override.
365         */
366        if ( !$this->isSignup() && !$this->mPosted && !$this->securityLevel &&
367            ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' ) &&
368            !$this->mAlwaysShowLogin &&
369            !$this->getUser()->isTemp() && $this->getUser()->isRegistered()
370        ) {
371            $this->successfulAction();
372            return;
373        }
374
375        // If logging in and not on HTTPS, either redirect to it or offer a link.
376        if ( $this->getRequest()->getProtocol() !== 'https' ) {
377            $title = $this->getFullTitle();
378            $query = $this->getPreservedParams() + [
379                    'title' => null,
380                    ( $this->mEntryErrorType === 'error' ? 'error'
381                        : 'warning' ) => $this->mEntryError,
382                ] + $this->getRequest()->getQueryValues();
383            $url = $title->getFullURL( $query, false, PROTO_HTTPS );
384            if ( $this->getConfig()->get( MainConfigNames::SecureLogin ) && !$this->mFromHTTP ) {
385                // Avoid infinite redirect
386                $url = wfAppendQuery( $url, 'fromhttp=1' );
387                $this->getOutput()->redirect( $url );
388                // Since we only do this redir to change proto, always vary
389                $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' );
390
391                return;
392            } else {
393                // A wiki without HTTPS login support should set $wgServer to
394                // http://somehost, in which case the secure URL generated
395                // above won't actually start with https://
396                if ( str_starts_with( $url, 'https://' ) ) {
397                    $this->mSecureLoginUrl = $url;
398                }
399            }
400        }
401
402        if ( !$this->isActionAllowed( $this->authAction ) ) {
403            // FIXME how do we explain this to the user? can we handle session loss better?
404            // messages used: authpage-cannot-login, authpage-cannot-login-continue,
405            // authpage-cannot-create, authpage-cannot-create-continue
406            $this->mainLoginForm( [], 'authpage-cannot-' . $this->authAction );
407            return;
408        }
409
410        if ( $this->canBypassForm( $button_name ) ) {
411            $this->setRequest( [], true );
412            $this->getRequest()->setVal( $this->getTokenName(), $this->getToken() );
413            if ( $button_name ) {
414                $this->getRequest()->setVal( $button_name, true );
415            }
416        }
417        $performer = $this->getUser();
418        $status = $this->trySubmit();
419
420        if ( !$status || !$status->isGood() ) {
421            $this->mainLoginForm( $this->authRequests, $status ? $status->getMessage() : '', 'error' );
422            return;
423        }
424
425        /** @var AuthenticationResponse $response */
426        $response = $status->getValue();
427
428        $returnToUrl = $this->getPageTitle( 'return' )
429            ->getFullURL( $this->getPreservedParams( [ 'withToken' => true ] ), false, PROTO_HTTPS );
430        switch ( $response->status ) {
431            case AuthenticationResponse::PASS:
432                $this->logAuthResult( true, $performer );
433                $this->proxyAccountCreation = $this->isSignup() && $this->getUser()->isNamed();
434                $this->targetUser = User::newFromName( $response->username );
435
436                if (
437                    !$this->proxyAccountCreation
438                    && $response->loginRequest
439                    && $authManager->canAuthenticateNow()
440                ) {
441                    // successful registration; log the user in instantly
442                    $response2 = $authManager->beginAuthentication( [ $response->loginRequest ],
443                        $returnToUrl );
444                    if ( $response2->status !== AuthenticationResponse::PASS ) {
445                        LoggerFactory::getInstance( 'login' )
446                            ->error( 'Could not log in after account creation' );
447                        $this->successfulAction( true, Status::newFatal( 'createacct-loginerror' ) );
448                        break;
449                    }
450                }
451
452                if ( !$this->proxyAccountCreation ) {
453                    $context = RequestContext::getMain();
454                    $localContext = $this->getContext();
455                    if ( $context !== $localContext ) {
456                        // remove AuthManagerSpecialPage context hack
457                        $this->setContext( $context );
458                    }
459                    // Ensure that the context user is the same as the session user.
460                    $this->getAuthManager()->setRequestContextUserFromSessionUser();
461                }
462
463                $this->successfulAction( true );
464                break;
465            case AuthenticationResponse::FAIL:
466                // fall through
467            case AuthenticationResponse::RESTART:
468                $this->authForm = null;
469                if ( $response->status === AuthenticationResponse::FAIL ) {
470                    $action = $this->getDefaultAction( $subPage );
471                    $messageType = 'error';
472                } else {
473                    $action = $this->getContinueAction( $this->authAction );
474                    $messageType = 'warning';
475                }
476                $this->logAuthResult( false, $performer, $response->message ? $response->message->getKey() : '-' );
477                $this->loadAuth( $subPage, $action, true );
478                $this->mainLoginForm( $this->authRequests, $response->message, $messageType );
479                break;
480            case AuthenticationResponse::REDIRECT:
481                $this->authForm = null;
482                $this->getOutput()->redirect( $response->redirectTarget );
483                break;
484            case AuthenticationResponse::UI:
485                $this->authForm = null;
486                $this->authAction = $this->isSignup() ? AuthManager::ACTION_CREATE_CONTINUE
487                    : AuthManager::ACTION_LOGIN_CONTINUE;
488                $this->authRequests = $response->neededRequests;
489                $this->mainLoginForm( $response->neededRequests, $response->message, $response->messageType );
490                break;
491            default:
492                throw new LogicException( 'invalid AuthenticationResponse' );
493        }
494    }
495
496    /**
497     * Determine if the login form can be bypassed. This will be the case when no more than one
498     * button is present and no other user input fields that are not marked as 'skippable' are
499     * present. If the login form were not bypassed, the user would be presented with a
500     * superfluous page on which they must press the single button to proceed with login.
501     * Not only does this cause an additional mouse click and page load, it confuses users,
502     * especially since there are a help link and forgotten password link that are
503     * provided on the login page that do not apply to this situation.
504     *
505     * @param string|null &$button_name if the form has a single button, returns
506     *   the name of the button; otherwise, returns null
507     * @return bool
508     */
509    private function canBypassForm( &$button_name ) {
510        $button_name = null;
511        if ( $this->isContinued() ) {
512            return false;
513        }
514        $fields = AuthenticationRequest::mergeFieldInfo( $this->authRequests );
515        foreach ( $fields as $fieldname => $field ) {
516            if ( !isset( $field['type'] ) ) {
517                return false;
518            }
519            if ( !empty( $field['skippable'] ) ) {
520                continue;
521            }
522            if ( $field['type'] === 'button' ) {
523                if ( $button_name !== null ) {
524                    $button_name = null;
525                    return false;
526                } else {
527                    $button_name = $fieldname;
528                }
529            } elseif ( $field['type'] !== 'null' ) {
530                return false;
531            }
532        }
533        return true;
534    }
535
536    /**
537     * Show the success page.
538     *
539     * @param string $type Condition of return to; see `executeReturnTo`
540     * @param Message $title Page's title
541     * @param string $msgname
542     * @param string $injected_html
543     * @param StatusValue|null $extraMessages
544     */
545    protected function showSuccessPage(
546        $type, $title, $msgname, $injected_html, $extraMessages
547    ) {
548        $out = $this->getOutput();
549        $out->setPageTitleMsg( $title );
550        if ( $msgname ) {
551            $out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) );
552        }
553        if ( $extraMessages ) {
554            $extraMessages = Status::wrap( $extraMessages );
555            $out->addWikiTextAsInterface(
556                $extraMessages->getWikiText( false, false, $this->getLanguage() )
557            );
558        }
559
560        $out->addHTML( $injected_html );
561
562        $helper = new LoginHelper( $this->getContext() );
563        $helper->showReturnToPage( $type, $this->mReturnTo, $this->mReturnToQuery,
564            $this->mStickHTTPS, $this->mReturnToAnchor );
565    }
566
567    /**
568     * @param AuthenticationRequest[] $requests A list of AuthenticationRequest objects,
569     *   used to generate the form fields. An empty array means a fatal error
570     *   (authentication cannot continue).
571     * @param string|Message $msg
572     * @param string $msgtype
573     * @throws ErrorPageError
574     * @throws Exception
575     * @throws FatalError
576     * @throws PermissionsError
577     * @throws ReadOnlyError
578     * @internal
579     */
580    protected function mainLoginForm( array $requests, $msg = '', $msgtype = 'error' ) {
581        $user = $this->getUser();
582        $out = $this->getOutput();
583
584        // FIXME how to handle empty $requests - restart, or no form, just an error message?
585        // no form would be better for no session type errors, restart is better when can* fails.
586        if ( !$requests ) {
587            $this->authAction = $this->getDefaultAction( $this->subPage );
588            $this->authForm = null;
589            $requests = MediaWikiServices::getInstance()->getAuthManager()
590                ->getAuthenticationRequests( $this->authAction, $user );
591        }
592
593        // Generic styles and scripts for both login and signup form
594        $out->addModuleStyles( [
595            'mediawiki.special.userlogin.common.styles',
596            'mediawiki.codex.messagebox.styles'
597        ] );
598        if ( $this->isSignup() ) {
599            // Additional styles and scripts for signup form
600            $out->addModules( 'mediawiki.special.createaccount' );
601            $out->addModuleStyles( [
602                'mediawiki.special.userlogin.signup.styles'
603            ] );
604        } else {
605            // Additional styles for login form
606            $out->addModuleStyles( [
607                'mediawiki.special.userlogin.login.styles'
608            ] );
609        }
610        $out->disallowUserJs(); // just in case...
611
612        $form = $this->getAuthForm( $requests, $this->authAction );
613        $form->prepareForm();
614
615        $submitStatus = Status::newGood();
616        if ( $msg && $msgtype === 'warning' ) {
617            $submitStatus->warning( $msg );
618        } elseif ( $msg && $msgtype === 'error' ) {
619            $submitStatus->fatal( $msg );
620
621            // T409431 Pass information about the error to the frontend to
622            //         be logged by Javascript instrumentation handlers.
623            $this->getOutput()->addJsConfigVars(
624                'wgErrorPageMessageKey',
625                is_string( $msg ) ? $msg : $msg->getKey()
626            );
627        }
628
629        // warning header for non-standard workflows (e.g. security reauthentication)
630        if ( $this->getUser()->isNamed() && !$this->isContinued() ) {
631            if ( !$this->isSignup() && $this->securityLevel ) {
632                $submitStatus->warning( 'userlogin-reauth', $this->getUser()->getName() );
633            } else {
634                // User is accessing the login or signup page while already logged in.
635                // Add a big warning and a button to leave this page (T284927),
636                // but allow using the form if they really want to.
637                $form->addPreHtml(
638                    Html::warningBox( $this->msg(
639                        $this->isSignup() ? 'createacct-loggedin' : 'userlogin-loggedin',
640                        $this->getUser()->getName()
641                    )->parse() ) .
642                    '<div class="cdx-field"><div class="cdx-field__control">' .
643                    Html::element( 'a',
644                        [
645                            'class' => 'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled ' .
646                                'cdx-button--action-progressive cdx-button--weight-primary mw-htmlform-submit ' .
647                                ( $this->getLoginHelper()->isDisplayModePopup() ? 'mw-authentication-popup-link' : '' ),
648                            'href' => ( Title::newFromText( $this->mReturnTo ) ?: Title::newMainPage() )
649                                ->createFragmentTarget( $this->mReturnToAnchor )->getLinkURL( $this->mReturnToQuery ),
650                        ],
651                        $this->msg(
652                            $this->isSignup() ? 'createacct-loggedin-continue-as' : 'userlogin-loggedin-continue-as',
653                            $this->getUser()->getName()
654                        )->text()
655                    ) .
656                    '</div></div>' .
657                    Html::element( 'h2', [], $this->msg(
658                        $this->isSignup() ? 'createacct-loggedin-heading' : 'userlogin-loggedin-heading'
659                    )->text() ) .
660                    $this->msg(
661                        $this->isSignup() ? 'createacct-loggedin-prompt' : 'userlogin-loggedin-prompt'
662                    )->parseAsBlock()
663                );
664            }
665        }
666
667        $formHtml = $form->getHTML( $submitStatus );
668
669        $out->addHTML( $this->getPageHtml( $formHtml ) );
670    }
671
672    /**
673     * Add page elements which are outside the form.
674     * FIXME this should probably be a template, but use a sensible language (handlebars?)
675     * @param string $formHtml
676     * @return string
677     */
678    protected function getPageHtml( $formHtml ) {
679        $loginPrompt = $this->isSignup() || $this->getLoginHelper()->isDisplayModePopup() || $this->isContinued()
680            ? ''
681            : Html::rawElement( 'div', [ 'id' => 'userloginprompt' ], $this->msg( 'loginprompt' )->parseAsBlock() );
682        $languageLinks = $this->getConfig()->get( MainConfigNames::LoginLanguageSelector )
683            ? $this->makeLanguageSelector() : '';
684        $signupStartMsg = $this->msg( 'signupstart' );
685        $signupStart = ( $this->isSignup() && !$signupStartMsg->isDisabled() )
686            ? Html::rawElement( 'div', [ 'id' => 'signupstart' ], $signupStartMsg->parseAsBlock() ) : '';
687        if ( $languageLinks ) {
688            $languageLinks = Html::rawElement( 'div', [ 'id' => 'languagelinks' ],
689                Html::rawElement( 'p', [], $languageLinks )
690            );
691        }
692        if ( $this->getUser()->isTemp() ) {
693            $noticeHtml = $this->getNoticeHtml();
694        } else {
695            $noticeHtml = '';
696        }
697        $formBlock = Html::rawElement( 'div', [ 'id' => 'userloginForm' ], $formHtml );
698        $formAndBenefits = $formBlock;
699        if ( $this->isSignup() && $this->showExtraInformation() && !$this->getUser()->isNamed() ) {
700            $benefitsContainerHtml = null;
701            $info = [
702                'context' => $this->getContext(),
703                'form' => $this->authForm,
704            ];
705            $options = [
706                'beforeForm' => false,
707            ];
708            $this->getHookRunner()->onSpecialCreateAccountBenefits(
709                $benefitsContainerHtml, $info, $options
710            );
711            $benefitsContainerHtml ??= $this->getBenefitsContainerHtml();
712            $formAndBenefits = $options['beforeForm']
713                ? ( $benefitsContainerHtml . $formBlock )
714                : ( $formBlock . $benefitsContainerHtml );
715        }
716
717        return $loginPrompt
718            . $languageLinks
719            . $signupStart
720            . $noticeHtml
721            . Html::rawElement( 'div', [ 'class' => 'mw-ui-container' ],
722                $formAndBenefits
723            );
724    }
725
726    /**
727     * The HTML to be shown in the "benefits to signing in / creating an account" section of the signup/login page.
728     *
729     * @unstable Experimental method added in 1.38. As noted in the comment from 2015 for getPageHtml,
730     *   this should use a template.
731     * @return string
732     */
733    protected function getBenefitsContainerHtml(): string {
734        $benefitsContainer = '';
735        $this->getOutput()->addModuleStyles( [ 'oojs-ui.styles.icons-user' ] );
736        if ( $this->isSignup() && $this->showExtraInformation() ) {
737            if ( !$this->getUser()->isTemp() ) {
738                // The following messages are used here:
739                // * createacct-benefit-icon1 createacct-benefit-head1 createacct-benefit-text1
740                // * createacct-benefit-icon2 createacct-benefit-head2 createacct-benefit-text2
741                // * createacct-benefit-icon3 createacct-benefit-head3 createacct-benefit-text3
742                $benefitCount = 3;
743                $benefitList = '';
744                for ( $benefitIdx = 1; $benefitIdx <= $benefitCount; $benefitIdx++ ) {
745                    $numberUnescaped = $this->msg( "createacct-benefit-head$benefitIdx" )->text();
746                    $numberHtml = Html::rawElement( 'strong', [], $numberUnescaped );
747                    $iconClass = $this->msg( "createacct-benefit-icon$benefitIdx" )->text();
748                    $benefitList .= Html::rawElement( 'div', [ 'class' => "mw-number-text $iconClass" ],
749                        Html::rawElement( 'p', [],
750                            $this->msg( "createacct-benefit-text$benefitIdx" )->params(
751                                $numberUnescaped,
752                                $numberHtml
753                            )->parse()
754                        )
755                    );
756                }
757                $benefitsContainer = Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ],
758                    Html::element( 'div', [ 'class' => 'mw-createacct-benefits-heading' ],
759                        $this->msg( 'createacct-benefit-heading' )->text()
760                    )
761                    . Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-list' ], $benefitList )
762                );
763            } else {
764                $benefitList = '';
765                $this->getOutput()->addModuleStyles(
766                    [
767                        'oojs-ui.styles.icons-moderation',
768                        'oojs-ui.styles.icons-interactions',
769                    ]
770                );
771                $benefits = [
772                    [
773                        'icon' => 'oo-ui-icon-unStar',
774                        'description' => $this->msg( "benefit-1-description" )->escaped()
775                    ],
776                    [
777                        'icon' => 'oo-ui-icon-userContributions',
778                        'description' => $this->msg( "benefit-2-description" )->escaped()
779                    ],
780                    [
781                        'icon' => 'oo-ui-icon-settings',
782                        'description' => $this->msg( "benefit-3-description" )->escaped()
783                    ]
784                ];
785                foreach ( $benefits as $benefit ) {
786                    $benefitContent = Html::rawElement( 'div', [ 'class' => 'mw-benefit-item' ],
787                        Html::rawElement( 'span', [ 'class' => $benefit[ 'icon' ] ] )
788                        . Html::rawElement( 'p', [], $benefit['description'] )
789                    );
790
791                    $benefitList .= Html::rawElement(
792                        'div', [ 'class' => 'mw-benefit-item-wrapper' ], $benefitContent );
793                }
794
795                $benefitsListWrapper = Html::rawElement(
796                    'div', [ 'class' => 'mw-benefit-list-wrapper' ], $benefitList );
797
798                $headingSubheadingWrapper = Html::rawElement( 'div', [ 'class' => 'mw-heading-subheading-wrapper' ],
799                    Html::element( 'h2', [],
800                        $this->msg( 'createacct-benefit-heading-temp-user' )->text()
801                    )
802                    . Html::element( 'p', [ 'class' => 'mw-benefit-subheading' ],
803                        $this->msg( 'createacct-benefit-subheading-temp-user' )->text()
804                    )
805                );
806
807                $benefitsContainer = Html::rawElement(
808                    'div', [ 'class' => 'mw-createacct-benefits-container' ],
809                    $headingSubheadingWrapper
810                    . $benefitsListWrapper
811                );
812            }
813        }
814        return $benefitsContainer;
815    }
816
817    /**
818     * Generates a form from the given request.
819     * @param AuthenticationRequest[] $requests
820     * @param string $action AuthManager action name
821     * @return HTMLForm
822     */
823    protected function getAuthForm( array $requests, $action ) {
824        // FIXME merge this with parent
825
826        if ( $this->authForm ) {
827            return $this->authForm;
828        }
829
830        $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
831
832        // get basic form description from the auth logic
833        $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
834        // this will call onAuthChangeFormFields()
835        $formDescriptor = $this->fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction );
836        $this->postProcessFormDescriptor( $formDescriptor, $requests );
837
838        $context = $this->getContext();
839        if ( $context->getRequest() !== $this->getRequest() ) {
840            // We have overridden the request, need to make sure the form uses that too.
841            $context = new DerivativeContext( $this->getContext() );
842            $context->setRequest( $this->getRequest() );
843        }
844        $form = HTMLForm::factory( 'codex', $formDescriptor, $context );
845
846        $form->addHiddenField( 'authAction', $this->authAction );
847        $form->addHiddenField( 'force', $this->securityLevel );
848        $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
849        $config = $this->getConfig();
850        if ( $config->get( MainConfigNames::SecureLogin ) &&
851        !$config->get( MainConfigNames::ForceHTTPS ) ) {
852            // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved
853            if ( !$this->isSignup() ) {
854                $form->addHiddenField( 'wpForceHttps', (int)$this->mStickHTTPS );
855                $form->addHiddenField( 'wpFromhttp', $usingHTTPS );
856            }
857        }
858
859        $form->setAction( $this->getPageTitle()->getLocalURL( $this->getPreservedParams(
860            // We have manually set authAction above, so we don't need it in the action URL.
861            [ 'reset' => true ]
862        ) ) );
863        $form->setName( 'userlogin' . ( $this->isSignup() ? '2' : '' ) );
864        if ( $this->isSignup() ) {
865            $form->setId( 'userlogin2' );
866        }
867
868        $form->suppressDefaultSubmit();
869
870        $this->authForm = $form;
871
872        return $form;
873    }
874
875    /** @inheritDoc */
876    public function onAuthChangeFormFields(
877        array $requests, array $fieldInfo, array &$formDescriptor, $action
878    ) {
879        $formDescriptor = self::mergeDefaultFormDescriptor( $fieldInfo, $formDescriptor,
880            $this->getFieldDefinitions( $fieldInfo, $requests ) );
881    }
882
883    /**
884     * Show extra information such as password recovery information, link from login to signup,
885     * CTA etc? Such information should only be shown on the "landing page", ie. when the user
886     * is at the first step of the authentication process.
887     * @return bool
888     */
889    protected function showExtraInformation() {
890        return $this->authAction !== $this->getContinueAction( $this->authAction )
891            && ( !$this->securityLevel || !$this->getUser()->isNamed() );
892    }
893
894    /**
895     * Create a HTMLForm descriptor for the core login fields.
896     *
897     * @param array $fieldInfo
898     * @param array $requests
899     *
900     * @return array
901     */
902    protected function getFieldDefinitions( array $fieldInfo, array $requests ) {
903        $isLoggedIn = $this->getUser()->isRegistered();
904        $continuePart = $this->isContinued() ? 'continue-' : '';
905        $anotherPart = $isLoggedIn ? 'another-' : '';
906        // @phan-suppress-next-line PhanUndeclaredMethod
907        $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
908        $expirationDays = ceil( $expiration / ( 3600 * 24 ) );
909        $secureLoginLink = '';
910        if ( $this->mSecureLoginUrl ) {
911            $secureLoginLink = Html::rawElement( 'a', [
912                'href' => $this->mSecureLoginUrl,
913                'class' => 'mw-login-flush-right mw-secure',
914            ], Html::element( 'span', [ 'class' => 'mw-secure--icon' ] ) .
915                $this->msg( 'userlogin-signwithsecure' )->parse() );
916        }
917        $usernameHelpLink = '';
918        if ( !$this->msg( 'createacct-helpusername' )->isDisabled() ) {
919            $usernameHelpLink = Html::rawElement( 'span', [
920                'class' => 'mw-login-flush-right',
921            ], $this->msg( 'createacct-helpusername' )->parse() );
922        }
923
924        if ( $this->isSignup() ) {
925            $config = $this->getConfig();
926            $hideIf = isset( $fieldInfo['mailpassword'] ) ? [ 'hide-if' => [ '===', 'mailpassword', '1' ] ] : [];
927            $fieldDefinitions = [
928                'username' => [
929                    'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $usernameHelpLink,
930                    'id' => 'wpName2',
931                    'placeholder-message' => $isLoggedIn ? 'createacct-another-username-ph'
932                        : 'userlogin-yourname-ph',
933                ],
934                'mailpassword' => [
935                    // create account without providing password, a temporary one will be mailed
936                    'type' => 'check',
937                    'label-message' => 'createaccountmail',
938                    'name' => 'wpCreateaccountMail',
939                    'id' => 'wpCreateaccountMail',
940                ],
941                'password' => [
942                    'id' => 'wpPassword2',
943                    'autocomplete' => 'new-password',
944                    'placeholder-message' => 'createacct-yourpassword-ph',
945                    'help-message' => 'createacct-useuniquepass',
946                ] + $hideIf,
947                'domain' => [],
948                'retype' => [
949                    'type' => 'password',
950                    'label-message' => 'createacct-yourpasswordagain',
951                    'id' => 'wpRetype',
952                    'cssclass' => 'loginPassword',
953                    'size' => 20,
954                    'autocomplete' => 'new-password',
955                    'validation-callback' => function ( $value, $alldata ) {
956                        if ( empty( $alldata['mailpassword'] ) && !empty( $alldata['password'] ) ) {
957                            if ( !$value ) {
958                                return $this->msg( 'htmlform-required' );
959                            } elseif ( $value !== $alldata['password'] ) {
960                                return $this->msg( 'badretype' );
961                            }
962                        }
963                        return true;
964                    },
965                    'placeholder-message' => 'createacct-yourpasswordagain-ph',
966                ] + $hideIf,
967                'email' => [
968                    'type' => 'email',
969                    'label-message' => $config->get( MainConfigNames::EmailConfirmToEdit )
970                        ? 'createacct-emailrequired' : 'createacct-emailoptional',
971                    'id' => 'wpEmail',
972                    'cssclass' => 'loginText',
973                    'size' => '20',
974                    'maxlength' => 255,
975                    'autocomplete' => 'email',
976                    // FIXME will break non-standard providers
977                    'required' => $config->get( MainConfigNames::EmailConfirmToEdit ),
978                    'validation-callback' => function ( $value, $alldata ) {
979                        // AuthManager will check most of these, but that will make the auth
980                        // session fail and this won't, so nicer to do it this way
981                        if ( !$value &&
982                            $this->getConfig()->get( MainConfigNames::EmailConfirmToEdit )
983                        ) {
984                            // no point in allowing registration without email when email is
985                            // required to edit
986                            return $this->msg( 'noemailtitle' );
987                        } elseif ( !$value && !empty( $alldata['mailpassword'] ) ) {
988                            // cannot send password via email when there is no email address
989                            return $this->msg( 'noemailcreate' );
990                        } elseif ( $value && !Sanitizer::validateEmail( $value ) ) {
991                            return $this->msg( 'invalidemailaddress' );
992                        } elseif ( is_string( $value ) && strlen( $value ) > 255 ) {
993                            return $this->msg( 'changeemail-maxlength' );
994                        }
995                        return true;
996                    },
997                    // The following messages are used here:
998                    // * createacct-email-ph
999                    // * createacct-another-email-ph
1000                    'placeholder-message' => 'createacct-' . $anotherPart . 'email-ph',
1001                ],
1002                'realname' => [
1003                    'type' => 'text',
1004                    'help-message' => $isLoggedIn ? 'createacct-another-realname-tip'
1005                        : 'prefs-help-realname',
1006                    'label-message' => 'createacct-realname',
1007                    'cssclass' => 'loginText',
1008                    'size' => 20,
1009                    'placeholder-message' => 'createacct-realname',
1010                    'id' => 'wpRealName',
1011                    'autocomplete' => 'name',
1012                ],
1013                'reason' => [
1014                    // comment for the user creation log
1015                    'type' => 'text',
1016                    'label-message' => 'createacct-reason',
1017                    'cssclass' => 'loginText',
1018                    'id' => 'wpReason',
1019                    'size' => '20',
1020                    'validation-callback' => function ( $value, $alldata ) {
1021                        // if the user sets an email address as the user creation reason, confirm that
1022                        // that was their intent
1023                        if ( $value && Sanitizer::validateEmail( $value ) ) {
1024                            if ( $this->reasonValidatorResult !== null ) {
1025                                return $this->reasonValidatorResult;
1026                            }
1027                            $this->reasonValidatorResult = true;
1028                            $authManager = MediaWikiServices::getInstance()->getAuthManager();
1029                            if ( !$authManager->getAuthenticationSessionData( 'reason-retry', false ) ) {
1030                                $authManager->setAuthenticationSessionData( 'reason-retry', true );
1031                                $this->reasonValidatorResult = $this->msg( 'createacct-reason-confirm' );
1032                            }
1033                            return $this->reasonValidatorResult;
1034                        }
1035                        return true;
1036                    },
1037                    'placeholder-message' => 'createacct-reason-ph',
1038                ],
1039                'createaccount' => [
1040                    // submit button
1041                    'type' => 'submit',
1042                    // The following messages are used here:
1043                    // * createacct-submit
1044                    // * createacct-another-submit
1045                    // * createacct-continue-submit
1046                    // * createacct-another-continue-submit
1047                    'default' => $this->msg( 'createacct-' . $anotherPart . $continuePart .
1048                        'submit' )->text(),
1049                    'name' => 'wpCreateaccount',
1050                    'id' => 'wpCreateaccount',
1051                    'weight' => 100,
1052                ],
1053            ];
1054
1055            if ( !$this->msg( 'createacct-username-help' )->isDisabled() ) {
1056                $fieldDefinitions['username']['help-message'] = 'createacct-username-help';
1057            }
1058
1059            if ( $this->loginHelper->isDisplayModePopup() ) {
1060                $fieldDefinitions['redirectnotice'] = [
1061                    'type' => 'info',
1062                    'default' => $this->msg( 'createacct-popup-redirect-notice' )->text(),
1063                    'cssclass' => 'mw-createacct-redirect-notice',
1064                    'weight' => -1,
1065                ];
1066            }
1067        } else {
1068            // When the user's password is too weak, they might be asked to provide a stronger one
1069            // as a followup step. That is a form with only two fields, 'password' and 'retype',
1070            // and they should behave more like account creation.
1071            $passwordRequest = AuthenticationRequest::getRequestByClass( $this->authRequests,
1072                PasswordAuthenticationRequest::class );
1073            $changePassword = $passwordRequest && $passwordRequest->action == AuthManager::ACTION_CHANGE;
1074            $fieldDefinitions = [
1075                'username' => (
1076                    [
1077                        'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $secureLoginLink,
1078                        'id' => 'wpName1',
1079                        'placeholder-message' => 'userlogin-yourname-ph',
1080                    ] + ( $changePassword ? [
1081                        // There is no username field on the AuthManager level when changing
1082                        // passwords. Fake one because password
1083                        'baseField' => 'password',
1084                        'nodata' => true,
1085                        'readonly' => true,
1086                        'cssclass' => 'mw-htmlform-hidden-field',
1087                    ] : [] )
1088                ),
1089                'password' => (
1090                    $changePassword ? [
1091                        'autocomplete' => 'new-password',
1092                        'placeholder-message' => 'createacct-yourpassword-ph',
1093                        'help-message' => 'createacct-useuniquepass',
1094                    ] : [
1095                        'id' => 'wpPassword1',
1096                        'autocomplete' => 'current-password',
1097                        'placeholder-message' => 'userlogin-yourpassword-ph',
1098                    ]
1099                ),
1100                'retype' => [
1101                    'type' => 'password',
1102                    'autocomplete' => 'new-password',
1103                    'placeholder-message' => 'createacct-yourpasswordagain-ph',
1104                ],
1105                'domain' => [],
1106                'rememberMe' => [
1107                    // option for saving the user token to a cookie
1108                    'type' => 'check',
1109                    'cssclass' => 'mw-userlogin-rememberme',
1110                    'name' => 'wpRemember',
1111                    'label-message' => $this->msg( 'userlogin-remembermypassword' )
1112                        ->numParams( $expirationDays ),
1113                    'id' => 'wpRemember',
1114                ],
1115                'loginattempt' => [
1116                    // submit button
1117                    'type' => 'submit',
1118                    // The following messages are used here:
1119                    // * pt-login-button
1120                    // * pt-login-continue-button
1121                    'default' => $this->msg( 'pt-login-' . $continuePart . 'button' )->text(),
1122                    'id' => 'wpLoginAttempt',
1123                    'weight' => 100,
1124                ],
1125                'linkcontainer' => [
1126                    // help link
1127                    'type' => 'info',
1128                    'cssclass' => 'mw-form-related-link-container mw-userlogin-help',
1129                    // 'id' => 'mw-userlogin-help', // FIXME HTMLInfoField ignores this
1130                    'raw' => true,
1131                    'default' => Html::element( 'a', [
1132                        'href' => Skin::makeInternalOrExternalUrl( $this->msg( 'helplogin-url' )
1133                            ->inContentLanguage()
1134                            ->text() ),
1135                    ], $this->msg( 'userlogin-helplink2' )->text() ),
1136                    'weight' => 200,
1137                ],
1138                // button for ResetPasswordSecondaryAuthenticationProvider
1139                'skipReset' => [
1140                    'weight' => 110,
1141                    'flags' => [],
1142                ],
1143            ];
1144        }
1145
1146        // T369641: We want to ensure that this transformation to the username and/or
1147        // password fields are applied only when we have matching requests within the
1148        // authentication manager.
1149        $isUsernameOrPasswordRequest =
1150            AuthenticationRequest::getRequestByClass( $requests, UsernameAuthenticationRequest::class ) ||
1151            AuthenticationRequest::getRequestByClass( $requests, PasswordAuthenticationRequest::class );
1152
1153        if ( $isUsernameOrPasswordRequest ) {
1154            $fieldDefinitions['username'] += [
1155                'type' => 'text',
1156                'name' => 'wpName',
1157                'cssclass' => 'loginText mw-userlogin-username',
1158                'size' => 20,
1159                'autocomplete' => 'username',
1160                // 'required' => true,
1161            ];
1162            $fieldDefinitions['password'] += [
1163                'type' => 'password',
1164                // 'label-message' => 'userlogin-yourpassword', // would override the changepassword label
1165                'name' => 'wpPassword',
1166                'cssclass' => 'loginPassword mw-userlogin-password',
1167                'size' => 20,
1168                // 'required' => true,
1169            ];
1170        }
1171
1172        if ( $this->mEntryError ) {
1173            $defaultHtml = '';
1174            if ( $this->mEntryErrorType === 'error' ) {
1175                $defaultHtml = Html::errorBox( $this->mEntryError );
1176            } elseif ( $this->mEntryErrorType === 'warning' ) {
1177                $defaultHtml = Html::warningBox( $this->mEntryError );
1178            } elseif ( $this->mEntryErrorType === 'notice' ) {
1179                $defaultHtml = Html::noticeBox( $this->mEntryError );
1180            }
1181            $fieldDefinitions['entryError'] = [
1182                'type' => 'info',
1183                'default' => $defaultHtml,
1184                'raw' => true,
1185                'rawrow' => true,
1186                'weight' => -100,
1187            ];
1188        }
1189        if ( !$this->showExtraInformation() ) {
1190            unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
1191        }
1192        if ( $this->isSignup() && $this->showExtraInformation() ) {
1193            // blank signup footer for site customization
1194            // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise
1195            $signupendMsg = $this->msg( 'signupend' );
1196            $signupendHttpsMsg = $this->msg( 'signupend-https' );
1197            if ( !$signupendMsg->isDisabled() ) {
1198                $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
1199                $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
1200                    ? $signupendHttpsMsg->parse() : $signupendMsg->parse();
1201                $fieldDefinitions['signupend'] = [
1202                    'type' => 'info',
1203                    'raw' => true,
1204                    'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ),
1205                    'weight' => 225,
1206                ];
1207            }
1208        }
1209        if ( !$this->isSignup() && $this->showExtraInformation() ) {
1210            $passwordReset = MediaWikiServices::getInstance()->getPasswordReset();
1211            if ( $passwordReset->isEnabled()->isGood() ) {
1212                $fieldDefinitions['passwordReset'] = [
1213                    'type' => 'info',
1214                    'raw' => true,
1215                    'cssclass' => 'mw-form-related-link-container',
1216                    'default' => $this->getLinkRenderer()->makeLink(
1217                        SpecialPage::getTitleFor( 'PasswordReset' ),
1218                        $this->msg( 'userlogin-resetpassword-link' )->text(),
1219                        [ 'class' => 'mw-authentication-popup-link' ],
1220                        $this->getPreservedParams()
1221                    ),
1222                    'weight' => 230,
1223                ];
1224            }
1225
1226            // Don't show a "create account" link if the user can't.
1227            if ( $this->showCreateAccountLink() ) {
1228                // link to the other action
1229                $linkTitle = SpecialPage::getTitleFor( $this->isSignup() ? 'Userlogin' : 'CreateAccount' );
1230                $linkq = wfArrayToCgi( $this->getPreservedParams( [ 'reset' => true ] ) );
1231                $isLoggedIn = $this->getUser()->isRegistered()
1232                    && !$this->getUser()->isTemp();
1233                $popupMode = $this->getLoginHelper()->isDisplayModePopup();
1234
1235                $fieldDefinitions['createOrLogin'] = [
1236                    'type' => 'info',
1237                    'raw' => true,
1238                    'linkQuery' => $linkq,
1239                    'default' => function ( $params ) use ( $isLoggedIn, $linkTitle ) {
1240                        $buttonClasses = 'cdx-button cdx-button--action-progressive '
1241                            . 'cdx-button--fake-button cdx-button--fake-button--enabled';
1242
1243                        return Html::rawElement( 'div',
1244                            // The following element IDs are used here:
1245                            // mw-createaccount, mw-createaccount-cta
1246                            [ 'id' => 'mw-createaccount' . ( !$isLoggedIn ? '-cta' : '' ),
1247                                'class' => ( $isLoggedIn ? 'mw-form-related-link-container' : '' ) ],
1248                            ( $isLoggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
1249                            . Html::element( 'a',
1250                                [
1251                                    // The following element IDs are used here:
1252                                    // mw-createaccount-join, mw-createaccount-join-loggedin
1253                                    'id' => 'mw-createaccount-join' . ( $isLoggedIn ? '-loggedin' : '' ),
1254                                    'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
1255                                    'class' => [ 'mw-authentication-popup-link', $buttonClasses => !$isLoggedIn ],
1256                                    'target' => '_self',
1257                                    'tabindex' => 100,
1258                                ],
1259                                $this->msg(
1260                                    $isLoggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
1261                                )->text()
1262                            )
1263                        );
1264                    },
1265                    'weight' => $popupMode ? -1 : 235,
1266                ];
1267            }
1268        }
1269
1270        return $fieldDefinitions;
1271    }
1272
1273    /**
1274     * Whether the login/create account form should display a link to the
1275     * other form (in addition to whatever the skin provides).
1276     * @return bool
1277     */
1278    private function showCreateAccountLink() {
1279        return $this->isSignup() ||
1280            $this->getContext()->getAuthority()->isAllowed( 'createaccount' );
1281    }
1282
1283    /**
1284     * @return string
1285     */
1286    protected function getTokenName() {
1287        return $this->isSignup() ? 'wpCreateaccountToken' : 'wpLoginToken';
1288    }
1289
1290    /**
1291     * Produce a bar of links which allow the user to select another language
1292     * during login/registration but retain "returnto"
1293     *
1294     * @return string
1295     */
1296    protected function makeLanguageSelector() {
1297        $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
1298        if ( $msg->isBlank() ) {
1299            return '';
1300        }
1301        $langs = explode( "\n", $msg->text() );
1302        $links = [];
1303        foreach ( $langs as $lang ) {
1304            $lang = trim( $lang, '* ' );
1305            $parts = explode( '|', $lang );
1306            if ( count( $parts ) >= 2 ) {
1307                $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) );
1308            }
1309        }
1310
1311        return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams(
1312            $this->getLanguage()->pipeList( $links ) )->escaped() : '';
1313    }
1314
1315    /**
1316     * Create a language selector link for a particular language
1317     * Links back to this page preserving type and returnto
1318     *
1319     * @param string $text Link text
1320     * @param string $lang Language code
1321     * @return string
1322     */
1323    protected function makeLanguageSelectorLink( $text, $lang ) {
1324        $services = MediaWikiServices::getInstance();
1325
1326        if ( $this->getLanguage()->getCode() == $lang
1327            || !$services->getLanguageNameUtils()->isValidCode( $lang )
1328        ) {
1329            // no link for currently used language
1330            // or invalid language code
1331            return htmlspecialchars( $text );
1332        }
1333
1334        $query = $this->getPreservedParams();
1335        $query['uselang'] = $lang;
1336
1337        $attr = [];
1338        $targetLanguage = $services->getLanguageFactory()->getLanguage( $lang );
1339        $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
1340        $attr['class'] = 'mw-authentication-popup-link';
1341        $attr['title'] = false;
1342
1343        return $this->getLinkRenderer()->makeKnownLink(
1344            $this->getPageTitle(),
1345            $text,
1346            $attr,
1347            $query
1348        );
1349    }
1350
1351    /** @inheritDoc */
1352    protected function getGroupName() {
1353        return 'login';
1354    }
1355
1356    /**
1357     * @param array &$formDescriptor
1358     * @param array $requests
1359     */
1360    protected function postProcessFormDescriptor( &$formDescriptor, $requests ) {
1361        // Pre-fill username (if not creating an account, T46775).
1362        if (
1363            isset( $formDescriptor['username'] ) &&
1364            !isset( $formDescriptor['username']['default'] ) &&
1365            !$this->isSignup()
1366        ) {
1367            $user = $this->getUser();
1368            if ( $user->isRegistered() && !$user->isTemp() ) {
1369                $formDescriptor['username']['default'] = $user->getName();
1370            } else {
1371                $formDescriptor['username']['default'] =
1372                    $this->getRequest()->getSession()->suggestLoginUsername();
1373            }
1374        }
1375
1376        // don't show a submit button if there is nothing to submit (i.e. the only form content
1377        // is other submit buttons, for redirect flows)
1378        if ( !$this->needsSubmitButton( $requests ) ) {
1379            unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] );
1380        }
1381
1382        if ( $this->getUser()->isNamed() && !$this->isContinued() ) {
1383            // Remove 'primary' flag from the default form submission button if the user is already logged in
1384            if ( isset( $formDescriptor['createaccount'] ) ) {
1385                $formDescriptor['createaccount']['flags'] = [ 'progressive' ];
1386            }
1387            if ( isset( $formDescriptor['loginattempt'] ) ) {
1388                $formDescriptor['loginattempt']['flags'] = [ 'progressive' ];
1389            }
1390        }
1391
1392        if ( !$this->isSignup() ) {
1393            // FIXME HACK don't focus on non-empty field
1394            // maybe there should be an autofocus-if similar to hide-if?
1395            if (
1396                isset( $formDescriptor['username'] )
1397                && empty( $formDescriptor['username']['default'] )
1398                && !$this->getRequest()->getCheck( 'wpName' )
1399            ) {
1400                $formDescriptor['username']['autofocus'] = true;
1401            } elseif ( isset( $formDescriptor['password'] ) ) {
1402                $formDescriptor['password']['autofocus'] = true;
1403            }
1404        }
1405
1406        $this->addTabIndex( $formDescriptor );
1407    }
1408
1409    /**
1410     * Generates the HTML for a notice box to be displayed to a temporary user.
1411     *
1412     * @return string HTML representing the notice box
1413     */
1414    protected function getNoticeHtml() {
1415        $noticeContent = $this->msg( 'createacct-temp-warning', $this->getUser()->getName() )->parse();
1416        return Html::noticeBox(
1417            $noticeContent,
1418            'mw-createaccount-temp-warning',
1419            '',
1420            'mw-userLogin-icon--user-temporary'
1421        );
1422    }
1423
1424}
1425
1426/** @deprecated class alias since 1.41 */
1427class_alias( LoginSignupSpecialPage::class, 'LoginSignupSpecialPage' );