MediaWiki REL1_35
LoginSignupSpecialPage.php
Go to the documentation of this file.
1<?php
31use Wikimedia\ScopedCallback;
32
39 protected $mReturnTo;
40 protected $mPosted;
41 protected $mAction;
42 protected $mLanguage;
43 protected $mReturnToQuery;
44 protected $mToken;
45 protected $mStickHTTPS;
46 protected $mFromHTTP;
47 protected $mEntryError = '';
48 protected $mEntryErrorType = 'error';
49
50 protected $mLoaded = false;
51 protected $mLoadedRequest = false;
53 private $reasonValidatorResult = null;
54
56 protected $securityLevel;
57
63 protected $targetUser;
64
66 protected $authForm;
67
68 abstract protected function isSignup();
69
76 abstract protected function successfulAction( $direct = false, $extraMessages = null );
77
83 abstract protected function logAuthResult( $success, $status = null );
84
85 public function __construct( $name ) {
87 parent::__construct( $name );
88
89 // Override UseMediaWikiEverywhere to true, to force login and create form to use mw ui
91 }
92
93 protected function setRequest( array $data, $wasPosted = null ) {
94 parent::setRequest( $data, $wasPosted );
95 $this->mLoadedRequest = false;
96 }
97
101 private function loadRequestParameters() {
102 if ( $this->mLoadedRequest ) {
103 return;
104 }
105 $this->mLoadedRequest = true;
106 $request = $this->getRequest();
107
108 $this->mPosted = $request->wasPosted();
109 $this->mAction = $request->getVal( 'action' );
110 $this->mFromHTTP = $request->getBool( 'fromhttp', false )
111 || $request->getBool( 'wpFromhttp', false );
112 $this->mStickHTTPS = $this->getConfig()->get( 'ForceHTTPS' )
113 || ( !$this->mFromHTTP && $request->getProtocol() === 'https' )
114 || $request->getBool( 'wpForceHttps', false );
115 $this->mLanguage = $request->getText( 'uselang' );
116 $this->mReturnTo = $request->getVal( 'returnto', '' );
117 $this->mReturnToQuery = $request->getVal( 'returntoquery', '' );
118 }
119
125 protected function load( $subPage ) {
126 global $wgSecureLogin;
127
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 (
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' => $wgSecureLogin && $this->mFromHTTP ? '1' : null,
162 ]
163 );
164
165 // Only show valid error or warning messages.
166 if ( $entryError->exists()
167 && in_array( $entryError->getKey(), LoginHelper::getValidErrorMessages(), true )
168 ) {
169 $this->mEntryErrorType = 'error';
170 $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse();
171
172 } elseif ( $entryWarning->exists()
173 && in_array( $entryWarning->getKey(), LoginHelper::getValidErrorMessages(), true )
174 ) {
175 $this->mEntryErrorType = 'warning';
176 $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse();
177 }
178
179 # 1. When switching accounts, it sucks to get automatically logged out
180 # 2. Do not return to PasswordReset after a successful password change
181 # but goto Wiki start page (Main_Page) instead ( T35997 )
182 $returnToTitle = Title::newFromText( $this->mReturnTo );
183 if ( is_object( $returnToTitle )
184 && ( $returnToTitle->isSpecial( 'Userlogout' )
185 || $returnToTitle->isSpecial( 'PasswordReset' ) )
186 ) {
187 $this->mReturnTo = '';
188 $this->mReturnToQuery = '';
189 }
190 }
191
192 protected function getPreservedParams( $withToken = false ) {
193 global $wgSecureLogin;
194
195 $params = parent::getPreservedParams( $withToken );
196 $params += [
197 'returnto' => $this->mReturnTo ?: null,
198 'returntoquery' => $this->mReturnToQuery ?: null,
199 ];
200 if ( $wgSecureLogin && !$this->isSignup() ) {
201 $params['fromhttp'] = $this->mFromHTTP ? '1' : null;
202 }
203 return $params;
204 }
205
206 protected function beforeExecute( $subPage ) {
207 // finish initializing the class before processing the request - T135924
208 $this->loadRequestParameters();
209 return parent::beforeExecute( $subPage );
210 }
211
216 public function execute( $subPage ) {
217 if ( $this->mPosted ) {
218 $time = microtime( true );
219 $profilingScope = new ScopedCallback( function () use ( $time ) {
220 $time = microtime( true ) - $time;
221 $statsd = MediaWikiServices::getInstance()->getStatsdDataFactory();
222 $statsd->timing( "timing.login.ui.{$this->authAction}", $time * 1000 );
223 } );
224 }
225
226 $authManager = MediaWikiServices::getInstance()->getAuthManager();
227 $session = SessionManager::getGlobalSession();
228
229 // Session data is used for various things in the authentication process, so we must make
230 // sure a session cookie or some equivalent mechanism is set.
231 $session->persist();
232 // Explicitly disable cache to ensure cookie blocks may be set (T152462).
233 // (Technically redundant with sessions persisting from this page.)
234 $this->getOutput()->enableClientCache( false );
235
236 $this->load( $subPage );
237 $this->setHeaders();
238 $this->checkPermissions();
239
240 // Make sure the system configuration allows log in / sign up
241 if ( !$this->isSignup() && !$authManager->canAuthenticateNow() ) {
242 if ( !$session->canSetUser() ) {
243 throw new ErrorPageError( 'cannotloginnow-title', 'cannotloginnow-text', [
244 $session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
245 ] );
246 }
247 throw new ErrorPageError( 'cannotlogin-title', 'cannotlogin-text' );
248 } elseif ( $this->isSignup() && !$authManager->canCreateAccounts() ) {
249 throw new ErrorPageError( 'cannotcreateaccount-title', 'cannotcreateaccount-text' );
250 }
251
252 /*
253 * In the case where the user is already logged in, and was redirected to
254 * the login form from a page that requires login, do not show the login
255 * page. The use case scenario for this is when a user opens a large number
256 * of tabs, is redirected to the login page on all of them, and then logs
257 * in on one, expecting all the others to work properly.
258 *
259 * However, do show the form if it was visited intentionally (no 'returnto'
260 * is present). People who often switch between several accounts have grown
261 * accustomed to this behavior.
262 *
263 * Also make an exception when force=<level> is set in the URL, which means the user must
264 * reauthenticate for security reasons.
265 */
266 if ( !$this->isSignup() && !$this->mPosted && !$this->securityLevel &&
267 ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' ) &&
268 $this->getUser()->isLoggedIn()
269 ) {
270 $this->successfulAction();
271 return;
272 }
273
274 // If logging in and not on HTTPS, either redirect to it or offer a link.
275 global $wgSecureLogin;
276 if ( $this->getRequest()->getProtocol() !== 'https' ) {
277 $title = $this->getFullTitle();
278 $query = $this->getPreservedParams( false ) + [
279 'title' => null,
280 ( $this->mEntryErrorType === 'error' ? 'error'
281 : 'warning' ) => $this->mEntryError,
282 ] + $this->getRequest()->getQueryValues();
283 $url = $title->getFullURL( $query, false, PROTO_HTTPS );
284 if ( $wgSecureLogin && !$this->mFromHTTP &&
285 wfCanIPUseHTTPS( $this->getRequest()->getIP() )
286 ) {
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()->isAnon();
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 $wgUser, $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
500 $wgUser = $user;
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()->isLoggedIn() &&
573 $this->authAction !== AuthManager::ACTION_LOGIN_CONTINUE
574 ) {
575 $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
576 $submitStatus->warning( $reauthMessage, $this->getUser()->getName() );
577 }
578
579 $formHtml = $form->getHTML( $submitStatus );
580
581 $out->addHTML( $this->getPageHtml( $formHtml ) );
582 }
583
590 protected function getPageHtml( $formHtml ) {
592
593 $loginPrompt = $this->isSignup() ? '' : Html::rawElement( 'div',
594 [ 'id' => 'userloginprompt' ], $this->msg( 'loginprompt' )->parseAsBlock() );
595 $languageLinks = $wgLoginLanguageSelector ? $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 $benefitsContainer = '';
606 if ( $this->isSignup() && $this->showExtraInformation() ) {
607 // messages used:
608 // createacct-benefit-icon1 createacct-benefit-head1 createacct-benefit-body1
609 // createacct-benefit-icon2 createacct-benefit-head2 createacct-benefit-body2
610 // createacct-benefit-icon3 createacct-benefit-head3 createacct-benefit-body3
611 $benefitCount = 3;
612 $benefitList = '';
613 for ( $benefitIdx = 1; $benefitIdx <= $benefitCount; $benefitIdx++ ) {
614 $headUnescaped = $this->msg( "createacct-benefit-head$benefitIdx" )->text();
615 $iconClass = $this->msg( "createacct-benefit-icon$benefitIdx" )->text();
616 $benefitList .= Html::rawElement( 'div', [ 'class' => "mw-number-text $iconClass" ],
617 Html::rawElement( 'h3', [],
618 $this->msg( "createacct-benefit-head$benefitIdx" )->escaped()
619 )
620 . Html::rawElement( 'p', [],
621 $this->msg( "createacct-benefit-body$benefitIdx" )->params( $headUnescaped )->escaped()
622 )
623 );
624 }
625 $benefitsContainer = Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ],
626 Html::rawElement( 'h2', [], $this->msg( 'createacct-benefit-heading' )->escaped() )
627 . Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-list' ],
628 $benefitList
629 )
630 );
631 }
632
633 $html = Html::rawElement( 'div', [ 'class' => 'mw-ui-container' ],
634 $loginPrompt
635 . $languageLinks
636 . $signupStart
637 . Html::rawElement( 'div', [ 'id' => 'userloginForm' ],
638 $formHtml
639 )
640 . $benefitsContainer
641 );
642
643 return $html;
644 }
645
654 protected function getAuthForm( array $requests, $action, $msg = '', $msgType = 'error' ) {
655 // FIXME merge this with parent
656
657 if ( isset( $this->authForm ) ) {
658 return $this->authForm;
659 }
660
661 $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
662
663 // get basic form description from the auth logic
664 $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
665 // this will call onAuthChangeFormFields()
666 $formDescriptor = $this->fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction );
667 $this->postProcessFormDescriptor( $formDescriptor, $requests );
668
669 $context = $this->getContext();
670 if ( $context->getRequest() !== $this->getRequest() ) {
671 // We have overridden the request, need to make sure the form uses that too.
672 $context = new DerivativeContext( $this->getContext() );
673 $context->setRequest( $this->getRequest() );
674 }
675 $form = HTMLForm::factory( 'vform', $formDescriptor, $context );
676
677 $form->addHiddenField( 'authAction', $this->authAction );
678 if ( $this->mLanguage ) {
679 $form->addHiddenField( 'uselang', $this->mLanguage );
680 }
681 $form->addHiddenField( 'force', $this->securityLevel );
682 $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
683 $config = $this->getConfig();
684 if ( $config->get( 'SecureLogin' ) && !$config->get( 'ForceHTTPS' ) ) {
685 // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved
686 if ( !$this->isSignup() ) {
687 $form->addHiddenField( 'wpForceHttps', (int)$this->mStickHTTPS );
688 $form->addHiddenField( 'wpFromhttp', $usingHTTPS );
689 }
690 }
691
692 // set properties of the form itself
693 $form->setAction( $this->getPageTitle()->getLocalURL( $this->getReturnToQueryStringFragment() ) );
694 $form->setName( 'userlogin' . ( $this->isSignup() ? '2' : '' ) );
695 if ( $this->isSignup() ) {
696 $form->setId( 'userlogin2' );
697 }
698
699 $form->suppressDefaultSubmit();
700
701 $this->authForm = $form;
702
703 return $form;
704 }
705
707 public function onAuthChangeFormFields(
708 array $requests, array $fieldInfo, array &$formDescriptor, $action
709 ) {
710 $formDescriptor = self::mergeDefaultFormDescriptor( $fieldInfo, $formDescriptor,
711 $this->getFieldDefinitions() );
712 }
713
720 protected function showExtraInformation() {
721 return $this->authAction !== $this->getContinueAction( $this->authAction )
723 }
724
729 protected function getFieldDefinitions() {
731
732 $isLoggedIn = $this->getUser()->isLoggedIn();
733 $continuePart = $this->isContinued() ? 'continue-' : '';
734 $anotherPart = $isLoggedIn ? 'another-' : '';
735 // @phan-suppress-next-line PhanUndeclaredMethod
736 $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
737 $expirationDays = ceil( $expiration / ( 3600 * 24 ) );
738 $secureLoginLink = '';
739 if ( $this->mSecureLoginUrl ) {
740 $secureLoginLink = Html::element( 'a', [
741 'href' => $this->mSecureLoginUrl,
742 'class' => 'mw-ui-flush-right mw-secure',
743 ], $this->msg( 'userlogin-signwithsecure' )->text() );
744 }
745 $usernameHelpLink = '';
746 if ( !$this->msg( 'createacct-helpusername' )->isDisabled() ) {
747 $usernameHelpLink = Html::rawElement( 'span', [
748 'class' => 'mw-ui-flush-right',
749 ], $this->msg( 'createacct-helpusername' )->parse() );
750 }
751
752 if ( $this->isSignup() ) {
753 $fieldDefinitions = [
754 'statusarea' => [
755 // Used by the mediawiki.special.createaccount module for error display.
756 // FIXME: Merge this with HTMLForm's normal status (error) area
757 'type' => 'info',
758 'raw' => true,
759 'default' => Html::element( 'div', [ 'id' => 'mw-createacct-status-area' ] ),
760 'weight' => -105,
761 ],
762 'username' => [
763 'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $usernameHelpLink,
764 'id' => 'wpName2',
765 'placeholder-message' => $isLoggedIn ? 'createacct-another-username-ph'
766 : 'userlogin-yourname-ph',
767 ],
768 'mailpassword' => [
769 // create account without providing password, a temporary one will be mailed
770 'type' => 'check',
771 'label-message' => 'createaccountmail',
772 'name' => 'wpCreateaccountMail',
773 'id' => 'wpCreateaccountMail',
774 ],
775 'password' => [
776 'id' => 'wpPassword2',
777 'autocomplete' => 'new-password',
778 'placeholder-message' => 'createacct-yourpassword-ph',
779 'help-message' => 'createacct-useuniquepass',
780 'hide-if' => [ '===', 'wpCreateaccountMail', '1' ],
781 ],
782 'domain' => [],
783 'retype' => [
784 'type' => 'password',
785 'label-message' => 'createacct-yourpasswordagain',
786 'id' => 'wpRetype',
787 'cssclass' => 'loginPassword',
788 'size' => 20,
789 'autocomplete' => 'new-password',
790 'validation-callback' => function ( $value, $alldata ) {
791 if ( empty( $alldata['mailpassword'] ) && !empty( $alldata['password'] ) ) {
792 if ( !$value ) {
793 return $this->msg( 'htmlform-required' );
794 } elseif ( $value !== $alldata['password'] ) {
795 return $this->msg( 'badretype' );
796 }
797 }
798 return true;
799 },
800 'hide-if' => [ '===', 'wpCreateaccountMail', '1' ],
801 'placeholder-message' => 'createacct-yourpasswordagain-ph',
802 ],
803 'email' => [
804 'type' => 'email',
805 'label-message' => $wgEmailConfirmToEdit ? 'createacct-emailrequired'
806 : 'createacct-emailoptional',
807 'id' => 'wpEmail',
808 'cssclass' => 'loginText',
809 'size' => '20',
810 'maxlength' => 255,
811 'autocomplete' => 'email',
812 // FIXME will break non-standard providers
813 'required' => $wgEmailConfirmToEdit,
814 'validation-callback' => function ( $value, $alldata ) {
816
817 // AuthManager will check most of these, but that will make the auth
818 // session fail and this won't, so nicer to do it this way
819 if ( !$value && $wgEmailConfirmToEdit ) {
820 // no point in allowing registration without email when email is
821 // required to edit
822 return $this->msg( 'noemailtitle' );
823 } elseif ( !$value && !empty( $alldata['mailpassword'] ) ) {
824 // cannot send password via email when there is no email address
825 return $this->msg( 'noemailcreate' );
826 } elseif ( $value && !Sanitizer::validateEmail( $value ) ) {
827 return $this->msg( 'invalidemailaddress' );
828 } elseif ( is_string( $value ) && strlen( $value ) > 255 ) {
829 return $this->msg( 'changeemail-maxlength' );
830 }
831 return true;
832 },
833 'placeholder-message' => 'createacct-' . $anotherPart . 'email-ph',
834 ],
835 'realname' => [
836 'type' => 'text',
837 'help-message' => $isLoggedIn ? 'createacct-another-realname-tip'
838 : 'prefs-help-realname',
839 'label-message' => 'createacct-realname',
840 'cssclass' => 'loginText',
841 'size' => 20,
842 'id' => 'wpRealName',
843 'autocomplete' => 'name',
844 ],
845 'reason' => [
846 // comment for the user creation log
847 'type' => 'text',
848 'label-message' => 'createacct-reason',
849 'cssclass' => 'loginText',
850 'id' => 'wpReason',
851 'size' => '20',
852 'validation-callback' => function ( $value, $alldata ) {
853 // if the user sets an email address as the user creation reason, confirm that
854 // that was their intent
855 if ( $value && Sanitizer::validateEmail( $value ) ) {
856 if ( $this->reasonValidatorResult !== null ) {
858 }
859 $this->reasonValidatorResult = true;
860 $authManager = MediaWikiServices::getInstance()->getAuthManager();
861 if ( !$authManager->getAuthenticationSessionData( 'reason-retry', false ) ) {
862 $authManager->setAuthenticationSessionData( 'reason-retry', true );
863 $this->reasonValidatorResult = $this->msg( 'createacct-reason-confirm' );
864 }
866 }
867 return true;
868 },
869 'placeholder-message' => 'createacct-reason-ph',
870 ],
871 'createaccount' => [
872 // submit button
873 'type' => 'submit',
874 'default' => $this->msg( 'createacct-' . $anotherPart . $continuePart .
875 'submit' )->text(),
876 'name' => 'wpCreateaccount',
877 'id' => 'wpCreateaccount',
878 'weight' => 100,
879 ],
880 ];
881 } else {
882 // When the user's password is too weak, they might be asked to provide a stronger one
883 // as a followup step. That is a form with only two fields, 'password' and 'retype',
884 // and they should behave more like account creation.
885 $passwordRequest = AuthenticationRequest::getRequestByClass( $this->authRequests,
886 PasswordAuthenticationRequest::class );
887 $changePassword = $passwordRequest && $passwordRequest->action == AuthManager::ACTION_CHANGE;
888 $fieldDefinitions = [
889 'username' => [
890 'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $secureLoginLink,
891 'id' => 'wpName1',
892 'placeholder-message' => 'userlogin-yourname-ph',
893 ],
894 'password' => (
895 $changePassword ? [
896 'autocomplete' => 'new-password',
897 'placeholder-message' => 'createacct-yourpassword-ph',
898 'help-message' => 'createacct-useuniquepass',
899 ] : [
900 'id' => 'wpPassword1',
901 'autocomplete' => 'current-password',
902 'placeholder-message' => 'userlogin-yourpassword-ph',
903 ]
904 ),
905 'retype' => [
906 'type' => 'password',
907 'autocomplete' => 'new-password',
908 'placeholder-message' => 'createacct-yourpasswordagain-ph',
909 ],
910 'domain' => [],
911 'rememberMe' => [
912 // option for saving the user token to a cookie
913 'type' => 'check',
914 'cssclass' => 'mw-userlogin-rememberme',
915 'name' => 'wpRemember',
916 'label-message' => $this->msg( 'userlogin-remembermypassword' )
917 ->numParams( $expirationDays ),
918 'id' => 'wpRemember',
919 ],
920 'loginattempt' => [
921 // submit button
922 'type' => 'submit',
923 'default' => $this->msg( 'pt-login-' . $continuePart . 'button' )->text(),
924 'id' => 'wpLoginAttempt',
925 'weight' => 100,
926 ],
927 'linkcontainer' => [
928 // help link
929 'type' => 'info',
930 'cssclass' => 'mw-form-related-link-container mw-userlogin-help',
931 // 'id' => 'mw-userlogin-help', // FIXME HTMLInfoField ignores this
932 'raw' => true,
933 'default' => Html::element( 'a', [
934 'href' => Skin::makeInternalOrExternalUrl( $this->msg( 'helplogin-url' )
935 ->inContentLanguage()
936 ->text() ),
937 ], $this->msg( 'userlogin-helplink2' )->text() ),
938 'weight' => 200,
939 ],
940 // button for ResetPasswordSecondaryAuthenticationProvider
941 'skipReset' => [
942 'weight' => 110,
943 'flags' => [],
944 ],
945 ];
946 }
947
948 $fieldDefinitions['username'] += [
949 'type' => 'text',
950 'name' => 'wpName',
951 'cssclass' => 'loginText',
952 'size' => 20,
953 'autocomplete' => 'username',
954 // 'required' => true,
955 ];
956 $fieldDefinitions['password'] += [
957 'type' => 'password',
958 // 'label-message' => 'userlogin-yourpassword', // would override the changepassword label
959 'name' => 'wpPassword',
960 'cssclass' => 'loginPassword',
961 'size' => 20,
962 // 'required' => true,
963 ];
964
965 if ( $this->mEntryError ) {
966 $fieldDefinitions['entryError'] = [
967 'type' => 'info',
968 'default' => Html::rawElement( 'div', [ 'class' => $this->mEntryErrorType . 'box', ],
969 $this->mEntryError ),
970 'raw' => true,
971 'rawrow' => true,
972 'weight' => -100,
973 ];
974 }
975 if ( !$this->showExtraInformation() ) {
976 unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
977 }
978 if ( $this->isSignup() && $this->showExtraInformation() ) {
979 // blank signup footer for site customization
980 // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise
981 $signupendMsg = $this->msg( 'signupend' );
982 $signupendHttpsMsg = $this->msg( 'signupend-https' );
983 if ( !$signupendMsg->isDisabled() ) {
984 $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
985 $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
986 ? $signupendHttpsMsg->parse() : $signupendMsg->parse();
987 $fieldDefinitions['signupend'] = [
988 'type' => 'info',
989 'raw' => true,
990 'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ),
991 'weight' => 225,
992 ];
993 }
994 }
995 if ( !$this->isSignup() && $this->showExtraInformation() ) {
996 $passwordReset = MediaWikiServices::getInstance()->getPasswordReset();
997 if ( $passwordReset->isAllowed( $this->getUser() )->isGood() ) {
998 $fieldDefinitions['passwordReset'] = [
999 'type' => 'info',
1000 'raw' => true,
1001 'cssclass' => 'mw-form-related-link-container',
1002 'default' => $this->getLinkRenderer()->makeLink(
1003 SpecialPage::getTitleFor( 'PasswordReset' ),
1004 $this->msg( 'userlogin-resetpassword-link' )->text()
1005 ),
1006 'weight' => 230,
1007 ];
1008 }
1009
1010 // Don't show a "create account" link if the user can't.
1011 if ( $this->showCreateAccountLink() ) {
1012 // link to the other action
1013 $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' : 'CreateAccount' );
1014 $linkq = $this->getReturnToQueryStringFragment();
1015 // Pass any language selection on to the mode switch link
1016 if ( $this->mLanguage ) {
1017 $linkq .= '&uselang=' . urlencode( $this->mLanguage );
1018 }
1019 $loggedIn = $this->getUser()->isLoggedIn();
1020
1021 $fieldDefinitions['createOrLogin'] = [
1022 'type' => 'info',
1023 'raw' => true,
1024 'linkQuery' => $linkq,
1025 'default' => function ( $params ) use ( $loggedIn, $linkTitle ) {
1026 return Html::rawElement( 'div',
1027 [ 'id' => 'mw-createaccount' . ( !$loggedIn ? '-cta' : '' ),
1028 'class' => ( $loggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
1029 ( $loggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
1030 . Html::element( 'a',
1031 [
1032 'id' => 'mw-createaccount-join' . ( $loggedIn ? '-loggedin' : '' ),
1033 'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
1034 'class' => ( $loggedIn ? '' : 'mw-ui-button' ),
1035 'tabindex' => 100,
1036 ],
1037 $this->msg(
1038 $loggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
1039 )->text()
1040 )
1041 );
1042 },
1043 'weight' => 235,
1044 ];
1045 }
1046 }
1047
1048 return $fieldDefinitions;
1049 }
1050
1060 protected function hasSessionCookie() {
1062
1063 return $wgDisableCookieCheck || (
1065 $this->getRequest()->getSession()->getId() === (string)$wgInitialSessionId
1066 );
1067 }
1068
1074 protected function getReturnToQueryStringFragment() {
1075 $returnto = '';
1076 if ( $this->mReturnTo !== '' ) {
1077 $returnto = 'returnto=' . wfUrlencode( $this->mReturnTo );
1078 if ( $this->mReturnToQuery !== '' ) {
1079 $returnto .= '&returntoquery=' . wfUrlencode( $this->mReturnToQuery );
1080 }
1081 }
1082 return $returnto;
1083 }
1084
1090 private function showCreateAccountLink() {
1091 if ( $this->isSignup() ) {
1092 return true;
1093 } elseif ( MediaWikiServices::getInstance()
1095 ->userHasRight( $this->getUser(), 'createaccount' )
1096 ) {
1097 return true;
1098 } else {
1099 return false;
1100 }
1101 }
1102
1103 protected function getTokenName() {
1104 return $this->isSignup() ? 'wpCreateaccountToken' : 'wpLoginToken';
1105 }
1106
1113 protected function makeLanguageSelector() {
1114 $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
1115 if ( $msg->isBlank() ) {
1116 return '';
1117 }
1118 $langs = explode( "\n", $msg->text() );
1119 $links = [];
1120 foreach ( $langs as $lang ) {
1121 $lang = trim( $lang, '* ' );
1122 $parts = explode( '|', $lang );
1123 if ( count( $parts ) >= 2 ) {
1124 $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) );
1125 }
1126 }
1127
1128 return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams(
1129 $this->getLanguage()->pipeList( $links ) )->escaped() : '';
1130 }
1131
1140 protected function makeLanguageSelectorLink( $text, $lang ) {
1141 if ( $this->getLanguage()->getCode() == $lang ) {
1142 // no link for currently used language
1143 return htmlspecialchars( $text );
1144 }
1145 $query = [ 'uselang' => $lang ];
1146 if ( $this->mReturnTo !== '' ) {
1147 $query['returnto'] = $this->mReturnTo;
1148 $query['returntoquery'] = $this->mReturnToQuery;
1149 }
1150
1151 $attr = [];
1152 $targetLanguage = MediaWikiServices::getInstance()->getLanguageFactory()
1153 ->getLanguage( $lang );
1154 $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
1155
1156 return $this->getLinkRenderer()->makeKnownLink(
1157 $this->getPageTitle(),
1158 $text,
1159 $attr,
1160 $query
1161 );
1162 }
1163
1164 protected function getGroupName() {
1165 return 'login';
1166 }
1167
1172 protected function postProcessFormDescriptor( &$formDescriptor, $requests ) {
1173 // Pre-fill username (if not creating an account, T46775).
1174 if (
1175 isset( $formDescriptor['username'] ) &&
1176 !isset( $formDescriptor['username']['default'] ) &&
1177 !$this->isSignup()
1178 ) {
1179 $user = $this->getUser();
1180 if ( $user->isLoggedIn() ) {
1181 $formDescriptor['username']['default'] = $user->getName();
1182 } else {
1183 $formDescriptor['username']['default'] =
1184 $this->getRequest()->getSession()->suggestLoginUsername();
1185 }
1186 }
1187
1188 // don't show a submit button if there is nothing to submit (i.e. the only form content
1189 // is other submit buttons, for redirect flows)
1190 if ( !$this->needsSubmitButton( $requests ) ) {
1191 unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] );
1192 }
1193
1194 if ( !$this->isSignup() ) {
1195 // FIXME HACK don't focus on non-empty field
1196 // maybe there should be an autofocus-if similar to hide-if?
1197 if (
1198 isset( $formDescriptor['username'] )
1199 && empty( $formDescriptor['username']['default'] )
1200 && !$this->getRequest()->getCheck( 'wpName' )
1201 ) {
1202 $formDescriptor['username']['autofocus'] = true;
1203 } elseif ( isset( $formDescriptor['password'] ) ) {
1204 $formDescriptor['password']['autofocus'] = true;
1205 }
1206 }
1207
1208 $this->addTabIndex( $formDescriptor );
1209 }
1210}
getPermissionManager()
$wgDisableCookieCheck
By default, MediaWiki checks if the client supports cookies during the login process,...
$wgLoginLanguageSelector
Show a bar of language selection links in the user login and user registration forms; edit the "login...
$wgSecureLogin
This is to let user authenticate using https when they come from http.
$wgEmailConfirmToEdit
Should editors be required to have a validated e-mail address before being allowed to edit?
$wgUseMediaWikiUIEverywhere
Temporary variable that applies MediaWiki UI wherever it can be supported.
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfCanIPUseHTTPS( $ip)
Determine whether the client at a given source IP is likely to be able to access the wiki via HTTPS.
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,...
MediaWiki Session SessionId null $wgInitialSessionId
The persistent session ID (if any) loaded at startup.
Definition Setup.php:708
$wgLang
Definition Setup.php:781
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.
fieldInfoToFormDescriptor(array $requests, array $fieldInfo, $action)
Turns a field info array into a form descriptor.
getDefaultAction( $subPage)
Get the default action for this special page, if none is given via URL/POST data.
needsSubmitButton(array $requests)
Returns true if the form built from the given AuthenticationRequests needs a submit button.
string $subPage
Subpage of the special page.
isContinued()
Returns true if this is not the first step of the authentication.
static mergeDefaultFormDescriptor(array $fieldInfo, array $formDescriptor, array $defaultFormDescriptor)
Apply defaults to a form descriptor, without creating non-existend fields.
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.
addTabIndex(&$formDescriptor)
Adds a sequential tabindex starting from 1 to all form elements.
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:135
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')
canBypassForm(&$button_name)
Determine if the login form can be bypassed.
getFieldDefinitions()
Create a HTMLForm descriptor for the core login fields.
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 ...
showSuccessPage( $type, $title, $msgname, $injected_html, $extraMessages)
Show the success page.
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.
loadRequestParameters()
Load basic request parameters for this Special page.
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.
showCreateAccountLink()
Whether the login/create account form should display a link to the other form (in addition to whateve...
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
beforeExecute( $subPage)
Stable to override.
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 Stable to extend.
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
This serves as the entry point to the MediaWiki session handling system.
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.
getLanguage()
Shortcut to get user's language.
getFullTitle()
Return the full title, including $par.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:541
const PROTO_HTTPS
Definition Defines.php:210
if(!isset( $args[0])) $lang