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  const ACTION_LOGIN = 'login';
91  const ACTION_LOGIN_CONTINUE = 'login-continue';
93  const ACTION_CREATE = 'create';
97  const ACTION_CREATE_CONTINUE = 'create-continue';
99  const ACTION_LINK = 'link';
103  const ACTION_LINK_CONTINUE = 'link-continue';
105  const ACTION_CHANGE = 'change';
107  const ACTION_REMOVE = 'remove';
109  const ACTION_UNLINK = 'unlink';
110 
112  const SEC_OK = 'ok';
114  const SEC_REAUTH = 'reauth';
116  const SEC_FAIL = 'fail';
117 
119  const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
120 
122  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( 'createaccount', $creator, 'secure' );
1001  if ( $permErrors ) {
1002  $status = Status::newGood();
1003  foreach ( $permErrors as $args ) {
1004  $status->fatal( ...$args );
1005  }
1006  return $status;
1007  }
1008 
1009  $block = $creator->isBlockedFromCreateAccount();
1010  if ( $block ) {
1011  $errorParams = [
1012  $block->getTarget(),
1013  $block->getReason() ?: wfMessage( 'blockednoreason' )->text(),
1014  $block->getByName()
1015  ];
1016 
1017  if ( $block->getType() === DatabaseBlock::TYPE_RANGE ) {
1018  $errorMessage = 'cantcreateaccount-range-text';
1019  $errorParams[] = $this->getRequest()->getIP();
1020  } else {
1021  $errorMessage = 'cantcreateaccount-text';
1022  }
1023 
1024  return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1025  }
1026 
1027  $ip = $this->getRequest()->getIP();
1028  if (
1029  MediaWikiServices::getInstance()->getBlockManager()
1030  ->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ )
1031  ) {
1032  return Status::newFatal( 'sorbs_create_account_reason' );
1033  }
1034 
1035  return Status::newGood();
1036  }
1037 
1057  public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1058  $session = $this->request->getSession();
1059  if ( !$this->canCreateAccounts() ) {
1060  // Caller should have called canCreateAccounts()
1061  $session->remove( 'AuthManager::accountCreationState' );
1062  throw new \LogicException( 'Account creation is not possible' );
1063  }
1064 
1065  try {
1067  } catch ( \UnexpectedValueException $ex ) {
1068  $username = null;
1069  }
1070  if ( $username === null ) {
1071  $this->logger->debug( __METHOD__ . ': No username provided' );
1072  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1073  }
1074 
1075  // Permissions check
1076  $status = $this->checkAccountCreatePermissions( $creator );
1077  if ( !$status->isGood() ) {
1078  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1079  'user' => $username,
1080  'creator' => $creator->getName(),
1081  'reason' => $status->getWikiText( null, null, 'en' )
1082  ] );
1083  return AuthenticationResponse::newFail( $status->getMessage() );
1084  }
1085 
1086  $status = $this->canCreateAccount(
1087  $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1088  );
1089  if ( !$status->isGood() ) {
1090  $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1091  'user' => $username,
1092  'creator' => $creator->getName(),
1093  'reason' => $status->getWikiText( null, null, 'en' )
1094  ] );
1095  return AuthenticationResponse::newFail( $status->getMessage() );
1096  }
1097 
1098  $user = User::newFromName( $username, 'creatable' );
1099  foreach ( $reqs as $req ) {
1100  $req->username = $username;
1101  $req->returnToUrl = $returnToUrl;
1102  if ( $req instanceof UserDataAuthenticationRequest ) {
1103  $status = $req->populateUser( $user );
1104  if ( !$status->isGood() ) {
1105  $status = Status::wrap( $status );
1106  $session->remove( 'AuthManager::accountCreationState' );
1107  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1108  'user' => $user->getName(),
1109  'creator' => $creator->getName(),
1110  'reason' => $status->getWikiText( null, null, 'en' ),
1111  ] );
1112  return AuthenticationResponse::newFail( $status->getMessage() );
1113  }
1114  }
1115  }
1116 
1117  $this->removeAuthenticationSessionData( null );
1118 
1119  $state = [
1120  'username' => $username,
1121  'userid' => 0,
1122  'creatorid' => $creator->getId(),
1123  'creatorname' => $creator->getName(),
1124  'reqs' => $reqs,
1125  'returnToUrl' => $returnToUrl,
1126  'primary' => null,
1127  'primaryResponse' => null,
1128  'secondary' => [],
1129  'continueRequests' => [],
1130  'maybeLink' => [],
1131  'ranPreTests' => false,
1132  ];
1133 
1134  // Special case: converting a login to an account creation
1136  $reqs, CreateFromLoginAuthenticationRequest::class
1137  );
1138  if ( $req ) {
1139  $state['maybeLink'] = $req->maybeLink;
1140 
1141  if ( $req->createRequest ) {
1142  $reqs[] = $req->createRequest;
1143  $state['reqs'][] = $req->createRequest;
1144  }
1145  }
1146 
1147  $session->setSecret( 'AuthManager::accountCreationState', $state );
1148  $session->persist();
1149 
1150  return $this->continueAccountCreation( $reqs );
1151  }
1152 
1158  public function continueAccountCreation( array $reqs ) {
1159  $session = $this->request->getSession();
1160  try {
1161  if ( !$this->canCreateAccounts() ) {
1162  // Caller should have called canCreateAccounts()
1163  $session->remove( 'AuthManager::accountCreationState' );
1164  throw new \LogicException( 'Account creation is not possible' );
1165  }
1166 
1167  $state = $session->getSecret( 'AuthManager::accountCreationState' );
1168  if ( !is_array( $state ) ) {
1170  wfMessage( 'authmanager-create-not-in-progress' )
1171  );
1172  }
1173  $state['continueRequests'] = [];
1174 
1175  // Step 0: Prepare and validate the input
1176 
1177  $user = User::newFromName( $state['username'], 'creatable' );
1178  if ( !is_object( $user ) ) {
1179  $session->remove( 'AuthManager::accountCreationState' );
1180  $this->logger->debug( __METHOD__ . ': Invalid username', [
1181  'user' => $state['username'],
1182  ] );
1183  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1184  }
1185 
1186  if ( $state['creatorid'] ) {
1187  $creator = User::newFromId( $state['creatorid'] );
1188  } else {
1189  $creator = new User;
1190  $creator->setName( $state['creatorname'] );
1191  }
1192 
1193  // Avoid account creation races on double submissions
1195  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1196  if ( !$lock ) {
1197  // Don't clear AuthManager::accountCreationState for this code
1198  // path because the process that won the race owns it.
1199  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1200  'user' => $user->getName(),
1201  'creator' => $creator->getName(),
1202  ] );
1203  return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1204  }
1205 
1206  // Permissions check
1207  $status = $this->checkAccountCreatePermissions( $creator );
1208  if ( !$status->isGood() ) {
1209  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1210  'user' => $user->getName(),
1211  'creator' => $creator->getName(),
1212  'reason' => $status->getWikiText( null, null, 'en' )
1213  ] );
1214  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1215  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1216  $session->remove( 'AuthManager::accountCreationState' );
1217  return $ret;
1218  }
1219 
1220  // Load from master for existence check
1221  $user->load( User::READ_LOCKING );
1222 
1223  if ( $state['userid'] === 0 ) {
1224  if ( $user->getId() !== 0 ) {
1225  $this->logger->debug( __METHOD__ . ': User exists locally', [
1226  'user' => $user->getName(),
1227  'creator' => $creator->getName(),
1228  ] );
1229  $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1230  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1231  $session->remove( 'AuthManager::accountCreationState' );
1232  return $ret;
1233  }
1234  } else {
1235  if ( $user->getId() === 0 ) {
1236  $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1237  'user' => $user->getName(),
1238  'creator' => $creator->getName(),
1239  'expected_id' => $state['userid'],
1240  ] );
1241  throw new \UnexpectedValueException(
1242  "User \"{$state['username']}\" should exist now, but doesn't!"
1243  );
1244  }
1245  if ( $user->getId() !== $state['userid'] ) {
1246  $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1247  'user' => $user->getName(),
1248  'creator' => $creator->getName(),
1249  'expected_id' => $state['userid'],
1250  'actual_id' => $user->getId(),
1251  ] );
1252  throw new \UnexpectedValueException(
1253  "User \"{$state['username']}\" exists, but " .
1254  "ID {$user->getId()} !== {$state['userid']}!"
1255  );
1256  }
1257  }
1258  foreach ( $state['reqs'] as $req ) {
1259  if ( $req instanceof UserDataAuthenticationRequest ) {
1260  $status = $req->populateUser( $user );
1261  if ( !$status->isGood() ) {
1262  // This should never happen...
1263  $status = Status::wrap( $status );
1264  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1265  'user' => $user->getName(),
1266  'creator' => $creator->getName(),
1267  'reason' => $status->getWikiText( null, null, 'en' ),
1268  ] );
1269  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1270  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1271  $session->remove( 'AuthManager::accountCreationState' );
1272  return $ret;
1273  }
1274  }
1275  }
1276 
1277  foreach ( $reqs as $req ) {
1278  $req->returnToUrl = $state['returnToUrl'];
1279  $req->username = $state['username'];
1280  }
1281 
1282  // Run pre-creation tests, if we haven't already
1283  if ( !$state['ranPreTests'] ) {
1284  $providers = $this->getPreAuthenticationProviders() +
1287  foreach ( $providers as $id => $provider ) {
1288  $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1289  if ( !$status->isGood() ) {
1290  $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1291  'user' => $user->getName(),
1292  'creator' => $creator->getName(),
1293  ] );
1295  Status::wrap( $status )->getMessage()
1296  );
1297  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1298  $session->remove( 'AuthManager::accountCreationState' );
1299  return $ret;
1300  }
1301  }
1302 
1303  $state['ranPreTests'] = true;
1304  }
1305 
1306  // Step 1: Choose a primary authentication provider and call it until it succeeds.
1307 
1308  if ( $state['primary'] === null ) {
1309  // We haven't picked a PrimaryAuthenticationProvider yet
1310  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1311  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1312  continue;
1313  }
1314  $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1315  switch ( $res->status ) {
1317  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1318  'user' => $user->getName(),
1319  'creator' => $creator->getName(),
1320  ] );
1321  $state['primary'] = $id;
1322  $state['primaryResponse'] = $res;
1323  break 2;
1325  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1326  'user' => $user->getName(),
1327  'creator' => $creator->getName(),
1328  ] );
1329  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1330  $session->remove( 'AuthManager::accountCreationState' );
1331  return $res;
1333  // Continue loop
1334  break;
1337  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1338  'user' => $user->getName(),
1339  'creator' => $creator->getName(),
1340  ] );
1341  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1342  $state['primary'] = $id;
1343  $state['continueRequests'] = $res->neededRequests;
1344  $session->setSecret( 'AuthManager::accountCreationState', $state );
1345  return $res;
1346 
1347  // @codeCoverageIgnoreStart
1348  default:
1349  throw new \DomainException(
1350  get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1351  );
1352  // @codeCoverageIgnoreEnd
1353  }
1354  }
1355  if ( $state['primary'] === null ) {
1356  $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1357  'user' => $user->getName(),
1358  'creator' => $creator->getName(),
1359  ] );
1361  wfMessage( 'authmanager-create-no-primary' )
1362  );
1363  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1364  $session->remove( 'AuthManager::accountCreationState' );
1365  return $ret;
1366  }
1367  } elseif ( $state['primaryResponse'] === null ) {
1368  $provider = $this->getAuthenticationProvider( $state['primary'] );
1369  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1370  // Configuration changed? Force them to start over.
1371  // @codeCoverageIgnoreStart
1373  wfMessage( 'authmanager-create-not-in-progress' )
1374  );
1375  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1376  $session->remove( 'AuthManager::accountCreationState' );
1377  return $ret;
1378  // @codeCoverageIgnoreEnd
1379  }
1380  $id = $provider->getUniqueId();
1381  $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1382  switch ( $res->status ) {
1384  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1385  'user' => $user->getName(),
1386  'creator' => $creator->getName(),
1387  ] );
1388  $state['primaryResponse'] = $res;
1389  break;
1391  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1392  'user' => $user->getName(),
1393  'creator' => $creator->getName(),
1394  ] );
1395  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1396  $session->remove( 'AuthManager::accountCreationState' );
1397  return $res;
1400  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1401  'user' => $user->getName(),
1402  'creator' => $creator->getName(),
1403  ] );
1404  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1405  $state['continueRequests'] = $res->neededRequests;
1406  $session->setSecret( 'AuthManager::accountCreationState', $state );
1407  return $res;
1408  default:
1409  throw new \DomainException(
1410  get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1411  );
1412  }
1413  }
1414 
1415  // Step 2: Primary authentication succeeded, create the User object
1416  // and add the user locally.
1417 
1418  if ( $state['userid'] === 0 ) {
1419  $this->logger->info( 'Creating user {user} during account creation', [
1420  'user' => $user->getName(),
1421  'creator' => $creator->getName(),
1422  ] );
1423  $status = $user->addToDatabase();
1424  if ( !$status->isOK() ) {
1425  // @codeCoverageIgnoreStart
1426  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1427  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1428  $session->remove( 'AuthManager::accountCreationState' );
1429  return $ret;
1430  // @codeCoverageIgnoreEnd
1431  }
1432  $this->setDefaultUserOptions( $user, $creator->isAnon() );
1433  \Hooks::runWithoutAbort( 'LocalUserCreated', [ $user, false ] );
1434  $user->saveSettings();
1435  $state['userid'] = $user->getId();
1436 
1437  // Update user count
1438  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1439 
1440  // Watch user's userpage and talk page
1441  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1442 
1443  // Inform the provider
1444  $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1445 
1446  // Log the creation
1447  if ( $this->config->get( 'NewUserLog' ) ) {
1448  $isAnon = $creator->isAnon();
1449  $logEntry = new \ManualLogEntry(
1450  'newusers',
1451  $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1452  );
1453  $logEntry->setPerformer( $isAnon ? $user : $creator );
1454  $logEntry->setTarget( $user->getUserPage() );
1457  $state['reqs'], CreationReasonAuthenticationRequest::class
1458  );
1459  $logEntry->setComment( $req ? $req->reason : '' );
1460  $logEntry->setParameters( [
1461  '4::userid' => $user->getId(),
1462  ] );
1463  $logid = $logEntry->insert();
1464  $logEntry->publish( $logid );
1465  }
1466  }
1467 
1468  // Step 3: Iterate over all the secondary authentication providers.
1469 
1470  $beginReqs = $state['reqs'];
1471 
1472  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1473  if ( !isset( $state['secondary'][$id] ) ) {
1474  // This provider isn't started yet, so we pass it the set
1475  // of reqs from beginAuthentication instead of whatever
1476  // might have been used by a previous provider in line.
1477  $func = 'beginSecondaryAccountCreation';
1478  $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1479  } elseif ( !$state['secondary'][$id] ) {
1480  $func = 'continueSecondaryAccountCreation';
1481  $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1482  } else {
1483  continue;
1484  }
1485  switch ( $res->status ) {
1487  $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1488  'user' => $user->getName(),
1489  'creator' => $creator->getName(),
1490  ] );
1491  // fall through
1493  $state['secondary'][$id] = true;
1494  break;
1497  $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1498  'user' => $user->getName(),
1499  'creator' => $creator->getName(),
1500  ] );
1501  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1502  $state['secondary'][$id] = false;
1503  $state['continueRequests'] = $res->neededRequests;
1504  $session->setSecret( 'AuthManager::accountCreationState', $state );
1505  return $res;
1507  throw new \DomainException(
1508  get_class( $provider ) . "::{$func}() returned $res->status." .
1509  ' Secondary providers are not allowed to fail account creation, that' .
1510  ' should have been done via testForAccountCreation().'
1511  );
1512  // @codeCoverageIgnoreStart
1513  default:
1514  throw new \DomainException(
1515  get_class( $provider ) . "::{$func}() returned $res->status"
1516  );
1517  // @codeCoverageIgnoreEnd
1518  }
1519  }
1520 
1521  $id = $user->getId();
1522  $name = $user->getName();
1523  $req = new CreatedAccountAuthenticationRequest( $id, $name );
1524  $ret = AuthenticationResponse::newPass( $name );
1525  $ret->loginRequest = $req;
1526  $this->createdAccountAuthenticationRequests[] = $req;
1527 
1528  $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1529  'user' => $user->getName(),
1530  'creator' => $creator->getName(),
1531  ] );
1532 
1533  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1534  $session->remove( 'AuthManager::accountCreationState' );
1535  $this->removeAuthenticationSessionData( null );
1536  return $ret;
1537  } catch ( \Exception $ex ) {
1538  $session->remove( 'AuthManager::accountCreationState' );
1539  throw $ex;
1540  }
1541  }
1542 
1560  public function autoCreateUser( User $user, $source, $login = true ) {
1561  if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1562  $source !== self::AUTOCREATE_SOURCE_MAINT &&
1564  ) {
1565  throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1566  }
1567 
1568  $username = $user->getName();
1569 
1570  // Try the local user from the replica DB
1571  $localId = User::idFromName( $username );
1572  $flags = User::READ_NORMAL;
1573 
1574  // Fetch the user ID from the master, so that we don't try to create the user
1575  // when they already exist, due to replication lag
1576  // @codeCoverageIgnoreStart
1577  if (
1578  !$localId &&
1579  MediaWikiServices::getInstance()->getDBLoadBalancer()->getReaderIndex() !== 0
1580  ) {
1581  $localId = User::idFromName( $username, User::READ_LATEST );
1582  $flags = User::READ_LATEST;
1583  }
1584  // @codeCoverageIgnoreEnd
1585 
1586  if ( $localId ) {
1587  $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1588  'username' => $username,
1589  ] );
1590  $user->setId( $localId );
1591  $user->loadFromId( $flags );
1592  if ( $login ) {
1593  $this->setSessionDataForUser( $user );
1594  }
1595  $status = Status::newGood();
1596  $status->warning( 'userexists' );
1597  return $status;
1598  }
1599 
1600  // Wiki is read-only?
1601  if ( wfReadOnly() ) {
1602  $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1603  'username' => $username,
1604  'reason' => wfReadOnlyReason(),
1605  ] );
1606  $user->setId( 0 );
1607  $user->loadFromId();
1608  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1609  }
1610 
1611  // Check the session, if we tried to create this user already there's
1612  // no point in retrying.
1613  $session = $this->request->getSession();
1614  if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1615  $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1616  'username' => $username,
1617  'sessionid' => $session->getId(),
1618  ] );
1619  $user->setId( 0 );
1620  $user->loadFromId();
1621  $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1622  if ( $reason instanceof StatusValue ) {
1623  return Status::wrap( $reason );
1624  } else {
1625  return Status::newFatal( $reason );
1626  }
1627  }
1628 
1629  // Is the username creatable?
1630  if ( !User::isCreatableName( $username ) ) {
1631  $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1632  'username' => $username,
1633  ] );
1634  $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1635  $user->setId( 0 );
1636  $user->loadFromId();
1637  return Status::newFatal( 'noname' );
1638  }
1639 
1640  // Is the IP user able to create accounts?
1641  $anon = new User;
1642  if ( $source !== self::AUTOCREATE_SOURCE_MAINT && !MediaWikiServices::getInstance()
1644  ->userHasAnyRight( $anon, 'createaccount', 'autocreateaccount' )
1645  ) {
1646  $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1647  'username' => $username,
1648  'ip' => $anon->getName(),
1649  ] );
1650  $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1651  $session->persist();
1652  $user->setId( 0 );
1653  $user->loadFromId();
1654  return Status::newFatal( 'authmanager-autocreate-noperm' );
1655  }
1656 
1657  // Avoid account creation races on double submissions
1659  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1660  if ( !$lock ) {
1661  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1662  'user' => $username,
1663  ] );
1664  $user->setId( 0 );
1665  $user->loadFromId();
1666  return Status::newFatal( 'usernameinprogress' );
1667  }
1668 
1669  // Denied by providers?
1670  $options = [
1671  'flags' => User::READ_LATEST,
1672  'creating' => true,
1673  ];
1674  $providers = $this->getPreAuthenticationProviders() +
1677  foreach ( $providers as $provider ) {
1678  $status = $provider->testUserForCreation( $user, $source, $options );
1679  if ( !$status->isGood() ) {
1680  $ret = Status::wrap( $status );
1681  $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1682  'username' => $username,
1683  'reason' => $ret->getWikiText( null, null, 'en' ),
1684  ] );
1685  $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1686  $user->setId( 0 );
1687  $user->loadFromId();
1688  return $ret;
1689  }
1690  }
1691 
1692  $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1693  if ( $cache->get( $backoffKey ) ) {
1694  $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1695  'username' => $username,
1696  ] );
1697  $user->setId( 0 );
1698  $user->loadFromId();
1699  return Status::newFatal( 'authmanager-autocreate-exception' );
1700  }
1701 
1702  // Checks passed, create the user...
1703  $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
1704  $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1705  'username' => $username,
1706  'from' => $from,
1707  ] );
1708 
1709  // Ignore warnings about master connections/writes...hard to avoid here
1710  $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1711  $old = $trxProfiler->setSilenced( true );
1712  try {
1713  $status = $user->addToDatabase();
1714  if ( !$status->isOK() ) {
1715  // Double-check for a race condition (T70012). We make use of the fact that when
1716  // addToDatabase fails due to the user already existing, the user object gets loaded.
1717  if ( $user->getId() ) {
1718  $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1719  'username' => $username,
1720  ] );
1721  if ( $login ) {
1722  $this->setSessionDataForUser( $user );
1723  }
1724  $status = Status::newGood();
1725  $status->warning( 'userexists' );
1726  } else {
1727  $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1728  'username' => $username,
1729  'msg' => $status->getWikiText( null, null, 'en' )
1730  ] );
1731  $user->setId( 0 );
1732  $user->loadFromId();
1733  }
1734  return $status;
1735  }
1736  } catch ( \Exception $ex ) {
1737  $trxProfiler->setSilenced( $old );
1738  $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1739  'username' => $username,
1740  'exception' => $ex,
1741  ] );
1742  // Do not keep throwing errors for a while
1743  $cache->set( $backoffKey, 1, 600 );
1744  // Bubble up error; which should normally trigger DB rollbacks
1745  throw $ex;
1746  }
1747 
1748  $this->setDefaultUserOptions( $user, false );
1749 
1750  // Inform the providers
1751  $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1752 
1753  \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1754  $user->saveSettings();
1755 
1756  // Update user count
1757  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1758  // Watch user's userpage and talk page
1759  \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1760  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1761  } );
1762 
1763  // Log the creation
1764  if ( $this->config->get( 'NewUserLog' ) ) {
1765  $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1766  $logEntry->setPerformer( $user );
1767  $logEntry->setTarget( $user->getUserPage() );
1768  $logEntry->setComment( '' );
1769  $logEntry->setParameters( [
1770  '4::userid' => $user->getId(),
1771  ] );
1772  $logEntry->insert();
1773  }
1774 
1775  $trxProfiler->setSilenced( $old );
1776 
1777  if ( $login ) {
1778  $this->setSessionDataForUser( $user );
1779  }
1780 
1781  return Status::newGood();
1782  }
1783 
1795  public function canLinkAccounts() {
1796  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1797  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1798  return true;
1799  }
1800  }
1801  return false;
1802  }
1803 
1813  public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1814  $session = $this->request->getSession();
1815  $session->remove( 'AuthManager::accountLinkState' );
1816 
1817  if ( !$this->canLinkAccounts() ) {
1818  // Caller should have called canLinkAccounts()
1819  throw new \LogicException( 'Account linking is not possible' );
1820  }
1821 
1822  if ( $user->getId() === 0 ) {
1823  if ( !User::isUsableName( $user->getName() ) ) {
1824  $msg = wfMessage( 'noname' );
1825  } else {
1826  $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1827  }
1828  return AuthenticationResponse::newFail( $msg );
1829  }
1830  foreach ( $reqs as $req ) {
1831  $req->username = $user->getName();
1832  $req->returnToUrl = $returnToUrl;
1833  }
1834 
1835  $this->removeAuthenticationSessionData( null );
1836 
1837  $providers = $this->getPreAuthenticationProviders();
1838  foreach ( $providers as $id => $provider ) {
1839  $status = $provider->testForAccountLink( $user );
1840  if ( !$status->isGood() ) {
1841  $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1842  'user' => $user->getName(),
1843  ] );
1845  Status::wrap( $status )->getMessage()
1846  );
1847  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1848  return $ret;
1849  }
1850  }
1851 
1852  $state = [
1853  'username' => $user->getName(),
1854  'userid' => $user->getId(),
1855  'returnToUrl' => $returnToUrl,
1856  'primary' => null,
1857  'continueRequests' => [],
1858  ];
1859 
1860  $providers = $this->getPrimaryAuthenticationProviders();
1861  foreach ( $providers as $id => $provider ) {
1862  if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1863  continue;
1864  }
1865 
1866  $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1867  switch ( $res->status ) {
1869  $this->logger->info( "Account linked to {user} by $id", [
1870  'user' => $user->getName(),
1871  ] );
1872  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1873  return $res;
1874 
1876  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1877  'user' => $user->getName(),
1878  ] );
1879  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1880  return $res;
1881 
1883  // Continue loop
1884  break;
1885 
1888  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1889  'user' => $user->getName(),
1890  ] );
1891  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1892  $state['primary'] = $id;
1893  $state['continueRequests'] = $res->neededRequests;
1894  $session->setSecret( 'AuthManager::accountLinkState', $state );
1895  $session->persist();
1896  return $res;
1897 
1898  // @codeCoverageIgnoreStart
1899  default:
1900  throw new \DomainException(
1901  get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1902  );
1903  // @codeCoverageIgnoreEnd
1904  }
1905  }
1906 
1907  $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1908  'user' => $user->getName(),
1909  ] );
1911  wfMessage( 'authmanager-link-no-primary' )
1912  );
1913  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1914  return $ret;
1915  }
1916 
1922  public function continueAccountLink( array $reqs ) {
1923  $session = $this->request->getSession();
1924  try {
1925  if ( !$this->canLinkAccounts() ) {
1926  // Caller should have called canLinkAccounts()
1927  $session->remove( 'AuthManager::accountLinkState' );
1928  throw new \LogicException( 'Account linking is not possible' );
1929  }
1930 
1931  $state = $session->getSecret( 'AuthManager::accountLinkState' );
1932  if ( !is_array( $state ) ) {
1934  wfMessage( 'authmanager-link-not-in-progress' )
1935  );
1936  }
1937  $state['continueRequests'] = [];
1938 
1939  // Step 0: Prepare and validate the input
1940 
1941  $user = User::newFromName( $state['username'], 'usable' );
1942  if ( !is_object( $user ) ) {
1943  $session->remove( 'AuthManager::accountLinkState' );
1944  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1945  }
1946  if ( $user->getId() !== $state['userid'] ) {
1947  throw new \UnexpectedValueException(
1948  "User \"{$state['username']}\" is valid, but " .
1949  "ID {$user->getId()} !== {$state['userid']}!"
1950  );
1951  }
1952 
1953  foreach ( $reqs as $req ) {
1954  $req->username = $state['username'];
1955  $req->returnToUrl = $state['returnToUrl'];
1956  }
1957 
1958  // Step 1: Call the primary again until it succeeds
1959 
1960  $provider = $this->getAuthenticationProvider( $state['primary'] );
1961  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1962  // Configuration changed? Force them to start over.
1963  // @codeCoverageIgnoreStart
1965  wfMessage( 'authmanager-link-not-in-progress' )
1966  );
1967  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1968  $session->remove( 'AuthManager::accountLinkState' );
1969  return $ret;
1970  // @codeCoverageIgnoreEnd
1971  }
1972  $id = $provider->getUniqueId();
1973  $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1974  switch ( $res->status ) {
1976  $this->logger->info( "Account linked to {user} by $id", [
1977  'user' => $user->getName(),
1978  ] );
1979  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1980  $session->remove( 'AuthManager::accountLinkState' );
1981  return $res;
1983  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1984  'user' => $user->getName(),
1985  ] );
1986  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1987  $session->remove( 'AuthManager::accountLinkState' );
1988  return $res;
1991  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1992  'user' => $user->getName(),
1993  ] );
1994  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1995  $state['continueRequests'] = $res->neededRequests;
1996  $session->setSecret( 'AuthManager::accountLinkState', $state );
1997  return $res;
1998  default:
1999  throw new \DomainException(
2000  get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
2001  );
2002  }
2003  } catch ( \Exception $ex ) {
2004  $session->remove( 'AuthManager::accountLinkState' );
2005  throw $ex;
2006  }
2007  }
2008 
2034  public function getAuthenticationRequests( $action, User $user = null ) {
2035  $options = [];
2036  $providerAction = $action;
2037 
2038  // Figure out which providers to query
2039  switch ( $action ) {
2040  case self::ACTION_LOGIN:
2041  case self::ACTION_CREATE:
2042  $providers = $this->getPreAuthenticationProviders() +
2045  break;
2046 
2047  case self::ACTION_LOGIN_CONTINUE:
2048  $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2049  return is_array( $state ) ? $state['continueRequests'] : [];
2050 
2051  case self::ACTION_CREATE_CONTINUE:
2052  $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2053  return is_array( $state ) ? $state['continueRequests'] : [];
2054 
2055  case self::ACTION_LINK:
2056  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2057  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2058  } );
2059  break;
2060 
2061  case self::ACTION_UNLINK:
2062  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2063  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2064  } );
2065 
2066  // To providers, unlink and remove are identical.
2067  $providerAction = self::ACTION_REMOVE;
2068  break;
2069 
2070  case self::ACTION_LINK_CONTINUE:
2071  $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2072  return is_array( $state ) ? $state['continueRequests'] : [];
2073 
2074  case self::ACTION_CHANGE:
2075  case self::ACTION_REMOVE:
2076  $providers = $this->getPrimaryAuthenticationProviders() +
2078  break;
2079 
2080  // @codeCoverageIgnoreStart
2081  default:
2082  throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2083  }
2084  // @codeCoverageIgnoreEnd
2085 
2086  return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2087  }
2088 
2099  $providerAction, array $options, array $providers, User $user = null
2100  ) {
2101  $user = $user ?: \RequestContext::getMain()->getUser();
2102  $options['username'] = $user->isAnon() ? null : $user->getName();
2103 
2104  // Query them and merge results
2105  $reqs = [];
2106  foreach ( $providers as $provider ) {
2107  $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2108  foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2109  $id = $req->getUniqueId();
2110 
2111  // If a required request if from a Primary, mark it as "primary-required" instead
2112  if ( $isPrimary && $req->required ) {
2113  $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2114  }
2115 
2116  if (
2117  !isset( $reqs[$id] )
2118  || $req->required === AuthenticationRequest::REQUIRED
2119  || $reqs[$id] === AuthenticationRequest::OPTIONAL
2120  ) {
2121  $reqs[$id] = $req;
2122  }
2123  }
2124  }
2125 
2126  // AuthManager has its own req for some actions
2127  switch ( $providerAction ) {
2128  case self::ACTION_LOGIN:
2129  $reqs[] = new RememberMeAuthenticationRequest;
2130  break;
2131 
2132  case self::ACTION_CREATE:
2133  $reqs[] = new UsernameAuthenticationRequest;
2134  $reqs[] = new UserDataAuthenticationRequest;
2135  if ( $options['username'] !== null ) {
2137  $options['username'] = null; // Don't fill in the username below
2138  }
2139  break;
2140  }
2141 
2142  // Fill in reqs data
2143  $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2144 
2145  // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2146  if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2147  $reqs = array_filter( $reqs, function ( $req ) {
2148  return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2149  } );
2150  }
2151 
2152  return array_values( $reqs );
2153  }
2154 
2162  private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2163  foreach ( $reqs as $req ) {
2164  if ( !$req->action || $forceAction ) {
2165  $req->action = $action;
2166  }
2167  if ( $req->username === null ) {
2168  $req->username = $username;
2169  }
2170  }
2171  }
2172 
2179  public function userExists( $username, $flags = User::READ_NORMAL ) {
2180  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2181  if ( $provider->testUserExists( $username, $flags ) ) {
2182  return true;
2183  }
2184  }
2185 
2186  return false;
2187  }
2188 
2200  public function allowsPropertyChange( $property ) {
2201  $providers = $this->getPrimaryAuthenticationProviders() +
2203  foreach ( $providers as $provider ) {
2204  if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2205  return false;
2206  }
2207  }
2208  return true;
2209  }
2210 
2219  public function getAuthenticationProvider( $id ) {
2220  // Fast version
2221  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2222  return $this->allAuthenticationProviders[$id];
2223  }
2224 
2225  // Slow version: instantiate each kind and check
2226  $providers = $this->getPrimaryAuthenticationProviders();
2227  if ( isset( $providers[$id] ) ) {
2228  return $providers[$id];
2229  }
2230  $providers = $this->getSecondaryAuthenticationProviders();
2231  if ( isset( $providers[$id] ) ) {
2232  return $providers[$id];
2233  }
2234  $providers = $this->getPreAuthenticationProviders();
2235  if ( isset( $providers[$id] ) ) {
2236  return $providers[$id];
2237  }
2238 
2239  return null;
2240  }
2241 
2255  public function setAuthenticationSessionData( $key, $data ) {
2256  $session = $this->request->getSession();
2257  $arr = $session->getSecret( 'authData' );
2258  if ( !is_array( $arr ) ) {
2259  $arr = [];
2260  }
2261  $arr[$key] = $data;
2262  $session->setSecret( 'authData', $arr );
2263  }
2264 
2272  public function getAuthenticationSessionData( $key, $default = null ) {
2273  $arr = $this->request->getSession()->getSecret( 'authData' );
2274  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2275  return $arr[$key];
2276  } else {
2277  return $default;
2278  }
2279  }
2280 
2286  public function removeAuthenticationSessionData( $key ) {
2287  $session = $this->request->getSession();
2288  if ( $key === null ) {
2289  $session->remove( 'authData' );
2290  } else {
2291  $arr = $session->getSecret( 'authData' );
2292  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2293  unset( $arr[$key] );
2294  $session->setSecret( 'authData', $arr );
2295  }
2296  }
2297  }
2298 
2305  protected function providerArrayFromSpecs( $class, array $specs ) {
2306  $i = 0;
2307  foreach ( $specs as &$spec ) {
2308  $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2309  }
2310  unset( $spec );
2311  // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2312  usort( $specs, function ( $a, $b ) {
2313  return $a['sort'] <=> $b['sort']
2314  ?: $a['sort2'] <=> $b['sort2'];
2315  } );
2316 
2317  $ret = [];
2318  foreach ( $specs as $spec ) {
2319  $provider = ObjectFactory::getObjectFromSpec( $spec );
2320  if ( !$provider instanceof $class ) {
2321  throw new \RuntimeException(
2322  "Expected instance of $class, got " . get_class( $provider )
2323  );
2324  }
2325  $provider->setLogger( $this->logger );
2326  $provider->setManager( $this );
2327  $provider->setConfig( $this->config );
2328  $id = $provider->getUniqueId();
2329  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2330  throw new \RuntimeException(
2331  "Duplicate specifications for id $id (classes " .
2332  get_class( $provider ) . ' and ' .
2333  get_class( $this->allAuthenticationProviders[$id] ) . ')'
2334  );
2335  }
2336  $this->allAuthenticationProviders[$id] = $provider;
2337  $ret[$id] = $provider;
2338  }
2339  return $ret;
2340  }
2341 
2346  private function getConfiguration() {
2347  return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2348  }
2349 
2354  protected function getPreAuthenticationProviders() {
2355  if ( $this->preAuthenticationProviders === null ) {
2356  $conf = $this->getConfiguration();
2357  $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2358  PreAuthenticationProvider::class, $conf['preauth']
2359  );
2360  }
2362  }
2363 
2368  protected function getPrimaryAuthenticationProviders() {
2369  if ( $this->primaryAuthenticationProviders === null ) {
2370  $conf = $this->getConfiguration();
2371  $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2372  PrimaryAuthenticationProvider::class, $conf['primaryauth']
2373  );
2374  }
2376  }
2377 
2383  if ( $this->secondaryAuthenticationProviders === null ) {
2384  $conf = $this->getConfiguration();
2385  $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2386  SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2387  );
2388  }
2390  }
2391 
2397  private function setSessionDataForUser( $user, $remember = null ) {
2398  $session = $this->request->getSession();
2399  $delay = $session->delaySave();
2400 
2401  $session->resetId();
2402  $session->resetAllTokens();
2403  if ( $session->canSetUser() ) {
2404  $session->setUser( $user );
2405  }
2406  if ( $remember !== null ) {
2407  $session->setRememberUser( $remember );
2408  }
2409  $session->set( 'AuthManager:lastAuthId', $user->getId() );
2410  $session->set( 'AuthManager:lastAuthTimestamp', time() );
2411  $session->persist();
2412 
2413  \Wikimedia\ScopedCallback::consume( $delay );
2414 
2415  \Hooks::run( 'UserLoggedIn', [ $user ] );
2416  }
2417 
2422  private function setDefaultUserOptions( User $user, $useContextLang ) {
2423  $user->setToken();
2424 
2425  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2426 
2427  $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $contLang;
2428  $user->setOption( 'language', $lang->getPreferredVariant() );
2429 
2430  if ( $contLang->hasVariants() ) {
2431  $user->setOption( 'variant', $contLang->getPreferredVariant() );
2432  }
2433  }
2434 
2440  private function callMethodOnProviders( $which, $method, array $args ) {
2441  $providers = [];
2442  if ( $which & 1 ) {
2443  $providers += $this->getPreAuthenticationProviders();
2444  }
2445  if ( $which & 2 ) {
2446  $providers += $this->getPrimaryAuthenticationProviders();
2447  }
2448  if ( $which & 4 ) {
2449  $providers += $this->getSecondaryAuthenticationProviders();
2450  }
2451  foreach ( $providers as $provider ) {
2452  $provider->$method( ...$args );
2453  }
2454  }
2455 
2460  public static function resetCache() {
2461  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2462  // @codeCoverageIgnoreStart
2463  throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2464  // @codeCoverageIgnoreEnd
2465  }
2466 
2467  self::$instance = null;
2468  }
2469 
2472 }
2473 
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:3659
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:3918
setId( $v)
Set the user and reload all fields according to a given ID.
Definition: User.php:2220
static instance()
Singleton.
Definition: Profiler.php:63
if(!isset( $args[0])) $lang
setName( $str)
Set the user name.
Definition: User.php:2257
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:2782
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:829
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:3002
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:2229
PreAuthenticationProvider [] $preAuthenticationProviders
CreatedAccountAuthenticationRequest [] $createdAccountAuthenticationRequests
A primary authentication provider is responsible for associating the submitted authentication data wi...
if( $line===false) $args
Definition: cdb.php:64
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.
$last
getPermissionManager()
loadFromId( $flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition: User.php:388
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition: User.php:4219
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:1041
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:966
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:55
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:539
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:2200
addToDatabase()
Add this existing user object to the database.
Definition: User.php:4103
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:4272
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:515
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.