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