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