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( $this->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
341  && $authManager->canAuthenticateNow()
342  ) {
343  // successful registration; log the user in instantly
344  $response2 = $authManager->beginAuthentication( [ $response->loginRequest ],
345  $returnToUrl );
346  if ( $response2->status !== AuthenticationResponse::PASS ) {
347  LoggerFactory::getInstance( 'login' )
348  ->error( 'Could not log in after account creation' );
349  $this->successfulAction( true, Status::newFatal( 'createacct-loginerror' ) );
350  break;
351  }
352  }
353 
354  if ( !$this->proxyAccountCreation ) {
355  // Ensure that the context user is the same as the session user.
357  }
358 
359  $this->successfulAction( true );
360  break;
361  case AuthenticationResponse::FAIL:
362  // fall through
363  case AuthenticationResponse::RESTART:
364  unset( $this->authForm );
365  if ( $response->status === AuthenticationResponse::FAIL ) {
366  $action = $this->getDefaultAction( $subPage );
367  $messageType = 'error';
368  } else {
369  $action = $this->getContinueAction( $this->authAction );
370  $messageType = 'warning';
371  }
372  $this->logAuthResult( false, $response->message ? $response->message->getKey() : '-' );
373  $this->loadAuth( $subPage, $action, true );
374  $this->mainLoginForm( $this->authRequests, $response->message, $messageType );
375  break;
376  case AuthenticationResponse::REDIRECT:
377  unset( $this->authForm );
378  $this->getOutput()->redirect( $response->redirectTarget );
379  break;
380  case AuthenticationResponse::UI:
381  unset( $this->authForm );
382  $this->authAction = $this->isSignup() ? AuthManager::ACTION_CREATE_CONTINUE
383  : AuthManager::ACTION_LOGIN_CONTINUE;
384  $this->authRequests = $response->neededRequests;
385  $this->mainLoginForm( $response->neededRequests, $response->message, $response->messageType );
386  break;
387  default:
388  throw new LogicException( 'invalid AuthenticationResponse' );
389  }
390  }
391 
405  private function canBypassForm( &$button_name ) {
406  $button_name = null;
407  if ( $this->isContinued() ) {
408  return false;
409  }
410  $fields = AuthenticationRequest::mergeFieldInfo( $this->authRequests );
411  foreach ( $fields as $fieldname => $field ) {
412  if ( !isset( $field['type'] ) ) {
413  return false;
414  }
415  if ( !empty( $field['skippable'] ) ) {
416  continue;
417  }
418  if ( $field['type'] === 'button' ) {
419  if ( $button_name !== null ) {
420  $button_name = null;
421  return false;
422  } else {
423  $button_name = $fieldname;
424  }
425  } elseif ( $field['type'] !== 'null' ) {
426  return false;
427  }
428  }
429  return true;
430  }
431 
441  protected function showSuccessPage(
442  $type, $title, $msgname, $injected_html, $extraMessages
443  ) {
444  $out = $this->getOutput();
445  $out->setPageTitle( $title );
446  if ( $msgname ) {
447  $out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) );
448  }
449  if ( $extraMessages ) {
450  $extraMessages = Status::wrap( $extraMessages );
451  $out->addWikiTextAsInterface(
452  $extraMessages->getWikiText( false, false, $this->getLanguage() )
453  );
454  }
455 
456  $out->addHTML( $injected_html );
457 
458  $helper = new LoginHelper( $this->getContext() );
459  $helper->showReturnToPage( $type, $this->mReturnTo, $this->mReturnToQuery, $this->mStickHTTPS );
460  }
461 
477  public function showReturnToPage(
478  $type, $returnTo = '', $returnToQuery = '', $stickHTTPS = false
479  ) {
480  $helper = new LoginHelper( $this->getContext() );
481  $helper->showReturnToPage( $type, $returnTo, $returnToQuery, $stickHTTPS );
482  }
483 
488  protected function setSessionUserForCurrentRequest() {
489  global $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  // The following messages are used here:
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  'maxlength' => 255,
815  'autocomplete' => 'email',
816  // FIXME will break non-standard providers
817  'required' => $config->get( MainConfigNames::EmailConfirmToEdit ),
818  'validation-callback' => function ( $value, $alldata ) {
819  // AuthManager will check most of these, but that will make the auth
820  // session fail and this won't, so nicer to do it this way
821  if ( !$value &&
822  $this->getConfig()->get( MainConfigNames::EmailConfirmToEdit )
823  ) {
824  // no point in allowing registration without email when email is
825  // required to edit
826  return $this->msg( 'noemailtitle' );
827  } elseif ( !$value && !empty( $alldata['mailpassword'] ) ) {
828  // cannot send password via email when there is no email address
829  return $this->msg( 'noemailcreate' );
830  } elseif ( $value && !Sanitizer::validateEmail( $value ) ) {
831  return $this->msg( 'invalidemailaddress' );
832  } elseif ( is_string( $value ) && strlen( $value ) > 255 ) {
833  return $this->msg( 'changeemail-maxlength' );
834  }
835  return true;
836  },
837  // The following messages are used here:
838  // * createacct-email-ph
839  // * createacct-another-email-ph
840  'placeholder-message' => 'createacct-' . $anotherPart . 'email-ph',
841  ],
842  'realname' => [
843  'type' => 'text',
844  'help-message' => $isLoggedIn ? 'createacct-another-realname-tip'
845  : 'prefs-help-realname',
846  'label-message' => 'createacct-realname',
847  'cssclass' => 'loginText',
848  'size' => 20,
849  'id' => 'wpRealName',
850  'autocomplete' => 'name',
851  ],
852  'reason' => [
853  // comment for the user creation log
854  'type' => 'text',
855  'label-message' => 'createacct-reason',
856  'cssclass' => 'loginText',
857  'id' => 'wpReason',
858  'size' => '20',
859  'validation-callback' => function ( $value, $alldata ) {
860  // if the user sets an email address as the user creation reason, confirm that
861  // that was their intent
862  if ( $value && Sanitizer::validateEmail( $value ) ) {
863  if ( $this->reasonValidatorResult !== null ) {
864  return $this->reasonValidatorResult;
865  }
866  $this->reasonValidatorResult = true;
867  $authManager = MediaWikiServices::getInstance()->getAuthManager();
868  if ( !$authManager->getAuthenticationSessionData( 'reason-retry', false ) ) {
869  $authManager->setAuthenticationSessionData( 'reason-retry', true );
870  $this->reasonValidatorResult = $this->msg( 'createacct-reason-confirm' );
871  }
872  return $this->reasonValidatorResult;
873  }
874  return true;
875  },
876  'placeholder-message' => 'createacct-reason-ph',
877  ],
878  'createaccount' => [
879  // submit button
880  'type' => 'submit',
881  // The following messages are used here:
882  // * createacct-submit
883  // * createacct-another-submit
884  // * createacct-continue-submit
885  // * createacct-another-continue-submit
886  'default' => $this->msg( 'createacct-' . $anotherPart . $continuePart .
887  'submit' )->text(),
888  'name' => 'wpCreateaccount',
889  'id' => 'wpCreateaccount',
890  'weight' => 100,
891  ],
892  ];
893  if ( !$this->msg( 'createacct-username-help' )->isDisabled() ) {
894  $fieldDefinitions['username']['help-message'] = 'createacct-username-help';
895  }
896  } else {
897  // When the user's password is too weak, they might be asked to provide a stronger one
898  // as a followup step. That is a form with only two fields, 'password' and 'retype',
899  // and they should behave more like account creation.
900  $passwordRequest = AuthenticationRequest::getRequestByClass( $this->authRequests,
901  PasswordAuthenticationRequest::class );
902  $changePassword = $passwordRequest && $passwordRequest->action == AuthManager::ACTION_CHANGE;
903  $fieldDefinitions = [
904  'username' => (
905  [
906  'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $secureLoginLink,
907  'id' => 'wpName1',
908  'placeholder-message' => 'userlogin-yourname-ph',
909  ] + ( $changePassword ? [
910  // There is no username field on the AuthManager level when changing
911  // passwords. Fake one because password
912  'baseField' => 'password',
913  'nodata' => true,
914  'readonly' => true,
915  'cssclass' => 'mw-htmlform-hidden-field',
916  ] : [] )
917  ),
918  'password' => (
919  $changePassword ? [
920  'autocomplete' => 'new-password',
921  'placeholder-message' => 'createacct-yourpassword-ph',
922  'help-message' => 'createacct-useuniquepass',
923  ] : [
924  'id' => 'wpPassword1',
925  'autocomplete' => 'current-password',
926  'placeholder-message' => 'userlogin-yourpassword-ph',
927  ]
928  ),
929  'retype' => [
930  'type' => 'password',
931  'autocomplete' => 'new-password',
932  'placeholder-message' => 'createacct-yourpasswordagain-ph',
933  ],
934  'domain' => [],
935  'rememberMe' => [
936  // option for saving the user token to a cookie
937  'type' => 'check',
938  'cssclass' => 'mw-userlogin-rememberme',
939  'name' => 'wpRemember',
940  'label-message' => $this->msg( 'userlogin-remembermypassword' )
941  ->numParams( $expirationDays ),
942  'id' => 'wpRemember',
943  ],
944  'loginattempt' => [
945  // submit button
946  'type' => 'submit',
947  // The following messages are used here:
948  // * pt-login-button
949  // * pt-login-continue-button
950  'default' => $this->msg( 'pt-login-' . $continuePart . 'button' )->text(),
951  'id' => 'wpLoginAttempt',
952  'weight' => 100,
953  ],
954  'linkcontainer' => [
955  // help link
956  'type' => 'info',
957  'cssclass' => 'mw-form-related-link-container mw-userlogin-help',
958  // 'id' => 'mw-userlogin-help', // FIXME HTMLInfoField ignores this
959  'raw' => true,
960  'default' => Html::element( 'a', [
961  'href' => Skin::makeInternalOrExternalUrl( $this->msg( 'helplogin-url' )
962  ->inContentLanguage()
963  ->text() ),
964  ], $this->msg( 'userlogin-helplink2' )->text() ),
965  'weight' => 200,
966  ],
967  // button for ResetPasswordSecondaryAuthenticationProvider
968  'skipReset' => [
969  'weight' => 110,
970  'flags' => [],
971  ],
972  ];
973  }
974 
975  $fieldDefinitions['username'] += [
976  'type' => 'text',
977  'name' => 'wpName',
978  'cssclass' => 'loginText',
979  'size' => 20,
980  'autocomplete' => 'username',
981  // 'required' => true,
982  ];
983  $fieldDefinitions['password'] += [
984  'type' => 'password',
985  // 'label-message' => 'userlogin-yourpassword', // would override the changepassword label
986  'name' => 'wpPassword',
987  'cssclass' => 'loginPassword',
988  'size' => 20,
989  // 'required' => true,
990  ];
991 
992  if ( $this->mEntryError ) {
993  $defaultHtml = '';
994  if ( $this->mEntryErrorType === 'error' ) {
995  $defaultHtml = Html::errorBox( $this->mEntryError );
996  } elseif ( $this->mEntryErrorType === 'warning' ) {
997  $defaultHtml = Html::warningBox( $this->mEntryError );
998  }
999  $fieldDefinitions['entryError'] = [
1000  'type' => 'info',
1001  'default' => $defaultHtml,
1002  'raw' => true,
1003  'rawrow' => true,
1004  'weight' => -100,
1005  ];
1006  }
1007  if ( $this->isSignup() && $this->getUser()->isTemp() ) {
1008  $fieldDefinitions['tempWarning'] = [
1009  'type' => 'info',
1010  'default' => Html::warningBox(
1011  $this->msg( 'createacct-temp-warning' )->parse()
1012  ),
1013  'raw' => true,
1014  'rawrow' => true,
1015  'weight' => -90,
1016  ];
1017  }
1018  if ( !$this->showExtraInformation() ) {
1019  unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
1020  }
1021  if ( $this->isSignup() && $this->showExtraInformation() ) {
1022  // blank signup footer for site customization
1023  // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise
1024  $signupendMsg = $this->msg( 'signupend' );
1025  $signupendHttpsMsg = $this->msg( 'signupend-https' );
1026  if ( !$signupendMsg->isDisabled() ) {
1027  $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
1028  $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
1029  ? $signupendHttpsMsg->parse() : $signupendMsg->parse();
1030  $fieldDefinitions['signupend'] = [
1031  'type' => 'info',
1032  'raw' => true,
1033  'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ),
1034  'weight' => 225,
1035  ];
1036  }
1037  }
1038  if ( !$this->isSignup() && $this->showExtraInformation() ) {
1039  $passwordReset = MediaWikiServices::getInstance()->getPasswordReset();
1040  if ( $passwordReset->isAllowed( $this->getUser() )->isGood() ) {
1041  $fieldDefinitions['passwordReset'] = [
1042  'type' => 'info',
1043  'raw' => true,
1044  'cssclass' => 'mw-form-related-link-container',
1045  'default' => $this->getLinkRenderer()->makeLink(
1046  SpecialPage::getTitleFor( 'PasswordReset' ),
1047  $this->msg( 'userlogin-resetpassword-link' )->text()
1048  ),
1049  'weight' => 230,
1050  ];
1051  }
1052 
1053  // Don't show a "create account" link if the user can't.
1054  if ( $this->showCreateAccountLink() ) {
1055  // link to the other action
1056  $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' : 'CreateAccount' );
1057  $linkq = $this->getReturnToQueryStringFragment();
1058  // Pass any language selection on to the mode switch link
1059  if ( $this->mLanguage ) {
1060  $linkq .= '&uselang=' . urlencode( $this->mLanguage );
1061  }
1062  $isLoggedIn = $this->getUser()->isRegistered()
1063  && !$this->getUser()->isTemp();
1064 
1065  $fieldDefinitions['createOrLogin'] = [
1066  'type' => 'info',
1067  'raw' => true,
1068  'linkQuery' => $linkq,
1069  'default' => function ( $params ) use ( $isLoggedIn, $linkTitle ) {
1070  return Html::rawElement( 'div',
1071  [ 'id' => 'mw-createaccount' . ( !$isLoggedIn ? '-cta' : '' ),
1072  'class' => ( $isLoggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
1073  ( $isLoggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
1074  . Html::element( 'a',
1075  [
1076  'id' => 'mw-createaccount-join' . ( $isLoggedIn ? '-loggedin' : '' ),
1077  'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
1078  'class' => ( $isLoggedIn ? '' : 'mw-ui-button' ),
1079  'tabindex' => 100,
1080  ],
1081  $this->msg(
1082  $isLoggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
1083  )->text()
1084  )
1085  );
1086  },
1087  'weight' => 235,
1088  ];
1089  }
1090  }
1091 
1092  return $fieldDefinitions;
1093  }
1094 
1104  protected function hasSessionCookie() {
1105  $config = $this->getConfig();
1106  return $config->get( MainConfigNames::DisableCookieCheck ) || (
1107  $config->get( 'InitialSessionId' ) &&
1108  $this->getRequest()->getSession()->getId() === (string)$config->get( 'InitialSessionId' )
1109  );
1110  }
1111 
1117  protected function getReturnToQueryStringFragment() {
1118  $returnto = '';
1119  if ( $this->mReturnTo !== '' ) {
1120  $returnto = 'returnto=' . wfUrlencode( $this->mReturnTo );
1121  if ( $this->mReturnToQuery !== '' ) {
1122  $returnto .= '&returntoquery=' . wfUrlencode( $this->mReturnToQuery );
1123  }
1124  }
1125  return $returnto;
1126  }
1127 
1133  private function showCreateAccountLink() {
1134  return $this->isSignup() ||
1135  $this->getContext()->getAuthority()->isAllowed( 'createaccount' );
1136  }
1137 
1138  protected function getTokenName() {
1139  return $this->isSignup() ? 'wpCreateaccountToken' : 'wpLoginToken';
1140  }
1141 
1148  protected function makeLanguageSelector() {
1149  $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
1150  if ( $msg->isBlank() ) {
1151  return '';
1152  }
1153  $langs = explode( "\n", $msg->text() );
1154  $links = [];
1155  foreach ( $langs as $lang ) {
1156  $lang = trim( $lang, '* ' );
1157  $parts = explode( '|', $lang );
1158  if ( count( $parts ) >= 2 ) {
1159  $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) );
1160  }
1161  }
1162 
1163  return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams(
1164  $this->getLanguage()->pipeList( $links ) )->escaped() : '';
1165  }
1166 
1175  protected function makeLanguageSelectorLink( $text, $lang ) {
1176  if ( $this->getLanguage()->getCode() == $lang ) {
1177  // no link for currently used language
1178  return htmlspecialchars( $text );
1179  }
1180  $query = [ 'uselang' => $lang ];
1181  if ( $this->mReturnTo !== '' ) {
1182  $query['returnto'] = $this->mReturnTo;
1183  $query['returntoquery'] = $this->mReturnToQuery;
1184  }
1185 
1186  $attr = [];
1187  $targetLanguage = MediaWikiServices::getInstance()->getLanguageFactory()
1188  ->getLanguage( $lang );
1189  $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
1190 
1191  return $this->getLinkRenderer()->makeKnownLink(
1192  $this->getPageTitle(),
1193  $text,
1194  $attr,
1195  $query
1196  );
1197  }
1198 
1199  protected function getGroupName() {
1200  return 'login';
1201  }
1202 
1207  protected function postProcessFormDescriptor( &$formDescriptor, $requests ) {
1208  // Pre-fill username (if not creating an account, T46775).
1209  if (
1210  isset( $formDescriptor['username'] ) &&
1211  !isset( $formDescriptor['username']['default'] ) &&
1212  !$this->isSignup()
1213  ) {
1214  $user = $this->getUser();
1215  if ( $user->isRegistered() && !$user->isTemp() ) {
1216  $formDescriptor['username']['default'] = $user->getName();
1217  } else {
1218  $formDescriptor['username']['default'] =
1219  $this->getRequest()->getSession()->suggestLoginUsername();
1220  }
1221  }
1222 
1223  // don't show a submit button if there is nothing to submit (i.e. the only form content
1224  // is other submit buttons, for redirect flows)
1225  if ( !$this->needsSubmitButton( $requests ) ) {
1226  unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] );
1227  }
1228 
1229  if ( !$this->isSignup() ) {
1230  // FIXME HACK don't focus on non-empty field
1231  // maybe there should be an autofocus-if similar to hide-if?
1232  if (
1233  isset( $formDescriptor['username'] )
1234  && empty( $formDescriptor['username']['default'] )
1235  && !$this->getRequest()->getCheck( 'wpName' )
1236  ) {
1237  $formDescriptor['username']['autofocus'] = true;
1238  } elseif ( isset( $formDescriptor['password'] ) ) {
1239  $formDescriptor['password']['autofocus'] = true;
1240  }
1241  }
1242 
1243  $this->addTabIndex( $formDescriptor );
1244  }
1245 }
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:497
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:345
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.
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:1877
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1093
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: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:370
static newFromName( $name, $validate='valid')
Definition: User.php:598
$wgUseMediaWikiUIEverywhere
Config variable stub for the UseMediaWikiUIEverywhere setting, for use by phpdoc and IDEs.
if(!isset( $args[0])) $lang