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';
90  const ACTION_LOGIN_CONTINUE = 'login-continue';
92  const ACTION_CREATE = 'create';
95  const ACTION_CREATE_CONTINUE = 'create-continue';
97  const ACTION_LINK = 'link';
100  const ACTION_LINK_CONTINUE = 'link-continue';
102  const ACTION_CHANGE = 'change';
104  const ACTION_REMOVE = 'remove';
106  const ACTION_UNLINK = 'unlink';
107 
109  const SEC_OK = 'ok';
111  const SEC_REAUTH = 'reauth';
113  const SEC_FAIL = 'fail';
114 
117 
119  const AUTOCREATE_SOURCE_MAINT = '::Maintenance::';
120 
122  private static $instance = null;
123 
125  private $request;
126 
128  private $config;
129 
131  private $logger;
132 
135 
138 
141 
144 
147 
152  public static function singleton() {
153  if ( self::$instance === null ) {
154  self::$instance = new self(
155  \RequestContext::getMain()->getRequest(),
156  MediaWikiServices::getInstance()->getMainConfig()
157  );
158  }
159  return self::$instance;
160  }
161 
167  $this->request = $request;
168  $this->config = $config;
169  $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
170  }
171 
175  public function setLogger( LoggerInterface $logger ) {
176  $this->logger = $logger;
177  }
178 
182  public function getRequest() {
183  return $this->request;
184  }
185 
192  public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
193  $this->logger->warning( "Overriding AuthManager primary authn because $why" );
194 
195  if ( $this->primaryAuthenticationProviders !== null ) {
196  $this->logger->warning(
197  'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
198  );
199 
200  $this->allAuthenticationProviders = array_diff_key(
201  $this->allAuthenticationProviders,
202  $this->primaryAuthenticationProviders
203  );
204  $session = $this->request->getSession();
205  $session->remove( 'AuthManager::authnState' );
206  $session->remove( 'AuthManager::accountCreationState' );
207  $session->remove( 'AuthManager::accountLinkState' );
208  $this->createdAccountAuthenticationRequests = [];
209  }
210 
211  $this->primaryAuthenticationProviders = [];
212  foreach ( $providers as $provider ) {
213  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
214  throw new \RuntimeException(
215  'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
216  get_class( $provider )
217  );
218  }
219  $provider->setLogger( $this->logger );
220  $provider->setManager( $this );
221  $provider->setConfig( $this->config );
222  $id = $provider->getUniqueId();
223  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
224  throw new \RuntimeException(
225  "Duplicate specifications for id $id (classes " .
226  get_class( $provider ) . ' and ' .
227  get_class( $this->allAuthenticationProviders[$id] ) . ')'
228  );
229  }
230  $this->allAuthenticationProviders[$id] = $provider;
231  $this->primaryAuthenticationProviders[$id] = $provider;
232  }
233  }
234 
246  public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
247  wfDeprecated( __METHOD__, '1.33' );
248  return $return;
249  }
250 
264  public function canAuthenticateNow() {
265  return $this->request->getSession()->canSetUser();
266  }
267 
286  public function beginAuthentication( array $reqs, $returnToUrl ) {
287  $session = $this->request->getSession();
288  if ( !$session->canSetUser() ) {
289  // Caller should have called canAuthenticateNow()
290  $session->remove( 'AuthManager::authnState' );
291  throw new \LogicException( 'Authentication is not possible now' );
292  }
293 
294  $guessUserName = null;
295  foreach ( $reqs as $req ) {
296  $req->returnToUrl = $returnToUrl;
297  // @codeCoverageIgnoreStart
298  if ( $req->username !== null && $req->username !== '' ) {
299  if ( $guessUserName === null ) {
300  $guessUserName = $req->username;
301  } elseif ( $guessUserName !== $req->username ) {
302  $guessUserName = null;
303  break;
304  }
305  }
306  // @codeCoverageIgnoreEnd
307  }
308 
309  // Check for special-case login of a just-created account
312  );
313  if ( $req ) {
314  if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
315  throw new \LogicException(
316  'CreatedAccountAuthenticationRequests are only valid on ' .
317  'the same AuthManager that created the account'
318  );
319  }
320 
321  $user = User::newFromName( $req->username );
322  // @codeCoverageIgnoreStart
323  if ( !$user ) {
324  throw new \UnexpectedValueException(
325  "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
326  );
327  } elseif ( $user->getId() != $req->id ) {
328  throw new \UnexpectedValueException(
329  "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
330  );
331  }
332  // @codeCoverageIgnoreEnd
333 
334  $this->logger->info( 'Logging in {user} after account creation', [
335  'user' => $user->getName(),
336  ] );
338  $this->setSessionDataForUser( $user );
339  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
340  $session->remove( 'AuthManager::authnState' );
341  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
342  return $ret;
343  }
344 
346 
347  foreach ( $this->getPreAuthenticationProviders() as $provider ) {
348  $status = $provider->testForAuthentication( $reqs );
349  if ( !$status->isGood() ) {
350  $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
352  Status::wrap( $status )->getMessage()
353  );
354  $this->callMethodOnProviders( 7, 'postAuthentication',
355  [ User::newFromName( $guessUserName ) ?: null, $ret ]
356  );
357  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName, [] ] );
358  return $ret;
359  }
360  }
361 
362  $state = [
363  'reqs' => $reqs,
364  'returnToUrl' => $returnToUrl,
365  'guessUserName' => $guessUserName,
366  'primary' => null,
367  'primaryResponse' => null,
368  'secondary' => [],
369  'maybeLink' => [],
370  'continueRequests' => [],
371  ];
372 
373  // Preserve state from a previous failed login
376  );
377  if ( $req ) {
378  $state['maybeLink'] = $req->maybeLink;
379  }
380 
381  $session = $this->request->getSession();
382  $session->setSecret( 'AuthManager::authnState', $state );
383  $session->persist();
384 
385  return $this->continueAuthentication( $reqs );
386  }
387 
410  public function continueAuthentication( array $reqs ) {
411  $session = $this->request->getSession();
412  try {
413  if ( !$session->canSetUser() ) {
414  // Caller should have called canAuthenticateNow()
415  // @codeCoverageIgnoreStart
416  throw new \LogicException( 'Authentication is not possible now' );
417  // @codeCoverageIgnoreEnd
418  }
419 
420  $state = $session->getSecret( 'AuthManager::authnState' );
421  if ( !is_array( $state ) ) {
423  wfMessage( 'authmanager-authn-not-in-progress' )
424  );
425  }
426  $state['continueRequests'] = [];
427 
428  $guessUserName = $state['guessUserName'];
429 
430  foreach ( $reqs as $req ) {
431  $req->returnToUrl = $state['returnToUrl'];
432  }
433 
434  // Step 1: Choose an primary authentication provider, and call it until it succeeds.
435 
436  if ( $state['primary'] === null ) {
437  // We haven't picked a PrimaryAuthenticationProvider yet
438  // @codeCoverageIgnoreStart
439  $guessUserName = null;
440  foreach ( $reqs as $req ) {
441  if ( $req->username !== null && $req->username !== '' ) {
442  if ( $guessUserName === null ) {
443  $guessUserName = $req->username;
444  } elseif ( $guessUserName !== $req->username ) {
445  $guessUserName = null;
446  break;
447  }
448  }
449  }
450  $state['guessUserName'] = $guessUserName;
451  // @codeCoverageIgnoreEnd
452  $state['reqs'] = $reqs;
453 
454  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
455  $res = $provider->beginPrimaryAuthentication( $reqs );
456  switch ( $res->status ) {
458  $state['primary'] = $id;
459  $state['primaryResponse'] = $res;
460  $this->logger->debug( "Primary login with $id succeeded" );
461  break 2;
463  $this->logger->debug( "Login failed in primary authentication by $id" );
464  if ( $res->createRequest || $state['maybeLink'] ) {
465  $res->createRequest = new CreateFromLoginAuthenticationRequest(
466  $res->createRequest, $state['maybeLink']
467  );
468  }
469  $this->callMethodOnProviders( 7, 'postAuthentication',
470  [ User::newFromName( $guessUserName ) ?: null, $res ]
471  );
472  $session->remove( 'AuthManager::authnState' );
473  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] );
474  return $res;
476  // Continue loop
477  break;
480  $this->logger->debug( "Primary login with $id returned $res->status" );
481  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
482  $state['primary'] = $id;
483  $state['continueRequests'] = $res->neededRequests;
484  $session->setSecret( 'AuthManager::authnState', $state );
485  return $res;
486 
487  // @codeCoverageIgnoreStart
488  default:
489  throw new \DomainException(
490  get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
491  );
492  // @codeCoverageIgnoreEnd
493  }
494  }
495  if ( $state['primary'] === null ) {
496  $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
498  wfMessage( 'authmanager-authn-no-primary' )
499  );
500  $this->callMethodOnProviders( 7, 'postAuthentication',
501  [ User::newFromName( $guessUserName ) ?: null, $ret ]
502  );
503  $session->remove( 'AuthManager::authnState' );
504  return $ret;
505  }
506  } elseif ( $state['primaryResponse'] === null ) {
507  $provider = $this->getAuthenticationProvider( $state['primary'] );
508  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
509  // Configuration changed? Force them to start over.
510  // @codeCoverageIgnoreStart
512  wfMessage( 'authmanager-authn-not-in-progress' )
513  );
514  $this->callMethodOnProviders( 7, 'postAuthentication',
515  [ User::newFromName( $guessUserName ) ?: null, $ret ]
516  );
517  $session->remove( 'AuthManager::authnState' );
518  return $ret;
519  // @codeCoverageIgnoreEnd
520  }
521  $id = $provider->getUniqueId();
522  $res = $provider->continuePrimaryAuthentication( $reqs );
523  switch ( $res->status ) {
525  $state['primaryResponse'] = $res;
526  $this->logger->debug( "Primary login with $id succeeded" );
527  break;
529  $this->logger->debug( "Login failed in primary authentication by $id" );
530  if ( $res->createRequest || $state['maybeLink'] ) {
531  $res->createRequest = new CreateFromLoginAuthenticationRequest(
532  $res->createRequest, $state['maybeLink']
533  );
534  }
535  $this->callMethodOnProviders( 7, 'postAuthentication',
536  [ User::newFromName( $guessUserName ) ?: null, $res ]
537  );
538  $session->remove( 'AuthManager::authnState' );
539  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] );
540  return $res;
543  $this->logger->debug( "Primary login with $id returned $res->status" );
544  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
545  $state['continueRequests'] = $res->neededRequests;
546  $session->setSecret( 'AuthManager::authnState', $state );
547  return $res;
548  default:
549  throw new \DomainException(
550  get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
551  );
552  }
553  }
554 
555  $res = $state['primaryResponse'];
556  if ( $res->username === null ) {
557  $provider = $this->getAuthenticationProvider( $state['primary'] );
558  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
559  // Configuration changed? Force them to start over.
560  // @codeCoverageIgnoreStart
562  wfMessage( 'authmanager-authn-not-in-progress' )
563  );
564  $this->callMethodOnProviders( 7, 'postAuthentication',
565  [ User::newFromName( $guessUserName ) ?: null, $ret ]
566  );
567  $session->remove( 'AuthManager::authnState' );
568  return $ret;
569  // @codeCoverageIgnoreEnd
570  }
571 
572  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
573  $res->linkRequest &&
574  // don't confuse the user with an incorrect message if linking is disabled
576  ) {
577  $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
578  $msg = 'authmanager-authn-no-local-user-link';
579  } else {
580  $msg = 'authmanager-authn-no-local-user';
581  }
582  $this->logger->debug(
583  "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
584  );
586  $ret->neededRequests = $this->getAuthenticationRequestsInternal(
587  self::ACTION_LOGIN,
588  [],
590  );
591  if ( $res->createRequest || $state['maybeLink'] ) {
592  $ret->createRequest = new CreateFromLoginAuthenticationRequest(
593  $res->createRequest, $state['maybeLink']
594  );
595  $ret->neededRequests[] = $ret->createRequest;
596  }
597  $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
598  $session->setSecret( 'AuthManager::authnState', [
599  'reqs' => [], // Will be filled in later
600  'primary' => null,
601  'primaryResponse' => null,
602  'secondary' => [],
603  'continueRequests' => $ret->neededRequests,
604  ] + $state );
605  return $ret;
606  }
607 
608  // Step 2: Primary authentication succeeded, create the User object
609  // (and add the user locally if necessary)
610 
611  $user = User::newFromName( $res->username, 'usable' );
612  if ( !$user ) {
613  $provider = $this->getAuthenticationProvider( $state['primary'] );
614  throw new \DomainException(
615  get_class( $provider ) . " returned an invalid username: {$res->username}"
616  );
617  }
618  if ( $user->getId() === 0 ) {
619  // User doesn't exist locally. Create it.
620  $this->logger->info( 'Auto-creating {user} on login', [
621  'user' => $user->getName(),
622  ] );
623  $status = $this->autoCreateUser( $user, $state['primary'], false );
624  if ( !$status->isGood() ) {
626  Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
627  );
628  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
629  $session->remove( 'AuthManager::authnState' );
630  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
631  return $ret;
632  }
633  }
634 
635  // Step 3: Iterate over all the secondary authentication providers.
636 
637  $beginReqs = $state['reqs'];
638 
639  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
640  if ( !isset( $state['secondary'][$id] ) ) {
641  // This provider isn't started yet, so we pass it the set
642  // of reqs from beginAuthentication instead of whatever
643  // might have been used by a previous provider in line.
644  $func = 'beginSecondaryAuthentication';
645  $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
646  } elseif ( !$state['secondary'][$id] ) {
647  $func = 'continueSecondaryAuthentication';
648  $res = $provider->continueSecondaryAuthentication( $user, $reqs );
649  } else {
650  continue;
651  }
652  switch ( $res->status ) {
654  $this->logger->debug( "Secondary login with $id succeeded" );
655  // fall through
657  $state['secondary'][$id] = true;
658  break;
660  $this->logger->debug( "Login failed in secondary authentication by $id" );
661  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
662  $session->remove( 'AuthManager::authnState' );
663  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName(), [] ] );
664  return $res;
667  $this->logger->debug( "Secondary login with $id returned " . $res->status );
668  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
669  $state['secondary'][$id] = false;
670  $state['continueRequests'] = $res->neededRequests;
671  $session->setSecret( 'AuthManager::authnState', $state );
672  return $res;
673 
674  // @codeCoverageIgnoreStart
675  default:
676  throw new \DomainException(
677  get_class( $provider ) . "::{$func}() returned $res->status"
678  );
679  // @codeCoverageIgnoreEnd
680  }
681  }
682 
683  // Step 4: Authentication complete! Set the user in the session and
684  // clean up.
685 
686  $this->logger->info( 'Login for {user} succeeded from {clientip}', [
687  'user' => $user->getName(),
688  'clientip' => $this->request->getIP(),
689  ] );
693  );
694  $this->setSessionDataForUser( $user, $req && $req->rememberMe );
696  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
697  $session->remove( 'AuthManager::authnState' );
699  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
700  return $ret;
701  } catch ( \Exception $ex ) {
702  $session->remove( 'AuthManager::authnState' );
703  throw $ex;
704  }
705  }
706 
718  public function securitySensitiveOperationStatus( $operation ) {
719  $status = self::SEC_OK;
720 
721  $this->logger->debug( __METHOD__ . ": Checking $operation" );
722 
723  $session = $this->request->getSession();
724  $aId = $session->getUser()->getId();
725  if ( $aId === 0 ) {
726  // User isn't authenticated. DWIM?
727  $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
728  $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
729  return $status;
730  }
731 
732  if ( $session->canSetUser() ) {
733  $id = $session->get( 'AuthManager:lastAuthId' );
734  $last = $session->get( 'AuthManager:lastAuthTimestamp' );
735  if ( $id !== $aId || $last === null ) {
736  $timeSinceLogin = PHP_INT_MAX; // Forever ago
737  } else {
738  $timeSinceLogin = max( 0, time() - $last );
739  }
740 
741  $thresholds = $this->config->get( 'ReauthenticateTime' );
742  if ( isset( $thresholds[$operation] ) ) {
743  $threshold = $thresholds[$operation];
744  } elseif ( isset( $thresholds['default'] ) ) {
745  $threshold = $thresholds['default'];
746  } else {
747  throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
748  }
749 
750  if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
751  $status = self::SEC_REAUTH;
752  }
753  } else {
754  $timeSinceLogin = -1;
755 
756  $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
757  if ( isset( $pass[$operation] ) ) {
758  $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
759  } elseif ( isset( $pass['default'] ) ) {
760  $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
761  } else {
762  throw new \UnexpectedValueException(
763  '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
764  );
765  }
766  }
767 
768  \Hooks::run( 'SecuritySensitiveOperationStatus', [
769  &$status, $operation, $session, $timeSinceLogin
770  ] );
771 
772  // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
773  if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
774  $status = self::SEC_FAIL;
775  }
776 
777  $this->logger->info( __METHOD__ . ": $operation is $status for '{user}'",
778  [
779  'user' => $session->getUser()->getName(),
780  'clientip' => $this->getRequest()->getIP(),
781  ]
782  );
783 
784  return $status;
785  }
786 
796  public function userCanAuthenticate( $username ) {
797  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
798  if ( $provider->testUserCanAuthenticate( $username ) ) {
799  return true;
800  }
801  }
802  return false;
803  }
804 
819  public function normalizeUsername( $username ) {
820  $ret = [];
821  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
822  $normalized = $provider->providerNormalizeUsername( $username );
823  if ( $normalized !== null ) {
824  $ret[$normalized] = true;
825  }
826  }
827  return array_keys( $ret );
828  }
829 
844  public function revokeAccessForUser( $username ) {
845  $this->logger->info( 'Revoking access for {user}', [
846  'user' => $username,
847  ] );
848  $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
849  }
850 
860  public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
861  $any = false;
862  $providers = $this->getPrimaryAuthenticationProviders() +
864  foreach ( $providers as $provider ) {
865  $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
866  if ( !$status->isGood() ) {
867  return Status::wrap( $status );
868  }
869  $any = $any || $status->value !== 'ignored';
870  }
871  if ( !$any ) {
872  $status = Status::newGood( 'ignored' );
873  $status->warning( 'authmanager-change-not-supported' );
874  return $status;
875  }
876  return Status::newGood();
877  }
878 
896  public function changeAuthenticationData( AuthenticationRequest $req, $isAddition = false ) {
897  $this->logger->info( 'Changing authentication data for {user} class {what}', [
898  'user' => is_string( $req->username ) ? $req->username : '<no name>',
899  'what' => get_class( $req ),
900  ] );
901 
902  $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
903 
904  // When the main account's authentication data is changed, invalidate
905  // all BotPasswords too.
906  if ( !$isAddition ) {
908  }
909  }
910 
922  public function canCreateAccounts() {
923  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
924  switch ( $provider->accountCreationType() ) {
927  return true;
928  }
929  }
930  return false;
931  }
932 
941  public function canCreateAccount( $username, $options = [] ) {
942  // Back compat
943  if ( is_int( $options ) ) {
944  $options = [ 'flags' => $options ];
945  }
946  $options += [
947  'flags' => User::READ_NORMAL,
948  'creating' => false,
949  ];
950  $flags = $options['flags'];
951 
952  if ( !$this->canCreateAccounts() ) {
953  return Status::newFatal( 'authmanager-create-disabled' );
954  }
955 
956  if ( $this->userExists( $username, $flags ) ) {
957  return Status::newFatal( 'userexists' );
958  }
959 
960  $user = User::newFromName( $username, 'creatable' );
961  if ( !is_object( $user ) ) {
962  return Status::newFatal( 'noname' );
963  } else {
964  $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
965  if ( $user->getId() !== 0 ) {
966  return Status::newFatal( 'userexists' );
967  }
968  }
969 
970  // Denied by providers?
971  $providers = $this->getPreAuthenticationProviders() +
974  foreach ( $providers as $provider ) {
975  $status = $provider->testUserForCreation( $user, false, $options );
976  if ( !$status->isGood() ) {
977  return Status::wrap( $status );
978  }
979  }
980 
981  return Status::newGood();
982  }
983 
989  public function checkAccountCreatePermissions( User $creator ) {
990  // Wiki is read-only?
991  if ( wfReadOnly() ) {
992  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
993  }
994 
995  // This is awful, this permission check really shouldn't go through Title.
996  $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
997  ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
998  if ( $permErrors ) {
1000  foreach ( $permErrors as $args ) {
1001  $status->fatal( ...$args );
1002  }
1003  return $status;
1004  }
1005 
1006  $block = $creator->isBlockedFromCreateAccount();
1007  if ( $block ) {
1008  $errorParams = [
1009  $block->getTarget(),
1010  $block->getReason() ?: wfMessage( 'blockednoreason' )->text(),
1011  $block->getByName()
1012  ];
1013 
1014  if ( $block->getType() === DatabaseBlock::TYPE_RANGE ) {
1015  $errorMessage = 'cantcreateaccount-range-text';
1016  $errorParams[] = $this->getRequest()->getIP();
1017  } else {
1018  $errorMessage = 'cantcreateaccount-text';
1019  }
1020 
1021  return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1022  }
1023 
1024  $ip = $this->getRequest()->getIP();
1025  if (
1026  MediaWikiServices::getInstance()->getBlockManager()
1027  ->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ )
1028  ) {
1029  return Status::newFatal( 'sorbs_create_account_reason' );
1030  }
1031 
1032  return Status::newGood();
1033  }
1034 
1054  public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1055  $session = $this->request->getSession();
1056  if ( !$this->canCreateAccounts() ) {
1057  // Caller should have called canCreateAccounts()
1058  $session->remove( 'AuthManager::accountCreationState' );
1059  throw new \LogicException( 'Account creation is not possible' );
1060  }
1061 
1062  try {
1064  } catch ( \UnexpectedValueException $ex ) {
1065  $username = null;
1066  }
1067  if ( $username === null ) {
1068  $this->logger->debug( __METHOD__ . ': No username provided' );
1069  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1070  }
1071 
1072  // Permissions check
1073  $status = $this->checkAccountCreatePermissions( $creator );
1074  if ( !$status->isGood() ) {
1075  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1076  'user' => $username,
1077  'creator' => $creator->getName(),
1078  'reason' => $status->getWikiText( null, null, 'en' )
1079  ] );
1080  return AuthenticationResponse::newFail( $status->getMessage() );
1081  }
1082 
1083  $status = $this->canCreateAccount(
1084  $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1085  );
1086  if ( !$status->isGood() ) {
1087  $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1088  'user' => $username,
1089  'creator' => $creator->getName(),
1090  'reason' => $status->getWikiText( null, null, 'en' )
1091  ] );
1092  return AuthenticationResponse::newFail( $status->getMessage() );
1093  }
1094 
1095  $user = User::newFromName( $username, 'creatable' );
1096  foreach ( $reqs as $req ) {
1097  $req->username = $username;
1098  $req->returnToUrl = $returnToUrl;
1099  if ( $req instanceof UserDataAuthenticationRequest ) {
1100  $status = $req->populateUser( $user );
1101  if ( !$status->isGood() ) {
1102  $status = Status::wrap( $status );
1103  $session->remove( 'AuthManager::accountCreationState' );
1104  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1105  'user' => $user->getName(),
1106  'creator' => $creator->getName(),
1107  'reason' => $status->getWikiText( null, null, 'en' ),
1108  ] );
1109  return AuthenticationResponse::newFail( $status->getMessage() );
1110  }
1111  }
1112  }
1113 
1115 
1116  $state = [
1117  'username' => $username,
1118  'userid' => 0,
1119  'creatorid' => $creator->getId(),
1120  'creatorname' => $creator->getName(),
1121  'reqs' => $reqs,
1122  'returnToUrl' => $returnToUrl,
1123  'primary' => null,
1124  'primaryResponse' => null,
1125  'secondary' => [],
1126  'continueRequests' => [],
1127  'maybeLink' => [],
1128  'ranPreTests' => false,
1129  ];
1130 
1131  // Special case: converting a login to an account creation
1134  );
1135  if ( $req ) {
1136  $state['maybeLink'] = $req->maybeLink;
1137 
1138  if ( $req->createRequest ) {
1139  $reqs[] = $req->createRequest;
1140  $state['reqs'][] = $req->createRequest;
1141  }
1142  }
1143 
1144  $session->setSecret( 'AuthManager::accountCreationState', $state );
1145  $session->persist();
1146 
1147  return $this->continueAccountCreation( $reqs );
1148  }
1149 
1155  public function continueAccountCreation( array $reqs ) {
1156  $session = $this->request->getSession();
1157  try {
1158  if ( !$this->canCreateAccounts() ) {
1159  // Caller should have called canCreateAccounts()
1160  $session->remove( 'AuthManager::accountCreationState' );
1161  throw new \LogicException( 'Account creation is not possible' );
1162  }
1163 
1164  $state = $session->getSecret( 'AuthManager::accountCreationState' );
1165  if ( !is_array( $state ) ) {
1167  wfMessage( 'authmanager-create-not-in-progress' )
1168  );
1169  }
1170  $state['continueRequests'] = [];
1171 
1172  // Step 0: Prepare and validate the input
1173 
1174  $user = User::newFromName( $state['username'], 'creatable' );
1175  if ( !is_object( $user ) ) {
1176  $session->remove( 'AuthManager::accountCreationState' );
1177  $this->logger->debug( __METHOD__ . ': Invalid username', [
1178  'user' => $state['username'],
1179  ] );
1180  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1181  }
1182 
1183  if ( $state['creatorid'] ) {
1184  $creator = User::newFromId( $state['creatorid'] );
1185  } else {
1186  $creator = new User;
1187  $creator->setName( $state['creatorname'] );
1188  }
1189 
1190  // Avoid account creation races on double submissions
1192  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1193  if ( !$lock ) {
1194  // Don't clear AuthManager::accountCreationState for this code
1195  // path because the process that won the race owns it.
1196  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1197  'user' => $user->getName(),
1198  'creator' => $creator->getName(),
1199  ] );
1200  return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1201  }
1202 
1203  // Permissions check
1204  $status = $this->checkAccountCreatePermissions( $creator );
1205  if ( !$status->isGood() ) {
1206  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1207  'user' => $user->getName(),
1208  'creator' => $creator->getName(),
1209  'reason' => $status->getWikiText( null, null, 'en' )
1210  ] );
1211  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1212  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1213  $session->remove( 'AuthManager::accountCreationState' );
1214  return $ret;
1215  }
1216 
1217  // Load from master for existence check
1218  $user->load( User::READ_LOCKING );
1219 
1220  if ( $state['userid'] === 0 ) {
1221  if ( $user->getId() !== 0 ) {
1222  $this->logger->debug( __METHOD__ . ': User exists locally', [
1223  'user' => $user->getName(),
1224  'creator' => $creator->getName(),
1225  ] );
1226  $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1227  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1228  $session->remove( 'AuthManager::accountCreationState' );
1229  return $ret;
1230  }
1231  } else {
1232  if ( $user->getId() === 0 ) {
1233  $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1234  'user' => $user->getName(),
1235  'creator' => $creator->getName(),
1236  'expected_id' => $state['userid'],
1237  ] );
1238  throw new \UnexpectedValueException(
1239  "User \"{$state['username']}\" should exist now, but doesn't!"
1240  );
1241  }
1242  if ( $user->getId() !== $state['userid'] ) {
1243  $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1244  'user' => $user->getName(),
1245  'creator' => $creator->getName(),
1246  'expected_id' => $state['userid'],
1247  'actual_id' => $user->getId(),
1248  ] );
1249  throw new \UnexpectedValueException(
1250  "User \"{$state['username']}\" exists, but " .
1251  "ID {$user->getId()} !== {$state['userid']}!"
1252  );
1253  }
1254  }
1255  foreach ( $state['reqs'] as $req ) {
1256  if ( $req instanceof UserDataAuthenticationRequest ) {
1257  $status = $req->populateUser( $user );
1258  if ( !$status->isGood() ) {
1259  // This should never happen...
1261  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1262  'user' => $user->getName(),
1263  'creator' => $creator->getName(),
1264  'reason' => $status->getWikiText( null, null, 'en' ),
1265  ] );
1266  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1267  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1268  $session->remove( 'AuthManager::accountCreationState' );
1269  return $ret;
1270  }
1271  }
1272  }
1273 
1274  foreach ( $reqs as $req ) {
1275  $req->returnToUrl = $state['returnToUrl'];
1276  $req->username = $state['username'];
1277  }
1278 
1279  // Run pre-creation tests, if we haven't already
1280  if ( !$state['ranPreTests'] ) {
1281  $providers = $this->getPreAuthenticationProviders() +
1284  foreach ( $providers as $id => $provider ) {
1285  $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1286  if ( !$status->isGood() ) {
1287  $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1288  'user' => $user->getName(),
1289  'creator' => $creator->getName(),
1290  ] );
1292  Status::wrap( $status )->getMessage()
1293  );
1294  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1295  $session->remove( 'AuthManager::accountCreationState' );
1296  return $ret;
1297  }
1298  }
1299 
1300  $state['ranPreTests'] = true;
1301  }
1302 
1303  // Step 1: Choose a primary authentication provider and call it until it succeeds.
1304 
1305  if ( $state['primary'] === null ) {
1306  // We haven't picked a PrimaryAuthenticationProvider yet
1307  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1308  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1309  continue;
1310  }
1311  $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1312  switch ( $res->status ) {
1314  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1315  'user' => $user->getName(),
1316  'creator' => $creator->getName(),
1317  ] );
1318  $state['primary'] = $id;
1319  $state['primaryResponse'] = $res;
1320  break 2;
1322  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1323  'user' => $user->getName(),
1324  'creator' => $creator->getName(),
1325  ] );
1326  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1327  $session->remove( 'AuthManager::accountCreationState' );
1328  return $res;
1330  // Continue loop
1331  break;
1334  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1335  'user' => $user->getName(),
1336  'creator' => $creator->getName(),
1337  ] );
1338  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1339  $state['primary'] = $id;
1340  $state['continueRequests'] = $res->neededRequests;
1341  $session->setSecret( 'AuthManager::accountCreationState', $state );
1342  return $res;
1343 
1344  // @codeCoverageIgnoreStart
1345  default:
1346  throw new \DomainException(
1347  get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1348  );
1349  // @codeCoverageIgnoreEnd
1350  }
1351  }
1352  if ( $state['primary'] === null ) {
1353  $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1354  'user' => $user->getName(),
1355  'creator' => $creator->getName(),
1356  ] );
1358  wfMessage( 'authmanager-create-no-primary' )
1359  );
1360  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1361  $session->remove( 'AuthManager::accountCreationState' );
1362  return $ret;
1363  }
1364  } elseif ( $state['primaryResponse'] === null ) {
1365  $provider = $this->getAuthenticationProvider( $state['primary'] );
1366  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1367  // Configuration changed? Force them to start over.
1368  // @codeCoverageIgnoreStart
1370  wfMessage( 'authmanager-create-not-in-progress' )
1371  );
1372  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1373  $session->remove( 'AuthManager::accountCreationState' );
1374  return $ret;
1375  // @codeCoverageIgnoreEnd
1376  }
1377  $id = $provider->getUniqueId();
1378  $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1379  switch ( $res->status ) {
1381  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1382  'user' => $user->getName(),
1383  'creator' => $creator->getName(),
1384  ] );
1385  $state['primaryResponse'] = $res;
1386  break;
1388  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1389  'user' => $user->getName(),
1390  'creator' => $creator->getName(),
1391  ] );
1392  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1393  $session->remove( 'AuthManager::accountCreationState' );
1394  return $res;
1397  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1398  'user' => $user->getName(),
1399  'creator' => $creator->getName(),
1400  ] );
1401  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1402  $state['continueRequests'] = $res->neededRequests;
1403  $session->setSecret( 'AuthManager::accountCreationState', $state );
1404  return $res;
1405  default:
1406  throw new \DomainException(
1407  get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1408  );
1409  }
1410  }
1411 
1412  // Step 2: Primary authentication succeeded, create the User object
1413  // and add the user locally.
1414 
1415  if ( $state['userid'] === 0 ) {
1416  $this->logger->info( 'Creating user {user} during account creation', [
1417  'user' => $user->getName(),
1418  'creator' => $creator->getName(),
1419  ] );
1420  $status = $user->addToDatabase();
1421  if ( !$status->isOK() ) {
1422  // @codeCoverageIgnoreStart
1423  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1424  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1425  $session->remove( 'AuthManager::accountCreationState' );
1426  return $ret;
1427  // @codeCoverageIgnoreEnd
1428  }
1429  $this->setDefaultUserOptions( $user, $creator->isAnon() );
1430  \Hooks::runWithoutAbort( 'LocalUserCreated', [ $user, false ] );
1431  $user->saveSettings();
1432  $state['userid'] = $user->getId();
1433 
1434  // Update user count
1435  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1436 
1437  // Watch user's userpage and talk page
1438  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1439 
1440  // Inform the provider
1441  $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1442 
1443  // Log the creation
1444  if ( $this->config->get( 'NewUserLog' ) ) {
1445  $isAnon = $creator->isAnon();
1446  $logEntry = new \ManualLogEntry(
1447  'newusers',
1448  $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1449  );
1450  $logEntry->setPerformer( $isAnon ? $user : $creator );
1451  $logEntry->setTarget( $user->getUserPage() );
1455  );
1456  $logEntry->setComment( $req ? $req->reason : '' );
1457  $logEntry->setParameters( [
1458  '4::userid' => $user->getId(),
1459  ] );
1460  $logid = $logEntry->insert();
1461  $logEntry->publish( $logid );
1462  }
1463  }
1464 
1465  // Step 3: Iterate over all the secondary authentication providers.
1466 
1467  $beginReqs = $state['reqs'];
1468 
1469  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1470  if ( !isset( $state['secondary'][$id] ) ) {
1471  // This provider isn't started yet, so we pass it the set
1472  // of reqs from beginAuthentication instead of whatever
1473  // might have been used by a previous provider in line.
1474  $func = 'beginSecondaryAccountCreation';
1475  $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1476  } elseif ( !$state['secondary'][$id] ) {
1477  $func = 'continueSecondaryAccountCreation';
1478  $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1479  } else {
1480  continue;
1481  }
1482  switch ( $res->status ) {
1484  $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1485  'user' => $user->getName(),
1486  'creator' => $creator->getName(),
1487  ] );
1488  // fall through
1490  $state['secondary'][$id] = true;
1491  break;
1494  $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1495  'user' => $user->getName(),
1496  'creator' => $creator->getName(),
1497  ] );
1498  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1499  $state['secondary'][$id] = false;
1500  $state['continueRequests'] = $res->neededRequests;
1501  $session->setSecret( 'AuthManager::accountCreationState', $state );
1502  return $res;
1504  throw new \DomainException(
1505  get_class( $provider ) . "::{$func}() returned $res->status." .
1506  ' Secondary providers are not allowed to fail account creation, that' .
1507  ' should have been done via testForAccountCreation().'
1508  );
1509  // @codeCoverageIgnoreStart
1510  default:
1511  throw new \DomainException(
1512  get_class( $provider ) . "::{$func}() returned $res->status"
1513  );
1514  // @codeCoverageIgnoreEnd
1515  }
1516  }
1517 
1518  $id = $user->getId();
1519  $name = $user->getName();
1520  $req = new CreatedAccountAuthenticationRequest( $id, $name );
1522  $ret->loginRequest = $req;
1523  $this->createdAccountAuthenticationRequests[] = $req;
1524 
1525  $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1526  'user' => $user->getName(),
1527  'creator' => $creator->getName(),
1528  ] );
1529 
1530  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1531  $session->remove( 'AuthManager::accountCreationState' );
1533  return $ret;
1534  } catch ( \Exception $ex ) {
1535  $session->remove( 'AuthManager::accountCreationState' );
1536  throw $ex;
1537  }
1538  }
1539 
1557  public function autoCreateUser( User $user, $source, $login = true ) {
1558  if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1559  $source !== self::AUTOCREATE_SOURCE_MAINT &&
1561  ) {
1562  throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1563  }
1564 
1565  $username = $user->getName();
1566 
1567  // Try the local user from the replica DB
1568  $localId = User::idFromName( $username );
1569  $flags = User::READ_NORMAL;
1570 
1571  // Fetch the user ID from the master, so that we don't try to create the user
1572  // when they already exist, due to replication lag
1573  // @codeCoverageIgnoreStart
1574  if (
1575  !$localId &&
1576  MediaWikiServices::getInstance()->getDBLoadBalancer()->getReaderIndex() !== 0
1577  ) {
1578  $localId = User::idFromName( $username, User::READ_LATEST );
1579  $flags = User::READ_LATEST;
1580  }
1581  // @codeCoverageIgnoreEnd
1582 
1583  if ( $localId ) {
1584  $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1585  'username' => $username,
1586  ] );
1587  $user->setId( $localId );
1588  $user->loadFromId( $flags );
1589  if ( $login ) {
1590  $this->setSessionDataForUser( $user );
1591  }
1593  $status->warning( 'userexists' );
1594  return $status;
1595  }
1596 
1597  // Wiki is read-only?
1598  if ( wfReadOnly() ) {
1599  $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1600  'username' => $username,
1601  'reason' => wfReadOnlyReason(),
1602  ] );
1603  $user->setId( 0 );
1604  $user->loadFromId();
1605  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1606  }
1607 
1608  // Check the session, if we tried to create this user already there's
1609  // no point in retrying.
1610  $session = $this->request->getSession();
1611  if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1612  $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1613  'username' => $username,
1614  'sessionid' => $session->getId(),
1615  ] );
1616  $user->setId( 0 );
1617  $user->loadFromId();
1618  $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1619  if ( $reason instanceof StatusValue ) {
1620  return Status::wrap( $reason );
1621  } else {
1622  return Status::newFatal( $reason );
1623  }
1624  }
1625 
1626  // Is the username creatable?
1627  if ( !User::isCreatableName( $username ) ) {
1628  $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1629  'username' => $username,
1630  ] );
1631  $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1632  $user->setId( 0 );
1633  $user->loadFromId();
1634  return Status::newFatal( 'noname' );
1635  }
1636 
1637  // Is the IP user able to create accounts?
1638  $anon = new User;
1639  if ( $source !== self::AUTOCREATE_SOURCE_MAINT &&
1640  !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
1641  ) {
1642  $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1643  'username' => $username,
1644  'ip' => $anon->getName(),
1645  ] );
1646  $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1647  $session->persist();
1648  $user->setId( 0 );
1649  $user->loadFromId();
1650  return Status::newFatal( 'authmanager-autocreate-noperm' );
1651  }
1652 
1653  // Avoid account creation races on double submissions
1655  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1656  if ( !$lock ) {
1657  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1658  'user' => $username,
1659  ] );
1660  $user->setId( 0 );
1661  $user->loadFromId();
1662  return Status::newFatal( 'usernameinprogress' );
1663  }
1664 
1665  // Denied by providers?
1666  $options = [
1667  'flags' => User::READ_LATEST,
1668  'creating' => true,
1669  ];
1670  $providers = $this->getPreAuthenticationProviders() +
1673  foreach ( $providers as $provider ) {
1674  $status = $provider->testUserForCreation( $user, $source, $options );
1675  if ( !$status->isGood() ) {
1676  $ret = Status::wrap( $status );
1677  $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1678  'username' => $username,
1679  'reason' => $ret->getWikiText( null, null, 'en' ),
1680  ] );
1681  $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1682  $user->setId( 0 );
1683  $user->loadFromId();
1684  return $ret;
1685  }
1686  }
1687 
1688  $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1689  if ( $cache->get( $backoffKey ) ) {
1690  $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1691  'username' => $username,
1692  ] );
1693  $user->setId( 0 );
1694  $user->loadFromId();
1695  return Status::newFatal( 'authmanager-autocreate-exception' );
1696  }
1697 
1698  // Checks passed, create the user...
1699  $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
1700  $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1701  'username' => $username,
1702  'from' => $from,
1703  ] );
1704 
1705  // Ignore warnings about master connections/writes...hard to avoid here
1706  $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1707  $old = $trxProfiler->setSilenced( true );
1708  try {
1709  $status = $user->addToDatabase();
1710  if ( !$status->isOK() ) {
1711  // Double-check for a race condition (T70012). We make use of the fact that when
1712  // addToDatabase fails due to the user already existing, the user object gets loaded.
1713  if ( $user->getId() ) {
1714  $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1715  'username' => $username,
1716  ] );
1717  if ( $login ) {
1718  $this->setSessionDataForUser( $user );
1719  }
1721  $status->warning( 'userexists' );
1722  } else {
1723  $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1724  'username' => $username,
1725  'msg' => $status->getWikiText( null, null, 'en' )
1726  ] );
1727  $user->setId( 0 );
1728  $user->loadFromId();
1729  }
1730  return $status;
1731  }
1732  } catch ( \Exception $ex ) {
1733  $trxProfiler->setSilenced( $old );
1734  $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1735  'username' => $username,
1736  'exception' => $ex,
1737  ] );
1738  // Do not keep throwing errors for a while
1739  $cache->set( $backoffKey, 1, 600 );
1740  // Bubble up error; which should normally trigger DB rollbacks
1741  throw $ex;
1742  }
1743 
1744  $this->setDefaultUserOptions( $user, false );
1745 
1746  // Inform the providers
1747  $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1748 
1749  \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1750  $user->saveSettings();
1751 
1752  // Update user count
1753  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1754  // Watch user's userpage and talk page
1755  \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1756  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1757  } );
1758 
1759  // Log the creation
1760  if ( $this->config->get( 'NewUserLog' ) ) {
1761  $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1762  $logEntry->setPerformer( $user );
1763  $logEntry->setTarget( $user->getUserPage() );
1764  $logEntry->setComment( '' );
1765  $logEntry->setParameters( [
1766  '4::userid' => $user->getId(),
1767  ] );
1768  $logEntry->insert();
1769  }
1770 
1771  $trxProfiler->setSilenced( $old );
1772 
1773  if ( $login ) {
1774  $this->setSessionDataForUser( $user );
1775  }
1776 
1777  return Status::newGood();
1778  }
1779 
1791  public function canLinkAccounts() {
1792  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1793  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1794  return true;
1795  }
1796  }
1797  return false;
1798  }
1799 
1809  public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1810  $session = $this->request->getSession();
1811  $session->remove( 'AuthManager::accountLinkState' );
1812 
1813  if ( !$this->canLinkAccounts() ) {
1814  // Caller should have called canLinkAccounts()
1815  throw new \LogicException( 'Account linking is not possible' );
1816  }
1817 
1818  if ( $user->getId() === 0 ) {
1819  if ( !User::isUsableName( $user->getName() ) ) {
1820  $msg = wfMessage( 'noname' );
1821  } else {
1822  $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1823  }
1824  return AuthenticationResponse::newFail( $msg );
1825  }
1826  foreach ( $reqs as $req ) {
1827  $req->username = $user->getName();
1828  $req->returnToUrl = $returnToUrl;
1829  }
1830 
1832 
1833  $providers = $this->getPreAuthenticationProviders();
1834  foreach ( $providers as $id => $provider ) {
1835  $status = $provider->testForAccountLink( $user );
1836  if ( !$status->isGood() ) {
1837  $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1838  'user' => $user->getName(),
1839  ] );
1841  Status::wrap( $status )->getMessage()
1842  );
1843  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1844  return $ret;
1845  }
1846  }
1847 
1848  $state = [
1849  'username' => $user->getName(),
1850  'userid' => $user->getId(),
1851  'returnToUrl' => $returnToUrl,
1852  'primary' => null,
1853  'continueRequests' => [],
1854  ];
1855 
1856  $providers = $this->getPrimaryAuthenticationProviders();
1857  foreach ( $providers as $id => $provider ) {
1858  if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1859  continue;
1860  }
1861 
1862  $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1863  switch ( $res->status ) {
1865  $this->logger->info( "Account linked to {user} by $id", [
1866  'user' => $user->getName(),
1867  ] );
1868  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1869  return $res;
1870 
1872  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1873  'user' => $user->getName(),
1874  ] );
1875  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1876  return $res;
1877 
1879  // Continue loop
1880  break;
1881 
1884  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1885  'user' => $user->getName(),
1886  ] );
1887  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1888  $state['primary'] = $id;
1889  $state['continueRequests'] = $res->neededRequests;
1890  $session->setSecret( 'AuthManager::accountLinkState', $state );
1891  $session->persist();
1892  return $res;
1893 
1894  // @codeCoverageIgnoreStart
1895  default:
1896  throw new \DomainException(
1897  get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1898  );
1899  // @codeCoverageIgnoreEnd
1900  }
1901  }
1902 
1903  $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1904  'user' => $user->getName(),
1905  ] );
1907  wfMessage( 'authmanager-link-no-primary' )
1908  );
1909  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1910  return $ret;
1911  }
1912 
1918  public function continueAccountLink( array $reqs ) {
1919  $session = $this->request->getSession();
1920  try {
1921  if ( !$this->canLinkAccounts() ) {
1922  // Caller should have called canLinkAccounts()
1923  $session->remove( 'AuthManager::accountLinkState' );
1924  throw new \LogicException( 'Account linking is not possible' );
1925  }
1926 
1927  $state = $session->getSecret( 'AuthManager::accountLinkState' );
1928  if ( !is_array( $state ) ) {
1930  wfMessage( 'authmanager-link-not-in-progress' )
1931  );
1932  }
1933  $state['continueRequests'] = [];
1934 
1935  // Step 0: Prepare and validate the input
1936 
1937  $user = User::newFromName( $state['username'], 'usable' );
1938  if ( !is_object( $user ) ) {
1939  $session->remove( 'AuthManager::accountLinkState' );
1940  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1941  }
1942  if ( $user->getId() !== $state['userid'] ) {
1943  throw new \UnexpectedValueException(
1944  "User \"{$state['username']}\" is valid, but " .
1945  "ID {$user->getId()} !== {$state['userid']}!"
1946  );
1947  }
1948 
1949  foreach ( $reqs as $req ) {
1950  $req->username = $state['username'];
1951  $req->returnToUrl = $state['returnToUrl'];
1952  }
1953 
1954  // Step 1: Call the primary again until it succeeds
1955 
1956  $provider = $this->getAuthenticationProvider( $state['primary'] );
1957  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1958  // Configuration changed? Force them to start over.
1959  // @codeCoverageIgnoreStart
1961  wfMessage( 'authmanager-link-not-in-progress' )
1962  );
1963  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1964  $session->remove( 'AuthManager::accountLinkState' );
1965  return $ret;
1966  // @codeCoverageIgnoreEnd
1967  }
1968  $id = $provider->getUniqueId();
1969  $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1970  switch ( $res->status ) {
1972  $this->logger->info( "Account linked to {user} by $id", [
1973  'user' => $user->getName(),
1974  ] );
1975  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1976  $session->remove( 'AuthManager::accountLinkState' );
1977  return $res;
1979  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1980  'user' => $user->getName(),
1981  ] );
1982  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1983  $session->remove( 'AuthManager::accountLinkState' );
1984  return $res;
1987  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1988  'user' => $user->getName(),
1989  ] );
1990  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1991  $state['continueRequests'] = $res->neededRequests;
1992  $session->setSecret( 'AuthManager::accountLinkState', $state );
1993  return $res;
1994  default:
1995  throw new \DomainException(
1996  get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1997  );
1998  }
1999  } catch ( \Exception $ex ) {
2000  $session->remove( 'AuthManager::accountLinkState' );
2001  throw $ex;
2002  }
2003  }
2004 
2031  $options = [];
2032  $providerAction = $action;
2033 
2034  // Figure out which providers to query
2035  switch ( $action ) {
2036  case self::ACTION_LOGIN:
2037  case self::ACTION_CREATE:
2038  $providers = $this->getPreAuthenticationProviders() +
2041  break;
2042 
2043  case self::ACTION_LOGIN_CONTINUE:
2044  $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2045  return is_array( $state ) ? $state['continueRequests'] : [];
2046 
2047  case self::ACTION_CREATE_CONTINUE:
2048  $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2049  return is_array( $state ) ? $state['continueRequests'] : [];
2050 
2051  case self::ACTION_LINK:
2052  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2053  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2054  } );
2055  break;
2056 
2057  case self::ACTION_UNLINK:
2058  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2059  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2060  } );
2061 
2062  // To providers, unlink and remove are identical.
2063  $providerAction = self::ACTION_REMOVE;
2064  break;
2065 
2066  case self::ACTION_LINK_CONTINUE:
2067  $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2068  return is_array( $state ) ? $state['continueRequests'] : [];
2069 
2070  case self::ACTION_CHANGE:
2071  case self::ACTION_REMOVE:
2072  $providers = $this->getPrimaryAuthenticationProviders() +
2074  break;
2075 
2076  // @codeCoverageIgnoreStart
2077  default:
2078  throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2079  }
2080  // @codeCoverageIgnoreEnd
2081 
2082  return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2083  }
2084 
2095  $providerAction, array $options, array $providers, User $user = null
2096  ) {
2097  $user = $user ?: \RequestContext::getMain()->getUser();
2098  $options['username'] = $user->isAnon() ? null : $user->getName();
2099 
2100  // Query them and merge results
2101  $reqs = [];
2102  foreach ( $providers as $provider ) {
2103  $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2104  foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2105  $id = $req->getUniqueId();
2106 
2107  // If a required request if from a Primary, mark it as "primary-required" instead
2108  if ( $isPrimary && $req->required ) {
2110  }
2111 
2112  if (
2113  !isset( $reqs[$id] )
2114  || $req->required === AuthenticationRequest::REQUIRED
2115  || $reqs[$id] === AuthenticationRequest::OPTIONAL
2116  ) {
2117  $reqs[$id] = $req;
2118  }
2119  }
2120  }
2121 
2122  // AuthManager has its own req for some actions
2123  switch ( $providerAction ) {
2124  case self::ACTION_LOGIN:
2125  $reqs[] = new RememberMeAuthenticationRequest;
2126  break;
2127 
2128  case self::ACTION_CREATE:
2129  $reqs[] = new UsernameAuthenticationRequest;
2130  $reqs[] = new UserDataAuthenticationRequest;
2131  if ( $options['username'] !== null ) {
2133  $options['username'] = null; // Don't fill in the username below
2134  }
2135  break;
2136  }
2137 
2138  // Fill in reqs data
2139  $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2140 
2141  // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2142  if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2143  $reqs = array_filter( $reqs, function ( $req ) {
2144  return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2145  } );
2146  }
2147 
2148  return array_values( $reqs );
2149  }
2150 
2158  private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2159  foreach ( $reqs as $req ) {
2160  if ( !$req->action || $forceAction ) {
2161  $req->action = $action;
2162  }
2163  if ( $req->username === null ) {
2164  $req->username = $username;
2165  }
2166  }
2167  }
2168 
2175  public function userExists( $username, $flags = User::READ_NORMAL ) {
2176  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2177  if ( $provider->testUserExists( $username, $flags ) ) {
2178  return true;
2179  }
2180  }
2181 
2182  return false;
2183  }
2184 
2196  public function allowsPropertyChange( $property ) {
2197  $providers = $this->getPrimaryAuthenticationProviders() +
2199  foreach ( $providers as $provider ) {
2200  if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2201  return false;
2202  }
2203  }
2204  return true;
2205  }
2206 
2215  public function getAuthenticationProvider( $id ) {
2216  // Fast version
2217  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2218  return $this->allAuthenticationProviders[$id];
2219  }
2220 
2221  // Slow version: instantiate each kind and check
2222  $providers = $this->getPrimaryAuthenticationProviders();
2223  if ( isset( $providers[$id] ) ) {
2224  return $providers[$id];
2225  }
2226  $providers = $this->getSecondaryAuthenticationProviders();
2227  if ( isset( $providers[$id] ) ) {
2228  return $providers[$id];
2229  }
2230  $providers = $this->getPreAuthenticationProviders();
2231  if ( isset( $providers[$id] ) ) {
2232  return $providers[$id];
2233  }
2234 
2235  return null;
2236  }
2237 
2251  public function setAuthenticationSessionData( $key, $data ) {
2252  $session = $this->request->getSession();
2253  $arr = $session->getSecret( 'authData' );
2254  if ( !is_array( $arr ) ) {
2255  $arr = [];
2256  }
2257  $arr[$key] = $data;
2258  $session->setSecret( 'authData', $arr );
2259  }
2260 
2268  public function getAuthenticationSessionData( $key, $default = null ) {
2269  $arr = $this->request->getSession()->getSecret( 'authData' );
2270  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2271  return $arr[$key];
2272  } else {
2273  return $default;
2274  }
2275  }
2276 
2282  public function removeAuthenticationSessionData( $key ) {
2283  $session = $this->request->getSession();
2284  if ( $key === null ) {
2285  $session->remove( 'authData' );
2286  } else {
2287  $arr = $session->getSecret( 'authData' );
2288  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2289  unset( $arr[$key] );
2290  $session->setSecret( 'authData', $arr );
2291  }
2292  }
2293  }
2294 
2301  protected function providerArrayFromSpecs( $class, array $specs ) {
2302  $i = 0;
2303  foreach ( $specs as &$spec ) {
2304  $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2305  }
2306  unset( $spec );
2307  // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2308  usort( $specs, function ( $a, $b ) {
2309  return $a['sort'] <=> $b['sort']
2310  ?: $a['sort2'] <=> $b['sort2'];
2311  } );
2312 
2313  $ret = [];
2314  foreach ( $specs as $spec ) {
2315  $provider = ObjectFactory::getObjectFromSpec( $spec );
2316  if ( !$provider instanceof $class ) {
2317  throw new \RuntimeException(
2318  "Expected instance of $class, got " . get_class( $provider )
2319  );
2320  }
2321  $provider->setLogger( $this->logger );
2322  $provider->setManager( $this );
2323  $provider->setConfig( $this->config );
2324  $id = $provider->getUniqueId();
2325  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2326  throw new \RuntimeException(
2327  "Duplicate specifications for id $id (classes " .
2328  get_class( $provider ) . ' and ' .
2329  get_class( $this->allAuthenticationProviders[$id] ) . ')'
2330  );
2331  }
2332  $this->allAuthenticationProviders[$id] = $provider;
2333  $ret[$id] = $provider;
2334  }
2335  return $ret;
2336  }
2337 
2342  private function getConfiguration() {
2343  return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2344  }
2345 
2350  protected function getPreAuthenticationProviders() {
2351  if ( $this->preAuthenticationProviders === null ) {
2352  $conf = $this->getConfiguration();
2353  $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2354  PreAuthenticationProvider::class, $conf['preauth']
2355  );
2356  }
2358  }
2359 
2364  protected function getPrimaryAuthenticationProviders() {
2365  if ( $this->primaryAuthenticationProviders === null ) {
2366  $conf = $this->getConfiguration();
2367  $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2368  PrimaryAuthenticationProvider::class, $conf['primaryauth']
2369  );
2370  }
2372  }
2373 
2379  if ( $this->secondaryAuthenticationProviders === null ) {
2380  $conf = $this->getConfiguration();
2381  $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2382  SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2383  );
2384  }
2386  }
2387 
2393  private function setSessionDataForUser( $user, $remember = null ) {
2394  $session = $this->request->getSession();
2395  $delay = $session->delaySave();
2396 
2397  $session->resetId();
2398  $session->resetAllTokens();
2399  if ( $session->canSetUser() ) {
2400  $session->setUser( $user );
2401  }
2402  if ( $remember !== null ) {
2403  $session->setRememberUser( $remember );
2404  }
2405  $session->set( 'AuthManager:lastAuthId', $user->getId() );
2406  $session->set( 'AuthManager:lastAuthTimestamp', time() );
2407  $session->persist();
2408 
2409  \Wikimedia\ScopedCallback::consume( $delay );
2410 
2411  \Hooks::run( 'UserLoggedIn', [ $user ] );
2412  }
2413 
2418  private function setDefaultUserOptions( User $user, $useContextLang ) {
2419  $user->setToken();
2420 
2421  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2422 
2423  $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $contLang;
2424  $user->setOption( 'language', $lang->getPreferredVariant() );
2425 
2426  if ( $contLang->hasVariants() ) {
2427  $user->setOption( 'variant', $contLang->getPreferredVariant() );
2428  }
2429  }
2430 
2436  private function callMethodOnProviders( $which, $method, array $args ) {
2437  $providers = [];
2438  if ( $which & 1 ) {
2439  $providers += $this->getPreAuthenticationProviders();
2440  }
2441  if ( $which & 2 ) {
2442  $providers += $this->getPrimaryAuthenticationProviders();
2443  }
2444  if ( $which & 4 ) {
2445  $providers += $this->getSecondaryAuthenticationProviders();
2446  }
2447  foreach ( $providers as $provider ) {
2448  $provider->$method( ...$args );
2449  }
2450  }
2451 
2456  public static function resetCache() {
2457  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2458  // @codeCoverageIgnoreStart
2459  throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2460  // @codeCoverageIgnoreEnd
2461  }
2462 
2463  self::$instance = null;
2464  }
2465 
2468 }
2469 
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:3808
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:4061
setId( $v)
Set the user and reload all fields according to a given ID.
Definition: User.php:2302
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:1982
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:2883
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:923
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:3115
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:2311
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:1263
loadFromId( $flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition: User.php:467
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition: User.php:4370
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:1135
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:1982
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:780
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition: User.php:1060
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:2159
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:97
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:979
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:780
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:618
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:2284
addToDatabase()
Add this existing user object to the database.
Definition: User.php:4250
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:90
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:4423
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:95
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:92
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:594
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:1473
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.