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