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