MediaWiki  master
AuthManager.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Auth;
25 
26 use Config;
31 use Status;
32 use StatusValue;
33 use User;
34 use WebRequest;
36 
85 class AuthManager implements LoggerAwareInterface {
87  public const ACTION_LOGIN = 'login';
91  public const ACTION_LOGIN_CONTINUE = 'login-continue';
93  public const ACTION_CREATE = 'create';
97  public const ACTION_CREATE_CONTINUE = 'create-continue';
99  public const ACTION_LINK = 'link';
103  public const ACTION_LINK_CONTINUE = 'link-continue';
105  public const ACTION_CHANGE = 'change';
107  public const ACTION_REMOVE = 'remove';
109  public const ACTION_UNLINK = 'unlink';
110 
112  public const SEC_OK = 'ok';
114  public const SEC_REAUTH = 'reauth';
116  public const SEC_FAIL = 'fail';
117 
119  public const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
120 
122  public const AUTOCREATE_SOURCE_MAINT = '::Maintenance::';
123 
125  private static $instance = null;
126 
128  private $request;
129 
131  private $config;
132 
134  private $logger;
135 
138 
141 
144 
147 
150 
155  public static function singleton() {
156  if ( self::$instance === null ) {
157  self::$instance = new self(
158  \RequestContext::getMain()->getRequest(),
159  MediaWikiServices::getInstance()->getMainConfig()
160  );
161  }
162  return self::$instance;
163  }
164 
170  $this->request = $request;
171  $this->config = $config;
172  $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
173  }
174 
178  public function setLogger( LoggerInterface $logger ) {
179  $this->logger = $logger;
180  }
181 
185  public function getRequest() {
186  return $this->request;
187  }
188 
195  public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
196  $this->logger->warning( "Overriding AuthManager primary authn because $why" );
197 
198  if ( $this->primaryAuthenticationProviders !== null ) {
199  $this->logger->warning(
200  'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
201  );
202 
203  $this->allAuthenticationProviders = array_diff_key(
204  $this->allAuthenticationProviders,
205  $this->primaryAuthenticationProviders
206  );
207  $session = $this->request->getSession();
208  $session->remove( 'AuthManager::authnState' );
209  $session->remove( 'AuthManager::accountCreationState' );
210  $session->remove( 'AuthManager::accountLinkState' );
211  $this->createdAccountAuthenticationRequests = [];
212  }
213 
214  $this->primaryAuthenticationProviders = [];
215  foreach ( $providers as $provider ) {
216  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
217  throw new \RuntimeException(
218  'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
219  get_class( $provider )
220  );
221  }
222  $provider->setLogger( $this->logger );
223  $provider->setManager( $this );
224  $provider->setConfig( $this->config );
225  $id = $provider->getUniqueId();
226  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
227  throw new \RuntimeException(
228  "Duplicate specifications for id $id (classes " .
229  get_class( $provider ) . ' and ' .
230  get_class( $this->allAuthenticationProviders[$id] ) . ')'
231  );
232  }
233  $this->allAuthenticationProviders[$id] = $provider;
234  $this->primaryAuthenticationProviders[$id] = $provider;
235  }
236  }
237 
249  public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
250  wfDeprecated( __METHOD__, '1.33' );
251  return $return;
252  }
253 
267  public function canAuthenticateNow() {
268  return $this->request->getSession()->canSetUser();
269  }
270 
289  public function beginAuthentication( array $reqs, $returnToUrl ) {
290  $session = $this->request->getSession();
291  if ( !$session->canSetUser() ) {
292  // Caller should have called canAuthenticateNow()
293  $session->remove( 'AuthManager::authnState' );
294  throw new \LogicException( 'Authentication is not possible now' );
295  }
296 
297  $guessUserName = null;
298  foreach ( $reqs as $req ) {
299  $req->returnToUrl = $returnToUrl;
300  // @codeCoverageIgnoreStart
301  if ( $req->username !== null && $req->username !== '' ) {
302  if ( $guessUserName === null ) {
303  $guessUserName = $req->username;
304  } elseif ( $guessUserName !== $req->username ) {
305  $guessUserName = null;
306  break;
307  }
308  }
309  // @codeCoverageIgnoreEnd
310  }
311 
312  // Check for special-case login of a just-created account
314  $reqs, CreatedAccountAuthenticationRequest::class
315  );
316  if ( $req ) {
317  if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
318  throw new \LogicException(
319  'CreatedAccountAuthenticationRequests are only valid on ' .
320  'the same AuthManager that created the account'
321  );
322  }
323 
324  $user = User::newFromName( $req->username );
325  // @codeCoverageIgnoreStart
326  if ( !$user ) {
327  throw new \UnexpectedValueException(
328  "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
329  );
330  } elseif ( $user->getId() != $req->id ) {
331  throw new \UnexpectedValueException(
332  "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
333  );
334  }
335  // @codeCoverageIgnoreEnd
336 
337  $this->logger->info( 'Logging in {user} after account creation', [
338  'user' => $user->getName(),
339  ] );
340  $ret = AuthenticationResponse::newPass( $user->getName() );
341  $this->setSessionDataForUser( $user );
342  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
343  $session->remove( 'AuthManager::authnState' );
344  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
345  return $ret;
346  }
347 
348  $this->removeAuthenticationSessionData( null );
349 
350  foreach ( $this->getPreAuthenticationProviders() as $provider ) {
351  $status = $provider->testForAuthentication( $reqs );
352  if ( !$status->isGood() ) {
353  $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
355  Status::wrap( $status )->getMessage()
356  );
357  $this->callMethodOnProviders( 7, 'postAuthentication',
358  [ User::newFromName( $guessUserName ) ?: null, $ret ]
359  );
360  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName, [] ] );
361  return $ret;
362  }
363  }
364 
365  $state = [
366  'reqs' => $reqs,
367  'returnToUrl' => $returnToUrl,
368  'guessUserName' => $guessUserName,
369  'primary' => null,
370  'primaryResponse' => null,
371  'secondary' => [],
372  'maybeLink' => [],
373  'continueRequests' => [],
374  ];
375 
376  // Preserve state from a previous failed login
378  $reqs, CreateFromLoginAuthenticationRequest::class
379  );
380  if ( $req ) {
381  $state['maybeLink'] = $req->maybeLink;
382  }
383 
384  $session = $this->request->getSession();
385  $session->setSecret( 'AuthManager::authnState', $state );
386  $session->persist();
387 
388  return $this->continueAuthentication( $reqs );
389  }
390 
413  public function continueAuthentication( array $reqs ) {
414  $session = $this->request->getSession();
415  try {
416  if ( !$session->canSetUser() ) {
417  // Caller should have called canAuthenticateNow()
418  // @codeCoverageIgnoreStart
419  throw new \LogicException( 'Authentication is not possible now' );
420  // @codeCoverageIgnoreEnd
421  }
422 
423  $state = $session->getSecret( 'AuthManager::authnState' );
424  if ( !is_array( $state ) ) {
426  wfMessage( 'authmanager-authn-not-in-progress' )
427  );
428  }
429  $state['continueRequests'] = [];
430 
431  $guessUserName = $state['guessUserName'];
432 
433  foreach ( $reqs as $req ) {
434  $req->returnToUrl = $state['returnToUrl'];
435  }
436 
437  // Step 1: Choose an primary authentication provider, and call it until it succeeds.
438 
439  if ( $state['primary'] === null ) {
440  // We haven't picked a PrimaryAuthenticationProvider yet
441  // @codeCoverageIgnoreStart
442  $guessUserName = null;
443  foreach ( $reqs as $req ) {
444  if ( $req->username !== null && $req->username !== '' ) {
445  if ( $guessUserName === null ) {
446  $guessUserName = $req->username;
447  } elseif ( $guessUserName !== $req->username ) {
448  $guessUserName = null;
449  break;
450  }
451  }
452  }
453  $state['guessUserName'] = $guessUserName;
454  // @codeCoverageIgnoreEnd
455  $state['reqs'] = $reqs;
456 
457  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
458  $res = $provider->beginPrimaryAuthentication( $reqs );
459  switch ( $res->status ) {
461  $state['primary'] = $id;
462  $state['primaryResponse'] = $res;
463  $this->logger->debug( "Primary login with $id succeeded" );
464  break 2;
466  $this->logger->debug( "Login failed in primary authentication by $id" );
467  if ( $res->createRequest || $state['maybeLink'] ) {
468  $res->createRequest = new CreateFromLoginAuthenticationRequest(
469  $res->createRequest, $state['maybeLink']
470  );
471  }
472  $this->callMethodOnProviders( 7, 'postAuthentication',
473  [ User::newFromName( $guessUserName ) ?: null, $res ]
474  );
475  $session->remove( 'AuthManager::authnState' );
476  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] );
477  return $res;
479  // Continue loop
480  break;
483  $this->logger->debug( "Primary login with $id returned $res->status" );
484  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
485  $state['primary'] = $id;
486  $state['continueRequests'] = $res->neededRequests;
487  $session->setSecret( 'AuthManager::authnState', $state );
488  return $res;
489 
490  // @codeCoverageIgnoreStart
491  default:
492  throw new \DomainException(
493  get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
494  );
495  // @codeCoverageIgnoreEnd
496  }
497  }
498  if ( $state['primary'] === null ) {
499  $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
501  wfMessage( 'authmanager-authn-no-primary' )
502  );
503  $this->callMethodOnProviders( 7, 'postAuthentication',
504  [ User::newFromName( $guessUserName ) ?: null, $ret ]
505  );
506  $session->remove( 'AuthManager::authnState' );
507  return $ret;
508  }
509  } elseif ( $state['primaryResponse'] === null ) {
510  $provider = $this->getAuthenticationProvider( $state['primary'] );
511  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
512  // Configuration changed? Force them to start over.
513  // @codeCoverageIgnoreStart
515  wfMessage( 'authmanager-authn-not-in-progress' )
516  );
517  $this->callMethodOnProviders( 7, 'postAuthentication',
518  [ User::newFromName( $guessUserName ) ?: null, $ret ]
519  );
520  $session->remove( 'AuthManager::authnState' );
521  return $ret;
522  // @codeCoverageIgnoreEnd
523  }
524  $id = $provider->getUniqueId();
525  $res = $provider->continuePrimaryAuthentication( $reqs );
526  switch ( $res->status ) {
528  $state['primaryResponse'] = $res;
529  $this->logger->debug( "Primary login with $id succeeded" );
530  break;
532  $this->logger->debug( "Login failed in primary authentication by $id" );
533  if ( $res->createRequest || $state['maybeLink'] ) {
534  $res->createRequest = new CreateFromLoginAuthenticationRequest(
535  $res->createRequest, $state['maybeLink']
536  );
537  }
538  $this->callMethodOnProviders( 7, 'postAuthentication',
539  [ User::newFromName( $guessUserName ) ?: null, $res ]
540  );
541  $session->remove( 'AuthManager::authnState' );
542  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] );
543  return $res;
546  $this->logger->debug( "Primary login with $id returned $res->status" );
547  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
548  $state['continueRequests'] = $res->neededRequests;
549  $session->setSecret( 'AuthManager::authnState', $state );
550  return $res;
551  default:
552  throw new \DomainException(
553  get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
554  );
555  }
556  }
557 
558  $res = $state['primaryResponse'];
559  if ( $res->username === null ) {
560  $provider = $this->getAuthenticationProvider( $state['primary'] );
561  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
562  // Configuration changed? Force them to start over.
563  // @codeCoverageIgnoreStart
565  wfMessage( 'authmanager-authn-not-in-progress' )
566  );
567  $this->callMethodOnProviders( 7, 'postAuthentication',
568  [ User::newFromName( $guessUserName ) ?: null, $ret ]
569  );
570  $session->remove( 'AuthManager::authnState' );
571  return $ret;
572  // @codeCoverageIgnoreEnd
573  }
574 
575  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
576  $res->linkRequest &&
577  // don't confuse the user with an incorrect message if linking is disabled
578  $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
579  ) {
580  $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
581  $msg = 'authmanager-authn-no-local-user-link';
582  } else {
583  $msg = 'authmanager-authn-no-local-user';
584  }
585  $this->logger->debug(
586  "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
587  );
589  $ret->neededRequests = $this->getAuthenticationRequestsInternal(
590  self::ACTION_LOGIN,
591  [],
593  );
594  if ( $res->createRequest || $state['maybeLink'] ) {
595  $ret->createRequest = new CreateFromLoginAuthenticationRequest(
596  $res->createRequest, $state['maybeLink']
597  );
598  $ret->neededRequests[] = $ret->createRequest;
599  }
600  $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
601  $session->setSecret( 'AuthManager::authnState', [
602  'reqs' => [], // Will be filled in later
603  'primary' => null,
604  'primaryResponse' => null,
605  'secondary' => [],
606  'continueRequests' => $ret->neededRequests,
607  ] + $state );
608  return $ret;
609  }
610 
611  // Step 2: Primary authentication succeeded, create the User object
612  // (and add the user locally if necessary)
613 
614  $user = User::newFromName( $res->username, 'usable' );
615  if ( !$user ) {
616  $provider = $this->getAuthenticationProvider( $state['primary'] );
617  throw new \DomainException(
618  get_class( $provider ) . " returned an invalid username: {$res->username}"
619  );
620  }
621  if ( $user->getId() === 0 ) {
622  // User doesn't exist locally. Create it.
623  $this->logger->info( 'Auto-creating {user} on login', [
624  'user' => $user->getName(),
625  ] );
626  $status = $this->autoCreateUser( $user, $state['primary'], false );
627  if ( !$status->isGood() ) {
629  Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
630  );
631  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
632  $session->remove( 'AuthManager::authnState' );
633  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
634  return $ret;
635  }
636  }
637 
638  // Step 3: Iterate over all the secondary authentication providers.
639 
640  $beginReqs = $state['reqs'];
641 
642  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
643  if ( !isset( $state['secondary'][$id] ) ) {
644  // This provider isn't started yet, so we pass it the set
645  // of reqs from beginAuthentication instead of whatever
646  // might have been used by a previous provider in line.
647  $func = 'beginSecondaryAuthentication';
648  $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
649  } elseif ( !$state['secondary'][$id] ) {
650  $func = 'continueSecondaryAuthentication';
651  $res = $provider->continueSecondaryAuthentication( $user, $reqs );
652  } else {
653  continue;
654  }
655  switch ( $res->status ) {
657  $this->logger->debug( "Secondary login with $id succeeded" );
658  // fall through
660  $state['secondary'][$id] = true;
661  break;
663  $this->logger->debug( "Login failed in secondary authentication by $id" );
664  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
665  $session->remove( 'AuthManager::authnState' );
666  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName(), [] ] );
667  return $res;
670  $this->logger->debug( "Secondary login with $id returned " . $res->status );
671  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
672  $state['secondary'][$id] = false;
673  $state['continueRequests'] = $res->neededRequests;
674  $session->setSecret( 'AuthManager::authnState', $state );
675  return $res;
676 
677  // @codeCoverageIgnoreStart
678  default:
679  throw new \DomainException(
680  get_class( $provider ) . "::{$func}() returned $res->status"
681  );
682  // @codeCoverageIgnoreEnd
683  }
684  }
685 
686  // Step 4: Authentication complete! Set the user in the session and
687  // clean up.
688 
689  $this->logger->info( 'Login for {user} succeeded from {clientip}', [
690  'user' => $user->getName(),
691  'clientip' => $this->request->getIP(),
692  ] );
695  $beginReqs, RememberMeAuthenticationRequest::class
696  );
697  $this->setSessionDataForUser( $user, $req && $req->rememberMe );
698  $ret = AuthenticationResponse::newPass( $user->getName() );
699  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
700  $session->remove( 'AuthManager::authnState' );
701  $this->removeAuthenticationSessionData( null );
702  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
703  return $ret;
704  } catch ( \Exception $ex ) {
705  $session->remove( 'AuthManager::authnState' );
706  throw $ex;
707  }
708  }
709 
721  public function securitySensitiveOperationStatus( $operation ) {
722  $status = self::SEC_OK;
723 
724  $this->logger->debug( __METHOD__ . ": Checking $operation" );
725 
726  $session = $this->request->getSession();
727  $aId = $session->getUser()->getId();
728  if ( $aId === 0 ) {
729  // User isn't authenticated. DWIM?
730  $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
731  $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
732  return $status;
733  }
734 
735  if ( $session->canSetUser() ) {
736  $id = $session->get( 'AuthManager:lastAuthId' );
737  $last = $session->get( 'AuthManager:lastAuthTimestamp' );
738  if ( $id !== $aId || $last === null ) {
739  $timeSinceLogin = PHP_INT_MAX; // Forever ago
740  } else {
741  $timeSinceLogin = max( 0, time() - $last );
742  }
743 
744  $thresholds = $this->config->get( 'ReauthenticateTime' );
745  if ( isset( $thresholds[$operation] ) ) {
746  $threshold = $thresholds[$operation];
747  } elseif ( isset( $thresholds['default'] ) ) {
748  $threshold = $thresholds['default'];
749  } else {
750  throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
751  }
752 
753  if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
754  $status = self::SEC_REAUTH;
755  }
756  } else {
757  $timeSinceLogin = -1;
758 
759  $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
760  if ( isset( $pass[$operation] ) ) {
761  $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
762  } elseif ( isset( $pass['default'] ) ) {
763  $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
764  } else {
765  throw new \UnexpectedValueException(
766  '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
767  );
768  }
769  }
770 
771  \Hooks::run( 'SecuritySensitiveOperationStatus', [
772  &$status, $operation, $session, $timeSinceLogin
773  ] );
774 
775  // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
776  if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
777  $status = self::SEC_FAIL;
778  }
779 
780  $this->logger->info( __METHOD__ . ": $operation is $status for '{user}'",
781  [
782  'user' => $session->getUser()->getName(),
783  'clientip' => $this->getRequest()->getIP(),
784  ]
785  );
786 
787  return $status;
788  }
789 
799  public function userCanAuthenticate( $username ) {
800  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
801  if ( $provider->testUserCanAuthenticate( $username ) ) {
802  return true;
803  }
804  }
805  return false;
806  }
807 
822  public function normalizeUsername( $username ) {
823  $ret = [];
824  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
825  $normalized = $provider->providerNormalizeUsername( $username );
826  if ( $normalized !== null ) {
827  $ret[$normalized] = true;
828  }
829  }
830  return array_keys( $ret );
831  }
832 
847  public function revokeAccessForUser( $username ) {
848  $this->logger->info( 'Revoking access for {user}', [
849  'user' => $username,
850  ] );
851  $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
852  }
853 
863  public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
864  $any = false;
865  $providers = $this->getPrimaryAuthenticationProviders() +
867  foreach ( $providers as $provider ) {
868  $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
869  if ( !$status->isGood() ) {
870  return Status::wrap( $status );
871  }
872  $any = $any || $status->value !== 'ignored';
873  }
874  if ( !$any ) {
875  $status = Status::newGood( 'ignored' );
876  $status->warning( 'authmanager-change-not-supported' );
877  return $status;
878  }
879  return Status::newGood();
880  }
881 
899  public function changeAuthenticationData( AuthenticationRequest $req, $isAddition = false ) {
900  $this->logger->info( 'Changing authentication data for {user} class {what}', [
901  'user' => is_string( $req->username ) ? $req->username : '<no name>',
902  'what' => get_class( $req ),
903  ] );
904 
905  $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
906 
907  // When the main account's authentication data is changed, invalidate
908  // all BotPasswords too.
909  if ( !$isAddition ) {
911  }
912  }
913 
925  public function canCreateAccounts() {
926  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
927  switch ( $provider->accountCreationType() ) {
930  return true;
931  }
932  }
933  return false;
934  }
935 
944  public function canCreateAccount( $username, $options = [] ) {
945  // Back compat
946  if ( is_int( $options ) ) {
947  $options = [ 'flags' => $options ];
948  }
949  $options += [
950  'flags' => User::READ_NORMAL,
951  'creating' => false,
952  ];
953  $flags = $options['flags'];
954 
955  if ( !$this->canCreateAccounts() ) {
956  return Status::newFatal( 'authmanager-create-disabled' );
957  }
958 
959  if ( $this->userExists( $username, $flags ) ) {
960  return Status::newFatal( 'userexists' );
961  }
962 
963  $user = User::newFromName( $username, 'creatable' );
964  if ( !is_object( $user ) ) {
965  return Status::newFatal( 'noname' );
966  } else {
967  $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
968  if ( $user->getId() !== 0 ) {
969  return Status::newFatal( 'userexists' );
970  }
971  }
972 
973  // Denied by providers?
974  $providers = $this->getPreAuthenticationProviders() +
977  foreach ( $providers as $provider ) {
978  $status = $provider->testUserForCreation( $user, false, $options );
979  if ( !$status->isGood() ) {
980  return Status::wrap( $status );
981  }
982  }
983 
984  return Status::newGood();
985  }
986 
992  public function checkAccountCreatePermissions( User $creator ) {
993  // Wiki is read-only?
994  if ( wfReadOnly() ) {
995  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
996  }
997 
998  // This is awful, this permission check really shouldn't go through Title.
999  $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
1000  ->getUserPermissionsErrors(
1001  'createaccount', $creator, PermissionManager::RIGOR_SECURE
1002  );
1003  if ( $permErrors ) {
1004  $status = Status::newGood();
1005  foreach ( $permErrors as $args ) {
1006  $status->fatal( ...$args );
1007  }
1008  return $status;
1009  }
1010 
1011  $ip = $this->getRequest()->getIP();
1012 
1013  $block = $creator->isBlockedFromCreateAccount();
1014  if ( $block ) {
1015  $language = \RequestContext::getMain()->getLanguage();
1016  $formatter = MediaWikiServices::getInstance()->getBlockErrorFormatter();
1017  return Status::newFatal(
1018  $formatter->getMessage( $block, $creator, $language, $ip )
1019  );
1020  }
1021 
1022  if (
1023  MediaWikiServices::getInstance()->getBlockManager()
1024  ->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ )
1025  ) {
1026  return Status::newFatal( 'sorbs_create_account_reason' );
1027  }
1028 
1029  return Status::newGood();
1030  }
1031 
1051  public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1052  $session = $this->request->getSession();
1053  if ( !$this->canCreateAccounts() ) {
1054  // Caller should have called canCreateAccounts()
1055  $session->remove( 'AuthManager::accountCreationState' );
1056  throw new \LogicException( 'Account creation is not possible' );
1057  }
1058 
1059  try {
1061  } catch ( \UnexpectedValueException $ex ) {
1062  $username = null;
1063  }
1064  if ( $username === null ) {
1065  $this->logger->debug( __METHOD__ . ': No username provided' );
1066  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1067  }
1068 
1069  // Permissions check
1070  $status = $this->checkAccountCreatePermissions( $creator );
1071  if ( !$status->isGood() ) {
1072  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1073  'user' => $username,
1074  'creator' => $creator->getName(),
1075  'reason' => $status->getWikiText( null, null, 'en' )
1076  ] );
1077  return AuthenticationResponse::newFail( $status->getMessage() );
1078  }
1079 
1080  $status = $this->canCreateAccount(
1081  $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1082  );
1083  if ( !$status->isGood() ) {
1084  $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1085  'user' => $username,
1086  'creator' => $creator->getName(),
1087  'reason' => $status->getWikiText( null, null, 'en' )
1088  ] );
1089  return AuthenticationResponse::newFail( $status->getMessage() );
1090  }
1091 
1092  $user = User::newFromName( $username, 'creatable' );
1093  foreach ( $reqs as $req ) {
1094  $req->username = $username;
1095  $req->returnToUrl = $returnToUrl;
1096  if ( $req instanceof UserDataAuthenticationRequest ) {
1097  $status = $req->populateUser( $user );
1098  if ( !$status->isGood() ) {
1099  $status = Status::wrap( $status );
1100  $session->remove( 'AuthManager::accountCreationState' );
1101  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1102  'user' => $user->getName(),
1103  'creator' => $creator->getName(),
1104  'reason' => $status->getWikiText( null, null, 'en' ),
1105  ] );
1106  return AuthenticationResponse::newFail( $status->getMessage() );
1107  }
1108  }
1109  }
1110 
1111  $this->removeAuthenticationSessionData( null );
1112 
1113  $state = [
1114  'username' => $username,
1115  'userid' => 0,
1116  'creatorid' => $creator->getId(),
1117  'creatorname' => $creator->getName(),
1118  'reqs' => $reqs,
1119  'returnToUrl' => $returnToUrl,
1120  'primary' => null,
1121  'primaryResponse' => null,
1122  'secondary' => [],
1123  'continueRequests' => [],
1124  'maybeLink' => [],
1125  'ranPreTests' => false,
1126  ];
1127 
1128  // Special case: converting a login to an account creation
1130  $reqs, CreateFromLoginAuthenticationRequest::class
1131  );
1132  if ( $req ) {
1133  $state['maybeLink'] = $req->maybeLink;
1134 
1135  if ( $req->createRequest ) {
1136  $reqs[] = $req->createRequest;
1137  $state['reqs'][] = $req->createRequest;
1138  }
1139  }
1140 
1141  $session->setSecret( 'AuthManager::accountCreationState', $state );
1142  $session->persist();
1143 
1144  return $this->continueAccountCreation( $reqs );
1145  }
1146 
1152  public function continueAccountCreation( array $reqs ) {
1153  $session = $this->request->getSession();
1154  try {
1155  if ( !$this->canCreateAccounts() ) {
1156  // Caller should have called canCreateAccounts()
1157  $session->remove( 'AuthManager::accountCreationState' );
1158  throw new \LogicException( 'Account creation is not possible' );
1159  }
1160 
1161  $state = $session->getSecret( 'AuthManager::accountCreationState' );
1162  if ( !is_array( $state ) ) {
1164  wfMessage( 'authmanager-create-not-in-progress' )
1165  );
1166  }
1167  $state['continueRequests'] = [];
1168 
1169  // Step 0: Prepare and validate the input
1170 
1171  $user = User::newFromName( $state['username'], 'creatable' );
1172  if ( !is_object( $user ) ) {
1173  $session->remove( 'AuthManager::accountCreationState' );
1174  $this->logger->debug( __METHOD__ . ': Invalid username', [
1175  'user' => $state['username'],
1176  ] );
1177  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1178  }
1179 
1180  if ( $state['creatorid'] ) {
1181  $creator = User::newFromId( $state['creatorid'] );
1182  } else {
1183  $creator = new User;
1184  $creator->setName( $state['creatorname'] );
1185  }
1186 
1187  // Avoid account creation races on double submissions
1189  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1190  if ( !$lock ) {
1191  // Don't clear AuthManager::accountCreationState for this code
1192  // path because the process that won the race owns it.
1193  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1194  'user' => $user->getName(),
1195  'creator' => $creator->getName(),
1196  ] );
1197  return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1198  }
1199 
1200  // Permissions check
1201  $status = $this->checkAccountCreatePermissions( $creator );
1202  if ( !$status->isGood() ) {
1203  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1204  'user' => $user->getName(),
1205  'creator' => $creator->getName(),
1206  'reason' => $status->getWikiText( null, null, 'en' )
1207  ] );
1208  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1209  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1210  $session->remove( 'AuthManager::accountCreationState' );
1211  return $ret;
1212  }
1213 
1214  // Load from master for existence check
1215  $user->load( User::READ_LOCKING );
1216 
1217  if ( $state['userid'] === 0 ) {
1218  if ( $user->getId() !== 0 ) {
1219  $this->logger->debug( __METHOD__ . ': User exists locally', [
1220  'user' => $user->getName(),
1221  'creator' => $creator->getName(),
1222  ] );
1223  $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1224  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1225  $session->remove( 'AuthManager::accountCreationState' );
1226  return $ret;
1227  }
1228  } else {
1229  if ( $user->getId() === 0 ) {
1230  $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1231  'user' => $user->getName(),
1232  'creator' => $creator->getName(),
1233  'expected_id' => $state['userid'],
1234  ] );
1235  throw new \UnexpectedValueException(
1236  "User \"{$state['username']}\" should exist now, but doesn't!"
1237  );
1238  }
1239  if ( $user->getId() !== $state['userid'] ) {
1240  $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1241  'user' => $user->getName(),
1242  'creator' => $creator->getName(),
1243  'expected_id' => $state['userid'],
1244  'actual_id' => $user->getId(),
1245  ] );
1246  throw new \UnexpectedValueException(
1247  "User \"{$state['username']}\" exists, but " .
1248  "ID {$user->getId()} !== {$state['userid']}!"
1249  );
1250  }
1251  }
1252  foreach ( $state['reqs'] as $req ) {
1253  if ( $req instanceof UserDataAuthenticationRequest ) {
1254  $status = $req->populateUser( $user );
1255  if ( !$status->isGood() ) {
1256  // This should never happen...
1257  $status = Status::wrap( $status );
1258  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1259  'user' => $user->getName(),
1260  'creator' => $creator->getName(),
1261  'reason' => $status->getWikiText( null, null, 'en' ),
1262  ] );
1263  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1264  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1265  $session->remove( 'AuthManager::accountCreationState' );
1266  return $ret;
1267  }
1268  }
1269  }
1270 
1271  foreach ( $reqs as $req ) {
1272  $req->returnToUrl = $state['returnToUrl'];
1273  $req->username = $state['username'];
1274  }
1275 
1276  // Run pre-creation tests, if we haven't already
1277  if ( !$state['ranPreTests'] ) {
1278  $providers = $this->getPreAuthenticationProviders() +
1281  foreach ( $providers as $id => $provider ) {
1282  $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1283  if ( !$status->isGood() ) {
1284  $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1285  'user' => $user->getName(),
1286  'creator' => $creator->getName(),
1287  ] );
1289  Status::wrap( $status )->getMessage()
1290  );
1291  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1292  $session->remove( 'AuthManager::accountCreationState' );
1293  return $ret;
1294  }
1295  }
1296 
1297  $state['ranPreTests'] = true;
1298  }
1299 
1300  // Step 1: Choose a primary authentication provider and call it until it succeeds.
1301 
1302  if ( $state['primary'] === null ) {
1303  // We haven't picked a PrimaryAuthenticationProvider yet
1304  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1305  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1306  continue;
1307  }
1308  $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1309  switch ( $res->status ) {
1311  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1312  'user' => $user->getName(),
1313  'creator' => $creator->getName(),
1314  ] );
1315  $state['primary'] = $id;
1316  $state['primaryResponse'] = $res;
1317  break 2;
1319  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1320  'user' => $user->getName(),
1321  'creator' => $creator->getName(),
1322  ] );
1323  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1324  $session->remove( 'AuthManager::accountCreationState' );
1325  return $res;
1327  // Continue loop
1328  break;
1331  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1332  'user' => $user->getName(),
1333  'creator' => $creator->getName(),
1334  ] );
1335  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1336  $state['primary'] = $id;
1337  $state['continueRequests'] = $res->neededRequests;
1338  $session->setSecret( 'AuthManager::accountCreationState', $state );
1339  return $res;
1340 
1341  // @codeCoverageIgnoreStart
1342  default:
1343  throw new \DomainException(
1344  get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1345  );
1346  // @codeCoverageIgnoreEnd
1347  }
1348  }
1349  if ( $state['primary'] === null ) {
1350  $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1351  'user' => $user->getName(),
1352  'creator' => $creator->getName(),
1353  ] );
1355  wfMessage( 'authmanager-create-no-primary' )
1356  );
1357  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1358  $session->remove( 'AuthManager::accountCreationState' );
1359  return $ret;
1360  }
1361  } elseif ( $state['primaryResponse'] === null ) {
1362  $provider = $this->getAuthenticationProvider( $state['primary'] );
1363  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1364  // Configuration changed? Force them to start over.
1365  // @codeCoverageIgnoreStart
1367  wfMessage( 'authmanager-create-not-in-progress' )
1368  );
1369  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1370  $session->remove( 'AuthManager::accountCreationState' );
1371  return $ret;
1372  // @codeCoverageIgnoreEnd
1373  }
1374  $id = $provider->getUniqueId();
1375  $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1376  switch ( $res->status ) {
1378  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1379  'user' => $user->getName(),
1380  'creator' => $creator->getName(),
1381  ] );
1382  $state['primaryResponse'] = $res;
1383  break;
1385  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1386  'user' => $user->getName(),
1387  'creator' => $creator->getName(),
1388  ] );
1389  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1390  $session->remove( 'AuthManager::accountCreationState' );
1391  return $res;
1394  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1395  'user' => $user->getName(),
1396  'creator' => $creator->getName(),
1397  ] );
1398  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1399  $state['continueRequests'] = $res->neededRequests;
1400  $session->setSecret( 'AuthManager::accountCreationState', $state );
1401  return $res;
1402  default:
1403  throw new \DomainException(
1404  get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1405  );
1406  }
1407  }
1408 
1409  // Step 2: Primary authentication succeeded, create the User object
1410  // and add the user locally.
1411 
1412  if ( $state['userid'] === 0 ) {
1413  $this->logger->info( 'Creating user {user} during account creation', [
1414  'user' => $user->getName(),
1415  'creator' => $creator->getName(),
1416  ] );
1417  $status = $user->addToDatabase();
1418  if ( !$status->isOK() ) {
1419  // @codeCoverageIgnoreStart
1420  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1421  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1422  $session->remove( 'AuthManager::accountCreationState' );
1423  return $ret;
1424  // @codeCoverageIgnoreEnd
1425  }
1426  $this->setDefaultUserOptions( $user, $creator->isAnon() );
1427  \Hooks::runWithoutAbort( 'LocalUserCreated', [ $user, false ] );
1428  $user->saveSettings();
1429  $state['userid'] = $user->getId();
1430 
1431  // Update user count
1432  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1433 
1434  // Watch user's userpage and talk page
1435  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1436 
1437  // Inform the provider
1438  $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1439 
1440  // Log the creation
1441  if ( $this->config->get( 'NewUserLog' ) ) {
1442  $isAnon = $creator->isAnon();
1443  $logEntry = new \ManualLogEntry(
1444  'newusers',
1445  $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1446  );
1447  $logEntry->setPerformer( $isAnon ? $user : $creator );
1448  $logEntry->setTarget( $user->getUserPage() );
1451  $state['reqs'], CreationReasonAuthenticationRequest::class
1452  );
1453  $logEntry->setComment( $req ? $req->reason : '' );
1454  $logEntry->setParameters( [
1455  '4::userid' => $user->getId(),
1456  ] );
1457  $logid = $logEntry->insert();
1458  $logEntry->publish( $logid );
1459  }
1460  }
1461 
1462  // Step 3: Iterate over all the secondary authentication providers.
1463 
1464  $beginReqs = $state['reqs'];
1465 
1466  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1467  if ( !isset( $state['secondary'][$id] ) ) {
1468  // This provider isn't started yet, so we pass it the set
1469  // of reqs from beginAuthentication instead of whatever
1470  // might have been used by a previous provider in line.
1471  $func = 'beginSecondaryAccountCreation';
1472  $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1473  } elseif ( !$state['secondary'][$id] ) {
1474  $func = 'continueSecondaryAccountCreation';
1475  $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1476  } else {
1477  continue;
1478  }
1479  switch ( $res->status ) {
1481  $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1482  'user' => $user->getName(),
1483  'creator' => $creator->getName(),
1484  ] );
1485  // fall through
1487  $state['secondary'][$id] = true;
1488  break;
1491  $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1492  'user' => $user->getName(),
1493  'creator' => $creator->getName(),
1494  ] );
1495  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1496  $state['secondary'][$id] = false;
1497  $state['continueRequests'] = $res->neededRequests;
1498  $session->setSecret( 'AuthManager::accountCreationState', $state );
1499  return $res;
1501  throw new \DomainException(
1502  get_class( $provider ) . "::{$func}() returned $res->status." .
1503  ' Secondary providers are not allowed to fail account creation, that' .
1504  ' should have been done via testForAccountCreation().'
1505  );
1506  // @codeCoverageIgnoreStart
1507  default:
1508  throw new \DomainException(
1509  get_class( $provider ) . "::{$func}() returned $res->status"
1510  );
1511  // @codeCoverageIgnoreEnd
1512  }
1513  }
1514 
1515  $id = $user->getId();
1516  $name = $user->getName();
1517  $req = new CreatedAccountAuthenticationRequest( $id, $name );
1518  $ret = AuthenticationResponse::newPass( $name );
1519  $ret->loginRequest = $req;
1520  $this->createdAccountAuthenticationRequests[] = $req;
1521 
1522  $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1523  'user' => $user->getName(),
1524  'creator' => $creator->getName(),
1525  ] );
1526 
1527  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1528  $session->remove( 'AuthManager::accountCreationState' );
1529  $this->removeAuthenticationSessionData( null );
1530  return $ret;
1531  } catch ( \Exception $ex ) {
1532  $session->remove( 'AuthManager::accountCreationState' );
1533  throw $ex;
1534  }
1535  }
1536 
1554  public function autoCreateUser( User $user, $source, $login = true ) {
1555  if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1556  $source !== self::AUTOCREATE_SOURCE_MAINT &&
1558  ) {
1559  throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1560  }
1561 
1562  $username = $user->getName();
1563 
1564  // Try the local user from the replica DB
1565  $localId = User::idFromName( $username );
1566  $flags = User::READ_NORMAL;
1567 
1568  // Fetch the user ID from the master, so that we don't try to create the user
1569  // when they already exist, due to replication lag
1570  // @codeCoverageIgnoreStart
1571  if (
1572  !$localId &&
1573  MediaWikiServices::getInstance()->getDBLoadBalancer()->getReaderIndex() !== 0
1574  ) {
1575  $localId = User::idFromName( $username, User::READ_LATEST );
1576  $flags = User::READ_LATEST;
1577  }
1578  // @codeCoverageIgnoreEnd
1579 
1580  if ( $localId ) {
1581  $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1582  'username' => $username,
1583  ] );
1584  $user->setId( $localId );
1585  $user->loadFromId( $flags );
1586  if ( $login ) {
1587  $this->setSessionDataForUser( $user );
1588  }
1589  $status = Status::newGood();
1590  $status->warning( 'userexists' );
1591  return $status;
1592  }
1593 
1594  // Wiki is read-only?
1595  if ( wfReadOnly() ) {
1596  $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1597  'username' => $username,
1598  'reason' => wfReadOnlyReason(),
1599  ] );
1600  $user->setId( 0 );
1601  $user->loadFromId();
1602  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1603  }
1604 
1605  // Check the session, if we tried to create this user already there's
1606  // no point in retrying.
1607  $session = $this->request->getSession();
1608  if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1609  $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1610  'username' => $username,
1611  'sessionid' => $session->getId(),
1612  ] );
1613  $user->setId( 0 );
1614  $user->loadFromId();
1615  $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1616  if ( $reason instanceof StatusValue ) {
1617  return Status::wrap( $reason );
1618  } else {
1619  return Status::newFatal( $reason );
1620  }
1621  }
1622 
1623  // Is the username creatable?
1624  if ( !User::isCreatableName( $username ) ) {
1625  $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1626  'username' => $username,
1627  ] );
1628  $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1629  $user->setId( 0 );
1630  $user->loadFromId();
1631  return Status::newFatal( 'noname' );
1632  }
1633 
1634  // Is the IP user able to create accounts?
1635  $anon = new User;
1636  if ( $source !== self::AUTOCREATE_SOURCE_MAINT && !MediaWikiServices::getInstance()
1638  ->userHasAnyRight( $anon, 'createaccount', 'autocreateaccount' )
1639  ) {
1640  $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1641  'username' => $username,
1642  'ip' => $anon->getName(),
1643  ] );
1644  $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1645  $session->persist();
1646  $user->setId( 0 );
1647  $user->loadFromId();
1648  return Status::newFatal( 'authmanager-autocreate-noperm' );
1649  }
1650 
1651  // Avoid account creation races on double submissions
1653  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1654  if ( !$lock ) {
1655  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1656  'user' => $username,
1657  ] );
1658  $user->setId( 0 );
1659  $user->loadFromId();
1660  return Status::newFatal( 'usernameinprogress' );
1661  }
1662 
1663  // Denied by providers?
1664  $options = [
1665  'flags' => User::READ_LATEST,
1666  'creating' => true,
1667  ];
1668  $providers = $this->getPreAuthenticationProviders() +
1671  foreach ( $providers as $provider ) {
1672  $status = $provider->testUserForCreation( $user, $source, $options );
1673  if ( !$status->isGood() ) {
1674  $ret = Status::wrap( $status );
1675  $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1676  'username' => $username,
1677  'reason' => $ret->getWikiText( null, null, 'en' ),
1678  ] );
1679  $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1680  $user->setId( 0 );
1681  $user->loadFromId();
1682  return $ret;
1683  }
1684  }
1685 
1686  $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1687  if ( $cache->get( $backoffKey ) ) {
1688  $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1689  'username' => $username,
1690  ] );
1691  $user->setId( 0 );
1692  $user->loadFromId();
1693  return Status::newFatal( 'authmanager-autocreate-exception' );
1694  }
1695 
1696  // Checks passed, create the user...
1697  $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
1698  $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1699  'username' => $username,
1700  'from' => $from,
1701  ] );
1702 
1703  // Ignore warnings about master connections/writes...hard to avoid here
1704  $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1705  $old = $trxProfiler->setSilenced( true );
1706  try {
1707  $status = $user->addToDatabase();
1708  if ( !$status->isOK() ) {
1709  // Double-check for a race condition (T70012). We make use of the fact that when
1710  // addToDatabase fails due to the user already existing, the user object gets loaded.
1711  if ( $user->getId() ) {
1712  $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1713  'username' => $username,
1714  ] );
1715  if ( $login ) {
1716  $this->setSessionDataForUser( $user );
1717  }
1718  $status = Status::newGood();
1719  $status->warning( 'userexists' );
1720  } else {
1721  $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1722  'username' => $username,
1723  'msg' => $status->getWikiText( null, null, 'en' )
1724  ] );
1725  $user->setId( 0 );
1726  $user->loadFromId();
1727  }
1728  return $status;
1729  }
1730  } catch ( \Exception $ex ) {
1731  $trxProfiler->setSilenced( $old );
1732  $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1733  'username' => $username,
1734  'exception' => $ex,
1735  ] );
1736  // Do not keep throwing errors for a while
1737  $cache->set( $backoffKey, 1, 600 );
1738  // Bubble up error; which should normally trigger DB rollbacks
1739  throw $ex;
1740  }
1741 
1742  $this->setDefaultUserOptions( $user, false );
1743 
1744  // Inform the providers
1745  $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1746 
1747  \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1748  $user->saveSettings();
1749 
1750  // Update user count
1751  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1752  // Watch user's userpage and talk page
1753  \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1754  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1755  } );
1756 
1757  // Log the creation
1758  if ( $this->config->get( 'NewUserLog' ) ) {
1759  $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1760  $logEntry->setPerformer( $user );
1761  $logEntry->setTarget( $user->getUserPage() );
1762  $logEntry->setComment( '' );
1763  $logEntry->setParameters( [
1764  '4::userid' => $user->getId(),
1765  ] );
1766  $logEntry->insert();
1767  }
1768 
1769  $trxProfiler->setSilenced( $old );
1770 
1771  if ( $login ) {
1772  $this->setSessionDataForUser( $user );
1773  }
1774 
1775  return Status::newGood();
1776  }
1777 
1789  public function canLinkAccounts() {
1790  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1791  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1792  return true;
1793  }
1794  }
1795  return false;
1796  }
1797 
1807  public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1808  $session = $this->request->getSession();
1809  $session->remove( 'AuthManager::accountLinkState' );
1810 
1811  if ( !$this->canLinkAccounts() ) {
1812  // Caller should have called canLinkAccounts()
1813  throw new \LogicException( 'Account linking is not possible' );
1814  }
1815 
1816  if ( $user->getId() === 0 ) {
1817  if ( !User::isUsableName( $user->getName() ) ) {
1818  $msg = wfMessage( 'noname' );
1819  } else {
1820  $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1821  }
1822  return AuthenticationResponse::newFail( $msg );
1823  }
1824  foreach ( $reqs as $req ) {
1825  $req->username = $user->getName();
1826  $req->returnToUrl = $returnToUrl;
1827  }
1828 
1829  $this->removeAuthenticationSessionData( null );
1830 
1831  $providers = $this->getPreAuthenticationProviders();
1832  foreach ( $providers as $id => $provider ) {
1833  $status = $provider->testForAccountLink( $user );
1834  if ( !$status->isGood() ) {
1835  $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1836  'user' => $user->getName(),
1837  ] );
1839  Status::wrap( $status )->getMessage()
1840  );
1841  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1842  return $ret;
1843  }
1844  }
1845 
1846  $state = [
1847  'username' => $user->getName(),
1848  'userid' => $user->getId(),
1849  'returnToUrl' => $returnToUrl,
1850  'primary' => null,
1851  'continueRequests' => [],
1852  ];
1853 
1854  $providers = $this->getPrimaryAuthenticationProviders();
1855  foreach ( $providers as $id => $provider ) {
1856  if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1857  continue;
1858  }
1859 
1860  $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1861  switch ( $res->status ) {
1863  $this->logger->info( "Account linked to {user} by $id", [
1864  'user' => $user->getName(),
1865  ] );
1866  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1867  return $res;
1868 
1870  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1871  'user' => $user->getName(),
1872  ] );
1873  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1874  return $res;
1875 
1877  // Continue loop
1878  break;
1879 
1882  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1883  'user' => $user->getName(),
1884  ] );
1885  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1886  $state['primary'] = $id;
1887  $state['continueRequests'] = $res->neededRequests;
1888  $session->setSecret( 'AuthManager::accountLinkState', $state );
1889  $session->persist();
1890  return $res;
1891 
1892  // @codeCoverageIgnoreStart
1893  default:
1894  throw new \DomainException(
1895  get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1896  );
1897  // @codeCoverageIgnoreEnd
1898  }
1899  }
1900 
1901  $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1902  'user' => $user->getName(),
1903  ] );
1905  wfMessage( 'authmanager-link-no-primary' )
1906  );
1907  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1908  return $ret;
1909  }
1910 
1916  public function continueAccountLink( array $reqs ) {
1917  $session = $this->request->getSession();
1918  try {
1919  if ( !$this->canLinkAccounts() ) {
1920  // Caller should have called canLinkAccounts()
1921  $session->remove( 'AuthManager::accountLinkState' );
1922  throw new \LogicException( 'Account linking is not possible' );
1923  }
1924 
1925  $state = $session->getSecret( 'AuthManager::accountLinkState' );
1926  if ( !is_array( $state ) ) {
1928  wfMessage( 'authmanager-link-not-in-progress' )
1929  );
1930  }
1931  $state['continueRequests'] = [];
1932 
1933  // Step 0: Prepare and validate the input
1934 
1935  $user = User::newFromName( $state['username'], 'usable' );
1936  if ( !is_object( $user ) ) {
1937  $session->remove( 'AuthManager::accountLinkState' );
1938  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1939  }
1940  if ( $user->getId() !== $state['userid'] ) {
1941  throw new \UnexpectedValueException(
1942  "User \"{$state['username']}\" is valid, but " .
1943  "ID {$user->getId()} !== {$state['userid']}!"
1944  );
1945  }
1946 
1947  foreach ( $reqs as $req ) {
1948  $req->username = $state['username'];
1949  $req->returnToUrl = $state['returnToUrl'];
1950  }
1951 
1952  // Step 1: Call the primary again until it succeeds
1953 
1954  $provider = $this->getAuthenticationProvider( $state['primary'] );
1955  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1956  // Configuration changed? Force them to start over.
1957  // @codeCoverageIgnoreStart
1959  wfMessage( 'authmanager-link-not-in-progress' )
1960  );
1961  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1962  $session->remove( 'AuthManager::accountLinkState' );
1963  return $ret;
1964  // @codeCoverageIgnoreEnd
1965  }
1966  $id = $provider->getUniqueId();
1967  $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1968  switch ( $res->status ) {
1970  $this->logger->info( "Account linked to {user} by $id", [
1971  'user' => $user->getName(),
1972  ] );
1973  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1974  $session->remove( 'AuthManager::accountLinkState' );
1975  return $res;
1977  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1978  'user' => $user->getName(),
1979  ] );
1980  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1981  $session->remove( 'AuthManager::accountLinkState' );
1982  return $res;
1985  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1986  'user' => $user->getName(),
1987  ] );
1988  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1989  $state['continueRequests'] = $res->neededRequests;
1990  $session->setSecret( 'AuthManager::accountLinkState', $state );
1991  return $res;
1992  default:
1993  throw new \DomainException(
1994  get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1995  );
1996  }
1997  } catch ( \Exception $ex ) {
1998  $session->remove( 'AuthManager::accountLinkState' );
1999  throw $ex;
2000  }
2001  }
2002 
2028  public function getAuthenticationRequests( $action, User $user = null ) {
2029  $options = [];
2030  $providerAction = $action;
2031 
2032  // Figure out which providers to query
2033  switch ( $action ) {
2034  case self::ACTION_LOGIN:
2035  case self::ACTION_CREATE:
2036  $providers = $this->getPreAuthenticationProviders() +
2039  break;
2040 
2041  case self::ACTION_LOGIN_CONTINUE:
2042  $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2043  return is_array( $state ) ? $state['continueRequests'] : [];
2044 
2045  case self::ACTION_CREATE_CONTINUE:
2046  $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2047  return is_array( $state ) ? $state['continueRequests'] : [];
2048 
2049  case self::ACTION_LINK:
2050  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2051  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2052  } );
2053  break;
2054 
2055  case self::ACTION_UNLINK:
2056  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2057  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2058  } );
2059 
2060  // To providers, unlink and remove are identical.
2061  $providerAction = self::ACTION_REMOVE;
2062  break;
2063 
2064  case self::ACTION_LINK_CONTINUE:
2065  $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2066  return is_array( $state ) ? $state['continueRequests'] : [];
2067 
2068  case self::ACTION_CHANGE:
2069  case self::ACTION_REMOVE:
2070  $providers = $this->getPrimaryAuthenticationProviders() +
2072  break;
2073 
2074  // @codeCoverageIgnoreStart
2075  default:
2076  throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2077  }
2078  // @codeCoverageIgnoreEnd
2079 
2080  return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2081  }
2082 
2093  $providerAction, array $options, array $providers, User $user = null
2094  ) {
2095  $user = $user ?: \RequestContext::getMain()->getUser();
2096  $options['username'] = $user->isAnon() ? null : $user->getName();
2097 
2098  // Query them and merge results
2099  $reqs = [];
2100  foreach ( $providers as $provider ) {
2101  $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2102  foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2103  $id = $req->getUniqueId();
2104 
2105  // If a required request if from a Primary, mark it as "primary-required" instead
2106  if ( $isPrimary && $req->required ) {
2107  $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2108  }
2109 
2110  if (
2111  !isset( $reqs[$id] )
2112  || $req->required === AuthenticationRequest::REQUIRED
2113  || $reqs[$id] === AuthenticationRequest::OPTIONAL
2114  ) {
2115  $reqs[$id] = $req;
2116  }
2117  }
2118  }
2119 
2120  // AuthManager has its own req for some actions
2121  switch ( $providerAction ) {
2122  case self::ACTION_LOGIN:
2123  $reqs[] = new RememberMeAuthenticationRequest;
2124  break;
2125 
2126  case self::ACTION_CREATE:
2127  $reqs[] = new UsernameAuthenticationRequest;
2128  $reqs[] = new UserDataAuthenticationRequest;
2129  if ( $options['username'] !== null ) {
2131  $options['username'] = null; // Don't fill in the username below
2132  }
2133  break;
2134  }
2135 
2136  // Fill in reqs data
2137  $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2138 
2139  // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2140  if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2141  $reqs = array_filter( $reqs, function ( $req ) {
2142  return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2143  } );
2144  }
2145 
2146  return array_values( $reqs );
2147  }
2148 
2156  private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2157  foreach ( $reqs as $req ) {
2158  if ( !$req->action || $forceAction ) {
2159  $req->action = $action;
2160  }
2161  if ( $req->username === null ) {
2162  $req->username = $username;
2163  }
2164  }
2165  }
2166 
2173  public function userExists( $username, $flags = User::READ_NORMAL ) {
2174  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2175  if ( $provider->testUserExists( $username, $flags ) ) {
2176  return true;
2177  }
2178  }
2179 
2180  return false;
2181  }
2182 
2194  public function allowsPropertyChange( $property ) {
2195  $providers = $this->getPrimaryAuthenticationProviders() +
2197  foreach ( $providers as $provider ) {
2198  if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2199  return false;
2200  }
2201  }
2202  return true;
2203  }
2204 
2213  public function getAuthenticationProvider( $id ) {
2214  // Fast version
2215  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2216  return $this->allAuthenticationProviders[$id];
2217  }
2218 
2219  // Slow version: instantiate each kind and check
2220  $providers = $this->getPrimaryAuthenticationProviders();
2221  if ( isset( $providers[$id] ) ) {
2222  return $providers[$id];
2223  }
2224  $providers = $this->getSecondaryAuthenticationProviders();
2225  if ( isset( $providers[$id] ) ) {
2226  return $providers[$id];
2227  }
2228  $providers = $this->getPreAuthenticationProviders();
2229  if ( isset( $providers[$id] ) ) {
2230  return $providers[$id];
2231  }
2232 
2233  return null;
2234  }
2235 
2249  public function setAuthenticationSessionData( $key, $data ) {
2250  $session = $this->request->getSession();
2251  $arr = $session->getSecret( 'authData' );
2252  if ( !is_array( $arr ) ) {
2253  $arr = [];
2254  }
2255  $arr[$key] = $data;
2256  $session->setSecret( 'authData', $arr );
2257  }
2258 
2266  public function getAuthenticationSessionData( $key, $default = null ) {
2267  $arr = $this->request->getSession()->getSecret( 'authData' );
2268  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2269  return $arr[$key];
2270  } else {
2271  return $default;
2272  }
2273  }
2274 
2280  public function removeAuthenticationSessionData( $key ) {
2281  $session = $this->request->getSession();
2282  if ( $key === null ) {
2283  $session->remove( 'authData' );
2284  } else {
2285  $arr = $session->getSecret( 'authData' );
2286  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2287  unset( $arr[$key] );
2288  $session->setSecret( 'authData', $arr );
2289  }
2290  }
2291  }
2292 
2299  protected function providerArrayFromSpecs( $class, array $specs ) {
2300  $i = 0;
2301  foreach ( $specs as &$spec ) {
2302  $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2303  }
2304  unset( $spec );
2305  // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2306  usort( $specs, function ( $a, $b ) {
2307  return $a['sort'] <=> $b['sort']
2308  ?: $a['sort2'] <=> $b['sort2'];
2309  } );
2310 
2311  $ret = [];
2312  foreach ( $specs as $spec ) {
2313  $provider = ObjectFactory::getObjectFromSpec( $spec );
2314  if ( !$provider instanceof $class ) {
2315  throw new \RuntimeException(
2316  "Expected instance of $class, got " . get_class( $provider )
2317  );
2318  }
2319  $provider->setLogger( $this->logger );
2320  $provider->setManager( $this );
2321  $provider->setConfig( $this->config );
2322  $id = $provider->getUniqueId();
2323  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2324  throw new \RuntimeException(
2325  "Duplicate specifications for id $id (classes " .
2326  get_class( $provider ) . ' and ' .
2327  get_class( $this->allAuthenticationProviders[$id] ) . ')'
2328  );
2329  }
2330  $this->allAuthenticationProviders[$id] = $provider;
2331  $ret[$id] = $provider;
2332  }
2333  return $ret;
2334  }
2335 
2340  private function getConfiguration() {
2341  return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2342  }
2343 
2348  protected function getPreAuthenticationProviders() {
2349  if ( $this->preAuthenticationProviders === null ) {
2350  $conf = $this->getConfiguration();
2351  $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2352  PreAuthenticationProvider::class, $conf['preauth']
2353  );
2354  }
2356  }
2357 
2362  protected function getPrimaryAuthenticationProviders() {
2363  if ( $this->primaryAuthenticationProviders === null ) {
2364  $conf = $this->getConfiguration();
2365  $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2366  PrimaryAuthenticationProvider::class, $conf['primaryauth']
2367  );
2368  }
2370  }
2371 
2377  if ( $this->secondaryAuthenticationProviders === null ) {
2378  $conf = $this->getConfiguration();
2379  $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2380  SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2381  );
2382  }
2384  }
2385 
2391  private function setSessionDataForUser( $user, $remember = null ) {
2392  $session = $this->request->getSession();
2393  $delay = $session->delaySave();
2394 
2395  $session->resetId();
2396  $session->resetAllTokens();
2397  if ( $session->canSetUser() ) {
2398  $session->setUser( $user );
2399  }
2400  if ( $remember !== null ) {
2401  $session->setRememberUser( $remember );
2402  }
2403  $session->set( 'AuthManager:lastAuthId', $user->getId() );
2404  $session->set( 'AuthManager:lastAuthTimestamp', time() );
2405  $session->persist();
2406 
2407  \Wikimedia\ScopedCallback::consume( $delay );
2408 
2409  \Hooks::run( 'UserLoggedIn', [ $user ] );
2410  }
2411 
2416  private function setDefaultUserOptions( User $user, $useContextLang ) {
2417  $user->setToken();
2418 
2419  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2420 
2421  $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $contLang;
2422  $user->setOption( 'language', $lang->getPreferredVariant() );
2423 
2424  if ( $contLang->hasVariants() ) {
2425  $user->setOption( 'variant', $contLang->getPreferredVariant() );
2426  }
2427  }
2428 
2434  private function callMethodOnProviders( $which, $method, array $args ) {
2435  $providers = [];
2436  if ( $which & 1 ) {
2437  $providers += $this->getPreAuthenticationProviders();
2438  }
2439  if ( $which & 2 ) {
2440  $providers += $this->getPrimaryAuthenticationProviders();
2441  }
2442  if ( $which & 4 ) {
2443  $providers += $this->getSecondaryAuthenticationProviders();
2444  }
2445  // @phan-suppress-next-line PhanEmptyForeach False positive
2446  foreach ( $providers as $provider ) {
2447  $provider->$method( ...$args );
2448  }
2449  }
2450 
2455  public static function resetCache() {
2456  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2457  // @codeCoverageIgnoreStart
2458  throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2459  // @codeCoverageIgnoreEnd
2460  }
2461 
2462  self::$instance = null;
2463  }
2464 
2467 }
2468 
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
This transfers state between the login and account creation flows.
const PRIMARY_REQUIRED
Indicates that the request is required by a primary authentication provider.
changeAuthenticationData(AuthenticationRequest $req, $isAddition=false)
Change authentication data (e.g.
addWatch( $title, $checkRights=self::CHECK_USER_RIGHTS)
Watch an article.
Definition: User.php:3733
securitySensitiveOperationStatus( $operation)
Whether security-sensitive operations should proceed.
const ABSTAIN
Indicates that the authentication provider does not handle this request.
beginAuthentication(array $reqs, $returnToUrl)
Start an authentication flow.
continueAuthentication(array $reqs)
Continue an authentication flow.
canCreateAccounts()
Determine whether accounts can be created.
saveSettings()
Save this user&#39;s settings into the database.
Definition: User.php:3992
setId( $v)
Set the user and reload all fields according to a given ID.
Definition: User.php:2274
static instance()
Singleton.
Definition: Profiler.php:63
if(!isset( $args[0])) $lang
setName( $str)
Set the user name.
Definition: User.php:2311
const ACTION_UNLINK
Like ACTION_REMOVE but for linking providers only.
removeAuthenticationSessionData( $key)
Remove authentication data.
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
$source
static getUsernameFromRequests(array $reqs)
Get the username from the set of requests.
allowsPropertyChange( $property)
Determine whether a user property should be allowed to be changed.
getUniqueId()
Return a unique identifier for this instance.
static getLocalClusterInstance()
Get the main cluster-local cache object.
static getInstance( $channel)
Get a named logger instance from the currently configured logger factory.
string $action
Cache what action this request is.
Definition: MediaWiki.php:42
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition: User.php:2836
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:885
continueAccountCreation(array $reqs)
Continue an account creation flow.
Authentication request for the reason given for account creation.
static getInstance()
Returns the global default instance of the top level service locator.
A helper class for throttling authentication attempts.
setOption( $oname, $val)
Set the given option for a user.
Definition: User.php:3057
getAuthenticationRequests( $action, User $user=null)
Return the applicable list of AuthenticationRequests.
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2283
PreAuthenticationProvider [] $preAuthenticationProviders
CreatedAccountAuthenticationRequest [] $createdAccountAuthenticationRequests
A primary authentication provider is responsible for associating the submitted authentication data wi...
beginAccountLink(User $user, array $reqs, $returnToUrl)
Start an account linking flow.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
canCreateAccount( $username, $options=[])
Determine whether a particular account can be created.
getPermissionManager()
loadFromId( $flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition: User.php:409
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition: User.php:4293
if( $line===false) $args
Definition: mcc.php:124
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
AuthenticationProvider [] $allAuthenticationProviders
wfReadOnly()
Check whether the wiki is in read-only mode.
static resetCache()
Reset the internal caching for unit testing.
static getMain()
Get the RequestContext object associated with the main request.
static isCreatableName( $name)
Usernames which fail to pass this function will be blocked from new account registrations, but may be used internally either by batch processes or by user accounts which have already been created.
Definition: User.php:1096
canLinkAccounts()
Determine whether accounts can be linked.
Interface for configuration instances.
Definition: Config.php:28
This represents additional user data requested on the account creation form.
const FAIL
Indicates that the authentication failed.
const TYPE_LINK
Provider can link to existing accounts elsewhere.
static callLegacyAuthPlugin( $method, array $params, $return=null)
This used to call a legacy AuthPlugin method, if necessary.
static factory(array $deltas)
static AuthManager null $instance
PrimaryAuthenticationProvider [] $primaryAuthenticationProviders
getAuthenticationProvider( $id)
Get a provider by ID.
static singleton()
Get the global AuthManager.
getAuthenticationRequestsInternal( $providerAction, array $options, array $providers, User $user=null)
Internal request lookup for self::getAuthenticationRequests.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
userExists( $username, $flags=User::READ_NORMAL)
Determine whether a username exists.
const ACTION_CHANGE
Change a user&#39;s credentials.
const SEC_FAIL
Security-sensitive should not be performed.
const AUTOCREATE_SOURCE_MAINT
Auto-creation is due to a Maintenance script.
const SEC_REAUTH
Security-sensitive operations should re-authenticate.
const AUTOCREATE_SOURCE_SESSION
Auto-creation is due to SessionManager.
const OPTIONAL
Indicates that the request is not required for authentication to proceed.
$cache
Definition: mcc.php:33
const IGNORE_USER_RIGHTS
Definition: User.php:83
getConfiguration()
Get the configuration.
const REQUIRED
Indicates that the request is required for authentication to proceed.
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition: User.php:1022
static runWithoutAbort( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:231
Returned from account creation to allow for logging into the created account.
This is an authentication request added by AuthManager to show a "remember me" checkbox.
const PASS
Indicates that the authentication succeeded.
This serves as the entry point to the authentication system.
Definition: AuthManager.php:85
canAuthenticateNow()
Indicate whether user authentication is possible.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:83
setLogger(LoggerInterface $logger)
AuthenticationRequest to ensure something with a username is present.
const TYPE_NONE
Provider cannot create or link to accounts.
const ACTION_LINK
Link an existing user to a third-party account.
Definition: AuthManager.php:99
checkAccountCreatePermissions(User $creator)
Basic permissions checks on whether a user can create accounts.
allowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
const REDIRECT
Indicates that the authentication needs to be redirected to a third party to proceed.
forcePrimaryAuthenticationProviders(array $providers, $why)
Force certain PrimaryAuthenticationProviders.
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:58
setAuthenticationSessionData( $key, $data)
Store authentication in the current session.
beginAccountCreation(User $creator, array $reqs, $returnToUrl)
Start an account creation flow.
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:560
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
callMethodOnProviders( $which, $method, array $args)
getId()
Get the user&#39;s ID.
Definition: User.php:2254
addToDatabase()
Add this existing user object to the database.
Definition: User.php:4177
revokeAccessForUser( $username)
Revoke any authentication credentials for a user.
getPrimaryAuthenticationProviders()
Get the list of PrimaryAuthenticationProviders.
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:91
const ACTION_REMOVE
Remove a user&#39;s credentials.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
autoCreateUser(User $user, $source, $login=true)
Auto-create an account, and log into that account.
getUserPage()
Get this user&#39;s personal page title.
Definition: User.php:4346
static invalidateAllPasswordsForUser( $username)
Invalidate all passwords for a user, by name.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
const ACTION_CREATE_CONTINUE
Continue a user creation process that was interrupted by the need for user input or communication wit...
Definition: AuthManager.php:97
continueAccountLink(array $reqs)
Continue an account linking flow.
setSessionDataForUser( $user, $remember=null)
Log the user in.
__construct(WebRequest $request, Config $config)
setDefaultUserOptions(User $user, $useContextLang)
const ACTION_CREATE
Create a new user.
Definition: AuthManager.php:93
providerArrayFromSpecs( $class, array $specs)
Create an array of AuthenticationProviders from an array of ObjectFactory specs.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
const SEC_OK
Security-sensitive operations are ok.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:536
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
Definition: AuthManager.php:87
const ACTION_LINK_CONTINUE
Continue a user linking process that was interrupted by the need for user input or communication with...
getAuthenticationSessionData( $key, $default=null)
Fetch authentication data from the current session.
fillRequests(array &$reqs, $action, $username, $forceAction=false)
Set values in an array of requests.
getPreAuthenticationProviders()
Get the list of PreAuthenticationProviders.
userCanAuthenticate( $username)
Determine whether a username can authenticate.
SecondaryAuthenticationProvider [] $secondaryAuthenticationProviders
This is a value object for authentication requests.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
normalizeUsername( $username)
Provide normalized versions of the username for security checks.
getSecondaryAuthenticationProviders()
Get the list of SecondaryAuthenticationProviders.
const UI
Indicates that the authentication needs further user input of some sort.