MediaWiki  master
AuthManager.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Auth;
25 
26 use Config;
31 use Status;
33 use User;
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 
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
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  ] );
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 
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
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
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  ] );
696  );
697  $this->setSessionDataForUser( $user, $req && $req->rememberMe );
699  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
700  $session->remove( 'AuthManager::authnState' );
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 ) {
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 
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
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...
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() );
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 );
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' );
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  }
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 &&
1643  !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
1644  ) {
1645  $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1646  'username' => $username,
1647  'ip' => $anon->getName(),
1648  ] );
1649  $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1650  $session->persist();
1651  $user->setId( 0 );
1652  $user->loadFromId();
1653  return Status::newFatal( 'authmanager-autocreate-noperm' );
1654  }
1655 
1656  // Avoid account creation races on double submissions
1658  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1659  if ( !$lock ) {
1660  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1661  'user' => $username,
1662  ] );
1663  $user->setId( 0 );
1664  $user->loadFromId();
1665  return Status::newFatal( 'usernameinprogress' );
1666  }
1667 
1668  // Denied by providers?
1669  $options = [
1670  'flags' => User::READ_LATEST,
1671  'creating' => true,
1672  ];
1673  $providers = $this->getPreAuthenticationProviders() +
1676  foreach ( $providers as $provider ) {
1677  $status = $provider->testUserForCreation( $user, $source, $options );
1678  if ( !$status->isGood() ) {
1679  $ret = Status::wrap( $status );
1680  $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1681  'username' => $username,
1682  'reason' => $ret->getWikiText( null, null, 'en' ),
1683  ] );
1684  $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1685  $user->setId( 0 );
1686  $user->loadFromId();
1687  return $ret;
1688  }
1689  }
1690 
1691  $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1692  if ( $cache->get( $backoffKey ) ) {
1693  $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1694  'username' => $username,
1695  ] );
1696  $user->setId( 0 );
1697  $user->loadFromId();
1698  return Status::newFatal( 'authmanager-autocreate-exception' );
1699  }
1700 
1701  // Checks passed, create the user...
1702  $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
1703  $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1704  'username' => $username,
1705  'from' => $from,
1706  ] );
1707 
1708  // Ignore warnings about master connections/writes...hard to avoid here
1709  $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1710  $old = $trxProfiler->setSilenced( true );
1711  try {
1712  $status = $user->addToDatabase();
1713  if ( !$status->isOK() ) {
1714  // Double-check for a race condition (T70012). We make use of the fact that when
1715  // addToDatabase fails due to the user already existing, the user object gets loaded.
1716  if ( $user->getId() ) {
1717  $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1718  'username' => $username,
1719  ] );
1720  if ( $login ) {
1721  $this->setSessionDataForUser( $user );
1722  }
1724  $status->warning( 'userexists' );
1725  } else {
1726  $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1727  'username' => $username,
1728  'msg' => $status->getWikiText( null, null, 'en' )
1729  ] );
1730  $user->setId( 0 );
1731  $user->loadFromId();
1732  }
1733  return $status;
1734  }
1735  } catch ( \Exception $ex ) {
1736  $trxProfiler->setSilenced( $old );
1737  $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1738  'username' => $username,
1739  'exception' => $ex,
1740  ] );
1741  // Do not keep throwing errors for a while
1742  $cache->set( $backoffKey, 1, 600 );
1743  // Bubble up error; which should normally trigger DB rollbacks
1744  throw $ex;
1745  }
1746 
1747  $this->setDefaultUserOptions( $user, false );
1748 
1749  // Inform the providers
1750  $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1751 
1752  \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1753  $user->saveSettings();
1754 
1755  // Update user count
1756  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1757  // Watch user's userpage and talk page
1758  \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1759  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1760  } );
1761 
1762  // Log the creation
1763  if ( $this->config->get( 'NewUserLog' ) ) {
1764  $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1765  $logEntry->setPerformer( $user );
1766  $logEntry->setTarget( $user->getUserPage() );
1767  $logEntry->setComment( '' );
1768  $logEntry->setParameters( [
1769  '4::userid' => $user->getId(),
1770  ] );
1771  $logEntry->insert();
1772  }
1773 
1774  $trxProfiler->setSilenced( $old );
1775 
1776  if ( $login ) {
1777  $this->setSessionDataForUser( $user );
1778  }
1779 
1780  return Status::newGood();
1781  }
1782 
1794  public function canLinkAccounts() {
1795  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1796  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1797  return true;
1798  }
1799  }
1800  return false;
1801  }
1802 
1812  public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1813  $session = $this->request->getSession();
1814  $session->remove( 'AuthManager::accountLinkState' );
1815 
1816  if ( !$this->canLinkAccounts() ) {
1817  // Caller should have called canLinkAccounts()
1818  throw new \LogicException( 'Account linking is not possible' );
1819  }
1820 
1821  if ( $user->getId() === 0 ) {
1822  if ( !User::isUsableName( $user->getName() ) ) {
1823  $msg = wfMessage( 'noname' );
1824  } else {
1825  $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1826  }
1827  return AuthenticationResponse::newFail( $msg );
1828  }
1829  foreach ( $reqs as $req ) {
1830  $req->username = $user->getName();
1831  $req->returnToUrl = $returnToUrl;
1832  }
1833 
1835 
1836  $providers = $this->getPreAuthenticationProviders();
1837  foreach ( $providers as $id => $provider ) {
1838  $status = $provider->testForAccountLink( $user );
1839  if ( !$status->isGood() ) {
1840  $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1841  'user' => $user->getName(),
1842  ] );
1844  Status::wrap( $status )->getMessage()
1845  );
1846  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1847  return $ret;
1848  }
1849  }
1850 
1851  $state = [
1852  'username' => $user->getName(),
1853  'userid' => $user->getId(),
1854  'returnToUrl' => $returnToUrl,
1855  'primary' => null,
1856  'continueRequests' => [],
1857  ];
1858 
1859  $providers = $this->getPrimaryAuthenticationProviders();
1860  foreach ( $providers as $id => $provider ) {
1861  if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1862  continue;
1863  }
1864 
1865  $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1866  switch ( $res->status ) {
1868  $this->logger->info( "Account linked to {user} by $id", [
1869  'user' => $user->getName(),
1870  ] );
1871  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1872  return $res;
1873 
1875  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1876  'user' => $user->getName(),
1877  ] );
1878  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1879  return $res;
1880 
1882  // Continue loop
1883  break;
1884 
1887  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1888  'user' => $user->getName(),
1889  ] );
1890  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1891  $state['primary'] = $id;
1892  $state['continueRequests'] = $res->neededRequests;
1893  $session->setSecret( 'AuthManager::accountLinkState', $state );
1894  $session->persist();
1895  return $res;
1896 
1897  // @codeCoverageIgnoreStart
1898  default:
1899  throw new \DomainException(
1900  get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1901  );
1902  // @codeCoverageIgnoreEnd
1903  }
1904  }
1905 
1906  $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1907  'user' => $user->getName(),
1908  ] );
1910  wfMessage( 'authmanager-link-no-primary' )
1911  );
1912  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1913  return $ret;
1914  }
1915 
1921  public function continueAccountLink( array $reqs ) {
1922  $session = $this->request->getSession();
1923  try {
1924  if ( !$this->canLinkAccounts() ) {
1925  // Caller should have called canLinkAccounts()
1926  $session->remove( 'AuthManager::accountLinkState' );
1927  throw new \LogicException( 'Account linking is not possible' );
1928  }
1929 
1930  $state = $session->getSecret( 'AuthManager::accountLinkState' );
1931  if ( !is_array( $state ) ) {
1933  wfMessage( 'authmanager-link-not-in-progress' )
1934  );
1935  }
1936  $state['continueRequests'] = [];
1937 
1938  // Step 0: Prepare and validate the input
1939 
1940  $user = User::newFromName( $state['username'], 'usable' );
1941  if ( !is_object( $user ) ) {
1942  $session->remove( 'AuthManager::accountLinkState' );
1943  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1944  }
1945  if ( $user->getId() !== $state['userid'] ) {
1946  throw new \UnexpectedValueException(
1947  "User \"{$state['username']}\" is valid, but " .
1948  "ID {$user->getId()} !== {$state['userid']}!"
1949  );
1950  }
1951 
1952  foreach ( $reqs as $req ) {
1953  $req->username = $state['username'];
1954  $req->returnToUrl = $state['returnToUrl'];
1955  }
1956 
1957  // Step 1: Call the primary again until it succeeds
1958 
1959  $provider = $this->getAuthenticationProvider( $state['primary'] );
1960  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1961  // Configuration changed? Force them to start over.
1962  // @codeCoverageIgnoreStart
1964  wfMessage( 'authmanager-link-not-in-progress' )
1965  );
1966  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1967  $session->remove( 'AuthManager::accountLinkState' );
1968  return $ret;
1969  // @codeCoverageIgnoreEnd
1970  }
1971  $id = $provider->getUniqueId();
1972  $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1973  switch ( $res->status ) {
1975  $this->logger->info( "Account linked to {user} by $id", [
1976  'user' => $user->getName(),
1977  ] );
1978  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1979  $session->remove( 'AuthManager::accountLinkState' );
1980  return $res;
1982  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1983  'user' => $user->getName(),
1984  ] );
1985  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1986  $session->remove( 'AuthManager::accountLinkState' );
1987  return $res;
1990  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1991  'user' => $user->getName(),
1992  ] );
1993  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1994  $state['continueRequests'] = $res->neededRequests;
1995  $session->setSecret( 'AuthManager::accountLinkState', $state );
1996  return $res;
1997  default:
1998  throw new \DomainException(
1999  get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
2000  );
2001  }
2002  } catch ( \Exception $ex ) {
2003  $session->remove( 'AuthManager::accountLinkState' );
2004  throw $ex;
2005  }
2006  }
2007 
2034  $options = [];
2035  $providerAction = $action;
2036 
2037  // Figure out which providers to query
2038  switch ( $action ) {
2039  case self::ACTION_LOGIN:
2040  case self::ACTION_CREATE:
2041  $providers = $this->getPreAuthenticationProviders() +
2044  break;
2045 
2046  case self::ACTION_LOGIN_CONTINUE:
2047  $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2048  return is_array( $state ) ? $state['continueRequests'] : [];
2049 
2050  case self::ACTION_CREATE_CONTINUE:
2051  $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2052  return is_array( $state ) ? $state['continueRequests'] : [];
2053 
2054  case self::ACTION_LINK:
2055  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2056  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2057  } );
2058  break;
2059 
2060  case self::ACTION_UNLINK:
2061  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2062  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2063  } );
2064 
2065  // To providers, unlink and remove are identical.
2066  $providerAction = self::ACTION_REMOVE;
2067  break;
2068 
2069  case self::ACTION_LINK_CONTINUE:
2070  $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2071  return is_array( $state ) ? $state['continueRequests'] : [];
2072 
2073  case self::ACTION_CHANGE:
2074  case self::ACTION_REMOVE:
2075  $providers = $this->getPrimaryAuthenticationProviders() +
2077  break;
2078 
2079  // @codeCoverageIgnoreStart
2080  default:
2081  throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2082  }
2083  // @codeCoverageIgnoreEnd
2084 
2085  return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2086  }
2087 
2098  $providerAction, array $options, array $providers, User $user = null
2099  ) {
2100  $user = $user ?: \RequestContext::getMain()->getUser();
2101  $options['username'] = $user->isAnon() ? null : $user->getName();
2102 
2103  // Query them and merge results
2104  $reqs = [];
2105  foreach ( $providers as $provider ) {
2106  $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2107  foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2108  $id = $req->getUniqueId();
2109 
2110  // If a required request if from a Primary, mark it as "primary-required" instead
2111  if ( $isPrimary && $req->required ) {
2113  }
2114 
2115  if (
2116  !isset( $reqs[$id] )
2117  || $req->required === AuthenticationRequest::REQUIRED
2118  || $reqs[$id] === AuthenticationRequest::OPTIONAL
2119  ) {
2120  $reqs[$id] = $req;
2121  }
2122  }
2123  }
2124 
2125  // AuthManager has its own req for some actions
2126  switch ( $providerAction ) {
2127  case self::ACTION_LOGIN:
2128  $reqs[] = new RememberMeAuthenticationRequest;
2129  break;
2130 
2131  case self::ACTION_CREATE:
2132  $reqs[] = new UsernameAuthenticationRequest;
2133  $reqs[] = new UserDataAuthenticationRequest;
2134  if ( $options['username'] !== null ) {
2136  $options['username'] = null; // Don't fill in the username below
2137  }
2138  break;
2139  }
2140 
2141  // Fill in reqs data
2142  $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2143 
2144  // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2145  if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2146  $reqs = array_filter( $reqs, function ( $req ) {
2147  return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2148  } );
2149  }
2150 
2151  return array_values( $reqs );
2152  }
2153 
2161  private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2162  foreach ( $reqs as $req ) {
2163  if ( !$req->action || $forceAction ) {
2164  $req->action = $action;
2165  }
2166  if ( $req->username === null ) {
2167  $req->username = $username;
2168  }
2169  }
2170  }
2171 
2178  public function userExists( $username, $flags = User::READ_NORMAL ) {
2179  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2180  if ( $provider->testUserExists( $username, $flags ) ) {
2181  return true;
2182  }
2183  }
2184 
2185  return false;
2186  }
2187 
2199  public function allowsPropertyChange( $property ) {
2200  $providers = $this->getPrimaryAuthenticationProviders() +
2202  foreach ( $providers as $provider ) {
2203  if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2204  return false;
2205  }
2206  }
2207  return true;
2208  }
2209 
2218  public function getAuthenticationProvider( $id ) {
2219  // Fast version
2220  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2221  return $this->allAuthenticationProviders[$id];
2222  }
2223 
2224  // Slow version: instantiate each kind and check
2225  $providers = $this->getPrimaryAuthenticationProviders();
2226  if ( isset( $providers[$id] ) ) {
2227  return $providers[$id];
2228  }
2229  $providers = $this->getSecondaryAuthenticationProviders();
2230  if ( isset( $providers[$id] ) ) {
2231  return $providers[$id];
2232  }
2233  $providers = $this->getPreAuthenticationProviders();
2234  if ( isset( $providers[$id] ) ) {
2235  return $providers[$id];
2236  }
2237 
2238  return null;
2239  }
2240 
2254  public function setAuthenticationSessionData( $key, $data ) {
2255  $session = $this->request->getSession();
2256  $arr = $session->getSecret( 'authData' );
2257  if ( !is_array( $arr ) ) {
2258  $arr = [];
2259  }
2260  $arr[$key] = $data;
2261  $session->setSecret( 'authData', $arr );
2262  }
2263 
2271  public function getAuthenticationSessionData( $key, $default = null ) {
2272  $arr = $this->request->getSession()->getSecret( 'authData' );
2273  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2274  return $arr[$key];
2275  } else {
2276  return $default;
2277  }
2278  }
2279 
2285  public function removeAuthenticationSessionData( $key ) {
2286  $session = $this->request->getSession();
2287  if ( $key === null ) {
2288  $session->remove( 'authData' );
2289  } else {
2290  $arr = $session->getSecret( 'authData' );
2291  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2292  unset( $arr[$key] );
2293  $session->setSecret( 'authData', $arr );
2294  }
2295  }
2296  }
2297 
2304  protected function providerArrayFromSpecs( $class, array $specs ) {
2305  $i = 0;
2306  foreach ( $specs as &$spec ) {
2307  $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2308  }
2309  unset( $spec );
2310  // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2311  usort( $specs, function ( $a, $b ) {
2312  return $a['sort'] <=> $b['sort']
2313  ?: $a['sort2'] <=> $b['sort2'];
2314  } );
2315 
2316  $ret = [];
2317  foreach ( $specs as $spec ) {
2318  $provider = ObjectFactory::getObjectFromSpec( $spec );
2319  if ( !$provider instanceof $class ) {
2320  throw new \RuntimeException(
2321  "Expected instance of $class, got " . get_class( $provider )
2322  );
2323  }
2324  $provider->setLogger( $this->logger );
2325  $provider->setManager( $this );
2326  $provider->setConfig( $this->config );
2327  $id = $provider->getUniqueId();
2328  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2329  throw new \RuntimeException(
2330  "Duplicate specifications for id $id (classes " .
2331  get_class( $provider ) . ' and ' .
2332  get_class( $this->allAuthenticationProviders[$id] ) . ')'
2333  );
2334  }
2335  $this->allAuthenticationProviders[$id] = $provider;
2336  $ret[$id] = $provider;
2337  }
2338  return $ret;
2339  }
2340 
2345  private function getConfiguration() {
2346  return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2347  }
2348 
2353  protected function getPreAuthenticationProviders() {
2354  if ( $this->preAuthenticationProviders === null ) {
2355  $conf = $this->getConfiguration();
2356  $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2357  PreAuthenticationProvider::class, $conf['preauth']
2358  );
2359  }
2361  }
2362 
2367  protected function getPrimaryAuthenticationProviders() {
2368  if ( $this->primaryAuthenticationProviders === null ) {
2369  $conf = $this->getConfiguration();
2370  $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2371  PrimaryAuthenticationProvider::class, $conf['primaryauth']
2372  );
2373  }
2375  }
2376 
2382  if ( $this->secondaryAuthenticationProviders === null ) {
2383  $conf = $this->getConfiguration();
2384  $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2385  SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2386  );
2387  }
2389  }
2390 
2396  private function setSessionDataForUser( $user, $remember = null ) {
2397  $session = $this->request->getSession();
2398  $delay = $session->delaySave();
2399 
2400  $session->resetId();
2401  $session->resetAllTokens();
2402  if ( $session->canSetUser() ) {
2403  $session->setUser( $user );
2404  }
2405  if ( $remember !== null ) {
2406  $session->setRememberUser( $remember );
2407  }
2408  $session->set( 'AuthManager:lastAuthId', $user->getId() );
2409  $session->set( 'AuthManager:lastAuthTimestamp', time() );
2410  $session->persist();
2411 
2412  \Wikimedia\ScopedCallback::consume( $delay );
2413 
2414  \Hooks::run( 'UserLoggedIn', [ $user ] );
2415  }
2416 
2421  private function setDefaultUserOptions( User $user, $useContextLang ) {
2422  $user->setToken();
2423 
2424  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2425 
2426  $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $contLang;
2427  $user->setOption( 'language', $lang->getPreferredVariant() );
2428 
2429  if ( $contLang->hasVariants() ) {
2430  $user->setOption( 'variant', $contLang->getPreferredVariant() );
2431  }
2432  }
2433 
2439  private function callMethodOnProviders( $which, $method, array $args ) {
2440  $providers = [];
2441  if ( $which & 1 ) {
2442  $providers += $this->getPreAuthenticationProviders();
2443  }
2444  if ( $which & 2 ) {
2445  $providers += $this->getPrimaryAuthenticationProviders();
2446  }
2447  if ( $which & 4 ) {
2448  $providers += $this->getSecondaryAuthenticationProviders();
2449  }
2450  foreach ( $providers as $provider ) {
2451  $provider->$method( ...$args );
2452  }
2453  }
2454 
2459  public static function resetCache() {
2460  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2461  // @codeCoverageIgnoreStart
2462  throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2463  // @codeCoverageIgnoreEnd
2464  }
2465 
2466  self::$instance = null;
2467  }
2468 
2471 }
2472 
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:3716
securitySensitiveOperationStatus( $operation)
Whether security-sensitive operations should proceed.
$property
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:3969
setId( $v)
Set the user and reload all fields according to a given ID.
Definition: User.php:2242
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1972
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
static instance()
Singleton.
Definition: Profiler.php:62
if(!isset( $args[0])) $lang
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.
static getLocalClusterInstance()
Get the main cluster-local cache object.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
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:48
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition: User.php:2823
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:864
continueAccountCreation(array $reqs)
Continue an account creation flow.
Authentication request for the reason given for account creation.
A helper class for throttling authentication attempts.
setOption( $oname, $val)
Set the given option for a user.
Definition: User.php:3055
static getInstance()
Returns the global default instance of the top level service locator.
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:2251
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
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:68
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition: hooks.txt:1244
loadFromId( $flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition: User.php:408
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition: User.php:4278
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.
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
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:1076
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.
$res
Definition: database.txt:21
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
$params
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1972
getConfiguration()
Get the configuration.
const REQUIRED
Indicates that the request is required for authentication to proceed.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:767
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition: User.php:1001
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
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging a wrapping ErrorException create2 Corresponds to logging log_action database field and which is displayed in the UI similar to $comment or false if none Defaults to false if not set multiOccurrence Can this option be passed multiple times Defaults to false if not set this hook should only be used to add variables that depend on the current page request
Definition: hooks.txt:2147
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.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
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.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
this hook is for auditing only $req
Definition: hooks.txt:960
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:767
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:559
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:2224
addToDatabase()
Add this existing user object to the database.
Definition: User.php:4158
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
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:4331
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)
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
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.
const SEC_OK
Security-sensitive operations are ok.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:535
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
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1454
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.