30 use Wikimedia\ScopedCallback;
66 abstract protected function isSignup();
74 abstract protected function successfulAction( $direct =
false, $extraMessages =
null );
85 parent::__construct( $name );
91 protected function setRequest( array $data, $wasPosted =
null ) {
92 parent::setRequest( $data, $wasPosted );
93 $this->mLoadedRequest =
false;
100 if ( $this->mLoadedRequest ) {
103 $this->mLoadedRequest =
true;
106 $this->mPosted = $request->wasPosted();
107 $this->mAction = $request->getVal(
'action' );
108 $this->mFromHTTP = $request->getBool(
'fromhttp',
false )
109 || $request->getBool(
'wpFromhttp',
false );
110 $this->mStickHTTPS = ( !$this->mFromHTTP && $request->getProtocol() ===
'https' )
111 || $request->getBool(
'wpForceHttps',
false );
112 $this->mLanguage = $request->getText(
'uselang' );
113 $this->mReturnTo = $request->getVal(
'returnto',
'' );
114 $this->mReturnToQuery = $request->getVal(
'returntoquery',
'' );
126 if ( $this->mLoaded ) {
129 $this->mLoaded =
true;
134 $securityLevel && AuthManager::singleton()->securitySensitiveOperationStatus(
142 $this->mToken = $request->getVal( $this->
getTokenName() );
145 $entryError = $this->
msg( $request->getVal(
'error',
'' ) );
146 $entryWarning = $this->
msg( $request->getVal(
'warning',
'' ) );
151 $this->
msg(
'loginreqlink' )->text(),
154 'returnto' => $this->mReturnTo,
155 'returntoquery' => $this->mReturnToQuery,
156 'uselang' => $this->mLanguage ?:
null,
162 if ( $entryError->exists()
165 $this->mEntryErrorType =
'error';
166 $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse();
168 } elseif ( $entryWarning->exists()
171 $this->mEntryErrorType =
'warning';
172 $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse();
175 # 1. When switching accounts, it sucks to get automatically logged out
176 # 2. Do not return to PasswordReset after a successful password change
177 # but goto Wiki start page (Main_Page) instead ( T35997 )
179 if ( is_object( $returnToTitle )
180 && ( $returnToTitle->isSpecial(
'Userlogout' )
181 || $returnToTitle->isSpecial(
'PasswordReset' ) )
183 $this->mReturnTo =
'';
184 $this->mReturnToQuery =
'';
191 $params = parent::getPreservedParams( $withToken );
193 'returnto' => $this->mReturnTo ?:
null,
194 'returntoquery' => $this->mReturnToQuery ?:
null,
197 $params[
'fromhttp'] = $this->mFromHTTP ?
'1' :
null;
205 return parent::beforeExecute(
$subPage );
213 if ( $this->mPosted ) {
214 $time = microtime(
true );
215 $profilingScope =
new ScopedCallback(
function () use ( $time ) {
216 $time = microtime(
true ) - $time;
217 $statsd = MediaWikiServices::getInstance()->getStatsdDataFactory();
218 $statsd->timing(
"timing.login.ui.{$this->authAction}", $time * 1000 );
222 $authManager = AuthManager::singleton();
223 $session = SessionManager::getGlobalSession();
234 if ( !$this->
isSignup() && !$authManager->canAuthenticateNow() ) {
235 if ( !$session->canSetUser() ) {
236 throw new ErrorPageError(
'cannotloginnow-title',
'cannotloginnow-text', [
240 throw new ErrorPageError(
'cannotlogin-title',
'cannotlogin-text' );
241 } elseif ( $this->
isSignup() && !$authManager->canCreateAccounts() ) {
242 throw new ErrorPageError(
'cannotcreateaccount-title',
'cannotcreateaccount-text' );
259 if ( !$this->
isSignup() && !$this->mPosted && !$this->securityLevel &&
260 ( $this->mReturnTo !==
'' || $this->mReturnToQuery !==
'' ) &&
269 if ( $this->
getRequest()->getProtocol() !==
'https' ) {
273 ( $this->mEntryErrorType ===
'error' ?
'error'
274 :
'warning' ) => $this->mEntryError,
284 $this->
getOutput()->addVaryHeader(
'X-Forwarded-Proto' );
291 if ( substr( $url, 0, 8 ) ===
'https://' ) {
292 $this->mSecureLoginUrl = $url;
301 $this->
mainLoginForm( [],
'authpage-cannot-' . $this->authAction );
308 if ( $button_name ) {
309 $this->
getRequest()->setVal( $button_name,
true );
326 case AuthenticationResponse::PASS:
328 $this->proxyAccountCreation = $this->
isSignup() && !$this->
getUser()->isAnon();
332 !$this->proxyAccountCreation
334 && $authManager->canAuthenticateNow()
337 $response2 = $authManager->beginAuthentication( [
$response->loginRequest ],
339 if ( $response2->status !== AuthenticationResponse::PASS ) {
340 LoggerFactory::getInstance(
'login' )
341 ->error(
'Could not log in after account creation' );
347 if ( !$this->proxyAccountCreation ) {
354 case AuthenticationResponse::FAIL:
356 case AuthenticationResponse::RESTART:
357 unset( $this->authForm );
358 if (
$response->status === AuthenticationResponse::FAIL ) {
360 $messageType =
'error';
363 $messageType =
'warning';
369 case AuthenticationResponse::REDIRECT:
370 unset( $this->authForm );
373 case AuthenticationResponse::UI:
374 unset( $this->authForm );
375 $this->authAction = $this->
isSignup() ? AuthManager::ACTION_CREATE_CONTINUE
376 : AuthManager::ACTION_LOGIN_CONTINUE;
377 $this->authRequests =
$response->neededRequests;
381 throw new LogicException(
'invalid AuthenticationResponse' );
403 $fields = AuthenticationRequest::mergeFieldInfo( $this->authRequests );
404 foreach ( $fields as $fieldname => $field ) {
405 if ( !isset( $field[
'type'] ) ) {
408 if ( !empty( $field[
'skippable'] ) ) {
411 if ( $field[
'type'] ===
'button' ) {
412 if ( $button_name !==
null ) {
416 $button_name = $fieldname;
418 } elseif ( $field[
'type'] !==
'null' ) {
435 $type,
$title, $msgname, $injected_html, $extraMessages
438 $out->setPageTitle(
$title );
442 if ( $extraMessages ) {
444 $out->addWikiTextAsInterface(
445 $extraMessages->getWikiText(
false,
false, $this->getLanguage() )
449 $out->addHTML( $injected_html );
452 $helper->showReturnToPage(
$type, $this->mReturnTo, $this->mReturnToQuery, $this->mStickHTTPS );
471 $type, $returnTo =
'', $returnToQuery =
'', $stickHTTPS =
false
474 $helper->showReturnToPage(
$type, $returnTo, $returnToQuery, $stickHTTPS );
491 $user =
$context->getRequest()->getSession()->getUser();
513 protected function mainLoginForm( array $requests, $msg =
'', $msgtype =
'error' ) {
521 $this->authForm =
null;
522 $requests = AuthManager::singleton()->getAuthenticationRequests( $this->authAction, $user );
526 $out->addModuleStyles( [
528 'mediawiki.ui.button',
529 'mediawiki.ui.checkbox',
530 'mediawiki.ui.input',
531 'mediawiki.special.userlogin.common.styles'
535 $out->addJsConfigVars(
'wgCreateacctImgcaptchaHelp',
536 $this->
msg(
'createacct-imgcaptcha-help' )->parse() );
540 'mediawiki.special.userlogin.signup.js'
542 $out->addModuleStyles( [
543 'mediawiki.special.userlogin.signup.styles'
547 $out->addModuleStyles( [
548 'mediawiki.special.userlogin.login.styles'
551 $out->disallowUserJs();
553 $form = $this->
getAuthForm( $requests, $this->authAction, $msg, $msgtype );
554 $form->prepareForm();
557 if ( $msg && $msgtype ===
'warning' ) {
558 $submitStatus->warning( $msg );
559 } elseif ( $msg && $msgtype ===
'error' ) {
560 $submitStatus->fatal( $msg );
566 $this->
getUser()->isLoggedIn() &&
567 $this->authAction !== AuthManager::ACTION_LOGIN_CONTINUE
569 $reauthMessage = $this->securityLevel ?
'userlogin-reauth' :
'userlogin-loggedin';
570 $submitStatus->warning( $reauthMessage, $this->
getUser()->
getName() );
573 $formHtml = $form->getHTML( $submitStatus );
587 $loginPrompt = $this->
isSignup() ?
'' : Html::rawElement(
'div',
588 [
'id' =>
'userloginprompt' ], $this->
msg(
'loginprompt' )->parseAsBlock() );
590 $signupStartMsg = $this->
msg(
'signupstart' );
591 $signupStart = ( $this->
isSignup() && !$signupStartMsg->isDisabled() )
592 ? Html::rawElement(
'div', [
'id' =>
'signupstart' ], $signupStartMsg->parseAsBlock() ) :
'';
593 if ( $languageLinks ) {
594 $languageLinks = Html::rawElement(
'div', [
'id' =>
'languagelinks' ],
595 Html::rawElement(
'p', [], $languageLinks )
599 $benefitsContainer =
'';
607 for ( $benefitIdx = 1; $benefitIdx <= $benefitCount; $benefitIdx++ ) {
608 $headUnescaped = $this->
msg(
"createacct-benefit-head$benefitIdx" )->text();
609 $iconClass = $this->
msg(
"createacct-benefit-icon$benefitIdx" )->text();
610 $benefitList .= Html::rawElement(
'div', [
'class' =>
"mw-number-text $iconClass" ],
611 Html::rawElement(
'h3', [],
612 $this->
msg(
"createacct-benefit-head$benefitIdx" )->escaped()
614 . Html::rawElement(
'p', [],
615 $this->
msg(
"createacct-benefit-body$benefitIdx" )->params( $headUnescaped )->escaped()
619 $benefitsContainer = Html::rawElement(
'div', [
'class' =>
'mw-createacct-benefits-container' ],
620 Html::rawElement(
'h2', [], $this->
msg(
'createacct-benefit-heading' )->escaped() )
621 . Html::rawElement(
'div', [
'class' =>
'mw-createacct-benefits-list' ],
627 $html = Html::rawElement(
'div', [
'class' =>
'mw-ui-container' ],
631 . Html::rawElement(
'div', [
'id' =>
'userloginForm' ],
648 protected function getAuthForm( array $requests, $action, $msg =
'', $msgType =
'error' ) {
652 if ( isset( $this->authForm ) ) {
656 $usingHTTPS = $this->
getRequest()->getProtocol() ===
'https';
659 $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
661 $formDescriptor = static::fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction );
672 $form->addHiddenField(
'authAction', $this->authAction );
673 if ( $this->mLanguage ) {
674 $form->addHiddenField(
'uselang', $this->mLanguage );
676 $form->addHiddenField(
'force', $this->securityLevel );
681 $form->addHiddenField(
'wpForceHttps', (
int)$this->mStickHTTPS );
682 $form->addHiddenField(
'wpFromhttp', $usingHTTPS );
688 $form->setName(
'userlogin' . ( $this->
isSignup() ?
'2' :
'' ) );
690 $form->setId(
'userlogin2' );
693 $form->suppressDefaultSubmit();
695 $this->authForm = $form;
701 array $requests, array $fieldInfo, array &$formDescriptor, $action
706 foreach ( $coreFieldDescriptors as $fieldName => $coreField ) {
707 $requestField = $formDescriptor[$fieldName] ?? [];
712 !isset( $fieldInfo[$fieldName] )
714 !isset( $coreField[
'baseField'] )
715 || !isset( $fieldInfo[$coreField[
'baseField']] )
718 !isset( $coreField[
'type'] )
719 || !in_array( $coreField[
'type'], [
'submit',
'info' ],
true )
722 $coreFieldDescriptors[$fieldName] =
null;
728 isset( $coreField[
'label'] )
729 || isset( $coreField[
'label-message'] )
730 || isset( $coreField[
'label-raw'] )
732 unset( $requestField[
'label'], $requestField[
'label-message'], $coreField[
'label-raw'] );
735 $coreFieldDescriptors[$fieldName] += $requestField;
738 $formDescriptor = array_filter( $coreFieldDescriptors + $formDescriptor );
760 $isLoggedIn = $this->
getUser()->isLoggedIn();
761 $continuePart = $this->
isContinued() ?
'continue-' :
'';
762 $anotherPart = $isLoggedIn ?
'another-' :
'';
764 $expiration = $this->
getRequest()->getSession()->getProvider()->getRememberUserDuration();
765 $expirationDays = ceil( $expiration / ( 3600 * 24 ) );
766 $secureLoginLink =
'';
767 if ( $this->mSecureLoginUrl ) {
768 $secureLoginLink = Html::element(
'a', [
769 'href' => $this->mSecureLoginUrl,
770 'class' =>
'mw-ui-flush-right mw-secure',
771 ], $this->
msg(
'userlogin-signwithsecure' )->text() );
773 $usernameHelpLink =
'';
774 if ( !$this->
msg(
'createacct-helpusername' )->isDisabled() ) {
775 $usernameHelpLink = Html::rawElement(
'span', [
776 'class' =>
'mw-ui-flush-right',
777 ], $this->
msg(
'createacct-helpusername' )->parse() );
781 $fieldDefinitions = [
787 'default' => Html::element(
'div', [
'id' =>
'mw-createacct-status-area' ] ),
791 'label-raw' => $this->
msg(
'userlogin-yourname' )->escaped() . $usernameHelpLink,
793 'placeholder-message' => $isLoggedIn ?
'createacct-another-username-ph'
794 :
'userlogin-yourname-ph',
799 'label-message' =>
'createaccountmail',
800 'name' =>
'wpCreateaccountMail',
801 'id' =>
'wpCreateaccountMail',
804 'id' =>
'wpPassword2',
805 'placeholder-message' =>
'createacct-yourpassword-ph',
806 'hide-if' => [
'===',
'wpCreateaccountMail',
'1' ],
810 'baseField' =>
'password',
811 'type' =>
'password',
812 'label-message' =>
'createacct-yourpasswordagain',
814 'cssclass' =>
'loginPassword',
816 'validation-callback' =>
function ( $value, $alldata ) {
817 if ( empty( $alldata[
'mailpassword'] ) && !empty( $alldata[
'password'] ) ) {
819 return $this->
msg(
'htmlform-required' );
820 } elseif ( $value !== $alldata[
'password'] ) {
821 return $this->
msg(
'badretype' );
826 'hide-if' => [
'===',
'wpCreateaccountMail',
'1' ],
827 'placeholder-message' =>
'createacct-yourpasswordagain-ph',
831 'label-message' => $wgEmailConfirmToEdit ?
'createacct-emailrequired'
832 :
'createacct-emailoptional',
834 'cssclass' =>
'loginText',
838 'validation-callback' =>
function ( $value, $alldata ) {
846 return $this->
msg(
'noemailtitle' );
847 } elseif ( !$value && !empty( $alldata[
'mailpassword'] ) ) {
849 return $this->
msg(
'noemailcreate' );
850 } elseif ( $value && !Sanitizer::validateEmail( $value ) ) {
851 return $this->
msg(
'invalidemailaddress' );
855 'placeholder-message' =>
'createacct-' . $anotherPart .
'email-ph',
859 'help-message' => $isLoggedIn ?
'createacct-another-realname-tip'
860 :
'prefs-help-realname',
861 'label-message' =>
'createacct-realname',
862 'cssclass' =>
'loginText',
864 'id' =>
'wpRealName',
869 'label-message' =>
'createacct-reason',
870 'cssclass' =>
'loginText',
873 'placeholder-message' =>
'createacct-reason-ph',
878 'default' => $this->
msg(
'createacct-' . $anotherPart . $continuePart .
880 'name' =>
'wpCreateaccount',
881 'id' =>
'wpCreateaccount',
886 $fieldDefinitions = [
888 'label-raw' => $this->
msg(
'userlogin-yourname' )->escaped() . $secureLoginLink,
890 'placeholder-message' =>
'userlogin-yourname-ph',
893 'id' =>
'wpPassword1',
894 'placeholder-message' =>
'userlogin-yourpassword-ph',
900 'name' =>
'wpRemember',
901 'label-message' => $this->
msg(
'userlogin-remembermypassword' )
902 ->numParams( $expirationDays ),
903 'id' =>
'wpRemember',
908 'default' => $this->
msg(
'pt-login-' . $continuePart .
'button' )->text(),
909 'id' =>
'wpLoginAttempt',
915 'cssclass' =>
'mw-form-related-link-container mw-userlogin-help',
918 'default' => Html::element(
'a', [
920 ->inContentLanguage()
922 ], $this->
msg(
'userlogin-helplink2' )->text() ),
933 $fieldDefinitions[
'username'] += [
936 'cssclass' =>
'loginText',
940 $fieldDefinitions[
'password'] += [
941 'type' =>
'password',
943 'name' =>
'wpPassword',
944 'cssclass' =>
'loginPassword',
949 if ( $this->mEntryError ) {
950 $fieldDefinitions[
'entryError'] = [
952 'default' => Html::rawElement(
'div', [
'class' => $this->mEntryErrorType .
'box', ],
953 $this->mEntryError ),
960 unset( $fieldDefinitions[
'linkcontainer'], $fieldDefinitions[
'signupend'] );
965 $signupendMsg = $this->
msg(
'signupend' );
966 $signupendHttpsMsg = $this->
msg(
'signupend-https' );
967 if ( !$signupendMsg->isDisabled() ) {
968 $usingHTTPS = $this->
getRequest()->getProtocol() ===
'https';
969 $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
970 ? $signupendHttpsMsg->parse() : $signupendMsg->parse();
971 $fieldDefinitions[
'signupend'] = [
974 'default' => Html::rawElement(
'div', [
'id' =>
'signupend' ], $signupendText ),
980 $passwordReset = MediaWikiServices::getInstance()->getPasswordReset();
981 if ( $passwordReset->isAllowed( $this->getUser() )->isGood() ) {
982 $fieldDefinitions[
'passwordReset'] = [
985 'cssclass' =>
'mw-form-related-link-container',
988 $this->
msg(
'userlogin-resetpassword-link' )->text()
1000 if ( $this->mLanguage ) {
1001 $linkq .=
'&uselang=' . urlencode( $this->mLanguage );
1003 $loggedIn = $this->
getUser()->isLoggedIn();
1005 $fieldDefinitions[
'createOrLogin'] = [
1008 'linkQuery' => $linkq,
1009 'default' =>
function ( $params ) use ( $loggedIn, $linkTitle ) {
1010 return Html::rawElement(
'div',
1011 [
'id' =>
'mw-createaccount' . ( !$loggedIn ?
'-cta' :
'' ),
1012 'class' => ( $loggedIn ?
'mw-form-related-link-container' :
'mw-ui-vform-field' ) ],
1013 ( $loggedIn ?
'' : $this->
msg(
'userlogin-noaccount' )->escaped() )
1014 . Html::element(
'a',
1016 'id' =>
'mw-createaccount-join' . ( $loggedIn ?
'-loggedin' :
'' ),
1017 'href' => $linkTitle->getLocalURL( $params[
'linkQuery'] ),
1018 'class' => ( $loggedIn ?
'' :
'mw-ui-button' ),
1022 $loggedIn ?
'userlogin-createanother' :
'userlogin-joinproject'
1032 return $fieldDefinitions;
1060 if ( $this->mReturnTo !==
'' ) {
1061 $returnto =
'returnto=' .
wfUrlencode( $this->mReturnTo );
1062 if ( $this->mReturnToQuery !==
'' ) {
1063 $returnto .=
'&returntoquery=' .
wfUrlencode( $this->mReturnToQuery );
1077 } elseif ( MediaWikiServices::getInstance()
1079 ->userHasRight( $this->
getUser(),
'createaccount' )
1088 return $this->
isSignup() ?
'wpCreateaccountToken' :
'wpLoginToken';
1098 $msg = $this->
msg(
'loginlanguagelinks' )->inContentLanguage();
1099 if ( $msg->isBlank() ) {
1102 $langs = explode(
"\n", $msg->text() );
1104 foreach ( $langs as
$lang ) {
1106 $parts = explode(
'|',
$lang );
1107 if ( count( $parts ) >= 2 ) {
1112 return count( $links ) > 0 ? $this->
msg(
'loginlanguagelabel' )->rawParams(
1113 $this->
getLanguage()->pipeList( $links ) )->escaped() :
'';
1127 return htmlspecialchars( $text );
1129 $query = [
'uselang' =>
$lang ];
1130 if ( $this->mReturnTo !==
'' ) {
1137 $attr[
'lang'] = $attr[
'hreflang'] = $targetLanguage->getHtmlCode();
1158 isset( $formDescriptor[
'username'] ) &&
1159 !isset( $formDescriptor[
'username'][
'default'] ) &&
1163 if ( $user->isLoggedIn() ) {
1164 $formDescriptor[
'username'][
'default'] = $user->getName();
1166 $formDescriptor[
'username'][
'default'] =
1167 $this->
getRequest()->getSession()->suggestLoginUsername();
1174 unset( $formDescriptor[
'createaccount'], $formDescriptor[
'loginattempt'] );
1181 isset( $formDescriptor[
'username'] )
1182 && empty( $formDescriptor[
'username'][
'default'] )
1183 && !$this->
getRequest()->getCheck(
'wpName' )
1185 $formDescriptor[
'username'][
'autofocus'] =
true;
1186 } elseif ( isset( $formDescriptor[
'password'] ) ) {
1187 $formDescriptor[
'password'][
'autofocus'] =
true;