MediaWiki  master
LoginSignupSpecialPage.php
Go to the documentation of this file.
1 <?php
32 use 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;
53  protected $mSecureLoginUrl;
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( RequestContext::getMain()->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
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 
500  StubGlobalUser::setUser( $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()->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  // messages used:
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  'autocomplete' => 'email',
815  // FIXME will break non-standard providers
816  'required' => $config->get( MainConfigNames::EmailConfirmToEdit ),
817  'validation-callback' => function ( $value, $alldata ) {
818  // AuthManager will check most of these, but that will make the auth
819  // session fail and this won't, so nicer to do it this way
820  if ( !$value &&
821  $this->getConfig()->get( MainConfigNames::EmailConfirmToEdit ) ) {
822  // no point in allowing registration without email when email is
823  // required to edit
824  return $this->msg( 'noemailtitle' );
825  } elseif ( !$value && !empty( $alldata['mailpassword'] ) ) {
826  // cannot send password via email when there is no email address
827  return $this->msg( 'noemailcreate' );
828  } elseif ( $value && !Sanitizer::validateEmail( $value ) ) {
829  return $this->msg( 'invalidemailaddress' );
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 ) {
857  return $this->reasonValidatorResult;
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  }
865  return $this->reasonValidatorResult;
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  [
891  'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $secureLoginLink,
892  'id' => 'wpName1',
893  'placeholder-message' => 'userlogin-yourname-ph',
894  ] + ( $changePassword ? [
895  // There is no username field on the AuthManager level when changing
896  // passwords. Fake one because password
897  'baseField' => 'password',
898  'nodata' => true,
899  'readonly' => true,
900  'cssclass' => 'mw-htmlform-hidden-field',
901  ] : [] )
902  ),
903  'password' => (
904  $changePassword ? [
905  'autocomplete' => 'new-password',
906  'placeholder-message' => 'createacct-yourpassword-ph',
907  'help-message' => 'createacct-useuniquepass',
908  ] : [
909  'id' => 'wpPassword1',
910  'autocomplete' => 'current-password',
911  'placeholder-message' => 'userlogin-yourpassword-ph',
912  ]
913  ),
914  'retype' => [
915  'type' => 'password',
916  'autocomplete' => 'new-password',
917  'placeholder-message' => 'createacct-yourpasswordagain-ph',
918  ],
919  'domain' => [],
920  'rememberMe' => [
921  // option for saving the user token to a cookie
922  'type' => 'check',
923  'cssclass' => 'mw-userlogin-rememberme',
924  'name' => 'wpRemember',
925  'label-message' => $this->msg( 'userlogin-remembermypassword' )
926  ->numParams( $expirationDays ),
927  'id' => 'wpRemember',
928  ],
929  'loginattempt' => [
930  // submit button
931  'type' => 'submit',
932  'default' => $this->msg( 'pt-login-' . $continuePart . 'button' )->text(),
933  'id' => 'wpLoginAttempt',
934  'weight' => 100,
935  ],
936  'linkcontainer' => [
937  // help link
938  'type' => 'info',
939  'cssclass' => 'mw-form-related-link-container mw-userlogin-help',
940  // 'id' => 'mw-userlogin-help', // FIXME HTMLInfoField ignores this
941  'raw' => true,
942  'default' => Html::element( 'a', [
943  'href' => Skin::makeInternalOrExternalUrl( $this->msg( 'helplogin-url' )
944  ->inContentLanguage()
945  ->text() ),
946  ], $this->msg( 'userlogin-helplink2' )->text() ),
947  'weight' => 200,
948  ],
949  // button for ResetPasswordSecondaryAuthenticationProvider
950  'skipReset' => [
951  'weight' => 110,
952  'flags' => [],
953  ],
954  ];
955  }
956 
957  $fieldDefinitions['username'] += [
958  'type' => 'text',
959  'name' => 'wpName',
960  'cssclass' => 'loginText',
961  'size' => 20,
962  'autocomplete' => 'username',
963  // 'required' => true,
964  ];
965  $fieldDefinitions['password'] += [
966  'type' => 'password',
967  // 'label-message' => 'userlogin-yourpassword', // would override the changepassword label
968  'name' => 'wpPassword',
969  'cssclass' => 'loginPassword',
970  'size' => 20,
971  // 'required' => true,
972  ];
973 
974  if ( $this->mEntryError ) {
975  $defaultHtml = '';
976  if ( $this->mEntryErrorType === 'error' ) {
977  $defaultHtml = Html::errorBox( $this->mEntryError );
978  } elseif ( $this->mEntryErrorType === 'warning' ) {
979  $defaultHtml = Html::warningBox( $this->mEntryError );
980  }
981  $fieldDefinitions['entryError'] = [
982  'type' => 'info',
983  'default' => $defaultHtml,
984  'raw' => true,
985  'rawrow' => true,
986  'weight' => -100,
987  ];
988  }
989  if ( $this->isSignup() && $this->getUser()->isTemp() ) {
990  $fieldDefinitions['tempWarning'] = [
991  'type' => 'info',
992  'default' => Html::warningBox(
993  $this->msg( 'createacct-temp-warning' )->parse()
994  ),
995  'raw' => true,
996  'rawrow' => true,
997  'weight' => -90,
998  ];
999  }
1000  if ( !$this->showExtraInformation() ) {
1001  unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
1002  }
1003  if ( $this->isSignup() && $this->showExtraInformation() ) {
1004  // blank signup footer for site customization
1005  // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise
1006  $signupendMsg = $this->msg( 'signupend' );
1007  $signupendHttpsMsg = $this->msg( 'signupend-https' );
1008  if ( !$signupendMsg->isDisabled() ) {
1009  $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
1010  $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
1011  ? $signupendHttpsMsg->parse() : $signupendMsg->parse();
1012  $fieldDefinitions['signupend'] = [
1013  'type' => 'info',
1014  'raw' => true,
1015  'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ),
1016  'weight' => 225,
1017  ];
1018  }
1019  }
1020  if ( !$this->isSignup() && $this->showExtraInformation() ) {
1021  $passwordReset = MediaWikiServices::getInstance()->getPasswordReset();
1022  if ( $passwordReset->isAllowed( $this->getUser() )->isGood() ) {
1023  $fieldDefinitions['passwordReset'] = [
1024  'type' => 'info',
1025  'raw' => true,
1026  'cssclass' => 'mw-form-related-link-container',
1027  'default' => $this->getLinkRenderer()->makeLink(
1028  SpecialPage::getTitleFor( 'PasswordReset' ),
1029  $this->msg( 'userlogin-resetpassword-link' )->text()
1030  ),
1031  'weight' => 230,
1032  ];
1033  }
1034 
1035  // Don't show a "create account" link if the user can't.
1036  if ( $this->showCreateAccountLink() ) {
1037  // link to the other action
1038  $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' : 'CreateAccount' );
1039  $linkq = $this->getReturnToQueryStringFragment();
1040  // Pass any language selection on to the mode switch link
1041  if ( $this->mLanguage ) {
1042  $linkq .= '&uselang=' . urlencode( $this->mLanguage );
1043  }
1044  $isLoggedIn = $this->getUser()->isRegistered()
1045  && !$this->getUser()->isTemp();
1046 
1047  $fieldDefinitions['createOrLogin'] = [
1048  'type' => 'info',
1049  'raw' => true,
1050  'linkQuery' => $linkq,
1051  'default' => function ( $params ) use ( $isLoggedIn, $linkTitle ) {
1052  return Html::rawElement( 'div',
1053  [ 'id' => 'mw-createaccount' . ( !$isLoggedIn ? '-cta' : '' ),
1054  'class' => ( $isLoggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
1055  ( $isLoggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
1056  . Html::element( 'a',
1057  [
1058  'id' => 'mw-createaccount-join' . ( $isLoggedIn ? '-loggedin' : '' ),
1059  'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
1060  'class' => ( $isLoggedIn ? '' : 'mw-ui-button' ),
1061  'tabindex' => 100,
1062  ],
1063  $this->msg(
1064  $isLoggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
1065  )->text()
1066  )
1067  );
1068  },
1069  'weight' => 235,
1070  ];
1071  }
1072  }
1073 
1074  return $fieldDefinitions;
1075  }
1076 
1086  protected function hasSessionCookie() {
1087  $config = $this->getConfig();
1088  return $config->get( MainConfigNames::DisableCookieCheck ) || (
1089  $config->get( 'InitialSessionId' ) &&
1090  $this->getRequest()->getSession()->getId() === (string)$config->get( 'InitialSessionId' )
1091  );
1092  }
1093 
1099  protected function getReturnToQueryStringFragment() {
1100  $returnto = '';
1101  if ( $this->mReturnTo !== '' ) {
1102  $returnto = 'returnto=' . wfUrlencode( $this->mReturnTo );
1103  if ( $this->mReturnToQuery !== '' ) {
1104  $returnto .= '&returntoquery=' . wfUrlencode( $this->mReturnToQuery );
1105  }
1106  }
1107  return $returnto;
1108  }
1109 
1115  private function showCreateAccountLink() {
1116  return $this->isSignup() ||
1117  $this->getContext()->getAuthority()->isAllowed( 'createaccount' );
1118  }
1119 
1120  protected function getTokenName() {
1121  return $this->isSignup() ? 'wpCreateaccountToken' : 'wpLoginToken';
1122  }
1123 
1130  protected function makeLanguageSelector() {
1131  $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
1132  if ( $msg->isBlank() ) {
1133  return '';
1134  }
1135  $langs = explode( "\n", $msg->text() );
1136  $links = [];
1137  foreach ( $langs as $lang ) {
1138  $lang = trim( $lang, '* ' );
1139  $parts = explode( '|', $lang );
1140  if ( count( $parts ) >= 2 ) {
1141  $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) );
1142  }
1143  }
1144 
1145  return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams(
1146  $this->getLanguage()->pipeList( $links ) )->escaped() : '';
1147  }
1148 
1157  protected function makeLanguageSelectorLink( $text, $lang ) {
1158  if ( $this->getLanguage()->getCode() == $lang ) {
1159  // no link for currently used language
1160  return htmlspecialchars( $text );
1161  }
1162  $query = [ 'uselang' => $lang ];
1163  if ( $this->mReturnTo !== '' ) {
1164  $query['returnto'] = $this->mReturnTo;
1165  $query['returntoquery'] = $this->mReturnToQuery;
1166  }
1167 
1168  $attr = [];
1169  $targetLanguage = MediaWikiServices::getInstance()->getLanguageFactory()
1170  ->getLanguage( $lang );
1171  $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
1172 
1173  return $this->getLinkRenderer()->makeKnownLink(
1174  $this->getPageTitle(),
1175  $text,
1176  $attr,
1177  $query
1178  );
1179  }
1180 
1181  protected function getGroupName() {
1182  return 'login';
1183  }
1184 
1189  protected function postProcessFormDescriptor( &$formDescriptor, $requests ) {
1190  // Pre-fill username (if not creating an account, T46775).
1191  if (
1192  isset( $formDescriptor['username'] ) &&
1193  !isset( $formDescriptor['username']['default'] ) &&
1194  !$this->isSignup()
1195  ) {
1196  $user = $this->getUser();
1197  if ( $user->isRegistered() && !$user->isTemp() ) {
1198  $formDescriptor['username']['default'] = $user->getName();
1199  } else {
1200  $formDescriptor['username']['default'] =
1201  $this->getRequest()->getSession()->suggestLoginUsername();
1202  }
1203  }
1204 
1205  // don't show a submit button if there is nothing to submit (i.e. the only form content
1206  // is other submit buttons, for redirect flows)
1207  if ( !$this->needsSubmitButton( $requests ) ) {
1208  unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] );
1209  }
1210 
1211  if ( !$this->isSignup() ) {
1212  // FIXME HACK don't focus on non-empty field
1213  // maybe there should be an autofocus-if similar to hide-if?
1214  if (
1215  isset( $formDescriptor['username'] )
1216  && empty( $formDescriptor['username']['default'] )
1217  && !$this->getRequest()->getCheck( 'wpName' )
1218  ) {
1219  $formDescriptor['username']['autofocus'] = true;
1220  } elseif ( isset( $formDescriptor['password'] ) ) {
1221  $formDescriptor['password']['autofocus'] = true;
1222  }
1223  }
1224 
1225  $this->addTabIndex( $formDescriptor );
1226  }
1227 }
getUser()
$wgUseMediaWikiUIEverywhere
Variable for the UseMediaWikiUIEverywhere setting, for use in LocalSettings.php.
const PROTO_HTTPS
Definition: Defines.php:193
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,...
$success
getContext()
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition: Setup.php:470
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.
static factory( $displayFormat, $descriptor, $context=null, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:336
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:236
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:775
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:788
Helper functions for the login form that need to be shared with other special pages (such as CentralA...
Definition: LoginHelper.php:11
static getValidErrorMessages()
Returns an array of all valid error messages.
Definition: LoginHelper.php:42
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.
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.
loadRequestParameters()
Load basic request parameters for this Special page.
__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.
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...
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.
beginAuthentication(array $reqs, $returnToUrl)
Start an authentication flow.
canCreateAccounts()
Determine whether accounts can be created.
canAuthenticateNow()
Indicate whether user authentication is possible.
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.
MediaWikiServices is the service locator for the application scope of MediaWiki.
This serves as the entry point to the MediaWiki session handling system.
static getMain()
Get the RequestContext object associated with the main request.
static validateEmail( $addr)
Does a string look like an e-mail address?
Definition: Sanitizer.php:1879
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1168
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,...
AuthManager null $authManager
Definition: SpecialPage.php:89
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 newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:62
static setUser( $user)
Reset the stub global user to a different "real" user object, while ensuring that any method calls on...
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:369
static newFromName( $name, $validate='valid')
Definition: User.php:599
if(!isset( $args[0])) $lang