MediaWiki REL1_34
LoginSignupSpecialPage.php
Go to the documentation of this file.
1<?php
30use Wikimedia\ScopedCallback;
31
38 protected $mReturnTo;
39 protected $mPosted;
40 protected $mAction;
41 protected $mLanguage;
42 protected $mReturnToQuery;
43 protected $mToken;
44 protected $mStickHTTPS;
45 protected $mFromHTTP;
46 protected $mEntryError = '';
47 protected $mEntryErrorType = 'error';
48
49 protected $mLoaded = false;
50 protected $mLoadedRequest = false;
52
54 protected $securityLevel;
55
61 protected $targetUser;
62
64 protected $authForm;
65
66 abstract protected function isSignup();
67
74 abstract protected function successfulAction( $direct = false, $extraMessages = null );
75
81 abstract protected function logAuthResult( $success, $status = null );
82
83 public function __construct( $name ) {
85 parent::__construct( $name );
86
87 // Override UseMediaWikiEverywhere to true, to force login and create form to use mw ui
89 }
90
91 protected function setRequest( array $data, $wasPosted = null ) {
92 parent::setRequest( $data, $wasPosted );
93 $this->mLoadedRequest = false;
94 }
95
99 private function loadRequestParameters() {
100 if ( $this->mLoadedRequest ) {
101 return;
102 }
103 $this->mLoadedRequest = true;
104 $request = $this->getRequest();
105
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', '' );
116 }
117
123 protected function load( $subPage ) {
124 global $wgSecureLogin;
125
126 $this->loadRequestParameters();
127 if ( $this->mLoaded ) {
128 return;
129 }
130 $this->mLoaded = true;
131 $request = $this->getRequest();
132
133 $securityLevel = $this->getRequest()->getText( 'force' );
134 if (
135 $securityLevel && AuthManager::singleton()->securitySensitiveOperationStatus(
136 $securityLevel ) === AuthManager::SEC_REAUTH
137 ) {
138 $this->securityLevel = $securityLevel;
139 }
140
141 $this->loadAuth( $subPage );
142
143 $this->mToken = $request->getVal( $this->getTokenName() );
144
145 // Show an error or warning passed on from a previous page
146 $entryError = $this->msg( $request->getVal( 'error', '' ) );
147 $entryWarning = $this->msg( $request->getVal( 'warning', '' ) );
148 // bc: provide login link as a parameter for messages where the translation
149 // was not updated
150 $loginreqlink = $this->getLinkRenderer()->makeKnownLink(
151 $this->getPageTitle(),
152 $this->msg( 'loginreqlink' )->text(),
153 [],
154 [
155 'returnto' => $this->mReturnTo,
156 'returntoquery' => $this->mReturnToQuery,
157 'uselang' => $this->mLanguage ?: null,
158 'fromhttp' => $wgSecureLogin && $this->mFromHTTP ? '1' : null,
159 ]
160 );
161
162 // Only show valid error or warning messages.
163 if ( $entryError->exists()
164 && in_array( $entryError->getKey(), LoginHelper::getValidErrorMessages(), true )
165 ) {
166 $this->mEntryErrorType = 'error';
167 $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse();
168
169 } elseif ( $entryWarning->exists()
170 && in_array( $entryWarning->getKey(), LoginHelper::getValidErrorMessages(), true )
171 ) {
172 $this->mEntryErrorType = 'warning';
173 $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse();
174 }
175
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 )
179 $returnToTitle = Title::newFromText( $this->mReturnTo );
180 if ( is_object( $returnToTitle )
181 && ( $returnToTitle->isSpecial( 'Userlogout' )
182 || $returnToTitle->isSpecial( 'PasswordReset' ) )
183 ) {
184 $this->mReturnTo = '';
185 $this->mReturnToQuery = '';
186 }
187 }
188
189 protected function getPreservedParams( $withToken = false ) {
190 global $wgSecureLogin;
191
192 $params = parent::getPreservedParams( $withToken );
193 $params += [
194 'returnto' => $this->mReturnTo ?: null,
195 'returntoquery' => $this->mReturnToQuery ?: null,
196 ];
197 if ( $wgSecureLogin && !$this->isSignup() ) {
198 $params['fromhttp'] = $this->mFromHTTP ? '1' : null;
199 }
200 return $params;
201 }
202
203 protected function beforeExecute( $subPage ) {
204 // finish initializing the class before processing the request - T135924
205 $this->loadRequestParameters();
206 return parent::beforeExecute( $subPage );
207 }
208
213 public function execute( $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 );
220 } );
221 }
222
223 $authManager = AuthManager::singleton();
224 $session = SessionManager::getGlobalSession();
225
226 // Session data is used for various things in the authentication process, so we must make
227 // sure a session cookie or some equivalent mechanism is set.
228 $session->persist();
229
230 $this->load( $subPage );
231 $this->setHeaders();
232 $this->checkPermissions();
233
234 // Make sure the system configuration allows log in / sign up
235 if ( !$this->isSignup() && !$authManager->canAuthenticateNow() ) {
236 if ( !$session->canSetUser() ) {
237 throw new ErrorPageError( 'cannotloginnow-title', 'cannotloginnow-text', [
238 $session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
239 ] );
240 }
241 throw new ErrorPageError( 'cannotlogin-title', 'cannotlogin-text' );
242 } elseif ( $this->isSignup() && !$authManager->canCreateAccounts() ) {
243 throw new ErrorPageError( 'cannotcreateaccount-title', 'cannotcreateaccount-text' );
244 }
245
246 /*
247 * In the case where the user is already logged in, and was redirected to
248 * the login form from a page that requires login, do not show the login
249 * page. The use case scenario for this is when a user opens a large number
250 * of tabs, is redirected to the login page on all of them, and then logs
251 * in on one, expecting all the others to work properly.
252 *
253 * However, do show the form if it was visited intentionally (no 'returnto'
254 * is present). People who often switch between several accounts have grown
255 * accustomed to this behavior.
256 *
257 * Also make an exception when force=<level> is set in the URL, which means the user must
258 * reauthenticate for security reasons.
259 */
260 if ( !$this->isSignup() && !$this->mPosted && !$this->securityLevel &&
261 ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' ) &&
262 $this->getUser()->isLoggedIn()
263 ) {
264 $this->successfulAction();
265 return;
266 }
267
268 // If logging in and not on HTTPS, either redirect to it or offer a link.
269 global $wgSecureLogin;
270 if ( $this->getRequest()->getProtocol() !== 'https' ) {
271 $title = $this->getFullTitle();
272 $query = $this->getPreservedParams( false ) + [
273 'title' => null,
274 ( $this->mEntryErrorType === 'error' ? 'error'
275 : 'warning' ) => $this->mEntryError,
276 ] + $this->getRequest()->getQueryValues();
277 $url = $title->getFullURL( $query, false, PROTO_HTTPS );
278 if ( $wgSecureLogin && !$this->mFromHTTP &&
279 wfCanIPUseHTTPS( $this->getRequest()->getIP() )
280 ) {
281 // Avoid infinite redirect
282 $url = wfAppendQuery( $url, 'fromhttp=1' );
283 $this->getOutput()->redirect( $url );
284 // Since we only do this redir to change proto, always vary
285 $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' );
286
287 return;
288 } else {
289 // A wiki without HTTPS login support should set $wgServer to
290 // http://somehost, in which case the secure URL generated
291 // above won't actually start with https://
292 if ( substr( $url, 0, 8 ) === 'https://' ) {
293 $this->mSecureLoginUrl = $url;
294 }
295 }
296 }
297
298 if ( !$this->isActionAllowed( $this->authAction ) ) {
299 // FIXME how do we explain this to the user? can we handle session loss better?
300 // messages used: authpage-cannot-login, authpage-cannot-login-continue,
301 // authpage-cannot-create, authpage-cannot-create-continue
302 $this->mainLoginForm( [], 'authpage-cannot-' . $this->authAction );
303 return;
304 }
305
306 if ( $this->canBypassForm( $button_name ) ) {
307 $this->setRequest( [], true );
308 $this->getRequest()->setVal( $this->getTokenName(), $this->getToken() );
309 if ( $button_name ) {
310 $this->getRequest()->setVal( $button_name, true );
311 }
312 }
313
314 $status = $this->trySubmit();
315
316 if ( !$status || !$status->isGood() ) {
317 $this->mainLoginForm( $this->authRequests, $status ? $status->getMessage() : '', 'error' );
318 return;
319 }
320
322 $response = $status->getValue();
323
324 $returnToUrl = $this->getPageTitle( 'return' )
325 ->getFullURL( $this->getPreservedParams( true ), false, PROTO_HTTPS );
326 switch ( $response->status ) {
327 case AuthenticationResponse::PASS:
328 $this->logAuthResult( true );
329 $this->proxyAccountCreation = $this->isSignup() && !$this->getUser()->isAnon();
330 $this->targetUser = User::newFromName( $response->username );
331
332 if (
333 !$this->proxyAccountCreation
334 && $response->loginRequest
335 && $authManager->canAuthenticateNow()
336 ) {
337 // successful registration; log the user in instantly
338 $response2 = $authManager->beginAuthentication( [ $response->loginRequest ],
339 $returnToUrl );
340 if ( $response2->status !== AuthenticationResponse::PASS ) {
341 LoggerFactory::getInstance( 'login' )
342 ->error( 'Could not log in after account creation' );
343 $this->successfulAction( true, Status::newFatal( 'createacct-loginerror' ) );
344 break;
345 }
346 }
347
348 if ( !$this->proxyAccountCreation ) {
349 // Ensure that the context user is the same as the session user.
351 }
352
353 $this->successfulAction( true );
354 break;
355 case AuthenticationResponse::FAIL:
356 // fall through
357 case AuthenticationResponse::RESTART:
358 unset( $this->authForm );
359 if ( $response->status === AuthenticationResponse::FAIL ) {
360 $action = $this->getDefaultAction( $subPage );
361 $messageType = 'error';
362 } else {
363 $action = $this->getContinueAction( $this->authAction );
364 $messageType = 'warning';
365 }
366 $this->logAuthResult( false, $response->message ? $response->message->getKey() : '-' );
367 $this->loadAuth( $subPage, $action, true );
368 $this->mainLoginForm( $this->authRequests, $response->message, $messageType );
369 break;
370 case AuthenticationResponse::REDIRECT:
371 unset( $this->authForm );
372 $this->getOutput()->redirect( $response->redirectTarget );
373 break;
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;
379 $this->mainLoginForm( $response->neededRequests, $response->message, $response->messageType );
380 break;
381 default:
382 throw new LogicException( 'invalid AuthenticationResponse' );
383 }
384 }
385
399 private function canBypassForm( &$button_name ) {
400 $button_name = null;
401 if ( $this->isContinued() ) {
402 return false;
403 }
404 $fields = AuthenticationRequest::mergeFieldInfo( $this->authRequests );
405 foreach ( $fields as $fieldname => $field ) {
406 if ( !isset( $field['type'] ) ) {
407 return false;
408 }
409 if ( !empty( $field['skippable'] ) ) {
410 continue;
411 }
412 if ( $field['type'] === 'button' ) {
413 if ( $button_name !== null ) {
414 $button_name = null;
415 return false;
416 } else {
417 $button_name = $fieldname;
418 }
419 } elseif ( $field['type'] !== 'null' ) {
420 return false;
421 }
422 }
423 return true;
424 }
425
435 protected function showSuccessPage(
436 $type, $title, $msgname, $injected_html, $extraMessages
437 ) {
438 $out = $this->getOutput();
439 $out->setPageTitle( $title );
440 if ( $msgname ) {
441 $out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) );
442 }
443 if ( $extraMessages ) {
444 $extraMessages = Status::wrap( $extraMessages );
445 $out->addWikiTextAsInterface(
446 $extraMessages->getWikiText( false, false, $this->getLanguage() )
447 );
448 }
449
450 $out->addHTML( $injected_html );
451
452 $helper = new LoginHelper( $this->getContext() );
453 $helper->showReturnToPage( $type, $this->mReturnTo, $this->mReturnToQuery, $this->mStickHTTPS );
454 }
455
471 public function showReturnToPage(
472 $type, $returnTo = '', $returnToQuery = '', $stickHTTPS = false
473 ) {
474 $helper = new LoginHelper( $this->getContext() );
475 $helper->showReturnToPage( $type, $returnTo, $returnToQuery, $stickHTTPS );
476 }
477
482 protected function setSessionUserForCurrentRequest() {
483 global $wgUser, $wgLang;
484
485 $context = RequestContext::getMain();
486 $localContext = $this->getContext();
487 if ( $context !== $localContext ) {
488 // remove AuthManagerSpecialPage context hack
489 $this->setContext( $context );
490 }
491
492 $user = $context->getRequest()->getSession()->getUser();
493
494 $wgUser = $user;
495 $context->setUser( $user );
496
497 $wgLang = $context->getLanguage();
498 }
499
514 protected function mainLoginForm( array $requests, $msg = '', $msgtype = 'error' ) {
515 $user = $this->getUser();
516 $out = $this->getOutput();
517
518 // FIXME how to handle empty $requests - restart, or no form, just an error message?
519 // no form would be better for no session type errors, restart is better when can* fails.
520 if ( !$requests ) {
521 $this->authAction = $this->getDefaultAction( $this->subPage );
522 $this->authForm = null;
523 $requests = AuthManager::singleton()->getAuthenticationRequests( $this->authAction, $user );
524 }
525
526 // Generic styles and scripts for both login and signup form
527 $out->addModuleStyles( [
528 'mediawiki.ui',
529 'mediawiki.ui.button',
530 'mediawiki.ui.checkbox',
531 'mediawiki.ui.input',
532 'mediawiki.special.userlogin.common.styles'
533 ] );
534 if ( $this->isSignup() ) {
535 // XXX hack pending RL or JS parse() support for complex content messages T27349
536 $out->addJsConfigVars( 'wgCreateacctImgcaptchaHelp',
537 $this->msg( 'createacct-imgcaptcha-help' )->parse() );
538
539 // Additional styles and scripts for signup form
540 $out->addModules( [
541 'mediawiki.special.userlogin.signup.js'
542 ] );
543 $out->addModuleStyles( [
544 'mediawiki.special.userlogin.signup.styles'
545 ] );
546 } else {
547 // Additional styles for login form
548 $out->addModuleStyles( [
549 'mediawiki.special.userlogin.login.styles'
550 ] );
551 }
552 $out->disallowUserJs(); // just in case...
553
554 $form = $this->getAuthForm( $requests, $this->authAction, $msg, $msgtype );
555 $form->prepareForm();
556
557 $submitStatus = Status::newGood();
558 if ( $msg && $msgtype === 'warning' ) {
559 $submitStatus->warning( $msg );
560 } elseif ( $msg && $msgtype === 'error' ) {
561 $submitStatus->fatal( $msg );
562 }
563
564 // warning header for non-standard workflows (e.g. security reauthentication)
565 if (
566 !$this->isSignup() &&
567 $this->getUser()->isLoggedIn() &&
568 $this->authAction !== AuthManager::ACTION_LOGIN_CONTINUE
569 ) {
570 $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
571 $submitStatus->warning( $reauthMessage, $this->getUser()->getName() );
572 }
573
574 $formHtml = $form->getHTML( $submitStatus );
575
576 $out->addHTML( $this->getPageHtml( $formHtml ) );
577 }
578
585 protected function getPageHtml( $formHtml ) {
587
588 $loginPrompt = $this->isSignup() ? '' : Html::rawElement( 'div',
589 [ 'id' => 'userloginprompt' ], $this->msg( 'loginprompt' )->parseAsBlock() );
590 $languageLinks = $wgLoginLanguageSelector ? $this->makeLanguageSelector() : '';
591 $signupStartMsg = $this->msg( 'signupstart' );
592 $signupStart = ( $this->isSignup() && !$signupStartMsg->isDisabled() )
593 ? Html::rawElement( 'div', [ 'id' => 'signupstart' ], $signupStartMsg->parseAsBlock() ) : '';
594 if ( $languageLinks ) {
595 $languageLinks = Html::rawElement( 'div', [ 'id' => 'languagelinks' ],
596 Html::rawElement( 'p', [], $languageLinks )
597 );
598 }
599
600 $benefitsContainer = '';
601 if ( $this->isSignup() && $this->showExtraInformation() ) {
602 // messages used:
603 // createacct-benefit-icon1 createacct-benefit-head1 createacct-benefit-body1
604 // createacct-benefit-icon2 createacct-benefit-head2 createacct-benefit-body2
605 // createacct-benefit-icon3 createacct-benefit-head3 createacct-benefit-body3
606 $benefitCount = 3;
607 $benefitList = '';
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" ],
612 Html::rawElement( 'h3', [],
613 $this->msg( "createacct-benefit-head$benefitIdx" )->escaped()
614 )
615 . Html::rawElement( 'p', [],
616 $this->msg( "createacct-benefit-body$benefitIdx" )->params( $headUnescaped )->escaped()
617 )
618 );
619 }
620 $benefitsContainer = Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ],
621 Html::rawElement( 'h2', [], $this->msg( 'createacct-benefit-heading' )->escaped() )
622 . Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-list' ],
623 $benefitList
624 )
625 );
626 }
627
628 $html = Html::rawElement( 'div', [ 'class' => 'mw-ui-container' ],
629 $loginPrompt
630 . $languageLinks
631 . $signupStart
632 . Html::rawElement( 'div', [ 'id' => 'userloginForm' ],
633 $formHtml
634 )
635 . $benefitsContainer
636 );
637
638 return $html;
639 }
640
649 protected function getAuthForm( array $requests, $action, $msg = '', $msgType = 'error' ) {
650 // FIXME merge this with parent
651
652 if ( isset( $this->authForm ) ) {
653 return $this->authForm;
654 }
655
656 $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
657
658 // get basic form description from the auth logic
659 $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
660 // this will call onAuthChangeFormFields()
661 $formDescriptor = static::fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction );
662 $this->postProcessFormDescriptor( $formDescriptor, $requests );
663
664 $context = $this->getContext();
665 if ( $context->getRequest() !== $this->getRequest() ) {
666 // We have overridden the request, need to make sure the form uses that too.
667 $context = new DerivativeContext( $this->getContext() );
668 $context->setRequest( $this->getRequest() );
669 }
670 $form = HTMLForm::factory( 'vform', $formDescriptor, $context );
671
672 $form->addHiddenField( 'authAction', $this->authAction );
673 if ( $this->mLanguage ) {
674 $form->addHiddenField( 'uselang', $this->mLanguage );
675 }
676 $form->addHiddenField( 'force', $this->securityLevel );
677 $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
678 $config = $this->getConfig();
679 if ( $config->get( 'SecureLogin' ) && !$config->get( 'ForceHTTPS' ) ) {
680 // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved
681 if ( !$this->isSignup() ) {
682 $form->addHiddenField( 'wpForceHttps', (int)$this->mStickHTTPS );
683 $form->addHiddenField( 'wpFromhttp', $usingHTTPS );
684 }
685 }
686
687 // set properties of the form itself
688 $form->setAction( $this->getPageTitle()->getLocalURL( $this->getReturnToQueryStringFragment() ) );
689 $form->setName( 'userlogin' . ( $this->isSignup() ? '2' : '' ) );
690 if ( $this->isSignup() ) {
691 $form->setId( 'userlogin2' );
692 }
693
694 $form->suppressDefaultSubmit();
695
696 $this->authForm = $form;
697
698 return $form;
699 }
700
701 public function onAuthChangeFormFields(
702 array $requests, array $fieldInfo, array &$formDescriptor, $action
703 ) {
704 $coreFieldDescriptors = $this->getFieldDefinitions();
705
706 // keep the ordering from getCoreFieldDescriptors() where there is no explicit weight
707 foreach ( $coreFieldDescriptors as $fieldName => $coreField ) {
708 $requestField = $formDescriptor[$fieldName] ?? [];
709
710 // remove everything that is not in the fieldinfo, is not marked as a supplemental field
711 // to something in the fieldinfo, and is not an info field or a submit button
712 if (
713 !isset( $fieldInfo[$fieldName] )
714 && (
715 !isset( $coreField['baseField'] )
716 || !isset( $fieldInfo[$coreField['baseField']] )
717 )
718 && (
719 !isset( $coreField['type'] )
720 || !in_array( $coreField['type'], [ 'submit', 'info' ], true )
721 )
722 ) {
723 $coreFieldDescriptors[$fieldName] = null;
724 continue;
725 }
726
727 // core message labels should always take priority
728 if (
729 isset( $coreField['label'] )
730 || isset( $coreField['label-message'] )
731 || isset( $coreField['label-raw'] )
732 ) {
733 unset( $requestField['label'], $requestField['label-message'], $coreField['label-raw'] );
734 }
735
736 $coreFieldDescriptors[$fieldName] += $requestField;
737 }
738
739 $formDescriptor = array_filter( $coreFieldDescriptors + $formDescriptor );
740 return true;
741 }
742
749 protected function showExtraInformation() {
750 return $this->authAction !== $this->getContinueAction( $this->authAction )
752 }
753
758 protected function getFieldDefinitions() {
760
761 $isLoggedIn = $this->getUser()->isLoggedIn();
762 $continuePart = $this->isContinued() ? 'continue-' : '';
763 $anotherPart = $isLoggedIn ? 'another-' : '';
764 // @phan-suppress-next-line PhanUndeclaredMethod
765 $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
766 $expirationDays = ceil( $expiration / ( 3600 * 24 ) );
767 $secureLoginLink = '';
768 if ( $this->mSecureLoginUrl ) {
769 $secureLoginLink = Html::element( 'a', [
770 'href' => $this->mSecureLoginUrl,
771 'class' => 'mw-ui-flush-right mw-secure',
772 ], $this->msg( 'userlogin-signwithsecure' )->text() );
773 }
774 $usernameHelpLink = '';
775 if ( !$this->msg( 'createacct-helpusername' )->isDisabled() ) {
776 $usernameHelpLink = Html::rawElement( 'span', [
777 'class' => 'mw-ui-flush-right',
778 ], $this->msg( 'createacct-helpusername' )->parse() );
779 }
780
781 if ( $this->isSignup() ) {
782 $fieldDefinitions = [
783 'statusarea' => [
784 // used by the mediawiki.special.userlogin.signup.js module for error display
785 // FIXME merge this with HTMLForm's normal status (error) area
786 'type' => 'info',
787 'raw' => true,
788 'default' => Html::element( 'div', [ 'id' => 'mw-createacct-status-area' ] ),
789 'weight' => -105,
790 ],
791 'username' => [
792 'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $usernameHelpLink,
793 'id' => 'wpName2',
794 'placeholder-message' => $isLoggedIn ? 'createacct-another-username-ph'
795 : 'userlogin-yourname-ph',
796 ],
797 'mailpassword' => [
798 // create account without providing password, a temporary one will be mailed
799 'type' => 'check',
800 'label-message' => 'createaccountmail',
801 'name' => 'wpCreateaccountMail',
802 'id' => 'wpCreateaccountMail',
803 ],
804 'password' => [
805 'id' => 'wpPassword2',
806 'placeholder-message' => 'createacct-yourpassword-ph',
807 'hide-if' => [ '===', 'wpCreateaccountMail', '1' ],
808 ],
809 'domain' => [],
810 'retype' => [
811 'baseField' => 'password',
812 'type' => 'password',
813 'label-message' => 'createacct-yourpasswordagain',
814 'id' => 'wpRetype',
815 'cssclass' => 'loginPassword',
816 'size' => 20,
817 'validation-callback' => function ( $value, $alldata ) {
818 if ( empty( $alldata['mailpassword'] ) && !empty( $alldata['password'] ) ) {
819 if ( !$value ) {
820 return $this->msg( 'htmlform-required' );
821 } elseif ( $value !== $alldata['password'] ) {
822 return $this->msg( 'badretype' );
823 }
824 }
825 return true;
826 },
827 'hide-if' => [ '===', 'wpCreateaccountMail', '1' ],
828 'placeholder-message' => 'createacct-yourpasswordagain-ph',
829 ],
830 'email' => [
831 'type' => 'email',
832 'label-message' => $wgEmailConfirmToEdit ? 'createacct-emailrequired'
833 : 'createacct-emailoptional',
834 'id' => 'wpEmail',
835 'cssclass' => 'loginText',
836 'size' => '20',
837 // FIXME will break non-standard providers
838 'required' => $wgEmailConfirmToEdit,
839 'validation-callback' => function ( $value, $alldata ) {
841
842 // AuthManager will check most of these, but that will make the auth
843 // session fail and this won't, so nicer to do it this way
844 if ( !$value && $wgEmailConfirmToEdit ) {
845 // no point in allowing registration without email when email is
846 // required to edit
847 return $this->msg( 'noemailtitle' );
848 } elseif ( !$value && !empty( $alldata['mailpassword'] ) ) {
849 // cannot send password via email when there is no email address
850 return $this->msg( 'noemailcreate' );
851 } elseif ( $value && !Sanitizer::validateEmail( $value ) ) {
852 return $this->msg( 'invalidemailaddress' );
853 }
854 return true;
855 },
856 'placeholder-message' => 'createacct-' . $anotherPart . 'email-ph',
857 ],
858 'realname' => [
859 'type' => 'text',
860 'help-message' => $isLoggedIn ? 'createacct-another-realname-tip'
861 : 'prefs-help-realname',
862 'label-message' => 'createacct-realname',
863 'cssclass' => 'loginText',
864 'size' => 20,
865 'id' => 'wpRealName',
866 ],
867 'reason' => [
868 // comment for the user creation log
869 'type' => 'text',
870 'label-message' => 'createacct-reason',
871 'cssclass' => 'loginText',
872 'id' => 'wpReason',
873 'size' => '20',
874 'placeholder-message' => 'createacct-reason-ph',
875 ],
876 'createaccount' => [
877 // submit button
878 'type' => 'submit',
879 'default' => $this->msg( 'createacct-' . $anotherPart . $continuePart .
880 'submit' )->text(),
881 'name' => 'wpCreateaccount',
882 'id' => 'wpCreateaccount',
883 'weight' => 100,
884 ],
885 ];
886 } else {
887 $fieldDefinitions = [
888 'username' => [
889 'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $secureLoginLink,
890 'id' => 'wpName1',
891 'placeholder-message' => 'userlogin-yourname-ph',
892 ],
893 'password' => [
894 'id' => 'wpPassword1',
895 'placeholder-message' => 'userlogin-yourpassword-ph',
896 ],
897 'domain' => [],
898 'rememberMe' => [
899 // option for saving the user token to a cookie
900 'type' => 'check',
901 'cssclass' => 'mw-userlogin-rememberme',
902 'name' => 'wpRemember',
903 'label-message' => $this->msg( 'userlogin-remembermypassword' )
904 ->numParams( $expirationDays ),
905 'id' => 'wpRemember',
906 ],
907 'loginattempt' => [
908 // submit button
909 'type' => 'submit',
910 'default' => $this->msg( 'pt-login-' . $continuePart . 'button' )->text(),
911 'id' => 'wpLoginAttempt',
912 'weight' => 100,
913 ],
914 'linkcontainer' => [
915 // help link
916 'type' => 'info',
917 'cssclass' => 'mw-form-related-link-container mw-userlogin-help',
918 // 'id' => 'mw-userlogin-help', // FIXME HTMLInfoField ignores this
919 'raw' => true,
920 'default' => Html::element( 'a', [
921 'href' => Skin::makeInternalOrExternalUrl( $this->msg( 'helplogin-url' )
922 ->inContentLanguage()
923 ->text() ),
924 ], $this->msg( 'userlogin-helplink2' )->text() ),
925 'weight' => 200,
926 ],
927 // button for ResetPasswordSecondaryAuthenticationProvider
928 'skipReset' => [
929 'weight' => 110,
930 'flags' => [],
931 ],
932 ];
933 }
934
935 $fieldDefinitions['username'] += [
936 'type' => 'text',
937 'name' => 'wpName',
938 'cssclass' => 'loginText',
939 'size' => 20,
940 // 'required' => true,
941 ];
942 $fieldDefinitions['password'] += [
943 'type' => 'password',
944 // 'label-message' => 'userlogin-yourpassword', // would override the changepassword label
945 'name' => 'wpPassword',
946 'cssclass' => 'loginPassword',
947 'size' => 20,
948 // 'required' => true,
949 ];
950
951 if ( $this->mEntryError ) {
952 $fieldDefinitions['entryError'] = [
953 'type' => 'info',
954 'default' => Html::rawElement( 'div', [ 'class' => $this->mEntryErrorType . 'box', ],
955 $this->mEntryError ),
956 'raw' => true,
957 'rawrow' => true,
958 'weight' => -100,
959 ];
960 }
961 if ( !$this->showExtraInformation() ) {
962 unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
963 }
964 if ( $this->isSignup() && $this->showExtraInformation() ) {
965 // blank signup footer for site customization
966 // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise
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'] = [
974 'type' => 'info',
975 'raw' => true,
976 'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ),
977 'weight' => 225,
978 ];
979 }
980 }
981 if ( !$this->isSignup() && $this->showExtraInformation() ) {
982 $passwordReset = MediaWikiServices::getInstance()->getPasswordReset();
983 if ( $passwordReset->isAllowed( $this->getUser() )->isGood() ) {
984 $fieldDefinitions['passwordReset'] = [
985 'type' => 'info',
986 'raw' => true,
987 'cssclass' => 'mw-form-related-link-container',
988 'default' => $this->getLinkRenderer()->makeLink(
989 SpecialPage::getTitleFor( 'PasswordReset' ),
990 $this->msg( 'userlogin-resetpassword-link' )->text()
991 ),
992 'weight' => 230,
993 ];
994 }
995
996 // Don't show a "create account" link if the user can't.
997 if ( $this->showCreateAccountLink() ) {
998 // link to the other action
999 $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' : 'CreateAccount' );
1000 $linkq = $this->getReturnToQueryStringFragment();
1001 // Pass any language selection on to the mode switch link
1002 if ( $this->mLanguage ) {
1003 $linkq .= '&uselang=' . urlencode( $this->mLanguage );
1004 }
1005 $loggedIn = $this->getUser()->isLoggedIn();
1006
1007 $fieldDefinitions['createOrLogin'] = [
1008 'type' => 'info',
1009 'raw' => true,
1010 'linkQuery' => $linkq,
1011 'default' => function ( $params ) use ( $loggedIn, $linkTitle ) {
1012 return Html::rawElement( 'div',
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() )
1016 . Html::element( 'a',
1017 [
1018 'id' => 'mw-createaccount-join' . ( $loggedIn ? '-loggedin' : '' ),
1019 'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
1020 'class' => ( $loggedIn ? '' : 'mw-ui-button' ),
1021 'tabindex' => 100,
1022 ],
1023 $this->msg(
1024 $loggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
1025 )->text()
1026 )
1027 );
1028 },
1029 'weight' => 235,
1030 ];
1031 }
1032 }
1033
1034 return $fieldDefinitions;
1035 }
1036
1046 protected function hasSessionCookie() {
1048
1049 return $wgDisableCookieCheck || (
1051 $this->getRequest()->getSession()->getId() === (string)$wgInitialSessionId
1052 );
1053 }
1054
1060 protected function getReturnToQueryStringFragment() {
1061 $returnto = '';
1062 if ( $this->mReturnTo !== '' ) {
1063 $returnto = 'returnto=' . wfUrlencode( $this->mReturnTo );
1064 if ( $this->mReturnToQuery !== '' ) {
1065 $returnto .= '&returntoquery=' . wfUrlencode( $this->mReturnToQuery );
1066 }
1067 }
1068 return $returnto;
1069 }
1070
1076 private function showCreateAccountLink() {
1077 if ( $this->isSignup() ) {
1078 return true;
1079 } elseif ( MediaWikiServices::getInstance()
1081 ->userHasRight( $this->getUser(), 'createaccount' )
1082 ) {
1083 return true;
1084 } else {
1085 return false;
1086 }
1087 }
1088
1089 protected function getTokenName() {
1090 return $this->isSignup() ? 'wpCreateaccountToken' : 'wpLoginToken';
1091 }
1092
1099 protected function makeLanguageSelector() {
1100 $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
1101 if ( $msg->isBlank() ) {
1102 return '';
1103 }
1104 $langs = explode( "\n", $msg->text() );
1105 $links = [];
1106 foreach ( $langs as $lang ) {
1107 $lang = trim( $lang, '* ' );
1108 $parts = explode( '|', $lang );
1109 if ( count( $parts ) >= 2 ) {
1110 $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) );
1111 }
1112 }
1113
1114 return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams(
1115 $this->getLanguage()->pipeList( $links ) )->escaped() : '';
1116 }
1117
1126 protected function makeLanguageSelectorLink( $text, $lang ) {
1127 if ( $this->getLanguage()->getCode() == $lang ) {
1128 // no link for currently used language
1129 return htmlspecialchars( $text );
1130 }
1131 $query = [ 'uselang' => $lang ];
1132 if ( $this->mReturnTo !== '' ) {
1133 $query['returnto'] = $this->mReturnTo;
1134 $query['returntoquery'] = $this->mReturnToQuery;
1135 }
1136
1137 $attr = [];
1138 $targetLanguage = Language::factory( $lang );
1139 $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
1140
1141 return $this->getLinkRenderer()->makeKnownLink(
1142 $this->getPageTitle(),
1143 $text,
1144 $attr,
1145 $query
1146 );
1147 }
1148
1149 protected function getGroupName() {
1150 return 'login';
1151 }
1152
1157 protected function postProcessFormDescriptor( &$formDescriptor, $requests ) {
1158 // Pre-fill username (if not creating an account, T46775).
1159 if (
1160 isset( $formDescriptor['username'] ) &&
1161 !isset( $formDescriptor['username']['default'] ) &&
1162 !$this->isSignup()
1163 ) {
1164 $user = $this->getUser();
1165 if ( $user->isLoggedIn() ) {
1166 $formDescriptor['username']['default'] = $user->getName();
1167 } else {
1168 $formDescriptor['username']['default'] =
1169 $this->getRequest()->getSession()->suggestLoginUsername();
1170 }
1171 }
1172
1173 // don't show a submit button if there is nothing to submit (i.e. the only form content
1174 // is other submit buttons, for redirect flows)
1175 if ( !$this->needsSubmitButton( $requests ) ) {
1176 unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] );
1177 }
1178
1179 if ( !$this->isSignup() ) {
1180 // FIXME HACK don't focus on non-empty field
1181 // maybe there should be an autofocus-if similar to hide-if?
1182 if (
1183 isset( $formDescriptor['username'] )
1184 && empty( $formDescriptor['username']['default'] )
1185 && !$this->getRequest()->getCheck( 'wpName' )
1186 ) {
1187 $formDescriptor['username']['autofocus'] = true;
1188 } elseif ( isset( $formDescriptor['password'] ) ) {
1189 $formDescriptor['password']['autofocus'] = true;
1190 }
1191 }
1192
1193 $this->addTabIndex( $formDescriptor );
1194 }
1195}
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,...
$wgInitialSessionId
Definition Setup.php:809
$wgLang
Definition Setup.php:880
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.
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.
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:131
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)
Gets called before.
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.
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:51
const PROTO_HTTPS
Definition Defines.php:209
$context
Definition load.php:45
if(!isset( $args[0])) $lang