MediaWiki  REL1_28
AuthManager.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Auth;
25 
26 use Config;
27 use Psr\Log\LoggerAwareInterface;
28 use Psr\Log\LoggerInterface;
29 use Status;
31 use User;
33 
81 class AuthManager implements LoggerAwareInterface {
83  const ACTION_LOGIN = 'login';
86  const ACTION_LOGIN_CONTINUE = 'login-continue';
88  const ACTION_CREATE = 'create';
91  const ACTION_CREATE_CONTINUE = 'create-continue';
93  const ACTION_LINK = 'link';
96  const ACTION_LINK_CONTINUE = 'link-continue';
98  const ACTION_CHANGE = 'change';
100  const ACTION_REMOVE = 'remove';
102  const ACTION_UNLINK = 'unlink';
103 
105  const SEC_OK = 'ok';
107  const SEC_REAUTH = 'reauth';
109  const SEC_FAIL = 'fail';
110 
113 
115  private static $instance = null;
116 
118  private $request;
119 
121  private $config;
122 
124  private $logger;
125 
128 
131 
134 
137 
140 
145  public static function singleton() {
146  if ( self::$instance === null ) {
147  self::$instance = new self(
148  \RequestContext::getMain()->getRequest(),
149  \ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
150  );
151  }
152  return self::$instance;
153  }
154 
160  $this->request = $request;
161  $this->config = $config;
162  $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
163  }
164 
168  public function setLogger( LoggerInterface $logger ) {
169  $this->logger = $logger;
170  }
171 
175  public function getRequest() {
176  return $this->request;
177  }
178 
185  public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
186  $this->logger->warning( "Overriding AuthManager primary authn because $why" );
187 
188  if ( $this->primaryAuthenticationProviders !== null ) {
189  $this->logger->warning(
190  'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
191  );
192 
193  $this->allAuthenticationProviders = array_diff_key(
194  $this->allAuthenticationProviders,
195  $this->primaryAuthenticationProviders
196  );
197  $session = $this->request->getSession();
198  $session->remove( 'AuthManager::authnState' );
199  $session->remove( 'AuthManager::accountCreationState' );
200  $session->remove( 'AuthManager::accountLinkState' );
201  $this->createdAccountAuthenticationRequests = [];
202  }
203 
204  $this->primaryAuthenticationProviders = [];
205  foreach ( $providers as $provider ) {
206  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
207  throw new \RuntimeException(
208  'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
209  get_class( $provider )
210  );
211  }
212  $provider->setLogger( $this->logger );
213  $provider->setManager( $this );
214  $provider->setConfig( $this->config );
215  $id = $provider->getUniqueId();
216  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
217  throw new \RuntimeException(
218  "Duplicate specifications for id $id (classes " .
219  get_class( $provider ) . ' and ' .
220  get_class( $this->allAuthenticationProviders[$id] ) . ')'
221  );
222  }
223  $this->allAuthenticationProviders[$id] = $provider;
224  $this->primaryAuthenticationProviders[$id] = $provider;
225  }
226  }
227 
237  public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
238  global $wgAuth;
239 
240  if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
241  return call_user_func_array( [ $wgAuth, $method ], $params );
242  } else {
243  return $return;
244  }
245  }
246 
260  public function canAuthenticateNow() {
261  return $this->request->getSession()->canSetUser();
262  }
263 
282  public function beginAuthentication( array $reqs, $returnToUrl ) {
283  $session = $this->request->getSession();
284  if ( !$session->canSetUser() ) {
285  // Caller should have called canAuthenticateNow()
286  $session->remove( 'AuthManager::authnState' );
287  throw new \LogicException( 'Authentication is not possible now' );
288  }
289 
290  $guessUserName = null;
291  foreach ( $reqs as $req ) {
292  $req->returnToUrl = $returnToUrl;
293  // @codeCoverageIgnoreStart
294  if ( $req->username !== null && $req->username !== '' ) {
295  if ( $guessUserName === null ) {
296  $guessUserName = $req->username;
297  } elseif ( $guessUserName !== $req->username ) {
298  $guessUserName = null;
299  break;
300  }
301  }
302  // @codeCoverageIgnoreEnd
303  }
304 
305  // Check for special-case login of a just-created account
308  );
309  if ( $req ) {
310  if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
311  throw new \LogicException(
312  'CreatedAccountAuthenticationRequests are only valid on ' .
313  'the same AuthManager that created the account'
314  );
315  }
316 
317  $user = User::newFromName( $req->username );
318  // @codeCoverageIgnoreStart
319  if ( !$user ) {
320  throw new \UnexpectedValueException(
321  "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
322  );
323  } elseif ( $user->getId() != $req->id ) {
324  throw new \UnexpectedValueException(
325  "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
326  );
327  }
328  // @codeCoverageIgnoreEnd
329 
330  $this->logger->info( 'Logging in {user} after account creation', [
331  'user' => $user->getName(),
332  ] );
334  $this->setSessionDataForUser( $user );
335  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
336  $session->remove( 'AuthManager::authnState' );
337  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
338  return $ret;
339  }
340 
341  $this->removeAuthenticationSessionData( null );
342 
343  foreach ( $this->getPreAuthenticationProviders() as $provider ) {
344  $status = $provider->testForAuthentication( $reqs );
345  if ( !$status->isGood() ) {
346  $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
348  Status::wrap( $status )->getMessage()
349  );
350  $this->callMethodOnProviders( 7, 'postAuthentication',
351  [ User::newFromName( $guessUserName ) ?: null, $ret ]
352  );
353  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
354  return $ret;
355  }
356  }
357 
358  $state = [
359  'reqs' => $reqs,
360  'returnToUrl' => $returnToUrl,
361  'guessUserName' => $guessUserName,
362  'primary' => null,
363  'primaryResponse' => null,
364  'secondary' => [],
365  'maybeLink' => [],
366  'continueRequests' => [],
367  ];
368 
369  // Preserve state from a previous failed login
372  );
373  if ( $req ) {
374  $state['maybeLink'] = $req->maybeLink;
375  }
376 
377  $session = $this->request->getSession();
378  $session->setSecret( 'AuthManager::authnState', $state );
379  $session->persist();
380 
381  return $this->continueAuthentication( $reqs );
382  }
383 
406  public function continueAuthentication( array $reqs ) {
407  $session = $this->request->getSession();
408  try {
409  if ( !$session->canSetUser() ) {
410  // Caller should have called canAuthenticateNow()
411  // @codeCoverageIgnoreStart
412  throw new \LogicException( 'Authentication is not possible now' );
413  // @codeCoverageIgnoreEnd
414  }
415 
416  $state = $session->getSecret( 'AuthManager::authnState' );
417  if ( !is_array( $state ) ) {
419  wfMessage( 'authmanager-authn-not-in-progress' )
420  );
421  }
422  $state['continueRequests'] = [];
423 
424  $guessUserName = $state['guessUserName'];
425 
426  foreach ( $reqs as $req ) {
427  $req->returnToUrl = $state['returnToUrl'];
428  }
429 
430  // Step 1: Choose an primary authentication provider, and call it until it succeeds.
431 
432  if ( $state['primary'] === null ) {
433  // We haven't picked a PrimaryAuthenticationProvider yet
434  // @codeCoverageIgnoreStart
435  $guessUserName = null;
436  foreach ( $reqs as $req ) {
437  if ( $req->username !== null && $req->username !== '' ) {
438  if ( $guessUserName === null ) {
439  $guessUserName = $req->username;
440  } elseif ( $guessUserName !== $req->username ) {
441  $guessUserName = null;
442  break;
443  }
444  }
445  }
446  $state['guessUserName'] = $guessUserName;
447  // @codeCoverageIgnoreEnd
448  $state['reqs'] = $reqs;
449 
450  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
451  $res = $provider->beginPrimaryAuthentication( $reqs );
452  switch ( $res->status ) {
454  $state['primary'] = $id;
455  $state['primaryResponse'] = $res;
456  $this->logger->debug( "Primary login with $id succeeded" );
457  break 2;
459  $this->logger->debug( "Login failed in primary authentication by $id" );
460  if ( $res->createRequest || $state['maybeLink'] ) {
461  $res->createRequest = new CreateFromLoginAuthenticationRequest(
462  $res->createRequest, $state['maybeLink']
463  );
464  }
465  $this->callMethodOnProviders( 7, 'postAuthentication',
466  [ User::newFromName( $guessUserName ) ?: null, $res ]
467  );
468  $session->remove( 'AuthManager::authnState' );
469  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
470  return $res;
472  // Continue loop
473  break;
476  $this->logger->debug( "Primary login with $id returned $res->status" );
477  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
478  $state['primary'] = $id;
479  $state['continueRequests'] = $res->neededRequests;
480  $session->setSecret( 'AuthManager::authnState', $state );
481  return $res;
482 
483  // @codeCoverageIgnoreStart
484  default:
485  throw new \DomainException(
486  get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
487  );
488  // @codeCoverageIgnoreEnd
489  }
490  }
491  if ( $state['primary'] === null ) {
492  $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
494  wfMessage( 'authmanager-authn-no-primary' )
495  );
496  $this->callMethodOnProviders( 7, 'postAuthentication',
497  [ User::newFromName( $guessUserName ) ?: null, $ret ]
498  );
499  $session->remove( 'AuthManager::authnState' );
500  return $ret;
501  }
502  } elseif ( $state['primaryResponse'] === null ) {
503  $provider = $this->getAuthenticationProvider( $state['primary'] );
504  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
505  // Configuration changed? Force them to start over.
506  // @codeCoverageIgnoreStart
508  wfMessage( 'authmanager-authn-not-in-progress' )
509  );
510  $this->callMethodOnProviders( 7, 'postAuthentication',
511  [ User::newFromName( $guessUserName ) ?: null, $ret ]
512  );
513  $session->remove( 'AuthManager::authnState' );
514  return $ret;
515  // @codeCoverageIgnoreEnd
516  }
517  $id = $provider->getUniqueId();
518  $res = $provider->continuePrimaryAuthentication( $reqs );
519  switch ( $res->status ) {
521  $state['primaryResponse'] = $res;
522  $this->logger->debug( "Primary login with $id succeeded" );
523  break;
525  $this->logger->debug( "Login failed in primary authentication by $id" );
526  if ( $res->createRequest || $state['maybeLink'] ) {
527  $res->createRequest = new CreateFromLoginAuthenticationRequest(
528  $res->createRequest, $state['maybeLink']
529  );
530  }
531  $this->callMethodOnProviders( 7, 'postAuthentication',
532  [ User::newFromName( $guessUserName ) ?: null, $res ]
533  );
534  $session->remove( 'AuthManager::authnState' );
535  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
536  return $res;
539  $this->logger->debug( "Primary login with $id returned $res->status" );
540  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
541  $state['continueRequests'] = $res->neededRequests;
542  $session->setSecret( 'AuthManager::authnState', $state );
543  return $res;
544  default:
545  throw new \DomainException(
546  get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
547  );
548  }
549  }
550 
551  $res = $state['primaryResponse'];
552  if ( $res->username === null ) {
553  $provider = $this->getAuthenticationProvider( $state['primary'] );
554  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
555  // Configuration changed? Force them to start over.
556  // @codeCoverageIgnoreStart
558  wfMessage( 'authmanager-authn-not-in-progress' )
559  );
560  $this->callMethodOnProviders( 7, 'postAuthentication',
561  [ User::newFromName( $guessUserName ) ?: null, $ret ]
562  );
563  $session->remove( 'AuthManager::authnState' );
564  return $ret;
565  // @codeCoverageIgnoreEnd
566  }
567 
568  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
569  $res->linkRequest &&
570  // don't confuse the user with an incorrect message if linking is disabled
572  ) {
573  $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
574  $msg = 'authmanager-authn-no-local-user-link';
575  } else {
576  $msg = 'authmanager-authn-no-local-user';
577  }
578  $this->logger->debug(
579  "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
580  );
582  $ret->neededRequests = $this->getAuthenticationRequestsInternal(
583  self::ACTION_LOGIN,
584  [],
586  );
587  if ( $res->createRequest || $state['maybeLink'] ) {
588  $ret->createRequest = new CreateFromLoginAuthenticationRequest(
589  $res->createRequest, $state['maybeLink']
590  );
591  $ret->neededRequests[] = $ret->createRequest;
592  }
593  $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
594  $session->setSecret( 'AuthManager::authnState', [
595  'reqs' => [], // Will be filled in later
596  'primary' => null,
597  'primaryResponse' => null,
598  'secondary' => [],
599  'continueRequests' => $ret->neededRequests,
600  ] + $state );
601  return $ret;
602  }
603 
604  // Step 2: Primary authentication succeeded, create the User object
605  // (and add the user locally if necessary)
606 
607  $user = User::newFromName( $res->username, 'usable' );
608  if ( !$user ) {
609  $provider = $this->getAuthenticationProvider( $state['primary'] );
610  throw new \DomainException(
611  get_class( $provider ) . " returned an invalid username: {$res->username}"
612  );
613  }
614  if ( $user->getId() === 0 ) {
615  // User doesn't exist locally. Create it.
616  $this->logger->info( 'Auto-creating {user} on login', [
617  'user' => $user->getName(),
618  ] );
619  $status = $this->autoCreateUser( $user, $state['primary'], false );
620  if ( !$status->isGood() ) {
622  Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
623  );
624  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
625  $session->remove( 'AuthManager::authnState' );
626  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
627  return $ret;
628  }
629  }
630 
631  // Step 3: Iterate over all the secondary authentication providers.
632 
633  $beginReqs = $state['reqs'];
634 
635  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
636  if ( !isset( $state['secondary'][$id] ) ) {
637  // This provider isn't started yet, so we pass it the set
638  // of reqs from beginAuthentication instead of whatever
639  // might have been used by a previous provider in line.
640  $func = 'beginSecondaryAuthentication';
641  $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
642  } elseif ( !$state['secondary'][$id] ) {
643  $func = 'continueSecondaryAuthentication';
644  $res = $provider->continueSecondaryAuthentication( $user, $reqs );
645  } else {
646  continue;
647  }
648  switch ( $res->status ) {
650  $this->logger->debug( "Secondary login with $id succeeded" );
651  // fall through
653  $state['secondary'][$id] = true;
654  break;
656  $this->logger->debug( "Login failed in secondary authentication by $id" );
657  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
658  $session->remove( 'AuthManager::authnState' );
659  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
660  return $res;
663  $this->logger->debug( "Secondary login with $id returned " . $res->status );
664  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
665  $state['secondary'][$id] = false;
666  $state['continueRequests'] = $res->neededRequests;
667  $session->setSecret( 'AuthManager::authnState', $state );
668  return $res;
669 
670  // @codeCoverageIgnoreStart
671  default:
672  throw new \DomainException(
673  get_class( $provider ) . "::{$func}() returned $res->status"
674  );
675  // @codeCoverageIgnoreEnd
676  }
677  }
678 
679  // Step 4: Authentication complete! Set the user in the session and
680  // clean up.
681 
682  $this->logger->info( 'Login for {user} succeeded', [
683  'user' => $user->getName(),
684  ] );
688  );
689  $this->setSessionDataForUser( $user, $req && $req->rememberMe );
691  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
692  $session->remove( 'AuthManager::authnState' );
693  $this->removeAuthenticationSessionData( null );
694  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
695  return $ret;
696  } catch ( \Exception $ex ) {
697  $session->remove( 'AuthManager::authnState' );
698  throw $ex;
699  }
700  }
701 
713  public function securitySensitiveOperationStatus( $operation ) {
715 
716  $this->logger->debug( __METHOD__ . ": Checking $operation" );
717 
718  $session = $this->request->getSession();
719  $aId = $session->getUser()->getId();
720  if ( $aId === 0 ) {
721  // User isn't authenticated. DWIM?
723  $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
724  return $status;
725  }
726 
727  if ( $session->canSetUser() ) {
728  $id = $session->get( 'AuthManager:lastAuthId' );
729  $last = $session->get( 'AuthManager:lastAuthTimestamp' );
730  if ( $id !== $aId || $last === null ) {
731  $timeSinceLogin = PHP_INT_MAX; // Forever ago
732  } else {
733  $timeSinceLogin = max( 0, time() - $last );
734  }
735 
736  $thresholds = $this->config->get( 'ReauthenticateTime' );
737  if ( isset( $thresholds[$operation] ) ) {
738  $threshold = $thresholds[$operation];
739  } elseif ( isset( $thresholds['default'] ) ) {
740  $threshold = $thresholds['default'];
741  } else {
742  throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
743  }
744 
745  if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
747  }
748  } else {
749  $timeSinceLogin = -1;
750 
751  $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
752  if ( isset( $pass[$operation] ) ) {
753  $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
754  } elseif ( isset( $pass['default'] ) ) {
755  $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
756  } else {
757  throw new \UnexpectedValueException(
758  '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
759  );
760  }
761  }
762 
763  \Hooks::run( 'SecuritySensitiveOperationStatus', [
764  &$status, $operation, $session, $timeSinceLogin
765  ] );
766 
767  // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
768  if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
770  }
771 
772  $this->logger->info( __METHOD__ . ": $operation is $status" );
773 
774  return $status;
775  }
776 
786  public function userCanAuthenticate( $username ) {
787  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
788  if ( $provider->testUserCanAuthenticate( $username ) ) {
789  return true;
790  }
791  }
792  return false;
793  }
794 
809  public function normalizeUsername( $username ) {
810  $ret = [];
811  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
812  $normalized = $provider->providerNormalizeUsername( $username );
813  if ( $normalized !== null ) {
814  $ret[$normalized] = true;
815  }
816  }
817  return array_keys( $ret );
818  }
819 
834  public function revokeAccessForUser( $username ) {
835  $this->logger->info( 'Revoking access for {user}', [
836  'user' => $username,
837  ] );
838  $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
839  }
840 
850  public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
851  $any = false;
852  $providers = $this->getPrimaryAuthenticationProviders() +
854  foreach ( $providers as $provider ) {
855  $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
856  if ( !$status->isGood() ) {
857  return Status::wrap( $status );
858  }
859  $any = $any || $status->value !== 'ignored';
860  }
861  if ( !$any ) {
862  $status = Status::newGood( 'ignored' );
863  $status->warning( 'authmanager-change-not-supported' );
864  return $status;
865  }
866  return Status::newGood();
867  }
868 
884  $this->logger->info( 'Changing authentication data for {user} class {what}', [
885  'user' => is_string( $req->username ) ? $req->username : '<no name>',
886  'what' => get_class( $req ),
887  ] );
888 
889  $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
890 
891  // When the main account's authentication data is changed, invalidate
892  // all BotPasswords too.
894  }
895 
907  public function canCreateAccounts() {
908  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
909  switch ( $provider->accountCreationType() ) {
912  return true;
913  }
914  }
915  return false;
916  }
917 
926  public function canCreateAccount( $username, $options = [] ) {
927  // Back compat
928  if ( is_int( $options ) ) {
929  $options = [ 'flags' => $options ];
930  }
931  $options += [
932  'flags' => User::READ_NORMAL,
933  'creating' => false,
934  ];
935  $flags = $options['flags'];
936 
937  if ( !$this->canCreateAccounts() ) {
938  return Status::newFatal( 'authmanager-create-disabled' );
939  }
940 
941  if ( $this->userExists( $username, $flags ) ) {
942  return Status::newFatal( 'userexists' );
943  }
944 
945  $user = User::newFromName( $username, 'creatable' );
946  if ( !is_object( $user ) ) {
947  return Status::newFatal( 'noname' );
948  } else {
949  $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
950  if ( $user->getId() !== 0 ) {
951  return Status::newFatal( 'userexists' );
952  }
953  }
954 
955  // Denied by providers?
956  $providers = $this->getPreAuthenticationProviders() +
959  foreach ( $providers as $provider ) {
960  $status = $provider->testUserForCreation( $user, false, $options );
961  if ( !$status->isGood() ) {
962  return Status::wrap( $status );
963  }
964  }
965 
966  return Status::newGood();
967  }
968 
974  public function checkAccountCreatePermissions( User $creator ) {
975  // Wiki is read-only?
976  if ( wfReadOnly() ) {
977  return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
978  }
979 
980  // This is awful, this permission check really shouldn't go through Title.
981  $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
982  ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
983  if ( $permErrors ) {
985  foreach ( $permErrors as $args ) {
986  call_user_func_array( [ $status, 'fatal' ], $args );
987  }
988  return $status;
989  }
990 
991  $block = $creator->isBlockedFromCreateAccount();
992  if ( $block ) {
993  $errorParams = [
994  $block->getTarget(),
995  $block->mReason ?: wfMessage( 'blockednoreason' )->text(),
996  $block->getByName()
997  ];
998 
999  if ( $block->getType() === \Block::TYPE_RANGE ) {
1000  $errorMessage = 'cantcreateaccount-range-text';
1001  $errorParams[] = $this->getRequest()->getIP();
1002  } else {
1003  $errorMessage = 'cantcreateaccount-text';
1004  }
1005 
1006  return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1007  }
1008 
1009  $ip = $this->getRequest()->getIP();
1010  if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1011  return Status::newFatal( 'sorbs_create_account_reason' );
1012  }
1013 
1014  return Status::newGood();
1015  }
1016 
1036  public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1037  $session = $this->request->getSession();
1038  if ( !$this->canCreateAccounts() ) {
1039  // Caller should have called canCreateAccounts()
1040  $session->remove( 'AuthManager::accountCreationState' );
1041  throw new \LogicException( 'Account creation is not possible' );
1042  }
1043 
1044  try {
1046  } catch ( \UnexpectedValueException $ex ) {
1047  $username = null;
1048  }
1049  if ( $username === null ) {
1050  $this->logger->debug( __METHOD__ . ': No username provided' );
1051  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1052  }
1053 
1054  // Permissions check
1055  $status = $this->checkAccountCreatePermissions( $creator );
1056  if ( !$status->isGood() ) {
1057  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1058  'user' => $username,
1059  'creator' => $creator->getName(),
1060  'reason' => $status->getWikiText( null, null, 'en' )
1061  ] );
1062  return AuthenticationResponse::newFail( $status->getMessage() );
1063  }
1064 
1065  $status = $this->canCreateAccount(
1066  $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1067  );
1068  if ( !$status->isGood() ) {
1069  $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1070  'user' => $username,
1071  'creator' => $creator->getName(),
1072  'reason' => $status->getWikiText( null, null, 'en' )
1073  ] );
1074  return AuthenticationResponse::newFail( $status->getMessage() );
1075  }
1076 
1077  $user = User::newFromName( $username, 'creatable' );
1078  foreach ( $reqs as $req ) {
1079  $req->username = $username;
1080  $req->returnToUrl = $returnToUrl;
1081  if ( $req instanceof UserDataAuthenticationRequest ) {
1082  $status = $req->populateUser( $user );
1083  if ( !$status->isGood() ) {
1085  $session->remove( 'AuthManager::accountCreationState' );
1086  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1087  'user' => $user->getName(),
1088  'creator' => $creator->getName(),
1089  'reason' => $status->getWikiText( null, null, 'en' ),
1090  ] );
1091  return AuthenticationResponse::newFail( $status->getMessage() );
1092  }
1093  }
1094  }
1095 
1096  $this->removeAuthenticationSessionData( null );
1097 
1098  $state = [
1099  'username' => $username,
1100  'userid' => 0,
1101  'creatorid' => $creator->getId(),
1102  'creatorname' => $creator->getName(),
1103  'reqs' => $reqs,
1104  'returnToUrl' => $returnToUrl,
1105  'primary' => null,
1106  'primaryResponse' => null,
1107  'secondary' => [],
1108  'continueRequests' => [],
1109  'maybeLink' => [],
1110  'ranPreTests' => false,
1111  ];
1112 
1113  // Special case: converting a login to an account creation
1116  );
1117  if ( $req ) {
1118  $state['maybeLink'] = $req->maybeLink;
1119 
1120  if ( $req->createRequest ) {
1121  $reqs[] = $req->createRequest;
1122  $state['reqs'][] = $req->createRequest;
1123  }
1124  }
1125 
1126  $session->setSecret( 'AuthManager::accountCreationState', $state );
1127  $session->persist();
1128 
1129  return $this->continueAccountCreation( $reqs );
1130  }
1131 
1137  public function continueAccountCreation( array $reqs ) {
1138  $session = $this->request->getSession();
1139  try {
1140  if ( !$this->canCreateAccounts() ) {
1141  // Caller should have called canCreateAccounts()
1142  $session->remove( 'AuthManager::accountCreationState' );
1143  throw new \LogicException( 'Account creation is not possible' );
1144  }
1145 
1146  $state = $session->getSecret( 'AuthManager::accountCreationState' );
1147  if ( !is_array( $state ) ) {
1149  wfMessage( 'authmanager-create-not-in-progress' )
1150  );
1151  }
1152  $state['continueRequests'] = [];
1153 
1154  // Step 0: Prepare and validate the input
1155 
1156  $user = User::newFromName( $state['username'], 'creatable' );
1157  if ( !is_object( $user ) ) {
1158  $session->remove( 'AuthManager::accountCreationState' );
1159  $this->logger->debug( __METHOD__ . ': Invalid username', [
1160  'user' => $state['username'],
1161  ] );
1162  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1163  }
1164 
1165  if ( $state['creatorid'] ) {
1166  $creator = User::newFromId( $state['creatorid'] );
1167  } else {
1168  $creator = new User;
1169  $creator->setName( $state['creatorname'] );
1170  }
1171 
1172  // Avoid account creation races on double submissions
1174  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1175  if ( !$lock ) {
1176  // Don't clear AuthManager::accountCreationState for this code
1177  // path because the process that won the race owns it.
1178  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1179  'user' => $user->getName(),
1180  'creator' => $creator->getName(),
1181  ] );
1182  return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1183  }
1184 
1185  // Permissions check
1186  $status = $this->checkAccountCreatePermissions( $creator );
1187  if ( !$status->isGood() ) {
1188  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1189  'user' => $user->getName(),
1190  'creator' => $creator->getName(),
1191  'reason' => $status->getWikiText( null, null, 'en' )
1192  ] );
1193  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1194  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1195  $session->remove( 'AuthManager::accountCreationState' );
1196  return $ret;
1197  }
1198 
1199  // Load from master for existence check
1200  $user->load( User::READ_LOCKING );
1201 
1202  if ( $state['userid'] === 0 ) {
1203  if ( $user->getId() != 0 ) {
1204  $this->logger->debug( __METHOD__ . ': User exists locally', [
1205  'user' => $user->getName(),
1206  'creator' => $creator->getName(),
1207  ] );
1208  $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1209  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1210  $session->remove( 'AuthManager::accountCreationState' );
1211  return $ret;
1212  }
1213  } else {
1214  if ( $user->getId() == 0 ) {
1215  $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1216  'user' => $user->getName(),
1217  'creator' => $creator->getName(),
1218  'expected_id' => $state['userid'],
1219  ] );
1220  throw new \UnexpectedValueException(
1221  "User \"{$state['username']}\" should exist now, but doesn't!"
1222  );
1223  }
1224  if ( $user->getId() != $state['userid'] ) {
1225  $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1226  'user' => $user->getName(),
1227  'creator' => $creator->getName(),
1228  'expected_id' => $state['userid'],
1229  'actual_id' => $user->getId(),
1230  ] );
1231  throw new \UnexpectedValueException(
1232  "User \"{$state['username']}\" exists, but " .
1233  "ID {$user->getId()} != {$state['userid']}!"
1234  );
1235  }
1236  }
1237  foreach ( $state['reqs'] as $req ) {
1238  if ( $req instanceof UserDataAuthenticationRequest ) {
1239  $status = $req->populateUser( $user );
1240  if ( !$status->isGood() ) {
1241  // This should never happen...
1243  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1244  'user' => $user->getName(),
1245  'creator' => $creator->getName(),
1246  'reason' => $status->getWikiText( null, null, 'en' ),
1247  ] );
1248  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1249  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1250  $session->remove( 'AuthManager::accountCreationState' );
1251  return $ret;
1252  }
1253  }
1254  }
1255 
1256  foreach ( $reqs as $req ) {
1257  $req->returnToUrl = $state['returnToUrl'];
1258  $req->username = $state['username'];
1259  }
1260 
1261  // Run pre-creation tests, if we haven't already
1262  if ( !$state['ranPreTests'] ) {
1263  $providers = $this->getPreAuthenticationProviders() +
1266  foreach ( $providers as $id => $provider ) {
1267  $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1268  if ( !$status->isGood() ) {
1269  $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1270  'user' => $user->getName(),
1271  'creator' => $creator->getName(),
1272  ] );
1274  Status::wrap( $status )->getMessage()
1275  );
1276  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1277  $session->remove( 'AuthManager::accountCreationState' );
1278  return $ret;
1279  }
1280  }
1281 
1282  $state['ranPreTests'] = true;
1283  }
1284 
1285  // Step 1: Choose a primary authentication provider and call it until it succeeds.
1286 
1287  if ( $state['primary'] === null ) {
1288  // We haven't picked a PrimaryAuthenticationProvider yet
1289  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1290  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1291  continue;
1292  }
1293  $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1294  switch ( $res->status ) {
1296  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1297  'user' => $user->getName(),
1298  'creator' => $creator->getName(),
1299  ] );
1300  $state['primary'] = $id;
1301  $state['primaryResponse'] = $res;
1302  break 2;
1304  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1305  'user' => $user->getName(),
1306  'creator' => $creator->getName(),
1307  ] );
1308  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1309  $session->remove( 'AuthManager::accountCreationState' );
1310  return $res;
1312  // Continue loop
1313  break;
1316  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1317  'user' => $user->getName(),
1318  'creator' => $creator->getName(),
1319  ] );
1320  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1321  $state['primary'] = $id;
1322  $state['continueRequests'] = $res->neededRequests;
1323  $session->setSecret( 'AuthManager::accountCreationState', $state );
1324  return $res;
1325 
1326  // @codeCoverageIgnoreStart
1327  default:
1328  throw new \DomainException(
1329  get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1330  );
1331  // @codeCoverageIgnoreEnd
1332  }
1333  }
1334  if ( $state['primary'] === null ) {
1335  $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1336  'user' => $user->getName(),
1337  'creator' => $creator->getName(),
1338  ] );
1340  wfMessage( 'authmanager-create-no-primary' )
1341  );
1342  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1343  $session->remove( 'AuthManager::accountCreationState' );
1344  return $ret;
1345  }
1346  } elseif ( $state['primaryResponse'] === null ) {
1347  $provider = $this->getAuthenticationProvider( $state['primary'] );
1348  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1349  // Configuration changed? Force them to start over.
1350  // @codeCoverageIgnoreStart
1352  wfMessage( 'authmanager-create-not-in-progress' )
1353  );
1354  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1355  $session->remove( 'AuthManager::accountCreationState' );
1356  return $ret;
1357  // @codeCoverageIgnoreEnd
1358  }
1359  $id = $provider->getUniqueId();
1360  $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1361  switch ( $res->status ) {
1363  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1364  'user' => $user->getName(),
1365  'creator' => $creator->getName(),
1366  ] );
1367  $state['primaryResponse'] = $res;
1368  break;
1370  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1371  'user' => $user->getName(),
1372  'creator' => $creator->getName(),
1373  ] );
1374  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1375  $session->remove( 'AuthManager::accountCreationState' );
1376  return $res;
1379  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1380  'user' => $user->getName(),
1381  'creator' => $creator->getName(),
1382  ] );
1383  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1384  $state['continueRequests'] = $res->neededRequests;
1385  $session->setSecret( 'AuthManager::accountCreationState', $state );
1386  return $res;
1387  default:
1388  throw new \DomainException(
1389  get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1390  );
1391  }
1392  }
1393 
1394  // Step 2: Primary authentication succeeded, create the User object
1395  // and add the user locally.
1396 
1397  if ( $state['userid'] === 0 ) {
1398  $this->logger->info( 'Creating user {user} during account creation', [
1399  'user' => $user->getName(),
1400  'creator' => $creator->getName(),
1401  ] );
1402  $status = $user->addToDatabase();
1403  if ( !$status->isOK() ) {
1404  // @codeCoverageIgnoreStart
1405  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1406  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1407  $session->remove( 'AuthManager::accountCreationState' );
1408  return $ret;
1409  // @codeCoverageIgnoreEnd
1410  }
1411  $this->setDefaultUserOptions( $user, $creator->isAnon() );
1412  \Hooks::run( 'LocalUserCreated', [ $user, false ] );
1413  $user->saveSettings();
1414  $state['userid'] = $user->getId();
1415 
1416  // Update user count
1417  \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1418 
1419  // Watch user's userpage and talk page
1420  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1421 
1422  // Inform the provider
1423  $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1424 
1425  // Log the creation
1426  if ( $this->config->get( 'NewUserLog' ) ) {
1427  $isAnon = $creator->isAnon();
1428  $logEntry = new \ManualLogEntry(
1429  'newusers',
1430  $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1431  );
1432  $logEntry->setPerformer( $isAnon ? $user : $creator );
1433  $logEntry->setTarget( $user->getUserPage() );
1437  );
1438  $logEntry->setComment( $req ? $req->reason : '' );
1439  $logEntry->setParameters( [
1440  '4::userid' => $user->getId(),
1441  ] );
1442  $logid = $logEntry->insert();
1443  $logEntry->publish( $logid );
1444  }
1445  }
1446 
1447  // Step 3: Iterate over all the secondary authentication providers.
1448 
1449  $beginReqs = $state['reqs'];
1450 
1451  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1452  if ( !isset( $state['secondary'][$id] ) ) {
1453  // This provider isn't started yet, so we pass it the set
1454  // of reqs from beginAuthentication instead of whatever
1455  // might have been used by a previous provider in line.
1456  $func = 'beginSecondaryAccountCreation';
1457  $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1458  } elseif ( !$state['secondary'][$id] ) {
1459  $func = 'continueSecondaryAccountCreation';
1460  $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1461  } else {
1462  continue;
1463  }
1464  switch ( $res->status ) {
1466  $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1467  'user' => $user->getName(),
1468  'creator' => $creator->getName(),
1469  ] );
1470  // fall through
1472  $state['secondary'][$id] = true;
1473  break;
1476  $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1477  'user' => $user->getName(),
1478  'creator' => $creator->getName(),
1479  ] );
1480  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1481  $state['secondary'][$id] = false;
1482  $state['continueRequests'] = $res->neededRequests;
1483  $session->setSecret( 'AuthManager::accountCreationState', $state );
1484  return $res;
1486  throw new \DomainException(
1487  get_class( $provider ) . "::{$func}() returned $res->status." .
1488  ' Secondary providers are not allowed to fail account creation, that' .
1489  ' should have been done via testForAccountCreation().'
1490  );
1491  // @codeCoverageIgnoreStart
1492  default:
1493  throw new \DomainException(
1494  get_class( $provider ) . "::{$func}() returned $res->status"
1495  );
1496  // @codeCoverageIgnoreEnd
1497  }
1498  }
1499 
1500  $id = $user->getId();
1501  $name = $user->getName();
1504  $ret->loginRequest = $req;
1505  $this->createdAccountAuthenticationRequests[] = $req;
1506 
1507  $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1508  'user' => $user->getName(),
1509  'creator' => $creator->getName(),
1510  ] );
1511 
1512  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1513  $session->remove( 'AuthManager::accountCreationState' );
1514  $this->removeAuthenticationSessionData( null );
1515  return $ret;
1516  } catch ( \Exception $ex ) {
1517  $session->remove( 'AuthManager::accountCreationState' );
1518  throw $ex;
1519  }
1520  }
1521 
1537  public function autoCreateUser( User $user, $source, $login = true ) {
1538  if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1540  ) {
1541  throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1542  }
1543 
1544  $username = $user->getName();
1545 
1546  // Try the local user from the replica DB
1547  $localId = User::idFromName( $username );
1548  $flags = User::READ_NORMAL;
1549 
1550  // Fetch the user ID from the master, so that we don't try to create the user
1551  // when they already exist, due to replication lag
1552  // @codeCoverageIgnoreStart
1553  if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
1554  $localId = User::idFromName( $username, User::READ_LATEST );
1555  $flags = User::READ_LATEST;
1556  }
1557  // @codeCoverageIgnoreEnd
1558 
1559  if ( $localId ) {
1560  $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1561  'username' => $username,
1562  ] );
1563  $user->setId( $localId );
1564  $user->loadFromId( $flags );
1565  if ( $login ) {
1566  $this->setSessionDataForUser( $user );
1567  }
1569  $status->warning( 'userexists' );
1570  return $status;
1571  }
1572 
1573  // Wiki is read-only?
1574  if ( wfReadOnly() ) {
1575  $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1576  'username' => $username,
1577  'reason' => wfReadOnlyReason(),
1578  ] );
1579  $user->setId( 0 );
1580  $user->loadFromId();
1581  return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
1582  }
1583 
1584  // Check the session, if we tried to create this user already there's
1585  // no point in retrying.
1586  $session = $this->request->getSession();
1587  if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1588  $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1589  'username' => $username,
1590  'sessionid' => $session->getId(),
1591  ] );
1592  $user->setId( 0 );
1593  $user->loadFromId();
1594  $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1595  if ( $reason instanceof StatusValue ) {
1596  return Status::wrap( $reason );
1597  } else {
1598  return Status::newFatal( $reason );
1599  }
1600  }
1601 
1602  // Is the username creatable?
1603  if ( !User::isCreatableName( $username ) ) {
1604  $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1605  'username' => $username,
1606  ] );
1607  $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1608  $user->setId( 0 );
1609  $user->loadFromId();
1610  return Status::newFatal( 'noname' );
1611  }
1612 
1613  // Is the IP user able to create accounts?
1614  $anon = new User;
1615  if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1616  $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1617  'username' => $username,
1618  'ip' => $anon->getName(),
1619  ] );
1620  $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1621  $session->persist();
1622  $user->setId( 0 );
1623  $user->loadFromId();
1624  return Status::newFatal( 'authmanager-autocreate-noperm' );
1625  }
1626 
1627  // Avoid account creation races on double submissions
1629  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1630  if ( !$lock ) {
1631  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1632  'user' => $username,
1633  ] );
1634  $user->setId( 0 );
1635  $user->loadFromId();
1636  return Status::newFatal( 'usernameinprogress' );
1637  }
1638 
1639  // Denied by providers?
1640  $options = [
1641  'flags' => User::READ_LATEST,
1642  'creating' => true,
1643  ];
1644  $providers = $this->getPreAuthenticationProviders() +
1647  foreach ( $providers as $provider ) {
1648  $status = $provider->testUserForCreation( $user, $source, $options );
1649  if ( !$status->isGood() ) {
1650  $ret = Status::wrap( $status );
1651  $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1652  'username' => $username,
1653  'reason' => $ret->getWikiText( null, null, 'en' ),
1654  ] );
1655  $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1656  $user->setId( 0 );
1657  $user->loadFromId();
1658  return $ret;
1659  }
1660  }
1661 
1662  $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1663  if ( $cache->get( $backoffKey ) ) {
1664  $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1665  'username' => $username,
1666  ] );
1667  $user->setId( 0 );
1668  $user->loadFromId();
1669  return Status::newFatal( 'authmanager-autocreate-exception' );
1670  }
1671 
1672  // Checks passed, create the user...
1673  $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
1674  $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1675  'username' => $username,
1676  'from' => $from,
1677  ] );
1678 
1679  // Ignore warnings about master connections/writes...hard to avoid here
1680  $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1681  $old = $trxProfiler->setSilenced( true );
1682  try {
1683  $status = $user->addToDatabase();
1684  if ( !$status->isOK() ) {
1685  // Double-check for a race condition (T70012). We make use of the fact that when
1686  // addToDatabase fails due to the user already existing, the user object gets loaded.
1687  if ( $user->getId() ) {
1688  $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1689  'username' => $username,
1690  ] );
1691  if ( $login ) {
1692  $this->setSessionDataForUser( $user );
1693  }
1695  $status->warning( 'userexists' );
1696  } else {
1697  $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1698  'username' => $username,
1699  'msg' => $status->getWikiText( null, null, 'en' )
1700  ] );
1701  $user->setId( 0 );
1702  $user->loadFromId();
1703  }
1704  return $status;
1705  }
1706  } catch ( \Exception $ex ) {
1707  $trxProfiler->setSilenced( $old );
1708  $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1709  'username' => $username,
1710  'exception' => $ex,
1711  ] );
1712  // Do not keep throwing errors for a while
1713  $cache->set( $backoffKey, 1, 600 );
1714  // Bubble up error; which should normally trigger DB rollbacks
1715  throw $ex;
1716  }
1717 
1718  $this->setDefaultUserOptions( $user, false );
1719 
1720  // Inform the providers
1721  $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1722 
1723  \Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1724  \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1725  $user->saveSettings();
1726 
1727  // Update user count
1728  \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1729  // Watch user's userpage and talk page
1730  \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1731  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1732  } );
1733 
1734  // Log the creation
1735  if ( $this->config->get( 'NewUserLog' ) ) {
1736  $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1737  $logEntry->setPerformer( $user );
1738  $logEntry->setTarget( $user->getUserPage() );
1739  $logEntry->setComment( '' );
1740  $logEntry->setParameters( [
1741  '4::userid' => $user->getId(),
1742  ] );
1743  $logEntry->insert();
1744  }
1745 
1746  $trxProfiler->setSilenced( $old );
1747 
1748  if ( $login ) {
1749  $this->setSessionDataForUser( $user );
1750  }
1751 
1752  return Status::newGood();
1753  }
1754 
1766  public function canLinkAccounts() {
1767  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1768  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1769  return true;
1770  }
1771  }
1772  return false;
1773  }
1774 
1784  public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1785  $session = $this->request->getSession();
1786  $session->remove( 'AuthManager::accountLinkState' );
1787 
1788  if ( !$this->canLinkAccounts() ) {
1789  // Caller should have called canLinkAccounts()
1790  throw new \LogicException( 'Account linking is not possible' );
1791  }
1792 
1793  if ( $user->getId() === 0 ) {
1794  if ( !User::isUsableName( $user->getName() ) ) {
1795  $msg = wfMessage( 'noname' );
1796  } else {
1797  $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1798  }
1799  return AuthenticationResponse::newFail( $msg );
1800  }
1801  foreach ( $reqs as $req ) {
1802  $req->username = $user->getName();
1803  $req->returnToUrl = $returnToUrl;
1804  }
1805 
1806  $this->removeAuthenticationSessionData( null );
1807 
1808  $providers = $this->getPreAuthenticationProviders();
1809  foreach ( $providers as $id => $provider ) {
1810  $status = $provider->testForAccountLink( $user );
1811  if ( !$status->isGood() ) {
1812  $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1813  'user' => $user->getName(),
1814  ] );
1816  Status::wrap( $status )->getMessage()
1817  );
1818  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1819  return $ret;
1820  }
1821  }
1822 
1823  $state = [
1824  'username' => $user->getName(),
1825  'userid' => $user->getId(),
1826  'returnToUrl' => $returnToUrl,
1827  'primary' => null,
1828  'continueRequests' => [],
1829  ];
1830 
1831  $providers = $this->getPrimaryAuthenticationProviders();
1832  foreach ( $providers as $id => $provider ) {
1833  if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1834  continue;
1835  }
1836 
1837  $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1838  switch ( $res->status ) {
1840  $this->logger->info( "Account linked to {user} by $id", [
1841  'user' => $user->getName(),
1842  ] );
1843  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1844  return $res;
1845 
1847  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1848  'user' => $user->getName(),
1849  ] );
1850  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1851  return $res;
1852 
1854  // Continue loop
1855  break;
1856 
1859  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1860  'user' => $user->getName(),
1861  ] );
1862  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1863  $state['primary'] = $id;
1864  $state['continueRequests'] = $res->neededRequests;
1865  $session->setSecret( 'AuthManager::accountLinkState', $state );
1866  $session->persist();
1867  return $res;
1868 
1869  // @codeCoverageIgnoreStart
1870  default:
1871  throw new \DomainException(
1872  get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1873  );
1874  // @codeCoverageIgnoreEnd
1875  }
1876  }
1877 
1878  $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1879  'user' => $user->getName(),
1880  ] );
1882  wfMessage( 'authmanager-link-no-primary' )
1883  );
1884  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1885  return $ret;
1886  }
1887 
1893  public function continueAccountLink( array $reqs ) {
1894  $session = $this->request->getSession();
1895  try {
1896  if ( !$this->canLinkAccounts() ) {
1897  // Caller should have called canLinkAccounts()
1898  $session->remove( 'AuthManager::accountLinkState' );
1899  throw new \LogicException( 'Account linking is not possible' );
1900  }
1901 
1902  $state = $session->getSecret( 'AuthManager::accountLinkState' );
1903  if ( !is_array( $state ) ) {
1905  wfMessage( 'authmanager-link-not-in-progress' )
1906  );
1907  }
1908  $state['continueRequests'] = [];
1909 
1910  // Step 0: Prepare and validate the input
1911 
1912  $user = User::newFromName( $state['username'], 'usable' );
1913  if ( !is_object( $user ) ) {
1914  $session->remove( 'AuthManager::accountLinkState' );
1915  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1916  }
1917  if ( $user->getId() != $state['userid'] ) {
1918  throw new \UnexpectedValueException(
1919  "User \"{$state['username']}\" is valid, but " .
1920  "ID {$user->getId()} != {$state['userid']}!"
1921  );
1922  }
1923 
1924  foreach ( $reqs as $req ) {
1925  $req->username = $state['username'];
1926  $req->returnToUrl = $state['returnToUrl'];
1927  }
1928 
1929  // Step 1: Call the primary again until it succeeds
1930 
1931  $provider = $this->getAuthenticationProvider( $state['primary'] );
1932  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1933  // Configuration changed? Force them to start over.
1934  // @codeCoverageIgnoreStart
1936  wfMessage( 'authmanager-link-not-in-progress' )
1937  );
1938  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1939  $session->remove( 'AuthManager::accountLinkState' );
1940  return $ret;
1941  // @codeCoverageIgnoreEnd
1942  }
1943  $id = $provider->getUniqueId();
1944  $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1945  switch ( $res->status ) {
1947  $this->logger->info( "Account linked to {user} by $id", [
1948  'user' => $user->getName(),
1949  ] );
1950  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1951  $session->remove( 'AuthManager::accountLinkState' );
1952  return $res;
1954  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1955  'user' => $user->getName(),
1956  ] );
1957  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1958  $session->remove( 'AuthManager::accountLinkState' );
1959  return $res;
1962  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1963  'user' => $user->getName(),
1964  ] );
1965  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1966  $state['continueRequests'] = $res->neededRequests;
1967  $session->setSecret( 'AuthManager::accountLinkState', $state );
1968  return $res;
1969  default:
1970  throw new \DomainException(
1971  get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1972  );
1973  }
1974  } catch ( \Exception $ex ) {
1975  $session->remove( 'AuthManager::accountLinkState' );
1976  throw $ex;
1977  }
1978  }
1979 
2005  public function getAuthenticationRequests( $action, User $user = null ) {
2006  $options = [];
2007  $providerAction = $action;
2008 
2009  // Figure out which providers to query
2010  switch ( $action ) {
2011  case self::ACTION_LOGIN:
2012  case self::ACTION_CREATE:
2013  $providers = $this->getPreAuthenticationProviders() +
2016  break;
2017 
2019  $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2020  return is_array( $state ) ? $state['continueRequests'] : [];
2021 
2023  $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2024  return is_array( $state ) ? $state['continueRequests'] : [];
2025 
2026  case self::ACTION_LINK:
2027  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2028  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2029  } );
2030  break;
2031 
2032  case self::ACTION_UNLINK:
2033  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2034  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2035  } );
2036 
2037  // To providers, unlink and remove are identical.
2038  $providerAction = self::ACTION_REMOVE;
2039  break;
2040 
2042  $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2043  return is_array( $state ) ? $state['continueRequests'] : [];
2044 
2045  case self::ACTION_CHANGE:
2046  case self::ACTION_REMOVE:
2047  $providers = $this->getPrimaryAuthenticationProviders() +
2049  break;
2050 
2051  // @codeCoverageIgnoreStart
2052  default:
2053  throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2054  }
2055  // @codeCoverageIgnoreEnd
2056 
2057  return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2058  }
2059 
2070  $providerAction, array $options, array $providers, User $user = null
2071  ) {
2072  $user = $user ?: \RequestContext::getMain()->getUser();
2073  $options['username'] = $user->isAnon() ? null : $user->getName();
2074 
2075  // Query them and merge results
2076  $reqs = [];
2077  foreach ( $providers as $provider ) {
2078  $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2079  foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2080  $id = $req->getUniqueId();
2081 
2082  // If a required request if from a Primary, mark it as "primary-required" instead
2083  if ( $isPrimary ) {
2084  if ( $req->required ) {
2086  }
2087  }
2088 
2089  if (
2090  !isset( $reqs[$id] )
2091  || $req->required === AuthenticationRequest::REQUIRED
2092  || $reqs[$id] === AuthenticationRequest::OPTIONAL
2093  ) {
2094  $reqs[$id] = $req;
2095  }
2096  }
2097  }
2098 
2099  // AuthManager has its own req for some actions
2100  switch ( $providerAction ) {
2101  case self::ACTION_LOGIN:
2102  $reqs[] = new RememberMeAuthenticationRequest;
2103  break;
2104 
2105  case self::ACTION_CREATE:
2106  $reqs[] = new UsernameAuthenticationRequest;
2107  $reqs[] = new UserDataAuthenticationRequest;
2108  if ( $options['username'] !== null ) {
2110  $options['username'] = null; // Don't fill in the username below
2111  }
2112  break;
2113  }
2114 
2115  // Fill in reqs data
2116  $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2117 
2118  // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2119  if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2120  $reqs = array_filter( $reqs, function ( $req ) {
2121  return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2122  } );
2123  }
2124 
2125  return array_values( $reqs );
2126  }
2127 
2135  private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2136  foreach ( $reqs as $req ) {
2137  if ( !$req->action || $forceAction ) {
2138  $req->action = $action;
2139  }
2140  if ( $req->username === null ) {
2141  $req->username = $username;
2142  }
2143  }
2144  }
2145 
2152  public function userExists( $username, $flags = User::READ_NORMAL ) {
2153  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2154  if ( $provider->testUserExists( $username, $flags ) ) {
2155  return true;
2156  }
2157  }
2158 
2159  return false;
2160  }
2161 
2173  public function allowsPropertyChange( $property ) {
2174  $providers = $this->getPrimaryAuthenticationProviders() +
2176  foreach ( $providers as $provider ) {
2177  if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2178  return false;
2179  }
2180  }
2181  return true;
2182  }
2183 
2192  public function getAuthenticationProvider( $id ) {
2193  // Fast version
2194  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2195  return $this->allAuthenticationProviders[$id];
2196  }
2197 
2198  // Slow version: instantiate each kind and check
2199  $providers = $this->getPrimaryAuthenticationProviders();
2200  if ( isset( $providers[$id] ) ) {
2201  return $providers[$id];
2202  }
2203  $providers = $this->getSecondaryAuthenticationProviders();
2204  if ( isset( $providers[$id] ) ) {
2205  return $providers[$id];
2206  }
2207  $providers = $this->getPreAuthenticationProviders();
2208  if ( isset( $providers[$id] ) ) {
2209  return $providers[$id];
2210  }
2211 
2212  return null;
2213  }
2214 
2228  public function setAuthenticationSessionData( $key, $data ) {
2229  $session = $this->request->getSession();
2230  $arr = $session->getSecret( 'authData' );
2231  if ( !is_array( $arr ) ) {
2232  $arr = [];
2233  }
2234  $arr[$key] = $data;
2235  $session->setSecret( 'authData', $arr );
2236  }
2237 
2245  public function getAuthenticationSessionData( $key, $default = null ) {
2246  $arr = $this->request->getSession()->getSecret( 'authData' );
2247  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2248  return $arr[$key];
2249  } else {
2250  return $default;
2251  }
2252  }
2253 
2259  public function removeAuthenticationSessionData( $key ) {
2260  $session = $this->request->getSession();
2261  if ( $key === null ) {
2262  $session->remove( 'authData' );
2263  } else {
2264  $arr = $session->getSecret( 'authData' );
2265  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2266  unset( $arr[$key] );
2267  $session->setSecret( 'authData', $arr );
2268  }
2269  }
2270  }
2271 
2278  protected function providerArrayFromSpecs( $class, array $specs ) {
2279  $i = 0;
2280  foreach ( $specs as &$spec ) {
2281  $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2282  }
2283  unset( $spec );
2284  usort( $specs, function ( $a, $b ) {
2285  return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2286  ?: $a['sort2'] - $b['sort2'];
2287  } );
2288 
2289  $ret = [];
2290  foreach ( $specs as $spec ) {
2291  $provider = \ObjectFactory::getObjectFromSpec( $spec );
2292  if ( !$provider instanceof $class ) {
2293  throw new \RuntimeException(
2294  "Expected instance of $class, got " . get_class( $provider )
2295  );
2296  }
2297  $provider->setLogger( $this->logger );
2298  $provider->setManager( $this );
2299  $provider->setConfig( $this->config );
2300  $id = $provider->getUniqueId();
2301  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2302  throw new \RuntimeException(
2303  "Duplicate specifications for id $id (classes " .
2304  get_class( $provider ) . ' and ' .
2305  get_class( $this->allAuthenticationProviders[$id] ) . ')'
2306  );
2307  }
2308  $this->allAuthenticationProviders[$id] = $provider;
2309  $ret[$id] = $provider;
2310  }
2311  return $ret;
2312  }
2313 
2318  private function getConfiguration() {
2319  return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2320  }
2321 
2326  protected function getPreAuthenticationProviders() {
2327  if ( $this->preAuthenticationProviders === null ) {
2328  $conf = $this->getConfiguration();
2329  $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2330  PreAuthenticationProvider::class, $conf['preauth']
2331  );
2332  }
2334  }
2335 
2340  protected function getPrimaryAuthenticationProviders() {
2341  if ( $this->primaryAuthenticationProviders === null ) {
2342  $conf = $this->getConfiguration();
2343  $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2344  PrimaryAuthenticationProvider::class, $conf['primaryauth']
2345  );
2346  }
2348  }
2349 
2355  if ( $this->secondaryAuthenticationProviders === null ) {
2356  $conf = $this->getConfiguration();
2357  $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2358  SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2359  );
2360  }
2362  }
2363 
2369  private function setSessionDataForUser( $user, $remember = null ) {
2370  $session = $this->request->getSession();
2371  $delay = $session->delaySave();
2372 
2373  $session->resetId();
2374  $session->resetAllTokens();
2375  if ( $session->canSetUser() ) {
2376  $session->setUser( $user );
2377  }
2378  if ( $remember !== null ) {
2379  $session->setRememberUser( $remember );
2380  }
2381  $session->set( 'AuthManager:lastAuthId', $user->getId() );
2382  $session->set( 'AuthManager:lastAuthTimestamp', time() );
2383  $session->persist();
2384 
2385  \Wikimedia\ScopedCallback::consume( $delay );
2386 
2387  \Hooks::run( 'UserLoggedIn', [ $user ] );
2388  }
2389 
2394  private function setDefaultUserOptions( User $user, $useContextLang ) {
2396 
2397  $user->setToken();
2398 
2399  $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
2400  $user->setOption( 'language', $lang->getPreferredVariant() );
2401 
2402  if ( $wgContLang->hasVariants() ) {
2403  $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2404  }
2405  }
2406 
2412  private function callMethodOnProviders( $which, $method, array $args ) {
2413  $providers = [];
2414  if ( $which & 1 ) {
2415  $providers += $this->getPreAuthenticationProviders();
2416  }
2417  if ( $which & 2 ) {
2418  $providers += $this->getPrimaryAuthenticationProviders();
2419  }
2420  if ( $which & 4 ) {
2421  $providers += $this->getSecondaryAuthenticationProviders();
2422  }
2423  foreach ( $providers as $provider ) {
2424  call_user_func_array( [ $provider, $method ], $args );
2425  }
2426  }
2427 
2432  public static function resetCache() {
2433  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2434  // @codeCoverageIgnoreStart
2435  throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2436  // @codeCoverageIgnoreEnd
2437  }
2438 
2439  self::$instance = null;
2440  }
2441 
2444 }
2445 
MediaWiki\Auth\AuthManager\continueAccountLink
continueAccountLink(array $reqs)
Continue an account linking flow.
Definition: AuthManager.php:1893
MediaWiki\Auth\AuthManager\continueAuthentication
continueAuthentication(array $reqs)
Continue an authentication flow.
Definition: AuthManager.php:406
MediaWiki\Auth\AuthenticationRequest\OPTIONAL
const OPTIONAL
Indicates that the request is not required for authentication to proceed.
Definition: AuthenticationRequest.php:40
MediaWiki\Auth\AuthManager\getRequest
getRequest()
Definition: AuthManager.php:175
MediaWiki\Auth\PrimaryAuthenticationProvider\TYPE_CREATE
const TYPE_CREATE
Provider can create accounts.
Definition: PrimaryAuthenticationProvider.php:77
MediaWiki\Auth\AuthManager\$preAuthenticationProviders
PreAuthenticationProvider[] $preAuthenticationProviders
Definition: AuthManager.php:130
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:548
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:42
MediaWiki\$action
String $action
Cache what action this request is.
Definition: MediaWiki.php:43
MediaWiki\Auth\AuthManager\SEC_REAUTH
const SEC_REAUTH
Security-sensitive operations should re-authenticate.
Definition: AuthManager.php:107
MediaWiki\Auth\PrimaryAuthenticationProvider\TYPE_NONE
const TYPE_NONE
Provider cannot create or link to accounts.
Definition: PrimaryAuthenticationProvider.php:81
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:357
use
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
Definition: APACHE-LICENSE-2.0.txt:3
User\getId
getId()
Get the user's ID.
Definition: User.php:2083
MediaWiki\Auth\AuthManager\fillRequests
fillRequests(array &$reqs, $action, $username, $forceAction=false)
Set values in an array of requests.
Definition: AuthManager.php:2135
MediaWiki\Auth\AuthManager\getPrimaryAuthenticationProviders
getPrimaryAuthenticationProviders()
Get the list of PrimaryAuthenticationProviders.
Definition: AuthManager.php:2340
wfMessage
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
Profiler\instance
static instance()
Singleton.
Definition: Profiler.php:61
array
the array() calling protocol came about after MediaWiki 1.4rc1.
MediaWiki\Auth\AuthManager\ACTION_UNLINK
const ACTION_UNLINK
Like ACTION_REMOVE but for linking providers only.
Definition: AuthManager.php:102
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:33
MediaWiki\Auth\AuthManager\AUTOCREATE_SOURCE_SESSION
const AUTOCREATE_SOURCE_SESSION
Auto-creation is due to SessionManager.
Definition: AuthManager.php:112
wfGetLB
wfGetLB( $wiki=false)
Get a load balancer object.
Definition: GlobalFunctions.php:3086
Block\TYPE_RANGE
const TYPE_RANGE
Definition: Block.php:80
MediaWiki\Auth\AuthManager\revokeAccessForUser
revokeAccessForUser( $username)
Revoke any authentication credentials for a user.
Definition: AuthManager.php:834
$last
$last
Definition: profileinfo.php:415
MediaWiki\Logger\LoggerFactory\getInstance
static getInstance( $channel)
Get a named logger instance from the currently configured logger factory.
Definition: LoggerFactory.php:93
MediaWiki\Auth\AuthManagerAuthPlugin
Backwards-compatibility wrapper for AuthManager via $wgAuth.
Definition: AuthManagerAuthPlugin.php:30
$ret
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1939
MediaWiki\Auth\AuthManager\autoCreateUser
autoCreateUser(User $user, $source, $login=true)
Auto-create an account, and log into that account.
Definition: AuthManager.php:1537
$from
$from
Definition: importImages.php:98
MediaWiki\Auth\AuthManager\$instance
static AuthManager null $instance
Definition: AuthManager.php:115
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
Definition: DeferredUpdates.php:73
MediaWiki\Auth\PrimaryAuthenticationProvider\TYPE_LINK
const TYPE_LINK
Provider can link to existing accounts elsewhere.
Definition: PrimaryAuthenticationProvider.php:79
MediaWiki\Auth\CreatedAccountAuthenticationRequest
Returned from account creation to allow for logging into the created account.
Definition: CreatedAccountAuthenticationRequest.php:29
StatusValue\newFatal
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:63
$params
$params
Definition: styleTest.css.php:40
MediaWiki\Auth\AuthManager\beginAuthentication
beginAuthentication(array $reqs, $returnToUrl)
Start an authentication flow.
Definition: AuthManager.php:282
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1273
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:525
MediaWiki\Auth\AuthManager\userExists
userExists( $username, $flags=User::READ_NORMAL)
Determine whether a username exists.
Definition: AuthManager.php:2152
SpecialPage\getTitleFor
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,...
Definition: SpecialPage.php:82
$res
$res
Definition: database.txt:21
MediaWiki\Auth\AuthManager\$allAuthenticationProviders
AuthenticationProvider[] $allAuthenticationProviders
Definition: AuthManager.php:127
User
User
Definition: All_system_messages.txt:425
MediaWiki\Auth\AuthManager\getPreAuthenticationProviders
getPreAuthenticationProviders()
Get the list of PreAuthenticationProviders.
Definition: AuthManager.php:2326
MediaWiki\Auth\AuthManager\getAuthenticationSessionData
getAuthenticationSessionData( $key, $default=null)
Fetch authentication data from the current session.
Definition: AuthManager.php:2245
$flags
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2706
MediaWiki\Auth\AuthManager\ACTION_LOGIN_CONTINUE
const ACTION_LOGIN_CONTINUE
Continue a login process that was interrupted by the need for user input or communication with an ext...
Definition: AuthManager.php:86
MediaWiki\Auth\AuthManager\__construct
__construct(WebRequest $request, Config $config)
Definition: AuthManager.php:159
MediaWiki\Auth\AuthManager\SEC_FAIL
const SEC_FAIL
Security-sensitive should not be performed.
Definition: AuthManager.php:109
MediaWiki\Auth\AuthenticationRequest\getRequestByClass
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
Definition: AuthenticationRequest.php:253
php
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
IDBAccessObject\READ_LOCKING
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
Definition: IDBAccessObject.php:62
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:40
MediaWiki\Auth\AuthManager\removeAuthenticationSessionData
removeAuthenticationSessionData( $key)
Remove authentication data.
Definition: AuthManager.php:2259
Config
Interface for configuration instances.
Definition: Config.php:28
MediaWiki\Auth\AuthenticationRequest\getUsernameFromRequests
static getUsernameFromRequests(array $reqs)
Get the username from the set of requests.
Definition: AuthenticationRequest.php:273
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, IDatabase $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:105
MediaWiki\Auth\AuthenticationRequest\PRIMARY_REQUIRED
const PRIMARY_REQUIRED
Indicates that the request is required by a primary authentication provider.
Definition: AuthenticationRequest.php:51
MediaWiki\Auth\AuthenticationResponse\UI
const UI
Indicates that the authentication needs further user input of some sort.
Definition: AuthenticationResponse.php:55
wfMemcKey
wfMemcKey()
Make a cache key for the local wiki.
Definition: GlobalFunctions.php:2974
MediaWiki\Auth\AuthManager\beginAccountCreation
beginAccountCreation(User $creator, array $reqs, $returnToUrl)
Start an account creation flow.
Definition: AuthManager.php:1036
MediaWiki\Auth\AuthManager\getAuthenticationProvider
getAuthenticationProvider( $id)
Get a provider by ID.
Definition: AuthManager.php:2192
MediaWiki\Auth\AuthManager\ACTION_LINK_CONTINUE
const ACTION_LINK_CONTINUE
Continue a user linking process that was interrupted by the need for user input or communication with...
Definition: AuthManager.php:96
MediaWiki\Auth\CreationReasonAuthenticationRequest
Authentication request for the reason given for account creation.
Definition: CreationReasonAuthenticationRequest.php:9
$property
$property
Definition: styleTest.css.php:44
MediaWiki\Auth\AuthManager\canCreateAccounts
canCreateAccounts()
Determine whether accounts can be created.
Definition: AuthManager.php:907
MediaWiki\Auth\AuthenticationResponse\REDIRECT
const REDIRECT
Indicates that the authentication needs to be redirected to a third party to proceed.
Definition: AuthenticationResponse.php:58
MediaWiki\Auth\PreAuthenticationProvider
A pre-authentication provider can prevent authentication early on.
Definition: PreAuthenticationProvider.php:44
Status\wrap
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:55
MediaWiki\Auth\AuthManager\changeAuthenticationData
changeAuthenticationData(AuthenticationRequest $req)
Change authentication data (e.g.
Definition: AuthManager.php:883
MediaWiki\Auth\CreateFromLoginAuthenticationRequest
This transfers state between the login and account creation flows.
Definition: CreateFromLoginAuthenticationRequest.php:34
MediaWiki
A helper class for throttling authentication attempts.
MediaWiki\Auth\AuthManager\getConfiguration
getConfiguration()
Get the configuration.
Definition: AuthManager.php:2318
ConfigFactory\getDefaultInstance
static getDefaultInstance()
Definition: ConfigFactory.php:51
MediaWiki\Auth\AuthenticationResponse\ABSTAIN
const ABSTAIN
Indicates that the authentication provider does not handle this request.
Definition: AuthenticationResponse.php:52
MediaWiki\Auth\AuthManager\canAuthenticateNow
canAuthenticateNow()
Indicate whether user authentication is possible.
Definition: AuthManager.php:260
MediaWiki\Auth\AuthManager\setAuthenticationSessionData
setAuthenticationSessionData( $key, $data)
Store authentication in the current session.
Definition: AuthManager.php:2228
global
when a variable name is used in a it is silently declared as a new masking the global
Definition: design.txt:93
MediaWiki\Auth\SecondaryAuthenticationProvider
A secondary provider mostly acts when the submitted authentication data has already been associated t...
Definition: SecondaryAuthenticationProvider.php:52
SiteStatsUpdate
Class for handling updates to the site_stats table.
Definition: SiteStatsUpdate.php:25
MediaWiki\Auth\UsernameAuthenticationRequest
AuthenticationRequest to ensure something with a username is present.
Definition: UsernameAuthenticationRequest.php:29
MediaWiki\Auth\AuthManager\setDefaultUserOptions
setDefaultUserOptions(User $user, $useContextLang)
Definition: AuthManager.php:2394
MediaWiki\Auth\UserDataAuthenticationRequest
This represents additional user data requested on the account creation form.
Definition: UserDataAuthenticationRequest.php:33
MediaWiki\Auth\AuthManager\callLegacyAuthPlugin
static callLegacyAuthPlugin( $method, array $params, $return=null)
Call a legacy AuthPlugin method, if necessary.
Definition: AuthManager.php:237
MediaWiki\Auth\AuthenticationResponse\FAIL
const FAIL
Indicates that the authentication failed.
Definition: AuthenticationResponse.php:42
MediaWiki\Auth\AuthManager\resetCache
static resetCache()
Reset the internal caching for unit testing.
Definition: AuthManager.php:2432
MediaWiki\Auth\AuthManager\$request
WebRequest $request
Definition: AuthManager.php:118
MediaWiki\Auth\AuthManager\beginAccountLink
beginAccountLink(User $user, array $reqs, $returnToUrl)
Start an account linking flow.
Definition: AuthManager.php:1784
MediaWiki\Auth\AuthManager\normalizeUsername
normalizeUsername( $username)
Provide normalized versions of the username for security checks.
Definition: AuthManager.php:809
$user
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account $user
Definition: hooks.txt:246
MediaWiki\Auth\AuthManager\canLinkAccounts
canLinkAccounts()
Determine whether accounts can be linked.
Definition: AuthManager.php:1766
MediaWiki\Auth\AuthManager\ACTION_CREATE_CONTINUE
const ACTION_CREATE_CONTINUE
Continue a user creation process that was interrupted by the need for user input or communication wit...
Definition: AuthManager.php:91
MediaWiki\Auth\AuthManager\ACTION_CREATE
const ACTION_CREATE
Create a new user.
Definition: AuthManager.php:88
MediaWiki\Auth\AuthManager\$logger
LoggerInterface $logger
Definition: AuthManager.php:124
request
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging a wrapping ErrorException instead of letting the login form give the generic error message that the account does not exist For when the account has been renamed or deleted or an array to pass a message key and parameters create2 Corresponds to logging log_action database field and which is displayed in the UI similar to $comment this hook should only be used to add variables that depend on the current page request
Definition: hooks.txt:2105
$options
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition: hooks.txt:1049
MediaWiki\Auth\AuthManager\$primaryAuthenticationProviders
PrimaryAuthenticationProvider[] $primaryAuthenticationProviders
Definition: AuthManager.php:133
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:76
User\isDnsBlacklisted
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition: User.php:1671
MediaWiki\Auth\AuthManager\continueAccountCreation
continueAccountCreation(array $reqs)
Continue an account creation flow.
Definition: AuthManager.php:1137
BotPassword\invalidateAllPasswordsForUser
static invalidateAllPasswordsForUser( $username)
Invalidate all passwords for a user, by name.
Definition: BotPassword.php:329
MediaWiki\Auth\AuthManager\$secondaryAuthenticationProviders
SecondaryAuthenticationProvider[] $secondaryAuthenticationProviders
Definition: AuthManager.php:136
MediaWiki\Auth\AuthManager\forcePrimaryAuthenticationProviders
forcePrimaryAuthenticationProviders(array $providers, $why)
Force certain PrimaryAuthenticationProviders.
Definition: AuthManager.php:185
MediaWiki\Auth\AuthManager\ACTION_CHANGE
const ACTION_CHANGE
Change a user's credentials.
Definition: AuthManager.php:98
MediaWiki\Auth\AuthManager\callMethodOnProviders
callMethodOnProviders( $which, $method, array $args)
Definition: AuthManager.php:2412
MediaWiki\Auth\AuthManager\ACTION_LINK
const ACTION_LINK
Link an existing user to a third-party account.
Definition: AuthManager.php:93
ObjectFactory\getObjectFromSpec
static getObjectFromSpec( $spec)
Instantiate an object based on a specification array.
Definition: ObjectFactory.php:59
MediaWiki\Auth\AuthManager\allowsPropertyChange
allowsPropertyChange( $property)
Determine whether a user property should be allowed to be changed.
Definition: AuthManager.php:2173
MediaWiki\Auth\RememberMeAuthenticationRequest
This is an authentication request added by AuthManager to show a "remember me" checkbox.
Definition: RememberMeAuthenticationRequest.php:33
RequestContext\getMain
static getMain()
Static methods.
Definition: RequestContext.php:468
MediaWiki\Auth\AuthManager\securitySensitiveOperationStatus
securitySensitiveOperationStatus( $operation)
Whether security-sensitive operations should proceed.
Definition: AuthManager.php:713
MediaWiki\Auth\AuthManager
This serves as the entry point to the authentication system.
Definition: AuthManager.php:81
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:38
$req
this hook is for auditing only $req
Definition: hooks.txt:1010
MediaWiki\Auth\AuthManager\ACTION_REMOVE
const ACTION_REMOVE
Remove a user's credentials.
Definition: AuthManager.php:100
MediaWiki\Auth\AuthManager\SEC_OK
const SEC_OK
Security-sensitive operations are ok.
Definition: AuthManager.php:105
$args
if( $line===false) $args
Definition: cdb.php:64
MediaWiki\Auth\AuthManager\getAuthenticationRequests
getAuthenticationRequests( $action, User $user=null)
Return the applicable list of AuthenticationRequests.
Definition: AuthManager.php:2005
wfReadOnlyReason
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
Definition: GlobalFunctions.php:1285
$cache
$cache
Definition: mcc.php:33
MediaWiki\Auth\AuthManager\singleton
static singleton()
Get the global AuthManager.
Definition: AuthManager.php:145
User\idFromName
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:728
MediaWiki\Auth\AuthenticationResponse\newFail
static newFail(Message $msg)
Definition: AuthenticationResponse.php:146
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
MediaWiki\Auth\AuthManager\setSessionDataForUser
setSessionDataForUser( $user, $remember=null)
Log the user in.
Definition: AuthManager.php:2369
MediaWiki\Auth\AuthManager\$createdAccountAuthenticationRequests
CreatedAccountAuthenticationRequest[] $createdAccountAuthenticationRequests
Definition: AuthManager.php:139
MediaWiki\Auth\AuthManager\$config
Config $config
Definition: AuthManager.php:121
MediaWiki\Auth\AuthManager\canCreateAccount
canCreateAccount( $username, $options=[])
Determine whether a particular account can be created.
Definition: AuthManager.php:926
$source
$source
Definition: mwdoc-filter.php:45
MediaWiki\Auth\AuthManager\ACTION_LOGIN
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
Definition: AuthManager.php:83
$username
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:805
$name
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:304
MediaWiki\Auth\AuthManager\getSecondaryAuthenticationProviders
getSecondaryAuthenticationProviders()
Get the list of SecondaryAuthenticationProviders.
Definition: AuthManager.php:2354
class
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
MediaWiki\Auth\AuthManager\userCanAuthenticate
userCanAuthenticate( $username)
Determine whether a username can authenticate.
Definition: AuthManager.php:786
User\IGNORE_USER_RIGHTS
const IGNORE_USER_RIGHTS
Definition: User.php:85
MediaWiki\Auth\AuthenticationResponse\PASS
const PASS
Indicates that the authentication succeeded.
Definition: AuthenticationResponse.php:39
User\isUsableName
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition: User.php:853
MediaWiki\Auth\AuthManager\allowsAuthenticationDataChange
allowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
Definition: AuthManager.php:850
MediaWiki\Auth\AuthenticationResponse\newRestart
static newRestart(Message $msg)
Definition: AuthenticationResponse.php:159
User\isBlockedFromCreateAccount
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition: User.php:4111
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:48
MediaWiki\Auth\PrimaryAuthenticationProvider
A primary authentication provider is responsible for associating the submitted authentication data wi...
Definition: PrimaryAuthenticationProvider.php:75
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:131
MediaWiki\Auth
Definition: AbstractAuthenticationProvider.php:22
MediaWiki\Auth\AuthManager\setLogger
setLogger(LoggerInterface $logger)
Definition: AuthManager.php:168
MediaWiki\Auth\AuthManager\providerArrayFromSpecs
providerArrayFromSpecs( $class, array $specs)
Create an array of AuthenticationProviders from an array of ObjectFactory specs.
Definition: AuthManager.php:2278
MediaWiki\Auth\AuthenticationResponse\newPass
static newPass( $username=null)
Definition: AuthenticationResponse.php:134
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2108
User\isCreatableName
static isCreatableName( $name)
Usernames which fail to pass this function will be blocked from new account registrations,...
Definition: User.php:928
MediaWiki\Auth\AuthManager\checkAccountCreatePermissions
checkAccountCreatePermissions(User $creator)
Basic permissions checks on whether a user can create accounts.
Definition: AuthManager.php:974
MediaWiki\Auth\AuthenticationRequest
This is a value object for authentication requests.
Definition: AuthenticationRequest.php:37
MediaWiki\Auth\AuthenticationRequest\REQUIRED
const REQUIRED
Indicates that the request is required for authentication to proceed.
Definition: AuthenticationRequest.php:46
$status
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:1049
$wgContLang
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the content language as $wgContLang
Definition: design.txt:56
MediaWiki\Auth\AuthManager\getAuthenticationRequestsInternal
getAuthenticationRequestsInternal( $providerAction, array $options, array $providers, User $user=null)
Internal request lookup for self::getAuthenticationRequests.
Definition: AuthManager.php:2069
MediaWiki\Auth\AuthenticationProvider
An AuthenticationProvider is used by AuthManager when authenticating users.
Definition: AuthenticationProvider.php:39