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