MediaWiki REL1_39
LoginSignupSpecialPage.php
Go to the documentation of this file.
1<?php
32use Wikimedia\ScopedCallback;
33
40 protected $mReturnTo;
41 protected $mPosted;
42 protected $mAction;
43 protected $mLanguage;
44 protected $mReturnToQuery;
45 protected $mToken;
46 protected $mStickHTTPS;
47 protected $mFromHTTP;
48 protected $mEntryError = '';
49 protected $mEntryErrorType = 'error';
50
51 protected $mLoaded = false;
52 protected $mLoadedRequest = false;
54 private $reasonValidatorResult = null;
55
57 protected $securityLevel;
58
64 protected $targetUser;
65
67 protected $authForm;
68
69 abstract protected function isSignup();
70
77 abstract protected function successfulAction( $direct = false, $extraMessages = null );
78
84 abstract protected function logAuthResult( $success, $status = null );
85
86 public function __construct( $name, $restriction = '' ) {
87 // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionConfigUsage
89 parent::__construct( $name, $restriction );
90
91 // Override UseMediaWikiEverywhere to true, to force login and create form to use mw ui
93 }
94
95 protected function setRequest( array $data, $wasPosted = null ) {
96 parent::setRequest( $data, $wasPosted );
97 $this->mLoadedRequest = false;
98 }
99
103 private function loadRequestParameters() {
104 if ( $this->mLoadedRequest ) {
105 return;
106 }
107 $this->mLoadedRequest = true;
108 $request = $this->getRequest();
109
110 $this->mPosted = $request->wasPosted();
111 $this->mAction = $request->getRawVal( 'action' );
112 $this->mFromHTTP = $request->getBool( 'fromhttp', false )
113 || $request->getBool( 'wpFromhttp', false );
114 $this->mStickHTTPS = $this->getConfig()->get( MainConfigNames::ForceHTTPS )
115 || ( !$this->mFromHTTP && $request->getProtocol() === 'https' )
116 || $request->getBool( 'wpForceHttps', false );
117 $this->mLanguage = $request->getText( 'uselang' );
118 $this->mReturnTo = $request->getVal( 'returnto', '' );
119 $this->mReturnToQuery = $request->getVal( 'returntoquery', '' );
120 }
121
127 protected function load( $subPage ) {
128 $this->loadRequestParameters();
129 if ( $this->mLoaded ) {
130 return;
131 }
132 $this->mLoaded = true;
133 $request = $this->getRequest();
134
135 $securityLevel = $this->getRequest()->getText( 'force' );
136 if (
137 $securityLevel &&
138 MediaWikiServices::getInstance()->getAuthManager()->securitySensitiveOperationStatus(
139 $securityLevel ) === AuthManager::SEC_REAUTH
140 ) {
141 $this->securityLevel = $securityLevel;
142 }
143
144 $this->loadAuth( $subPage );
145
146 $this->mToken = $request->getVal( $this->getTokenName() );
147
148 // Show an error or warning passed on from a previous page
149 $entryError = $this->msg( $request->getVal( 'error', '' ) );
150 $entryWarning = $this->msg( $request->getVal( 'warning', '' ) );
151 // bc: provide login link as a parameter for messages where the translation
152 // was not updated
153 $loginreqlink = $this->getLinkRenderer()->makeKnownLink(
154 $this->getPageTitle(),
155 $this->msg( 'loginreqlink' )->text(),
156 [],
157 [
158 'returnto' => $this->mReturnTo,
159 'returntoquery' => $this->mReturnToQuery,
160 'uselang' => $this->mLanguage ?: null,
161 'fromhttp' => $this->getConfig()->get( MainConfigNames::SecureLogin ) &&
162 $this->mFromHTTP ? '1' : null,
163 ]
164 );
165
166 // Only show valid error or warning messages.
167 if ( $entryError->exists()
168 && in_array( $entryError->getKey(), LoginHelper::getValidErrorMessages(), true )
169 ) {
170 $this->mEntryErrorType = 'error';
171 $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse();
172
173 } elseif ( $entryWarning->exists()
174 && in_array( $entryWarning->getKey(), LoginHelper::getValidErrorMessages(), true )
175 ) {
176 $this->mEntryErrorType = 'warning';
177 $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse();
178 }
179
180 # 1. When switching accounts, it sucks to get automatically logged out
181 # 2. Do not return to PasswordReset after a successful password change
182 # but goto Wiki start page (Main_Page) instead ( T35997 )
183 $returnToTitle = Title::newFromText( $this->mReturnTo );
184 if ( is_object( $returnToTitle )
185 && ( $returnToTitle->isSpecial( 'Userlogout' )
186 || $returnToTitle->isSpecial( 'PasswordReset' ) )
187 ) {
188 $this->mReturnTo = '';
189 $this->mReturnToQuery = '';
190 }
191 }
192
193 protected function getPreservedParams( $withToken = false ) {
194 $params = parent::getPreservedParams( $withToken );
195 $params += [
196 'returnto' => $this->mReturnTo ?: null,
197 'returntoquery' => $this->mReturnToQuery ?: null,
198 ];
199 if ( $this->getConfig()->get( MainConfigNames::SecureLogin ) && !$this->isSignup() ) {
200 $params['fromhttp'] = $this->mFromHTTP ? '1' : null;
201 }
202 return $params;
203 }
204
205 protected function beforeExecute( $subPage ) {
206 // finish initializing the class before processing the request - T135924
207 $this->loadRequestParameters();
208 return parent::beforeExecute( $subPage );
209 }
210
215 public function execute( $subPage ) {
216 if ( $this->mPosted ) {
217 $time = microtime( true );
218 $profilingScope = new ScopedCallback( function () use ( $time ) {
219 $time = microtime( true ) - $time;
220 $statsd = MediaWikiServices::getInstance()->getStatsdDataFactory();
221 $statsd->timing( "timing.login.ui.{$this->authAction}", $time * 1000 );
222 } );
223 }
224
225 $authManager = MediaWikiServices::getInstance()->getAuthManager();
226 $session = SessionManager::getGlobalSession();
227
228 // Session data is used for various things in the authentication process, so we must make
229 // sure a session cookie or some equivalent mechanism is set.
230 $session->persist();
231 // Explicitly disable cache to ensure cookie blocks may be set (T152462).
232 // (Technically redundant with sessions persisting from this page.)
233 $this->getOutput()->disableClientCache();
234
235 $this->load( $subPage );
236 $this->setHeaders();
237 $this->checkPermissions();
238
239 // Make sure the system configuration allows log in / sign up
240 if ( !$this->isSignup() && !$authManager->canAuthenticateNow() ) {
241 if ( !$session->canSetUser() ) {
242 throw new ErrorPageError( 'cannotloginnow-title', 'cannotloginnow-text', [
243 $session->getProvider()->describe( $this->getLanguage() )
244 ] );
245 }
246 throw new ErrorPageError( 'cannotlogin-title', 'cannotlogin-text' );
247 } elseif ( $this->isSignup() && !$authManager->canCreateAccounts() ) {
248 throw new ErrorPageError( 'cannotcreateaccount-title', 'cannotcreateaccount-text' );
249 }
250
251 /*
252 * In the case where the user is already logged in, and was redirected to
253 * the login form from a page that requires login, do not show the login
254 * page. The use case scenario for this is when a user opens a large number
255 * of tabs, is redirected to the login page on all of them, and then logs
256 * in on one, expecting all the others to work properly.
257 *
258 * However, do show the form if it was visited intentionally (no 'returnto'
259 * is present). People who often switch between several accounts have grown
260 * accustomed to this behavior.
261 *
262 * For temporary users, the form is always shown, since the UI presents
263 * temporary users as not logged in and offers to discard their temporary
264 * account by logging in.
265 *
266 * Also make an exception when force=<level> is set in the URL, which means the user must
267 * reauthenticate for security reasons.
268 */
269 if ( !$this->isSignup() && !$this->mPosted && !$this->securityLevel &&
270 ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' ) &&
271 !$this->getUser()->isTemp() && $this->getUser()->isRegistered()
272 ) {
273 $this->successfulAction();
274 return;
275 }
276
277 // If logging in and not on HTTPS, either redirect to it or offer a link.
278 if ( $this->getRequest()->getProtocol() !== 'https' ) {
279 $title = $this->getFullTitle();
280 $query = $this->getPreservedParams( false ) + [
281 'title' => null,
282 ( $this->mEntryErrorType === 'error' ? 'error'
283 : 'warning' ) => $this->mEntryError,
284 ] + $this->getRequest()->getQueryValues();
285 $url = $title->getFullURL( $query, false, PROTO_HTTPS );
286 if ( $this->getConfig()->get( MainConfigNames::SecureLogin ) && !$this->mFromHTTP ) {
287 // Avoid infinite redirect
288 $url = wfAppendQuery( $url, 'fromhttp=1' );
289 $this->getOutput()->redirect( $url );
290 // Since we only do this redir to change proto, always vary
291 $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' );
292
293 return;
294 } else {
295 // A wiki without HTTPS login support should set $wgServer to
296 // http://somehost, in which case the secure URL generated
297 // above won't actually start with https://
298 if ( substr( $url, 0, 8 ) === 'https://' ) {
299 $this->mSecureLoginUrl = $url;
300 }
301 }
302 }
303
304 if ( !$this->isActionAllowed( $this->authAction ) ) {
305 // FIXME how do we explain this to the user? can we handle session loss better?
306 // messages used: authpage-cannot-login, authpage-cannot-login-continue,
307 // authpage-cannot-create, authpage-cannot-create-continue
308 $this->mainLoginForm( [], 'authpage-cannot-' . $this->authAction );
309 return;
310 }
311
312 if ( $this->canBypassForm( $button_name ) ) {
313 $this->setRequest( [], true );
314 $this->getRequest()->setVal( $this->getTokenName(), $this->getToken() );
315 if ( $button_name ) {
316 $this->getRequest()->setVal( $button_name, true );
317 }
318 }
319
320 $status = $this->trySubmit();
321
322 if ( !$status || !$status->isGood() ) {
323 $this->mainLoginForm( $this->authRequests, $status ? $status->getMessage() : '', 'error' );
324 return;
325 }
326
328 $response = $status->getValue();
329
330 $returnToUrl = $this->getPageTitle( 'return' )
331 ->getFullURL( $this->getPreservedParams( true ), false, PROTO_HTTPS );
332 switch ( $response->status ) {
333 case AuthenticationResponse::PASS:
334 $this->logAuthResult( true );
335 $this->proxyAccountCreation = $this->isSignup() && $this->getUser()->isNamed();
336 $this->targetUser = User::newFromName( $response->username );
337
338 if (
339 !$this->proxyAccountCreation
340 && $response->loginRequest
341 && $authManager->canAuthenticateNow()
342 ) {
343 // successful registration; log the user in instantly
344 $response2 = $authManager->beginAuthentication( [ $response->loginRequest ],
345 $returnToUrl );
346 if ( $response2->status !== AuthenticationResponse::PASS ) {
347 LoggerFactory::getInstance( 'login' )
348 ->error( 'Could not log in after account creation' );
349 $this->successfulAction( true, Status::newFatal( 'createacct-loginerror' ) );
350 break;
351 }
352 }
353
354 if ( !$this->proxyAccountCreation ) {
355 // Ensure that the context user is the same as the session user.
357 }
358
359 $this->successfulAction( true );
360 break;
361 case AuthenticationResponse::FAIL:
362 // fall through
363 case AuthenticationResponse::RESTART:
364 unset( $this->authForm );
365 if ( $response->status === AuthenticationResponse::FAIL ) {
366 $action = $this->getDefaultAction( $subPage );
367 $messageType = 'error';
368 } else {
369 $action = $this->getContinueAction( $this->authAction );
370 $messageType = 'warning';
371 }
372 $this->logAuthResult( false, $response->message ? $response->message->getKey() : '-' );
373 $this->loadAuth( $subPage, $action, true );
374 $this->mainLoginForm( $this->authRequests, $response->message, $messageType );
375 break;
376 case AuthenticationResponse::REDIRECT:
377 unset( $this->authForm );
378 $this->getOutput()->redirect( $response->redirectTarget );
379 break;
380 case AuthenticationResponse::UI:
381 unset( $this->authForm );
382 $this->authAction = $this->isSignup() ? AuthManager::ACTION_CREATE_CONTINUE
383 : AuthManager::ACTION_LOGIN_CONTINUE;
384 $this->authRequests = $response->neededRequests;
385 $this->mainLoginForm( $response->neededRequests, $response->message, $response->messageType );
386 break;
387 default:
388 throw new LogicException( 'invalid AuthenticationResponse' );
389 }
390 }
391
405 private function canBypassForm( &$button_name ) {
406 $button_name = null;
407 if ( $this->isContinued() ) {
408 return false;
409 }
410 $fields = AuthenticationRequest::mergeFieldInfo( $this->authRequests );
411 foreach ( $fields as $fieldname => $field ) {
412 if ( !isset( $field['type'] ) ) {
413 return false;
414 }
415 if ( !empty( $field['skippable'] ) ) {
416 continue;
417 }
418 if ( $field['type'] === 'button' ) {
419 if ( $button_name !== null ) {
420 $button_name = null;
421 return false;
422 } else {
423 $button_name = $fieldname;
424 }
425 } elseif ( $field['type'] !== 'null' ) {
426 return false;
427 }
428 }
429 return true;
430 }
431
441 protected function showSuccessPage(
442 $type, $title, $msgname, $injected_html, $extraMessages
443 ) {
444 $out = $this->getOutput();
445 $out->setPageTitle( $title );
446 if ( $msgname ) {
447 $out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) );
448 }
449 if ( $extraMessages ) {
450 $extraMessages = Status::wrap( $extraMessages );
451 $out->addWikiTextAsInterface(
452 $extraMessages->getWikiText( false, false, $this->getLanguage() )
453 );
454 }
455
456 $out->addHTML( $injected_html );
457
458 $helper = new LoginHelper( $this->getContext() );
459 $helper->showReturnToPage( $type, $this->mReturnTo, $this->mReturnToQuery, $this->mStickHTTPS );
460 }
461
477 public function showReturnToPage(
478 $type, $returnTo = '', $returnToQuery = '', $stickHTTPS = false
479 ) {
480 $helper = new LoginHelper( $this->getContext() );
481 $helper->showReturnToPage( $type, $returnTo, $returnToQuery, $stickHTTPS );
482 }
483
488 protected function setSessionUserForCurrentRequest() {
489 global $wgLang;
490
491 $context = RequestContext::getMain();
492 $localContext = $this->getContext();
493 if ( $context !== $localContext ) {
494 // remove AuthManagerSpecialPage context hack
495 $this->setContext( $context );
496 }
497
498 $user = $context->getRequest()->getSession()->getUser();
499
501 $context->setUser( $user );
502
503 $wgLang = $context->getLanguage();
504 }
505
520 protected function mainLoginForm( array $requests, $msg = '', $msgtype = 'error' ) {
521 $user = $this->getUser();
522 $out = $this->getOutput();
523
524 // FIXME how to handle empty $requests - restart, or no form, just an error message?
525 // no form would be better for no session type errors, restart is better when can* fails.
526 if ( !$requests ) {
527 $this->authAction = $this->getDefaultAction( $this->subPage );
528 $this->authForm = null;
529 $requests = MediaWikiServices::getInstance()->getAuthManager()
530 ->getAuthenticationRequests( $this->authAction, $user );
531 }
532
533 // Generic styles and scripts for both login and signup form
534 $out->addModuleStyles( [
535 'mediawiki.ui',
536 'mediawiki.ui.button',
537 'mediawiki.ui.checkbox',
538 'mediawiki.ui.input',
539 'mediawiki.special.userlogin.common.styles'
540 ] );
541 if ( $this->isSignup() ) {
542 // XXX hack pending RL or JS parse() support for complex content messages T27349
543 $out->addJsConfigVars( 'wgCreateacctImgcaptchaHelp',
544 $this->msg( 'createacct-imgcaptcha-help' )->parse() );
545
546 // Additional styles and scripts for signup form
547 $out->addModules( 'mediawiki.special.createaccount' );
548 $out->addModuleStyles( [
549 'mediawiki.special.userlogin.signup.styles'
550 ] );
551 } else {
552 // Additional styles for login form
553 $out->addModuleStyles( [
554 'mediawiki.special.userlogin.login.styles'
555 ] );
556 }
557 $out->disallowUserJs(); // just in case...
558
559 $form = $this->getAuthForm( $requests, $this->authAction, $msg, $msgtype );
560 $form->prepareForm();
561
562 $submitStatus = Status::newGood();
563 if ( $msg && $msgtype === 'warning' ) {
564 $submitStatus->warning( $msg );
565 } elseif ( $msg && $msgtype === 'error' ) {
566 $submitStatus->fatal( $msg );
567 }
568
569 // warning header for non-standard workflows (e.g. security reauthentication)
570 if (
571 !$this->isSignup() &&
572 $this->getUser()->isRegistered() &&
573 !$this->getUser()->isTemp() &&
574 $this->authAction !== AuthManager::ACTION_LOGIN_CONTINUE
575 ) {
576 $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
577 $submitStatus->warning( $reauthMessage, $this->getUser()->getName() );
578 }
579
580 $formHtml = $form->getHTML( $submitStatus );
581
582 $out->addHTML( $this->getPageHtml( $formHtml ) );
583 }
584
591 protected function getPageHtml( $formHtml ) {
592 $loginPrompt = $this->isSignup() ? '' : Html::rawElement( 'div',
593 [ 'id' => 'userloginprompt' ], $this->msg( 'loginprompt' )->parseAsBlock() );
594 $languageLinks = $this->getConfig()->get( MainConfigNames::LoginLanguageSelector )
595 ? $this->makeLanguageSelector() : '';
596 $signupStartMsg = $this->msg( 'signupstart' );
597 $signupStart = ( $this->isSignup() && !$signupStartMsg->isDisabled() )
598 ? Html::rawElement( 'div', [ 'id' => 'signupstart' ], $signupStartMsg->parseAsBlock() ) : '';
599 if ( $languageLinks ) {
600 $languageLinks = Html::rawElement( 'div', [ 'id' => 'languagelinks' ],
601 Html::rawElement( 'p', [], $languageLinks )
602 );
603 }
604
605 return Html::rawElement( 'div', [ 'class' => 'mw-ui-container' ],
606 $loginPrompt
607 . $languageLinks
608 . $signupStart
609 . Html::rawElement( 'div', [ 'id' => 'userloginForm' ], $formHtml )
610 . $this->getBenefitsContainerHtml()
611 );
612 }
613
621 protected function getBenefitsContainerHtml(): string {
622 $benefitsContainer = '';
623 if ( $this->isSignup() && $this->showExtraInformation() ) {
624 // The following messages are used here:
625 // * createacct-benefit-icon1 createacct-benefit-head1 createacct-benefit-body1
626 // * createacct-benefit-icon2 createacct-benefit-head2 createacct-benefit-body2
627 // * createacct-benefit-icon3 createacct-benefit-head3 createacct-benefit-body3
628 $benefitCount = 3;
629 $benefitList = '';
630 for ( $benefitIdx = 1; $benefitIdx <= $benefitCount; $benefitIdx++ ) {
631 $headUnescaped = $this->msg( "createacct-benefit-head$benefitIdx" )->text();
632 $iconClass = $this->msg( "createacct-benefit-icon$benefitIdx" )->text();
633 $benefitList .= Html::rawElement( 'div', [ 'class' => "mw-number-text $iconClass" ],
634 Html::rawElement( 'h3', [],
635 $this->msg( "createacct-benefit-head$benefitIdx" )->escaped()
636 )
637 . Html::rawElement( 'p', [],
638 $this->msg( "createacct-benefit-body$benefitIdx" )->params( $headUnescaped )->escaped()
639 )
640 );
641 }
642 $benefitsContainer = Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ],
643 Html::rawElement( 'h2', [], $this->msg( 'createacct-benefit-heading' )->escaped() )
644 . Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-list' ], $benefitList )
645 );
646 }
647 return $benefitsContainer;
648 }
649
658 protected function getAuthForm( array $requests, $action, $msg = '', $msgType = 'error' ) {
659 // FIXME merge this with parent
660
661 if ( isset( $this->authForm ) ) {
662 return $this->authForm;
663 }
664
665 $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
666
667 // get basic form description from the auth logic
668 $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
669 // this will call onAuthChangeFormFields()
670 $formDescriptor = $this->fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction );
671 $this->postProcessFormDescriptor( $formDescriptor, $requests );
672
673 $context = $this->getContext();
674 if ( $context->getRequest() !== $this->getRequest() ) {
675 // We have overridden the request, need to make sure the form uses that too.
676 $context = new DerivativeContext( $this->getContext() );
677 $context->setRequest( $this->getRequest() );
678 }
679 $form = HTMLForm::factory( 'vform', $formDescriptor, $context );
680
681 $form->addHiddenField( 'authAction', $this->authAction );
682 if ( $this->mLanguage ) {
683 $form->addHiddenField( 'uselang', $this->mLanguage );
684 }
685 $form->addHiddenField( 'force', $this->securityLevel );
686 $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
687 $config = $this->getConfig();
688 if ( $config->get( MainConfigNames::SecureLogin ) &&
689 !$config->get( MainConfigNames::ForceHTTPS ) ) {
690 // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved
691 if ( !$this->isSignup() ) {
692 $form->addHiddenField( 'wpForceHttps', (int)$this->mStickHTTPS );
693 $form->addHiddenField( 'wpFromhttp', $usingHTTPS );
694 }
695 }
696
697 // set properties of the form itself
698 $form->setAction( $this->getPageTitle()->getLocalURL( $this->getReturnToQueryStringFragment() ) );
699 $form->setName( 'userlogin' . ( $this->isSignup() ? '2' : '' ) );
700 if ( $this->isSignup() ) {
701 $form->setId( 'userlogin2' );
702 }
703
704 $form->suppressDefaultSubmit();
705
706 $this->authForm = $form;
707
708 return $form;
709 }
710
712 public function onAuthChangeFormFields(
713 array $requests, array $fieldInfo, array &$formDescriptor, $action
714 ) {
715 $formDescriptor = self::mergeDefaultFormDescriptor( $fieldInfo, $formDescriptor,
716 $this->getFieldDefinitions( $fieldInfo ) );
717 }
718
725 protected function showExtraInformation() {
726 return $this->authAction !== $this->getContinueAction( $this->authAction )
727 && !$this->securityLevel;
728 }
729
735 protected function getFieldDefinitions( array $fieldInfo ) {
736 $isLoggedIn = $this->getUser()->isRegistered();
737 $continuePart = $this->isContinued() ? 'continue-' : '';
738 $anotherPart = $isLoggedIn ? 'another-' : '';
739 // @phan-suppress-next-line PhanUndeclaredMethod
740 $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
741 $expirationDays = ceil( $expiration / ( 3600 * 24 ) );
742 $secureLoginLink = '';
743 if ( $this->mSecureLoginUrl ) {
744 $secureLoginLink = Html::element( 'a', [
745 'href' => $this->mSecureLoginUrl,
746 'class' => 'mw-ui-flush-right mw-secure',
747 ], $this->msg( 'userlogin-signwithsecure' )->text() );
748 }
749 $usernameHelpLink = '';
750 if ( !$this->msg( 'createacct-helpusername' )->isDisabled() ) {
751 $usernameHelpLink = Html::rawElement( 'span', [
752 'class' => 'mw-ui-flush-right',
753 ], $this->msg( 'createacct-helpusername' )->parse() );
754 }
755
756 if ( $this->isSignup() ) {
757 $config = $this->getConfig();
758 $hideIf = isset( $fieldInfo['mailpassword'] ) ? [ 'hide-if' => [ '===', 'mailpassword', '1' ] ] : [];
759 $fieldDefinitions = [
760 'statusarea' => [
761 // Used by the mediawiki.special.createaccount module for error display.
762 // FIXME: Merge this with HTMLForm's normal status (error) area
763 'type' => 'info',
764 'raw' => true,
765 'default' => Html::element( 'div', [ 'id' => 'mw-createacct-status-area' ] ),
766 'weight' => -105,
767 ],
768 'username' => [
769 'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $usernameHelpLink,
770 'id' => 'wpName2',
771 'placeholder-message' => $isLoggedIn ? 'createacct-another-username-ph'
772 : 'userlogin-yourname-ph',
773 ],
774 'mailpassword' => [
775 // create account without providing password, a temporary one will be mailed
776 'type' => 'check',
777 'label-message' => 'createaccountmail',
778 'name' => 'wpCreateaccountMail',
779 'id' => 'wpCreateaccountMail',
780 ],
781 'password' => [
782 'id' => 'wpPassword2',
783 'autocomplete' => 'new-password',
784 'placeholder-message' => 'createacct-yourpassword-ph',
785 'help-message' => 'createacct-useuniquepass',
786 ] + $hideIf,
787 'domain' => [],
788 'retype' => [
789 'type' => 'password',
790 'label-message' => 'createacct-yourpasswordagain',
791 'id' => 'wpRetype',
792 'cssclass' => 'loginPassword',
793 'size' => 20,
794 'autocomplete' => 'new-password',
795 'validation-callback' => function ( $value, $alldata ) {
796 if ( empty( $alldata['mailpassword'] ) && !empty( $alldata['password'] ) ) {
797 if ( !$value ) {
798 return $this->msg( 'htmlform-required' );
799 } elseif ( $value !== $alldata['password'] ) {
800 return $this->msg( 'badretype' );
801 }
802 }
803 return true;
804 },
805 'placeholder-message' => 'createacct-yourpasswordagain-ph',
806 ] + $hideIf,
807 'email' => [
808 'type' => 'email',
809 'label-message' => $config->get( MainConfigNames::EmailConfirmToEdit )
810 ? 'createacct-emailrequired' : 'createacct-emailoptional',
811 'id' => 'wpEmail',
812 'cssclass' => 'loginText',
813 'size' => '20',
814 'maxlength' => 255,
815 'autocomplete' => 'email',
816 // FIXME will break non-standard providers
817 'required' => $config->get( MainConfigNames::EmailConfirmToEdit ),
818 'validation-callback' => function ( $value, $alldata ) {
819 // AuthManager will check most of these, but that will make the auth
820 // session fail and this won't, so nicer to do it this way
821 if ( !$value &&
822 $this->getConfig()->get( MainConfigNames::EmailConfirmToEdit )
823 ) {
824 // no point in allowing registration without email when email is
825 // required to edit
826 return $this->msg( 'noemailtitle' );
827 } elseif ( !$value && !empty( $alldata['mailpassword'] ) ) {
828 // cannot send password via email when there is no email address
829 return $this->msg( 'noemailcreate' );
830 } elseif ( $value && !Sanitizer::validateEmail( $value ) ) {
831 return $this->msg( 'invalidemailaddress' );
832 } elseif ( is_string( $value ) && strlen( $value ) > 255 ) {
833 return $this->msg( 'changeemail-maxlength' );
834 }
835 return true;
836 },
837 // The following messages are used here:
838 // * createacct-email-ph
839 // * createacct-another-email-ph
840 'placeholder-message' => 'createacct-' . $anotherPart . 'email-ph',
841 ],
842 'realname' => [
843 'type' => 'text',
844 'help-message' => $isLoggedIn ? 'createacct-another-realname-tip'
845 : 'prefs-help-realname',
846 'label-message' => 'createacct-realname',
847 'cssclass' => 'loginText',
848 'size' => 20,
849 'id' => 'wpRealName',
850 'autocomplete' => 'name',
851 ],
852 'reason' => [
853 // comment for the user creation log
854 'type' => 'text',
855 'label-message' => 'createacct-reason',
856 'cssclass' => 'loginText',
857 'id' => 'wpReason',
858 'size' => '20',
859 'validation-callback' => function ( $value, $alldata ) {
860 // if the user sets an email address as the user creation reason, confirm that
861 // that was their intent
862 if ( $value && Sanitizer::validateEmail( $value ) ) {
863 if ( $this->reasonValidatorResult !== null ) {
864 return $this->reasonValidatorResult;
865 }
866 $this->reasonValidatorResult = true;
867 $authManager = MediaWikiServices::getInstance()->getAuthManager();
868 if ( !$authManager->getAuthenticationSessionData( 'reason-retry', false ) ) {
869 $authManager->setAuthenticationSessionData( 'reason-retry', true );
870 $this->reasonValidatorResult = $this->msg( 'createacct-reason-confirm' );
871 }
872 return $this->reasonValidatorResult;
873 }
874 return true;
875 },
876 'placeholder-message' => 'createacct-reason-ph',
877 ],
878 'createaccount' => [
879 // submit button
880 'type' => 'submit',
881 // The following messages are used here:
882 // * createacct-submit
883 // * createacct-another-submit
884 // * createacct-continue-submit
885 // * createacct-another-continue-submit
886 'default' => $this->msg( 'createacct-' . $anotherPart . $continuePart .
887 'submit' )->text(),
888 'name' => 'wpCreateaccount',
889 'id' => 'wpCreateaccount',
890 'weight' => 100,
891 ],
892 ];
893 if ( !$this->msg( 'createacct-username-help' )->isDisabled() ) {
894 $fieldDefinitions['username']['help-message'] = 'createacct-username-help';
895 }
896 } else {
897 // When the user's password is too weak, they might be asked to provide a stronger one
898 // as a followup step. That is a form with only two fields, 'password' and 'retype',
899 // and they should behave more like account creation.
900 $passwordRequest = AuthenticationRequest::getRequestByClass( $this->authRequests,
901 PasswordAuthenticationRequest::class );
902 $changePassword = $passwordRequest && $passwordRequest->action == AuthManager::ACTION_CHANGE;
903 $fieldDefinitions = [
904 'username' => (
905 [
906 'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $secureLoginLink,
907 'id' => 'wpName1',
908 'placeholder-message' => 'userlogin-yourname-ph',
909 ] + ( $changePassword ? [
910 // There is no username field on the AuthManager level when changing
911 // passwords. Fake one because password
912 'baseField' => 'password',
913 'nodata' => true,
914 'readonly' => true,
915 'cssclass' => 'mw-htmlform-hidden-field',
916 ] : [] )
917 ),
918 'password' => (
919 $changePassword ? [
920 'autocomplete' => 'new-password',
921 'placeholder-message' => 'createacct-yourpassword-ph',
922 'help-message' => 'createacct-useuniquepass',
923 ] : [
924 'id' => 'wpPassword1',
925 'autocomplete' => 'current-password',
926 'placeholder-message' => 'userlogin-yourpassword-ph',
927 ]
928 ),
929 'retype' => [
930 'type' => 'password',
931 'autocomplete' => 'new-password',
932 'placeholder-message' => 'createacct-yourpasswordagain-ph',
933 ],
934 'domain' => [],
935 'rememberMe' => [
936 // option for saving the user token to a cookie
937 'type' => 'check',
938 'cssclass' => 'mw-userlogin-rememberme',
939 'name' => 'wpRemember',
940 'label-message' => $this->msg( 'userlogin-remembermypassword' )
941 ->numParams( $expirationDays ),
942 'id' => 'wpRemember',
943 ],
944 'loginattempt' => [
945 // submit button
946 'type' => 'submit',
947 // The following messages are used here:
948 // * pt-login-button
949 // * pt-login-continue-button
950 'default' => $this->msg( 'pt-login-' . $continuePart . 'button' )->text(),
951 'id' => 'wpLoginAttempt',
952 'weight' => 100,
953 ],
954 'linkcontainer' => [
955 // help link
956 'type' => 'info',
957 'cssclass' => 'mw-form-related-link-container mw-userlogin-help',
958 // 'id' => 'mw-userlogin-help', // FIXME HTMLInfoField ignores this
959 'raw' => true,
960 'default' => Html::element( 'a', [
961 'href' => Skin::makeInternalOrExternalUrl( $this->msg( 'helplogin-url' )
962 ->inContentLanguage()
963 ->text() ),
964 ], $this->msg( 'userlogin-helplink2' )->text() ),
965 'weight' => 200,
966 ],
967 // button for ResetPasswordSecondaryAuthenticationProvider
968 'skipReset' => [
969 'weight' => 110,
970 'flags' => [],
971 ],
972 ];
973 }
974
975 $fieldDefinitions['username'] += [
976 'type' => 'text',
977 'name' => 'wpName',
978 'cssclass' => 'loginText',
979 'size' => 20,
980 'autocomplete' => 'username',
981 // 'required' => true,
982 ];
983 $fieldDefinitions['password'] += [
984 'type' => 'password',
985 // 'label-message' => 'userlogin-yourpassword', // would override the changepassword label
986 'name' => 'wpPassword',
987 'cssclass' => 'loginPassword',
988 'size' => 20,
989 // 'required' => true,
990 ];
991
992 if ( $this->mEntryError ) {
993 $defaultHtml = '';
994 if ( $this->mEntryErrorType === 'error' ) {
995 $defaultHtml = Html::errorBox( $this->mEntryError );
996 } elseif ( $this->mEntryErrorType === 'warning' ) {
997 $defaultHtml = Html::warningBox( $this->mEntryError );
998 }
999 $fieldDefinitions['entryError'] = [
1000 'type' => 'info',
1001 'default' => $defaultHtml,
1002 'raw' => true,
1003 'rawrow' => true,
1004 'weight' => -100,
1005 ];
1006 }
1007 if ( $this->isSignup() && $this->getUser()->isTemp() ) {
1008 $fieldDefinitions['tempWarning'] = [
1009 'type' => 'info',
1010 'default' => Html::warningBox(
1011 $this->msg( 'createacct-temp-warning' )->parse()
1012 ),
1013 'raw' => true,
1014 'rawrow' => true,
1015 'weight' => -90,
1016 ];
1017 }
1018 if ( !$this->showExtraInformation() ) {
1019 unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
1020 }
1021 if ( $this->isSignup() && $this->showExtraInformation() ) {
1022 // blank signup footer for site customization
1023 // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise
1024 $signupendMsg = $this->msg( 'signupend' );
1025 $signupendHttpsMsg = $this->msg( 'signupend-https' );
1026 if ( !$signupendMsg->isDisabled() ) {
1027 $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
1028 $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
1029 ? $signupendHttpsMsg->parse() : $signupendMsg->parse();
1030 $fieldDefinitions['signupend'] = [
1031 'type' => 'info',
1032 'raw' => true,
1033 'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ),
1034 'weight' => 225,
1035 ];
1036 }
1037 }
1038 if ( !$this->isSignup() && $this->showExtraInformation() ) {
1039 $passwordReset = MediaWikiServices::getInstance()->getPasswordReset();
1040 if ( $passwordReset->isAllowed( $this->getUser() )->isGood() ) {
1041 $fieldDefinitions['passwordReset'] = [
1042 'type' => 'info',
1043 'raw' => true,
1044 'cssclass' => 'mw-form-related-link-container',
1045 'default' => $this->getLinkRenderer()->makeLink(
1046 SpecialPage::getTitleFor( 'PasswordReset' ),
1047 $this->msg( 'userlogin-resetpassword-link' )->text()
1048 ),
1049 'weight' => 230,
1050 ];
1051 }
1052
1053 // Don't show a "create account" link if the user can't.
1054 if ( $this->showCreateAccountLink() ) {
1055 // link to the other action
1056 $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' : 'CreateAccount' );
1057 $linkq = $this->getReturnToQueryStringFragment();
1058 // Pass any language selection on to the mode switch link
1059 if ( $this->mLanguage ) {
1060 $linkq .= '&uselang=' . urlencode( $this->mLanguage );
1061 }
1062 $isLoggedIn = $this->getUser()->isRegistered()
1063 && !$this->getUser()->isTemp();
1064
1065 $fieldDefinitions['createOrLogin'] = [
1066 'type' => 'info',
1067 'raw' => true,
1068 'linkQuery' => $linkq,
1069 'default' => function ( $params ) use ( $isLoggedIn, $linkTitle ) {
1070 return Html::rawElement( 'div',
1071 [ 'id' => 'mw-createaccount' . ( !$isLoggedIn ? '-cta' : '' ),
1072 'class' => ( $isLoggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
1073 ( $isLoggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
1074 . Html::element( 'a',
1075 [
1076 'id' => 'mw-createaccount-join' . ( $isLoggedIn ? '-loggedin' : '' ),
1077 'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
1078 'class' => ( $isLoggedIn ? '' : 'mw-ui-button' ),
1079 'tabindex' => 100,
1080 ],
1081 $this->msg(
1082 $isLoggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
1083 )->text()
1084 )
1085 );
1086 },
1087 'weight' => 235,
1088 ];
1089 }
1090 }
1091
1092 return $fieldDefinitions;
1093 }
1094
1104 protected function hasSessionCookie() {
1105 $config = $this->getConfig();
1106 return $config->get( MainConfigNames::DisableCookieCheck ) || (
1107 $config->get( 'InitialSessionId' ) &&
1108 $this->getRequest()->getSession()->getId() === (string)$config->get( 'InitialSessionId' )
1109 );
1110 }
1111
1117 protected function getReturnToQueryStringFragment() {
1118 $returnto = '';
1119 if ( $this->mReturnTo !== '' ) {
1120 $returnto = 'returnto=' . wfUrlencode( $this->mReturnTo );
1121 if ( $this->mReturnToQuery !== '' ) {
1122 $returnto .= '&returntoquery=' . wfUrlencode( $this->mReturnToQuery );
1123 }
1124 }
1125 return $returnto;
1126 }
1127
1133 private function showCreateAccountLink() {
1134 return $this->isSignup() ||
1135 $this->getContext()->getAuthority()->isAllowed( 'createaccount' );
1136 }
1137
1138 protected function getTokenName() {
1139 return $this->isSignup() ? 'wpCreateaccountToken' : 'wpLoginToken';
1140 }
1141
1148 protected function makeLanguageSelector() {
1149 $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
1150 if ( $msg->isBlank() ) {
1151 return '';
1152 }
1153 $langs = explode( "\n", $msg->text() );
1154 $links = [];
1155 foreach ( $langs as $lang ) {
1156 $lang = trim( $lang, '* ' );
1157 $parts = explode( '|', $lang );
1158 if ( count( $parts ) >= 2 ) {
1159 $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) );
1160 }
1161 }
1162
1163 return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams(
1164 $this->getLanguage()->pipeList( $links ) )->escaped() : '';
1165 }
1166
1175 protected function makeLanguageSelectorLink( $text, $lang ) {
1176 if ( $this->getLanguage()->getCode() == $lang ) {
1177 // no link for currently used language
1178 return htmlspecialchars( $text );
1179 }
1180 $query = [ 'uselang' => $lang ];
1181 if ( $this->mReturnTo !== '' ) {
1182 $query['returnto'] = $this->mReturnTo;
1183 $query['returntoquery'] = $this->mReturnToQuery;
1184 }
1185
1186 $attr = [];
1187 $targetLanguage = MediaWikiServices::getInstance()->getLanguageFactory()
1188 ->getLanguage( $lang );
1189 $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
1190
1191 return $this->getLinkRenderer()->makeKnownLink(
1192 $this->getPageTitle(),
1193 $text,
1194 $attr,
1195 $query
1196 );
1197 }
1198
1199 protected function getGroupName() {
1200 return 'login';
1201 }
1202
1207 protected function postProcessFormDescriptor( &$formDescriptor, $requests ) {
1208 // Pre-fill username (if not creating an account, T46775).
1209 if (
1210 isset( $formDescriptor['username'] ) &&
1211 !isset( $formDescriptor['username']['default'] ) &&
1212 !$this->isSignup()
1213 ) {
1214 $user = $this->getUser();
1215 if ( $user->isRegistered() && !$user->isTemp() ) {
1216 $formDescriptor['username']['default'] = $user->getName();
1217 } else {
1218 $formDescriptor['username']['default'] =
1219 $this->getRequest()->getSession()->suggestLoginUsername();
1220 }
1221 }
1222
1223 // don't show a submit button if there is nothing to submit (i.e. the only form content
1224 // is other submit buttons, for redirect flows)
1225 if ( !$this->needsSubmitButton( $requests ) ) {
1226 unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] );
1227 }
1228
1229 if ( !$this->isSignup() ) {
1230 // FIXME HACK don't focus on non-empty field
1231 // maybe there should be an autofocus-if similar to hide-if?
1232 if (
1233 isset( $formDescriptor['username'] )
1234 && empty( $formDescriptor['username']['default'] )
1235 && !$this->getRequest()->getCheck( 'wpName' )
1236 ) {
1237 $formDescriptor['username']['autofocus'] = true;
1238 } elseif ( isset( $formDescriptor['password'] ) ) {
1239 $formDescriptor['password']['autofocus'] = true;
1240 }
1241 }
1242
1243 $this->addTabIndex( $formDescriptor );
1244 }
1245}
getUser()
const PROTO_HTTPS
Definition Defines.php:194
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
getContext()
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition Setup.php:497
A special page subclass for authentication-related special pages.
getContinueAction( $action)
Gets the _CONTINUE version of an action.
isActionAllowed( $action)
Checks whether AuthManager is ready to perform the action.
loadAuth( $subPage, $authAction=null, $reset=false)
Load or initialize $authAction, $authRequests and $subPage.
getDefaultAction( $subPage)
Get the default action for this special page, if none is given via URL/POST data.
string $subPage
Subpage of the special page.
isContinued()
Returns true if this is not the first step of the authentication.
getRequest()
Get the WebRequest being used for this instance.
trySubmit()
Attempts to do an authentication step with the submitted data.
getToken()
Returns the CSRF token.
An IContextSource implementation which will inherit context from another source but allow individual ...
An error page which can definitely be safely rendered using the OutputPage.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:150
Helper functions for the login form that need to be shared with other special pages (such as CentralA...
static getValidErrorMessages()
Returns an array of all valid error messages.
Holds shared logic for login and account creation pages.
mainLoginForm(array $requests, $msg='', $msgtype='error')
getPreservedParams( $withToken=false)
Returns URL query parameters which can be used to reload the page (or leave and return) while preserv...
logAuthResult( $success, $status=null)
Logs to the authmanager-stats channel.
onAuthChangeFormFields(array $requests, array $fieldInfo, array &$formDescriptor, $action)
Change the form descriptor that determines how a field will look in the authentication form....
setSessionUserForCurrentRequest()
Replace some globals to make sure the fact that the user has just been logged in is reflected in the ...
getBenefitsContainerHtml()
The HTML to be shown in the "benefits to signing in / creating an account" section of the signup/logi...
showSuccessPage( $type, $title, $msgname, $injected_html, $extraMessages)
Show the success page.
getFieldDefinitions(array $fieldInfo)
Create a HTMLForm descriptor for the core login fields.
getReturnToQueryStringFragment()
Returns a string that can be appended to the URL (without encoding) to preserve the return target.
User $targetUser
FIXME another flag for passing data.
successfulAction( $direct=false, $extraMessages=null)
showExtraInformation()
Show extra information such as password recovery information, link from login to signup,...
getPageHtml( $formHtml)
Add page elements which are outside the form.
hasSessionCookie()
Check if a session cookie is present.
__construct( $name, $restriction='')
getAuthForm(array $requests, $action, $msg='', $msgType='error')
Generates a form from the given request.
getTokenName()
Returns the name of the CSRF token (under which it should be found in the POST or GET data).
makeLanguageSelectorLink( $text, $lang)
Create a language selector link for a particular language Links back to this page preserving type and...
bool $proxyAccountCreation
True if the user if creating an account for someone else.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
postProcessFormDescriptor(&$formDescriptor, $requests)
setRequest(array $data, $wasPosted=null)
Override the POST data, GET data from the real request is preserved.
showReturnToPage( $type, $returnTo='', $returnToQuery='', $stickHTTPS=false)
Add a "return to" link or redirect to it.
makeLanguageSelector()
Produce a bar of links which allow the user to select another language during login/registration but ...
load( $subPage)
Load data from request.
This serves as the entry point to the authentication system.
This is a value object for authentication requests.
This is a value object to hold authentication response data.
This is a value object for authentication requests with a username and password.
PSR-3 logger instance factory.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
This serves as the entry point to the MediaWiki session handling system.
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition Skin.php:1146
setContext( $context)
Sets the context this SpecialPage is executed in.
getName()
Get the name of this Special Page.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getPageTitle( $subpage=false)
Get a self-referential title object.
getFullTitle()
Return the full title, including $par.
static setUser( $user)
Reset the stub global user to a different "real" user object, while ensuring that any method calls on...
internal since 1.36
Definition User.php:70
static newFromName( $name, $validate='valid')
Definition User.php:598
$wgUseMediaWikiUIEverywhere
Config variable stub for the UseMediaWikiUIEverywhere setting, for use by phpdoc and IDEs.
if(!isset( $args[0])) $lang