30 use Wikimedia\ScopedCallback;
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->
getConfig()->get(
'ForceHTTPS' )
111 || ( !$this->mFromHTTP && $request->getProtocol() ===
'https' )
112 || $request->getBool(
'wpForceHttps',
false );
113 $this->mLanguage = $request->getText(
'uselang' );
114 $this->mReturnTo = $request->getVal(
'returnto',
'' );
115 $this->mReturnToQuery = $request->getVal(
'returntoquery',
'' );
127 if ( $this->mLoaded ) {
130 $this->mLoaded =
true;
135 $securityLevel && AuthManager::singleton()->securitySensitiveOperationStatus(
143 $this->mToken = $request->getVal( $this->
getTokenName() );
146 $entryError = $this->
msg( $request->getVal(
'error',
'' ) );
147 $entryWarning = $this->
msg( $request->getVal(
'warning',
'' ) );
152 $this->
msg(
'loginreqlink' )->text(),
155 'returnto' => $this->mReturnTo,
156 'returntoquery' => $this->mReturnToQuery,
157 'uselang' => $this->mLanguage ?:
null,
163 if ( $entryError->exists()
166 $this->mEntryErrorType =
'error';
167 $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse();
169 } elseif ( $entryWarning->exists()
172 $this->mEntryErrorType =
'warning';
173 $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse();
176 # 1. When switching accounts, it sucks to get automatically logged out
177 # 2. Do not return to PasswordReset after a successful password change
178 # but goto Wiki start page (Main_Page) instead ( T35997 )
180 if ( is_object( $returnToTitle )
181 && ( $returnToTitle->isSpecial(
'Userlogout' )
182 || $returnToTitle->isSpecial(
'PasswordReset' ) )
184 $this->mReturnTo =
'';
185 $this->mReturnToQuery =
'';
192 $params = parent::getPreservedParams( $withToken );
194 'returnto' => $this->mReturnTo ?:
null,
195 'returntoquery' => $this->mReturnToQuery ?:
null,
198 $params[
'fromhttp'] = $this->mFromHTTP ?
'1' :
null;
206 return parent::beforeExecute(
$subPage );
214 if ( $this->mPosted ) {
215 $time = microtime(
true );
216 $profilingScope =
new ScopedCallback(
function () use ( $time ) {
217 $time = microtime(
true ) - $time;
218 $statsd = MediaWikiServices::getInstance()->getStatsdDataFactory();
219 $statsd->timing(
"timing.login.ui.{$this->authAction}", $time * 1000 );
223 $authManager = AuthManager::singleton();
224 $session = SessionManager::getGlobalSession();
235 if ( !$this->
isSignup() && !$authManager->canAuthenticateNow() ) {
236 if ( !$session->canSetUser() ) {
237 throw new ErrorPageError(
'cannotloginnow-title',
'cannotloginnow-text', [
241 throw new ErrorPageError(
'cannotlogin-title',
'cannotlogin-text' );
242 } elseif ( $this->
isSignup() && !$authManager->canCreateAccounts() ) {
243 throw new ErrorPageError(
'cannotcreateaccount-title',
'cannotcreateaccount-text' );
260 if ( !$this->
isSignup() && !$this->mPosted && !$this->securityLevel &&
261 ( $this->mReturnTo !==
'' || $this->mReturnToQuery !==
'' ) &&
270 if ( $this->
getRequest()->getProtocol() !==
'https' ) {
274 ( $this->mEntryErrorType ===
'error' ?
'error'
275 :
'warning' ) => $this->mEntryError,
285 $this->
getOutput()->addVaryHeader(
'X-Forwarded-Proto' );
292 if ( substr( $url, 0, 8 ) ===
'https://' ) {
293 $this->mSecureLoginUrl = $url;
302 $this->
mainLoginForm( [],
'authpage-cannot-' . $this->authAction );
309 if ( $button_name ) {
310 $this->
getRequest()->setVal( $button_name,
true );
316 if ( !$status || !$status->isGood() ) {
317 $this->
mainLoginForm( $this->authRequests, $status ? $status->getMessage() :
'',
'error' );
327 case AuthenticationResponse::PASS:
329 $this->proxyAccountCreation = $this->
isSignup() && !$this->
getUser()->isAnon();
333 !$this->proxyAccountCreation
335 && $authManager->canAuthenticateNow()
338 $response2 = $authManager->beginAuthentication( [
$response->loginRequest ],
340 if ( $response2->status !== AuthenticationResponse::PASS ) {
341 LoggerFactory::getInstance(
'login' )
342 ->error(
'Could not log in after account creation' );
348 if ( !$this->proxyAccountCreation ) {
355 case AuthenticationResponse::FAIL:
357 case AuthenticationResponse::RESTART:
358 unset( $this->authForm );
359 if (
$response->status === AuthenticationResponse::FAIL ) {
361 $messageType =
'error';
364 $messageType =
'warning';
370 case AuthenticationResponse::REDIRECT:
371 unset( $this->authForm );
374 case AuthenticationResponse::UI:
375 unset( $this->authForm );
376 $this->authAction = $this->
isSignup() ? AuthManager::ACTION_CREATE_CONTINUE
377 : AuthManager::ACTION_LOGIN_CONTINUE;
378 $this->authRequests =
$response->neededRequests;
382 throw new LogicException(
'invalid AuthenticationResponse' );
404 $fields = AuthenticationRequest::mergeFieldInfo( $this->authRequests );
405 foreach ( $fields as $fieldname => $field ) {
406 if ( !isset( $field[
'type'] ) ) {
409 if ( !empty( $field[
'skippable'] ) ) {
412 if ( $field[
'type'] ===
'button' ) {
413 if ( $button_name !==
null ) {
417 $button_name = $fieldname;
419 } elseif ( $field[
'type'] !==
'null' ) {
436 $type,
$title, $msgname, $injected_html, $extraMessages
439 $out->setPageTitle(
$title );
443 if ( $extraMessages ) {
445 $out->addWikiTextAsInterface(
446 $extraMessages->getWikiText(
false,
false, $this->getLanguage() )
450 $out->addHTML( $injected_html );
453 $helper->showReturnToPage(
$type, $this->mReturnTo, $this->mReturnToQuery, $this->mStickHTTPS );
472 $type, $returnTo =
'', $returnToQuery =
'', $stickHTTPS =
false
475 $helper->showReturnToPage(
$type, $returnTo, $returnToQuery, $stickHTTPS );
492 $user =
$context->getRequest()->getSession()->getUser();
514 protected function mainLoginForm( array $requests, $msg =
'', $msgtype =
'error' ) {
522 $this->authForm =
null;
523 $requests = AuthManager::singleton()->getAuthenticationRequests( $this->authAction, $user );
527 $out->addModuleStyles( [
529 'mediawiki.ui.button',
530 'mediawiki.ui.checkbox',
531 'mediawiki.ui.input',
532 'mediawiki.special.userlogin.common.styles'
536 $out->addJsConfigVars(
'wgCreateacctImgcaptchaHelp',
537 $this->
msg(
'createacct-imgcaptcha-help' )->parse() );
541 'mediawiki.special.userlogin.signup.js'
543 $out->addModuleStyles( [
544 'mediawiki.special.userlogin.signup.styles'
548 $out->addModuleStyles( [
549 'mediawiki.special.userlogin.login.styles'
552 $out->disallowUserJs();
554 $form = $this->
getAuthForm( $requests, $this->authAction, $msg, $msgtype );
555 $form->prepareForm();
558 if ( $msg && $msgtype ===
'warning' ) {
559 $submitStatus->warning( $msg );
560 } elseif ( $msg && $msgtype ===
'error' ) {
561 $submitStatus->fatal( $msg );
567 $this->
getUser()->isLoggedIn() &&
568 $this->authAction !== AuthManager::ACTION_LOGIN_CONTINUE
570 $reauthMessage = $this->securityLevel ?
'userlogin-reauth' :
'userlogin-loggedin';
571 $submitStatus->warning( $reauthMessage, $this->
getUser()->
getName() );
574 $formHtml = $form->getHTML( $submitStatus );
589 [
'id' =>
'userloginprompt' ], $this->
msg(
'loginprompt' )->parseAsBlock() );
591 $signupStartMsg = $this->
msg(
'signupstart' );
592 $signupStart = ( $this->
isSignup() && !$signupStartMsg->isDisabled() )
593 ?
Html::rawElement(
'div', [
'id' =>
'signupstart' ], $signupStartMsg->parseAsBlock() ) :
'';
594 if ( $languageLinks ) {
600 $benefitsContainer =
'';
608 for ( $benefitIdx = 1; $benefitIdx <= $benefitCount; $benefitIdx++ ) {
609 $headUnescaped = $this->
msg(
"createacct-benefit-head$benefitIdx" )->text();
610 $iconClass = $this->
msg(
"createacct-benefit-icon$benefitIdx" )->text();
611 $benefitList .=
Html::rawElement(
'div', [
'class' =>
"mw-number-text $iconClass" ],
613 $this->
msg(
"createacct-benefit-head$benefitIdx" )->escaped()
616 $this->
msg(
"createacct-benefit-body$benefitIdx" )->params( $headUnescaped )->escaped()
620 $benefitsContainer =
Html::rawElement(
'div', [
'class' =>
'mw-createacct-benefits-container' ],
649 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 );
665 if (
$context->getRequest() !== $this->getRequest() ) {
672 $form->addHiddenField(
'authAction', $this->authAction );
673 if ( $this->mLanguage ) {
674 $form->addHiddenField(
'uselang', $this->mLanguage );
676 $form->addHiddenField(
'force', $this->securityLevel );
679 if ( $config->get(
'SecureLogin' ) && !$config->get(
'ForceHTTPS' ) ) {
682 $form->addHiddenField(
'wpForceHttps', (
int)$this->mStickHTTPS );
683 $form->addHiddenField(
'wpFromhttp', $usingHTTPS );
689 $form->setName(
'userlogin' . ( $this->
isSignup() ?
'2' :
'' ) );
691 $form->setId(
'userlogin2' );
694 $form->suppressDefaultSubmit();
696 $this->authForm = $form;
702 array $requests, array $fieldInfo, array &$formDescriptor, $action
707 foreach ( $coreFieldDescriptors as $fieldName => $coreField ) {
708 $requestField = $formDescriptor[$fieldName] ?? [];
713 !isset( $fieldInfo[$fieldName] )
715 !isset( $coreField[
'baseField'] )
716 || !isset( $fieldInfo[$coreField[
'baseField']] )
719 !isset( $coreField[
'type'] )
720 || !in_array( $coreField[
'type'], [
'submit',
'info' ],
true )
723 $coreFieldDescriptors[$fieldName] =
null;
729 isset( $coreField[
'label'] )
730 || isset( $coreField[
'label-message'] )
731 || isset( $coreField[
'label-raw'] )
733 unset( $requestField[
'label'], $requestField[
'label-message'], $coreField[
'label-raw'] );
736 $coreFieldDescriptors[$fieldName] += $requestField;
739 $formDescriptor = array_filter( $coreFieldDescriptors + $formDescriptor );
761 $isLoggedIn = $this->
getUser()->isLoggedIn();
762 $continuePart = $this->
isContinued() ?
'continue-' :
'';
763 $anotherPart = $isLoggedIn ?
'another-' :
'';
765 $expiration = $this->
getRequest()->getSession()->getProvider()->getRememberUserDuration();
766 $expirationDays = ceil( $expiration / ( 3600 * 24 ) );
767 $secureLoginLink =
'';
768 if ( $this->mSecureLoginUrl ) {
770 'href' => $this->mSecureLoginUrl,
771 'class' =>
'mw-ui-flush-right mw-secure',
772 ], $this->
msg(
'userlogin-signwithsecure' )->text() );
774 $usernameHelpLink =
'';
775 if ( !$this->
msg(
'createacct-helpusername' )->isDisabled() ) {
777 'class' =>
'mw-ui-flush-right',
778 ], $this->
msg(
'createacct-helpusername' )->parse() );
782 $fieldDefinitions = [
788 'default' =>
Html::element(
'div', [
'id' =>
'mw-createacct-status-area' ] ),
792 'label-raw' => $this->
msg(
'userlogin-yourname' )->escaped() . $usernameHelpLink,
794 'placeholder-message' => $isLoggedIn ?
'createacct-another-username-ph'
795 :
'userlogin-yourname-ph',
800 'label-message' =>
'createaccountmail',
801 'name' =>
'wpCreateaccountMail',
802 'id' =>
'wpCreateaccountMail',
805 'id' =>
'wpPassword2',
806 'placeholder-message' =>
'createacct-yourpassword-ph',
807 'hide-if' => [
'===',
'wpCreateaccountMail',
'1' ],
811 'baseField' =>
'password',
812 'type' =>
'password',
813 'label-message' =>
'createacct-yourpasswordagain',
815 'cssclass' =>
'loginPassword',
817 'validation-callback' =>
function ( $value, $alldata ) {
818 if ( empty( $alldata[
'mailpassword'] ) && !empty( $alldata[
'password'] ) ) {
820 return $this->
msg(
'htmlform-required' );
821 } elseif ( $value !== $alldata[
'password'] ) {
822 return $this->
msg(
'badretype' );
827 'hide-if' => [
'===',
'wpCreateaccountMail',
'1' ],
828 'placeholder-message' =>
'createacct-yourpasswordagain-ph',
832 'label-message' => $wgEmailConfirmToEdit ?
'createacct-emailrequired'
833 :
'createacct-emailoptional',
835 'cssclass' =>
'loginText',
839 'validation-callback' =>
function ( $value, $alldata ) {
847 return $this->
msg(
'noemailtitle' );
848 } elseif ( !$value && !empty( $alldata[
'mailpassword'] ) ) {
850 return $this->
msg(
'noemailcreate' );
852 return $this->
msg(
'invalidemailaddress' );
856 'placeholder-message' =>
'createacct-' . $anotherPart .
'email-ph',
860 'help-message' => $isLoggedIn ?
'createacct-another-realname-tip'
861 :
'prefs-help-realname',
862 'label-message' =>
'createacct-realname',
863 'cssclass' =>
'loginText',
865 'id' =>
'wpRealName',
870 'label-message' =>
'createacct-reason',
871 'cssclass' =>
'loginText',
874 'placeholder-message' =>
'createacct-reason-ph',
879 'default' => $this->
msg(
'createacct-' . $anotherPart . $continuePart .
881 'name' =>
'wpCreateaccount',
882 'id' =>
'wpCreateaccount',
887 $fieldDefinitions = [
889 'label-raw' => $this->
msg(
'userlogin-yourname' )->escaped() . $secureLoginLink,
891 'placeholder-message' =>
'userlogin-yourname-ph',
894 'id' =>
'wpPassword1',
895 'placeholder-message' =>
'userlogin-yourpassword-ph',
901 'cssclass' =>
'mw-userlogin-rememberme',
902 'name' =>
'wpRemember',
903 'label-message' => $this->
msg(
'userlogin-remembermypassword' )
904 ->numParams( $expirationDays ),
905 'id' =>
'wpRemember',
910 'default' => $this->
msg(
'pt-login-' . $continuePart .
'button' )->text(),
911 'id' =>
'wpLoginAttempt',
917 'cssclass' =>
'mw-form-related-link-container mw-userlogin-help',
922 ->inContentLanguage()
924 ], $this->
msg(
'userlogin-helplink2' )->text() ),
935 $fieldDefinitions[
'username'] += [
938 'cssclass' =>
'loginText',
942 $fieldDefinitions[
'password'] += [
943 'type' =>
'password',
945 'name' =>
'wpPassword',
946 'cssclass' =>
'loginPassword',
951 if ( $this->mEntryError ) {
952 $fieldDefinitions[
'entryError'] = [
954 'default' =>
Html::rawElement(
'div', [
'class' => $this->mEntryErrorType .
'box', ],
955 $this->mEntryError ),
962 unset( $fieldDefinitions[
'linkcontainer'], $fieldDefinitions[
'signupend'] );
967 $signupendMsg = $this->
msg(
'signupend' );
968 $signupendHttpsMsg = $this->
msg(
'signupend-https' );
969 if ( !$signupendMsg->isDisabled() ) {
970 $usingHTTPS = $this->
getRequest()->getProtocol() ===
'https';
971 $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
972 ? $signupendHttpsMsg->parse() : $signupendMsg->parse();
973 $fieldDefinitions[
'signupend'] = [
976 'default' =>
Html::rawElement(
'div', [
'id' =>
'signupend' ], $signupendText ),
982 $passwordReset = MediaWikiServices::getInstance()->getPasswordReset();
983 if ( $passwordReset->isAllowed( $this->getUser() )->isGood() ) {
984 $fieldDefinitions[
'passwordReset'] = [
987 'cssclass' =>
'mw-form-related-link-container',
990 $this->
msg(
'userlogin-resetpassword-link' )->text()
1002 if ( $this->mLanguage ) {
1003 $linkq .=
'&uselang=' . urlencode( $this->mLanguage );
1005 $loggedIn = $this->
getUser()->isLoggedIn();
1007 $fieldDefinitions[
'createOrLogin'] = [
1010 'linkQuery' => $linkq,
1011 'default' =>
function ( $params ) use ( $loggedIn, $linkTitle ) {
1013 [
'id' =>
'mw-createaccount' . ( !$loggedIn ?
'-cta' :
'' ),
1014 'class' => ( $loggedIn ?
'mw-form-related-link-container' :
'mw-ui-vform-field' ) ],
1015 ( $loggedIn ?
'' : $this->
msg(
'userlogin-noaccount' )->escaped() )
1018 'id' =>
'mw-createaccount-join' . ( $loggedIn ?
'-loggedin' :
'' ),
1019 'href' => $linkTitle->getLocalURL( $params[
'linkQuery'] ),
1020 'class' => ( $loggedIn ?
'' :
'mw-ui-button' ),
1024 $loggedIn ?
'userlogin-createanother' :
'userlogin-joinproject'
1034 return $fieldDefinitions;
1062 if ( $this->mReturnTo !==
'' ) {
1063 $returnto =
'returnto=' .
wfUrlencode( $this->mReturnTo );
1064 if ( $this->mReturnToQuery !==
'' ) {
1065 $returnto .=
'&returntoquery=' .
wfUrlencode( $this->mReturnToQuery );
1079 } elseif ( MediaWikiServices::getInstance()
1081 ->userHasRight( $this->
getUser(),
'createaccount' )
1090 return $this->
isSignup() ?
'wpCreateaccountToken' :
'wpLoginToken';
1100 $msg = $this->
msg(
'loginlanguagelinks' )->inContentLanguage();
1101 if ( $msg->isBlank() ) {
1104 $langs = explode(
"\n", $msg->text() );
1106 foreach ( $langs as
$lang ) {
1108 $parts = explode(
'|',
$lang );
1109 if ( count( $parts ) >= 2 ) {
1114 return count( $links ) > 0 ? $this->
msg(
'loginlanguagelabel' )->rawParams(
1115 $this->
getLanguage()->pipeList( $links ) )->escaped() :
'';
1129 return htmlspecialchars( $text );
1131 $query = [
'uselang' =>
$lang ];
1132 if ( $this->mReturnTo !==
'' ) {
1139 $attr[
'lang'] = $attr[
'hreflang'] = $targetLanguage->getHtmlCode();
1160 isset( $formDescriptor[
'username'] ) &&
1161 !isset( $formDescriptor[
'username'][
'default'] ) &&
1165 if ( $user->isLoggedIn() ) {
1166 $formDescriptor[
'username'][
'default'] = $user->getName();
1168 $formDescriptor[
'username'][
'default'] =
1169 $this->
getRequest()->getSession()->suggestLoginUsername();
1176 unset( $formDescriptor[
'createaccount'], $formDescriptor[
'loginattempt'] );
1183 isset( $formDescriptor[
'username'] )
1184 && empty( $formDescriptor[
'username'][
'default'] )
1185 && !$this->
getRequest()->getCheck(
'wpName' )
1187 $formDescriptor[
'username'][
'autofocus'] =
true;
1188 } elseif ( isset( $formDescriptor[
'password'] ) ) {
1189 $formDescriptor[
'password'][
'autofocus'] =
true;