MediaWiki  master
AuthManager.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Auth;
25 
26 use Config;
30 use Status;
32 use User;
35 
83 class AuthManager implements LoggerAwareInterface {
85  const ACTION_LOGIN = 'login';
88  const ACTION_LOGIN_CONTINUE = 'login-continue';
90  const ACTION_CREATE = 'create';
93  const ACTION_CREATE_CONTINUE = 'create-continue';
95  const ACTION_LINK = 'link';
98  const ACTION_LINK_CONTINUE = 'link-continue';
100  const ACTION_CHANGE = 'change';
102  const ACTION_REMOVE = 'remove';
104  const ACTION_UNLINK = 'unlink';
105 
107  const SEC_OK = 'ok';
109  const SEC_REAUTH = 'reauth';
111  const SEC_FAIL = 'fail';
112 
115 
117  private static $instance = null;
118 
120  private $request;
121 
123  private $config;
124 
126  private $logger;
127 
130 
133 
136 
139 
142 
147  public static function singleton() {
148  if ( self::$instance === null ) {
149  self::$instance = new self(
150  \RequestContext::getMain()->getRequest(),
151  MediaWikiServices::getInstance()->getMainConfig()
152  );
153  }
154  return self::$instance;
155  }
156 
162  $this->request = $request;
163  $this->config = $config;
164  $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
165  }
166 
170  public function setLogger( LoggerInterface $logger ) {
171  $this->logger = $logger;
172  }
173 
177  public function getRequest() {
178  return $this->request;
179  }
180 
187  public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
188  $this->logger->warning( "Overriding AuthManager primary authn because $why" );
189 
190  if ( $this->primaryAuthenticationProviders !== null ) {
191  $this->logger->warning(
192  'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
193  );
194 
195  $this->allAuthenticationProviders = array_diff_key(
196  $this->allAuthenticationProviders,
197  $this->primaryAuthenticationProviders
198  );
199  $session = $this->request->getSession();
200  $session->remove( 'AuthManager::authnState' );
201  $session->remove( 'AuthManager::accountCreationState' );
202  $session->remove( 'AuthManager::accountLinkState' );
203  $this->createdAccountAuthenticationRequests = [];
204  }
205 
206  $this->primaryAuthenticationProviders = [];
207  foreach ( $providers as $provider ) {
208  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
209  throw new \RuntimeException(
210  'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
211  get_class( $provider )
212  );
213  }
214  $provider->setLogger( $this->logger );
215  $provider->setManager( $this );
216  $provider->setConfig( $this->config );
217  $id = $provider->getUniqueId();
218  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
219  throw new \RuntimeException(
220  "Duplicate specifications for id $id (classes " .
221  get_class( $provider ) . ' and ' .
222  get_class( $this->allAuthenticationProviders[$id] ) . ')'
223  );
224  }
225  $this->allAuthenticationProviders[$id] = $provider;
226  $this->primaryAuthenticationProviders[$id] = $provider;
227  }
228  }
229 
239  public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
240  global $wgAuth;
241 
242  if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
243  return $wgAuth->$method( ...$params );
244  } else {
245  return $return;
246  }
247  }
248 
262  public function canAuthenticateNow() {
263  return $this->request->getSession()->canSetUser();
264  }
265 
284  public function beginAuthentication( array $reqs, $returnToUrl ) {
285  $session = $this->request->getSession();
286  if ( !$session->canSetUser() ) {
287  // Caller should have called canAuthenticateNow()
288  $session->remove( 'AuthManager::authnState' );
289  throw new \LogicException( 'Authentication is not possible now' );
290  }
291 
292  $guessUserName = null;
293  foreach ( $reqs as $req ) {
294  $req->returnToUrl = $returnToUrl;
295  // @codeCoverageIgnoreStart
296  if ( $req->username !== null && $req->username !== '' ) {
297  if ( $guessUserName === null ) {
298  $guessUserName = $req->username;
299  } elseif ( $guessUserName !== $req->username ) {
300  $guessUserName = null;
301  break;
302  }
303  }
304  // @codeCoverageIgnoreEnd
305  }
306 
307  // Check for special-case login of a just-created account
310  );
311  if ( $req ) {
312  if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
313  throw new \LogicException(
314  'CreatedAccountAuthenticationRequests are only valid on ' .
315  'the same AuthManager that created the account'
316  );
317  }
318 
319  $user = User::newFromName( $req->username );
320  // @codeCoverageIgnoreStart
321  if ( !$user ) {
322  throw new \UnexpectedValueException(
323  "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
324  );
325  } elseif ( $user->getId() != $req->id ) {
326  throw new \UnexpectedValueException(
327  "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
328  );
329  }
330  // @codeCoverageIgnoreEnd
331 
332  $this->logger->info( 'Logging in {user} after account creation', [
333  'user' => $user->getName(),
334  ] );
336  $this->setSessionDataForUser( $user );
337  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
338  $session->remove( 'AuthManager::authnState' );
339  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
340  return $ret;
341  }
342 
343  $this->removeAuthenticationSessionData( null );
344 
345  foreach ( $this->getPreAuthenticationProviders() as $provider ) {
346  $status = $provider->testForAuthentication( $reqs );
347  if ( !$status->isGood() ) {
348  $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
350  Status::wrap( $status )->getMessage()
351  );
352  $this->callMethodOnProviders( 7, 'postAuthentication',
353  [ User::newFromName( $guessUserName ) ?: null, $ret ]
354  );
355  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
356  return $ret;
357  }
358  }
359 
360  $state = [
361  'reqs' => $reqs,
362  'returnToUrl' => $returnToUrl,
363  'guessUserName' => $guessUserName,
364  'primary' => null,
365  'primaryResponse' => null,
366  'secondary' => [],
367  'maybeLink' => [],
368  'continueRequests' => [],
369  ];
370 
371  // Preserve state from a previous failed login
374  );
375  if ( $req ) {
376  $state['maybeLink'] = $req->maybeLink;
377  }
378 
379  $session = $this->request->getSession();
380  $session->setSecret( 'AuthManager::authnState', $state );
381  $session->persist();
382 
383  return $this->continueAuthentication( $reqs );
384  }
385 
408  public function continueAuthentication( array $reqs ) {
409  $session = $this->request->getSession();
410  try {
411  if ( !$session->canSetUser() ) {
412  // Caller should have called canAuthenticateNow()
413  // @codeCoverageIgnoreStart
414  throw new \LogicException( 'Authentication is not possible now' );
415  // @codeCoverageIgnoreEnd
416  }
417 
418  $state = $session->getSecret( 'AuthManager::authnState' );
419  if ( !is_array( $state ) ) {
421  wfMessage( 'authmanager-authn-not-in-progress' )
422  );
423  }
424  $state['continueRequests'] = [];
425 
426  $guessUserName = $state['guessUserName'];
427 
428  foreach ( $reqs as $req ) {
429  $req->returnToUrl = $state['returnToUrl'];
430  }
431 
432  // Step 1: Choose an primary authentication provider, and call it until it succeeds.
433 
434  if ( $state['primary'] === null ) {
435  // We haven't picked a PrimaryAuthenticationProvider yet
436  // @codeCoverageIgnoreStart
437  $guessUserName = null;
438  foreach ( $reqs as $req ) {
439  if ( $req->username !== null && $req->username !== '' ) {
440  if ( $guessUserName === null ) {
441  $guessUserName = $req->username;
442  } elseif ( $guessUserName !== $req->username ) {
443  $guessUserName = null;
444  break;
445  }
446  }
447  }
448  $state['guessUserName'] = $guessUserName;
449  // @codeCoverageIgnoreEnd
450  $state['reqs'] = $reqs;
451 
452  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
453  $res = $provider->beginPrimaryAuthentication( $reqs );
454  switch ( $res->status ) {
456  $state['primary'] = $id;
457  $state['primaryResponse'] = $res;
458  $this->logger->debug( "Primary login with $id succeeded" );
459  break 2;
461  $this->logger->debug( "Login failed in primary authentication by $id" );
462  if ( $res->createRequest || $state['maybeLink'] ) {
463  $res->createRequest = new CreateFromLoginAuthenticationRequest(
464  $res->createRequest, $state['maybeLink']
465  );
466  }
467  $this->callMethodOnProviders( 7, 'postAuthentication',
468  [ User::newFromName( $guessUserName ) ?: null, $res ]
469  );
470  $session->remove( 'AuthManager::authnState' );
471  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
472  return $res;
474  // Continue loop
475  break;
478  $this->logger->debug( "Primary login with $id returned $res->status" );
479  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
480  $state['primary'] = $id;
481  $state['continueRequests'] = $res->neededRequests;
482  $session->setSecret( 'AuthManager::authnState', $state );
483  return $res;
484 
485  // @codeCoverageIgnoreStart
486  default:
487  throw new \DomainException(
488  get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
489  );
490  // @codeCoverageIgnoreEnd
491  }
492  }
493  if ( $state['primary'] === null ) {
494  $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
496  wfMessage( 'authmanager-authn-no-primary' )
497  );
498  $this->callMethodOnProviders( 7, 'postAuthentication',
499  [ User::newFromName( $guessUserName ) ?: null, $ret ]
500  );
501  $session->remove( 'AuthManager::authnState' );
502  return $ret;
503  }
504  } elseif ( $state['primaryResponse'] === null ) {
505  $provider = $this->getAuthenticationProvider( $state['primary'] );
506  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
507  // Configuration changed? Force them to start over.
508  // @codeCoverageIgnoreStart
510  wfMessage( 'authmanager-authn-not-in-progress' )
511  );
512  $this->callMethodOnProviders( 7, 'postAuthentication',
513  [ User::newFromName( $guessUserName ) ?: null, $ret ]
514  );
515  $session->remove( 'AuthManager::authnState' );
516  return $ret;
517  // @codeCoverageIgnoreEnd
518  }
519  $id = $provider->getUniqueId();
520  $res = $provider->continuePrimaryAuthentication( $reqs );
521  switch ( $res->status ) {
523  $state['primaryResponse'] = $res;
524  $this->logger->debug( "Primary login with $id succeeded" );
525  break;
527  $this->logger->debug( "Login failed in primary authentication by $id" );
528  if ( $res->createRequest || $state['maybeLink'] ) {
529  $res->createRequest = new CreateFromLoginAuthenticationRequest(
530  $res->createRequest, $state['maybeLink']
531  );
532  }
533  $this->callMethodOnProviders( 7, 'postAuthentication',
534  [ User::newFromName( $guessUserName ) ?: null, $res ]
535  );
536  $session->remove( 'AuthManager::authnState' );
537  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
538  return $res;
541  $this->logger->debug( "Primary login with $id returned $res->status" );
542  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
543  $state['continueRequests'] = $res->neededRequests;
544  $session->setSecret( 'AuthManager::authnState', $state );
545  return $res;
546  default:
547  throw new \DomainException(
548  get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
549  );
550  }
551  }
552 
553  $res = $state['primaryResponse'];
554  if ( $res->username === null ) {
555  $provider = $this->getAuthenticationProvider( $state['primary'] );
556  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
557  // Configuration changed? Force them to start over.
558  // @codeCoverageIgnoreStart
560  wfMessage( 'authmanager-authn-not-in-progress' )
561  );
562  $this->callMethodOnProviders( 7, 'postAuthentication',
563  [ User::newFromName( $guessUserName ) ?: null, $ret ]
564  );
565  $session->remove( 'AuthManager::authnState' );
566  return $ret;
567  // @codeCoverageIgnoreEnd
568  }
569 
570  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
571  $res->linkRequest &&
572  // don't confuse the user with an incorrect message if linking is disabled
574  ) {
575  $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
576  $msg = 'authmanager-authn-no-local-user-link';
577  } else {
578  $msg = 'authmanager-authn-no-local-user';
579  }
580  $this->logger->debug(
581  "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
582  );
584  $ret->neededRequests = $this->getAuthenticationRequestsInternal(
585  self::ACTION_LOGIN,
586  [],
588  );
589  if ( $res->createRequest || $state['maybeLink'] ) {
590  $ret->createRequest = new CreateFromLoginAuthenticationRequest(
591  $res->createRequest, $state['maybeLink']
592  );
593  $ret->neededRequests[] = $ret->createRequest;
594  }
595  $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
596  $session->setSecret( 'AuthManager::authnState', [
597  'reqs' => [], // Will be filled in later
598  'primary' => null,
599  'primaryResponse' => null,
600  'secondary' => [],
601  'continueRequests' => $ret->neededRequests,
602  ] + $state );
603  return $ret;
604  }
605 
606  // Step 2: Primary authentication succeeded, create the User object
607  // (and add the user locally if necessary)
608 
609  $user = User::newFromName( $res->username, 'usable' );
610  if ( !$user ) {
611  $provider = $this->getAuthenticationProvider( $state['primary'] );
612  throw new \DomainException(
613  get_class( $provider ) . " returned an invalid username: {$res->username}"
614  );
615  }
616  if ( $user->getId() === 0 ) {
617  // User doesn't exist locally. Create it.
618  $this->logger->info( 'Auto-creating {user} on login', [
619  'user' => $user->getName(),
620  ] );
621  $status = $this->autoCreateUser( $user, $state['primary'], false );
622  if ( !$status->isGood() ) {
624  Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
625  );
626  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
627  $session->remove( 'AuthManager::authnState' );
628  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
629  return $ret;
630  }
631  }
632 
633  // Step 3: Iterate over all the secondary authentication providers.
634 
635  $beginReqs = $state['reqs'];
636 
637  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
638  if ( !isset( $state['secondary'][$id] ) ) {
639  // This provider isn't started yet, so we pass it the set
640  // of reqs from beginAuthentication instead of whatever
641  // might have been used by a previous provider in line.
642  $func = 'beginSecondaryAuthentication';
643  $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
644  } elseif ( !$state['secondary'][$id] ) {
645  $func = 'continueSecondaryAuthentication';
646  $res = $provider->continueSecondaryAuthentication( $user, $reqs );
647  } else {
648  continue;
649  }
650  switch ( $res->status ) {
652  $this->logger->debug( "Secondary login with $id succeeded" );
653  // fall through
655  $state['secondary'][$id] = true;
656  break;
658  $this->logger->debug( "Login failed in secondary authentication by $id" );
659  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
660  $session->remove( 'AuthManager::authnState' );
661  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
662  return $res;
665  $this->logger->debug( "Secondary login with $id returned " . $res->status );
666  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
667  $state['secondary'][$id] = false;
668  $state['continueRequests'] = $res->neededRequests;
669  $session->setSecret( 'AuthManager::authnState', $state );
670  return $res;
671 
672  // @codeCoverageIgnoreStart
673  default:
674  throw new \DomainException(
675  get_class( $provider ) . "::{$func}() returned $res->status"
676  );
677  // @codeCoverageIgnoreEnd
678  }
679  }
680 
681  // Step 4: Authentication complete! Set the user in the session and
682  // clean up.
683 
684  $this->logger->info( 'Login for {user} succeeded from {clientip}', [
685  'user' => $user->getName(),
686  'clientip' => $this->request->getIP(),
687  ] );
691  );
692  $this->setSessionDataForUser( $user, $req && $req->rememberMe );
694  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
695  $session->remove( 'AuthManager::authnState' );
696  $this->removeAuthenticationSessionData( null );
697  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
698  return $ret;
699  } catch ( \Exception $ex ) {
700  $session->remove( 'AuthManager::authnState' );
701  throw $ex;
702  }
703  }
704 
716  public function securitySensitiveOperationStatus( $operation ) {
717  $status = self::SEC_OK;
718 
719  $this->logger->debug( __METHOD__ . ": Checking $operation" );
720 
721  $session = $this->request->getSession();
722  $aId = $session->getUser()->getId();
723  if ( $aId === 0 ) {
724  // User isn't authenticated. DWIM?
725  $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
726  $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
727  return $status;
728  }
729 
730  if ( $session->canSetUser() ) {
731  $id = $session->get( 'AuthManager:lastAuthId' );
732  $last = $session->get( 'AuthManager:lastAuthTimestamp' );
733  if ( $id !== $aId || $last === null ) {
734  $timeSinceLogin = PHP_INT_MAX; // Forever ago
735  } else {
736  $timeSinceLogin = max( 0, time() - $last );
737  }
738 
739  $thresholds = $this->config->get( 'ReauthenticateTime' );
740  if ( isset( $thresholds[$operation] ) ) {
741  $threshold = $thresholds[$operation];
742  } elseif ( isset( $thresholds['default'] ) ) {
743  $threshold = $thresholds['default'];
744  } else {
745  throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
746  }
747 
748  if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
749  $status = self::SEC_REAUTH;
750  }
751  } else {
752  $timeSinceLogin = -1;
753 
754  $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
755  if ( isset( $pass[$operation] ) ) {
756  $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
757  } elseif ( isset( $pass['default'] ) ) {
758  $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
759  } else {
760  throw new \UnexpectedValueException(
761  '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
762  );
763  }
764  }
765 
766  \Hooks::run( 'SecuritySensitiveOperationStatus', [
767  &$status, $operation, $session, $timeSinceLogin
768  ] );
769 
770  // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
771  if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
772  $status = self::SEC_FAIL;
773  }
774 
775  $this->logger->info( __METHOD__ . ": $operation is $status for '{user}'",
776  [
777  'user' => $session->getUser()->getName(),
778  'clientip' => $this->getRequest()->getIP(),
779  ]
780  );
781 
782  return $status;
783  }
784 
794  public function userCanAuthenticate( $username ) {
795  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
796  if ( $provider->testUserCanAuthenticate( $username ) ) {
797  return true;
798  }
799  }
800  return false;
801  }
802 
817  public function normalizeUsername( $username ) {
818  $ret = [];
819  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
820  $normalized = $provider->providerNormalizeUsername( $username );
821  if ( $normalized !== null ) {
822  $ret[$normalized] = true;
823  }
824  }
825  return array_keys( $ret );
826  }
827 
842  public function revokeAccessForUser( $username ) {
843  $this->logger->info( 'Revoking access for {user}', [
844  'user' => $username,
845  ] );
846  $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
847  }
848 
858  public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
859  $any = false;
860  $providers = $this->getPrimaryAuthenticationProviders() +
862  foreach ( $providers as $provider ) {
863  $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
864  if ( !$status->isGood() ) {
865  return Status::wrap( $status );
866  }
867  $any = $any || $status->value !== 'ignored';
868  }
869  if ( !$any ) {
870  $status = Status::newGood( 'ignored' );
871  $status->warning( 'authmanager-change-not-supported' );
872  return $status;
873  }
874  return Status::newGood();
875  }
876 
894  public function changeAuthenticationData( AuthenticationRequest $req, $isAddition = false ) {
895  $this->logger->info( 'Changing authentication data for {user} class {what}', [
896  'user' => is_string( $req->username ) ? $req->username : '<no name>',
897  'what' => get_class( $req ),
898  ] );
899 
900  $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
901 
902  // When the main account's authentication data is changed, invalidate
903  // all BotPasswords too.
904  if ( !$isAddition ) {
906  }
907  }
908 
920  public function canCreateAccounts() {
921  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
922  switch ( $provider->accountCreationType() ) {
925  return true;
926  }
927  }
928  return false;
929  }
930 
939  public function canCreateAccount( $username, $options = [] ) {
940  // Back compat
941  if ( is_int( $options ) ) {
942  $options = [ 'flags' => $options ];
943  }
944  $options += [
945  'flags' => User::READ_NORMAL,
946  'creating' => false,
947  ];
948  $flags = $options['flags'];
949 
950  if ( !$this->canCreateAccounts() ) {
951  return Status::newFatal( 'authmanager-create-disabled' );
952  }
953 
954  if ( $this->userExists( $username, $flags ) ) {
955  return Status::newFatal( 'userexists' );
956  }
957 
958  $user = User::newFromName( $username, 'creatable' );
959  if ( !is_object( $user ) ) {
960  return Status::newFatal( 'noname' );
961  } else {
962  $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
963  if ( $user->getId() !== 0 ) {
964  return Status::newFatal( 'userexists' );
965  }
966  }
967 
968  // Denied by providers?
969  $providers = $this->getPreAuthenticationProviders() +
972  foreach ( $providers as $provider ) {
973  $status = $provider->testUserForCreation( $user, false, $options );
974  if ( !$status->isGood() ) {
975  return Status::wrap( $status );
976  }
977  }
978 
979  return Status::newGood();
980  }
981 
987  public function checkAccountCreatePermissions( User $creator ) {
988  // Wiki is read-only?
989  if ( wfReadOnly() ) {
990  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
991  }
992 
993  // This is awful, this permission check really shouldn't go through Title.
994  $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
995  ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
996  if ( $permErrors ) {
998  foreach ( $permErrors as $args ) {
999  $status->fatal( ...$args );
1000  }
1001  return $status;
1002  }
1003 
1004  $block = $creator->isBlockedFromCreateAccount();
1005  if ( $block ) {
1006  $errorParams = [
1007  $block->getTarget(),
1008  $block->mReason ?: wfMessage( 'blockednoreason' )->text(),
1009  $block->getByName()
1010  ];
1011 
1012  if ( $block->getType() === \Block::TYPE_RANGE ) {
1013  $errorMessage = 'cantcreateaccount-range-text';
1014  $errorParams[] = $this->getRequest()->getIP();
1015  } else {
1016  $errorMessage = 'cantcreateaccount-text';
1017  }
1018 
1019  return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1020  }
1021 
1022  $ip = $this->getRequest()->getIP();
1023  if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1024  return Status::newFatal( 'sorbs_create_account_reason' );
1025  }
1026 
1027  return Status::newGood();
1028  }
1029 
1049  public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1050  $session = $this->request->getSession();
1051  if ( !$this->canCreateAccounts() ) {
1052  // Caller should have called canCreateAccounts()
1053  $session->remove( 'AuthManager::accountCreationState' );
1054  throw new \LogicException( 'Account creation is not possible' );
1055  }
1056 
1057  try {
1059  } catch ( \UnexpectedValueException $ex ) {
1060  $username = null;
1061  }
1062  if ( $username === null ) {
1063  $this->logger->debug( __METHOD__ . ': No username provided' );
1064  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1065  }
1066 
1067  // Permissions check
1068  $status = $this->checkAccountCreatePermissions( $creator );
1069  if ( !$status->isGood() ) {
1070  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1071  'user' => $username,
1072  'creator' => $creator->getName(),
1073  'reason' => $status->getWikiText( null, null, 'en' )
1074  ] );
1075  return AuthenticationResponse::newFail( $status->getMessage() );
1076  }
1077 
1078  $status = $this->canCreateAccount(
1079  $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1080  );
1081  if ( !$status->isGood() ) {
1082  $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1083  'user' => $username,
1084  'creator' => $creator->getName(),
1085  'reason' => $status->getWikiText( null, null, 'en' )
1086  ] );
1087  return AuthenticationResponse::newFail( $status->getMessage() );
1088  }
1089 
1090  $user = User::newFromName( $username, 'creatable' );
1091  foreach ( $reqs as $req ) {
1092  $req->username = $username;
1093  $req->returnToUrl = $returnToUrl;
1094  if ( $req instanceof UserDataAuthenticationRequest ) {
1095  $status = $req->populateUser( $user );
1096  if ( !$status->isGood() ) {
1097  $status = Status::wrap( $status );
1098  $session->remove( 'AuthManager::accountCreationState' );
1099  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1100  'user' => $user->getName(),
1101  'creator' => $creator->getName(),
1102  'reason' => $status->getWikiText( null, null, 'en' ),
1103  ] );
1104  return AuthenticationResponse::newFail( $status->getMessage() );
1105  }
1106  }
1107  }
1108 
1109  $this->removeAuthenticationSessionData( null );
1110 
1111  $state = [
1112  'username' => $username,
1113  'userid' => 0,
1114  'creatorid' => $creator->getId(),
1115  'creatorname' => $creator->getName(),
1116  'reqs' => $reqs,
1117  'returnToUrl' => $returnToUrl,
1118  'primary' => null,
1119  'primaryResponse' => null,
1120  'secondary' => [],
1121  'continueRequests' => [],
1122  'maybeLink' => [],
1123  'ranPreTests' => false,
1124  ];
1125 
1126  // Special case: converting a login to an account creation
1129  );
1130  if ( $req ) {
1131  $state['maybeLink'] = $req->maybeLink;
1132 
1133  if ( $req->createRequest ) {
1134  $reqs[] = $req->createRequest;
1135  $state['reqs'][] = $req->createRequest;
1136  }
1137  }
1138 
1139  $session->setSecret( 'AuthManager::accountCreationState', $state );
1140  $session->persist();
1141 
1142  return $this->continueAccountCreation( $reqs );
1143  }
1144 
1150  public function continueAccountCreation( array $reqs ) {
1151  $session = $this->request->getSession();
1152  try {
1153  if ( !$this->canCreateAccounts() ) {
1154  // Caller should have called canCreateAccounts()
1155  $session->remove( 'AuthManager::accountCreationState' );
1156  throw new \LogicException( 'Account creation is not possible' );
1157  }
1158 
1159  $state = $session->getSecret( 'AuthManager::accountCreationState' );
1160  if ( !is_array( $state ) ) {
1162  wfMessage( 'authmanager-create-not-in-progress' )
1163  );
1164  }
1165  $state['continueRequests'] = [];
1166 
1167  // Step 0: Prepare and validate the input
1168 
1169  $user = User::newFromName( $state['username'], 'creatable' );
1170  if ( !is_object( $user ) ) {
1171  $session->remove( 'AuthManager::accountCreationState' );
1172  $this->logger->debug( __METHOD__ . ': Invalid username', [
1173  'user' => $state['username'],
1174  ] );
1175  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1176  }
1177 
1178  if ( $state['creatorid'] ) {
1179  $creator = User::newFromId( $state['creatorid'] );
1180  } else {
1181  $creator = new User;
1182  $creator->setName( $state['creatorname'] );
1183  }
1184 
1185  // Avoid account creation races on double submissions
1187  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1188  if ( !$lock ) {
1189  // Don't clear AuthManager::accountCreationState for this code
1190  // path because the process that won the race owns it.
1191  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1192  'user' => $user->getName(),
1193  'creator' => $creator->getName(),
1194  ] );
1195  return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1196  }
1197 
1198  // Permissions check
1199  $status = $this->checkAccountCreatePermissions( $creator );
1200  if ( !$status->isGood() ) {
1201  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1202  'user' => $user->getName(),
1203  'creator' => $creator->getName(),
1204  'reason' => $status->getWikiText( null, null, 'en' )
1205  ] );
1206  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1207  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1208  $session->remove( 'AuthManager::accountCreationState' );
1209  return $ret;
1210  }
1211 
1212  // Load from master for existence check
1213  $user->load( User::READ_LOCKING );
1214 
1215  if ( $state['userid'] === 0 ) {
1216  if ( $user->getId() != 0 ) {
1217  $this->logger->debug( __METHOD__ . ': User exists locally', [
1218  'user' => $user->getName(),
1219  'creator' => $creator->getName(),
1220  ] );
1221  $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1222  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1223  $session->remove( 'AuthManager::accountCreationState' );
1224  return $ret;
1225  }
1226  } else {
1227  if ( $user->getId() === 0 ) {
1228  $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1229  'user' => $user->getName(),
1230  'creator' => $creator->getName(),
1231  'expected_id' => $state['userid'],
1232  ] );
1233  throw new \UnexpectedValueException(
1234  "User \"{$state['username']}\" should exist now, but doesn't!"
1235  );
1236  }
1237  if ( $user->getId() != $state['userid'] ) {
1238  $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1239  'user' => $user->getName(),
1240  'creator' => $creator->getName(),
1241  'expected_id' => $state['userid'],
1242  'actual_id' => $user->getId(),
1243  ] );
1244  throw new \UnexpectedValueException(
1245  "User \"{$state['username']}\" exists, but " .
1246  "ID {$user->getId()} != {$state['userid']}!"
1247  );
1248  }
1249  }
1250  foreach ( $state['reqs'] as $req ) {
1251  if ( $req instanceof UserDataAuthenticationRequest ) {
1252  $status = $req->populateUser( $user );
1253  if ( !$status->isGood() ) {
1254  // This should never happen...
1256  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1257  'user' => $user->getName(),
1258  'creator' => $creator->getName(),
1259  'reason' => $status->getWikiText( null, null, 'en' ),
1260  ] );
1261  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1262  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1263  $session->remove( 'AuthManager::accountCreationState' );
1264  return $ret;
1265  }
1266  }
1267  }
1268 
1269  foreach ( $reqs as $req ) {
1270  $req->returnToUrl = $state['returnToUrl'];
1271  $req->username = $state['username'];
1272  }
1273 
1274  // Run pre-creation tests, if we haven't already
1275  if ( !$state['ranPreTests'] ) {
1276  $providers = $this->getPreAuthenticationProviders() +
1279  foreach ( $providers as $id => $provider ) {
1280  $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1281  if ( !$status->isGood() ) {
1282  $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1283  'user' => $user->getName(),
1284  'creator' => $creator->getName(),
1285  ] );
1287  Status::wrap( $status )->getMessage()
1288  );
1289  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1290  $session->remove( 'AuthManager::accountCreationState' );
1291  return $ret;
1292  }
1293  }
1294 
1295  $state['ranPreTests'] = true;
1296  }
1297 
1298  // Step 1: Choose a primary authentication provider and call it until it succeeds.
1299 
1300  if ( $state['primary'] === null ) {
1301  // We haven't picked a PrimaryAuthenticationProvider yet
1302  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1303  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1304  continue;
1305  }
1306  $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1307  switch ( $res->status ) {
1309  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1310  'user' => $user->getName(),
1311  'creator' => $creator->getName(),
1312  ] );
1313  $state['primary'] = $id;
1314  $state['primaryResponse'] = $res;
1315  break 2;
1317  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1318  'user' => $user->getName(),
1319  'creator' => $creator->getName(),
1320  ] );
1321  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1322  $session->remove( 'AuthManager::accountCreationState' );
1323  return $res;
1325  // Continue loop
1326  break;
1329  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1330  'user' => $user->getName(),
1331  'creator' => $creator->getName(),
1332  ] );
1333  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1334  $state['primary'] = $id;
1335  $state['continueRequests'] = $res->neededRequests;
1336  $session->setSecret( 'AuthManager::accountCreationState', $state );
1337  return $res;
1338 
1339  // @codeCoverageIgnoreStart
1340  default:
1341  throw new \DomainException(
1342  get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1343  );
1344  // @codeCoverageIgnoreEnd
1345  }
1346  }
1347  if ( $state['primary'] === null ) {
1348  $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1349  'user' => $user->getName(),
1350  'creator' => $creator->getName(),
1351  ] );
1353  wfMessage( 'authmanager-create-no-primary' )
1354  );
1355  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1356  $session->remove( 'AuthManager::accountCreationState' );
1357  return $ret;
1358  }
1359  } elseif ( $state['primaryResponse'] === null ) {
1360  $provider = $this->getAuthenticationProvider( $state['primary'] );
1361  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1362  // Configuration changed? Force them to start over.
1363  // @codeCoverageIgnoreStart
1365  wfMessage( 'authmanager-create-not-in-progress' )
1366  );
1367  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1368  $session->remove( 'AuthManager::accountCreationState' );
1369  return $ret;
1370  // @codeCoverageIgnoreEnd
1371  }
1372  $id = $provider->getUniqueId();
1373  $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1374  switch ( $res->status ) {
1376  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1377  'user' => $user->getName(),
1378  'creator' => $creator->getName(),
1379  ] );
1380  $state['primaryResponse'] = $res;
1381  break;
1383  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1384  'user' => $user->getName(),
1385  'creator' => $creator->getName(),
1386  ] );
1387  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1388  $session->remove( 'AuthManager::accountCreationState' );
1389  return $res;
1392  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1393  'user' => $user->getName(),
1394  'creator' => $creator->getName(),
1395  ] );
1396  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1397  $state['continueRequests'] = $res->neededRequests;
1398  $session->setSecret( 'AuthManager::accountCreationState', $state );
1399  return $res;
1400  default:
1401  throw new \DomainException(
1402  get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1403  );
1404  }
1405  }
1406 
1407  // Step 2: Primary authentication succeeded, create the User object
1408  // and add the user locally.
1409 
1410  if ( $state['userid'] === 0 ) {
1411  $this->logger->info( 'Creating user {user} during account creation', [
1412  'user' => $user->getName(),
1413  'creator' => $creator->getName(),
1414  ] );
1415  $status = $user->addToDatabase();
1416  if ( !$status->isOK() ) {
1417  // @codeCoverageIgnoreStart
1418  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1419  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1420  $session->remove( 'AuthManager::accountCreationState' );
1421  return $ret;
1422  // @codeCoverageIgnoreEnd
1423  }
1424  $this->setDefaultUserOptions( $user, $creator->isAnon() );
1425  \Hooks::run( 'LocalUserCreated', [ $user, false ] );
1426  $user->saveSettings();
1427  $state['userid'] = $user->getId();
1428 
1429  // Update user count
1430  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1431 
1432  // Watch user's userpage and talk page
1433  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1434 
1435  // Inform the provider
1436  $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1437 
1438  // Log the creation
1439  if ( $this->config->get( 'NewUserLog' ) ) {
1440  $isAnon = $creator->isAnon();
1441  $logEntry = new \ManualLogEntry(
1442  'newusers',
1443  $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1444  );
1445  $logEntry->setPerformer( $isAnon ? $user : $creator );
1446  $logEntry->setTarget( $user->getUserPage() );
1450  );
1451  $logEntry->setComment( $req ? $req->reason : '' );
1452  $logEntry->setParameters( [
1453  '4::userid' => $user->getId(),
1454  ] );
1455  $logid = $logEntry->insert();
1456  $logEntry->publish( $logid );
1457  }
1458  }
1459 
1460  // Step 3: Iterate over all the secondary authentication providers.
1461 
1462  $beginReqs = $state['reqs'];
1463 
1464  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1465  if ( !isset( $state['secondary'][$id] ) ) {
1466  // This provider isn't started yet, so we pass it the set
1467  // of reqs from beginAuthentication instead of whatever
1468  // might have been used by a previous provider in line.
1469  $func = 'beginSecondaryAccountCreation';
1470  $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1471  } elseif ( !$state['secondary'][$id] ) {
1472  $func = 'continueSecondaryAccountCreation';
1473  $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1474  } else {
1475  continue;
1476  }
1477  switch ( $res->status ) {
1479  $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1480  'user' => $user->getName(),
1481  'creator' => $creator->getName(),
1482  ] );
1483  // fall through
1485  $state['secondary'][$id] = true;
1486  break;
1489  $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1490  'user' => $user->getName(),
1491  'creator' => $creator->getName(),
1492  ] );
1493  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1494  $state['secondary'][$id] = false;
1495  $state['continueRequests'] = $res->neededRequests;
1496  $session->setSecret( 'AuthManager::accountCreationState', $state );
1497  return $res;
1499  throw new \DomainException(
1500  get_class( $provider ) . "::{$func}() returned $res->status." .
1501  ' Secondary providers are not allowed to fail account creation, that' .
1502  ' should have been done via testForAccountCreation().'
1503  );
1504  // @codeCoverageIgnoreStart
1505  default:
1506  throw new \DomainException(
1507  get_class( $provider ) . "::{$func}() returned $res->status"
1508  );
1509  // @codeCoverageIgnoreEnd
1510  }
1511  }
1512 
1513  $id = $user->getId();
1514  $name = $user->getName();
1515  $req = new CreatedAccountAuthenticationRequest( $id, $name );
1517  $ret->loginRequest = $req;
1518  $this->createdAccountAuthenticationRequests[] = $req;
1519 
1520  $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1521  'user' => $user->getName(),
1522  'creator' => $creator->getName(),
1523  ] );
1524 
1525  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1526  $session->remove( 'AuthManager::accountCreationState' );
1527  $this->removeAuthenticationSessionData( null );
1528  return $ret;
1529  } catch ( \Exception $ex ) {
1530  $session->remove( 'AuthManager::accountCreationState' );
1531  throw $ex;
1532  }
1533  }
1534 
1550  public function autoCreateUser( User $user, $source, $login = true ) {
1551  if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1553  ) {
1554  throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1555  }
1556 
1557  $username = $user->getName();
1558 
1559  // Try the local user from the replica DB
1560  $localId = User::idFromName( $username );
1561  $flags = User::READ_NORMAL;
1562 
1563  // Fetch the user ID from the master, so that we don't try to create the user
1564  // when they already exist, due to replication lag
1565  // @codeCoverageIgnoreStart
1566  if (
1567  !$localId &&
1568  MediaWikiServices::getInstance()->getDBLoadBalancer()->getReaderIndex() != 0
1569  ) {
1570  $localId = User::idFromName( $username, User::READ_LATEST );
1571  $flags = User::READ_LATEST;
1572  }
1573  // @codeCoverageIgnoreEnd
1574 
1575  if ( $localId ) {
1576  $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1577  'username' => $username,
1578  ] );
1579  $user->setId( $localId );
1580  $user->loadFromId( $flags );
1581  if ( $login ) {
1582  $this->setSessionDataForUser( $user );
1583  }
1585  $status->warning( 'userexists' );
1586  return $status;
1587  }
1588 
1589  // Wiki is read-only?
1590  if ( wfReadOnly() ) {
1591  $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1592  'username' => $username,
1593  'reason' => wfReadOnlyReason(),
1594  ] );
1595  $user->setId( 0 );
1596  $user->loadFromId();
1597  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1598  }
1599 
1600  // Check the session, if we tried to create this user already there's
1601  // no point in retrying.
1602  $session = $this->request->getSession();
1603  if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1604  $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1605  'username' => $username,
1606  'sessionid' => $session->getId(),
1607  ] );
1608  $user->setId( 0 );
1609  $user->loadFromId();
1610  $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1611  if ( $reason instanceof StatusValue ) {
1612  return Status::wrap( $reason );
1613  } else {
1614  return Status::newFatal( $reason );
1615  }
1616  }
1617 
1618  // Is the username creatable?
1619  if ( !User::isCreatableName( $username ) ) {
1620  $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1621  'username' => $username,
1622  ] );
1623  $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1624  $user->setId( 0 );
1625  $user->loadFromId();
1626  return Status::newFatal( 'noname' );
1627  }
1628 
1629  // Is the IP user able to create accounts?
1630  $anon = new User;
1631  if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1632  $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1633  'username' => $username,
1634  'ip' => $anon->getName(),
1635  ] );
1636  $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1637  $session->persist();
1638  $user->setId( 0 );
1639  $user->loadFromId();
1640  return Status::newFatal( 'authmanager-autocreate-noperm' );
1641  }
1642 
1643  // Avoid account creation races on double submissions
1645  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1646  if ( !$lock ) {
1647  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1648  'user' => $username,
1649  ] );
1650  $user->setId( 0 );
1651  $user->loadFromId();
1652  return Status::newFatal( 'usernameinprogress' );
1653  }
1654 
1655  // Denied by providers?
1656  $options = [
1657  'flags' => User::READ_LATEST,
1658  'creating' => true,
1659  ];
1660  $providers = $this->getPreAuthenticationProviders() +
1663  foreach ( $providers as $provider ) {
1664  $status = $provider->testUserForCreation( $user, $source, $options );
1665  if ( !$status->isGood() ) {
1666  $ret = Status::wrap( $status );
1667  $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1668  'username' => $username,
1669  'reason' => $ret->getWikiText( null, null, 'en' ),
1670  ] );
1671  $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1672  $user->setId( 0 );
1673  $user->loadFromId();
1674  return $ret;
1675  }
1676  }
1677 
1678  $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1679  if ( $cache->get( $backoffKey ) ) {
1680  $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1681  'username' => $username,
1682  ] );
1683  $user->setId( 0 );
1684  $user->loadFromId();
1685  return Status::newFatal( 'authmanager-autocreate-exception' );
1686  }
1687 
1688  // Checks passed, create the user...
1689  $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
1690  $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1691  'username' => $username,
1692  'from' => $from,
1693  ] );
1694 
1695  // Ignore warnings about master connections/writes...hard to avoid here
1696  $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1697  $old = $trxProfiler->setSilenced( true );
1698  try {
1699  $status = $user->addToDatabase();
1700  if ( !$status->isOK() ) {
1701  // Double-check for a race condition (T70012). We make use of the fact that when
1702  // addToDatabase fails due to the user already existing, the user object gets loaded.
1703  if ( $user->getId() ) {
1704  $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1705  'username' => $username,
1706  ] );
1707  if ( $login ) {
1708  $this->setSessionDataForUser( $user );
1709  }
1711  $status->warning( 'userexists' );
1712  } else {
1713  $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1714  'username' => $username,
1715  'msg' => $status->getWikiText( null, null, 'en' )
1716  ] );
1717  $user->setId( 0 );
1718  $user->loadFromId();
1719  }
1720  return $status;
1721  }
1722  } catch ( \Exception $ex ) {
1723  $trxProfiler->setSilenced( $old );
1724  $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1725  'username' => $username,
1726  'exception' => $ex,
1727  ] );
1728  // Do not keep throwing errors for a while
1729  $cache->set( $backoffKey, 1, 600 );
1730  // Bubble up error; which should normally trigger DB rollbacks
1731  throw $ex;
1732  }
1733 
1734  $this->setDefaultUserOptions( $user, false );
1735 
1736  // Inform the providers
1737  $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1738 
1739  \Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1740  \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1741  $user->saveSettings();
1742 
1743  // Update user count
1744  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1745  // Watch user's userpage and talk page
1746  \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1747  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1748  } );
1749 
1750  // Log the creation
1751  if ( $this->config->get( 'NewUserLog' ) ) {
1752  $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1753  $logEntry->setPerformer( $user );
1754  $logEntry->setTarget( $user->getUserPage() );
1755  $logEntry->setComment( '' );
1756  $logEntry->setParameters( [
1757  '4::userid' => $user->getId(),
1758  ] );
1759  $logEntry->insert();
1760  }
1761 
1762  $trxProfiler->setSilenced( $old );
1763 
1764  if ( $login ) {
1765  $this->setSessionDataForUser( $user );
1766  }
1767 
1768  return Status::newGood();
1769  }
1770 
1782  public function canLinkAccounts() {
1783  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1784  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1785  return true;
1786  }
1787  }
1788  return false;
1789  }
1790 
1800  public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1801  $session = $this->request->getSession();
1802  $session->remove( 'AuthManager::accountLinkState' );
1803 
1804  if ( !$this->canLinkAccounts() ) {
1805  // Caller should have called canLinkAccounts()
1806  throw new \LogicException( 'Account linking is not possible' );
1807  }
1808 
1809  if ( $user->getId() === 0 ) {
1810  if ( !User::isUsableName( $user->getName() ) ) {
1811  $msg = wfMessage( 'noname' );
1812  } else {
1813  $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1814  }
1815  return AuthenticationResponse::newFail( $msg );
1816  }
1817  foreach ( $reqs as $req ) {
1818  $req->username = $user->getName();
1819  $req->returnToUrl = $returnToUrl;
1820  }
1821 
1822  $this->removeAuthenticationSessionData( null );
1823 
1824  $providers = $this->getPreAuthenticationProviders();
1825  foreach ( $providers as $id => $provider ) {
1826  $status = $provider->testForAccountLink( $user );
1827  if ( !$status->isGood() ) {
1828  $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1829  'user' => $user->getName(),
1830  ] );
1832  Status::wrap( $status )->getMessage()
1833  );
1834  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1835  return $ret;
1836  }
1837  }
1838 
1839  $state = [
1840  'username' => $user->getName(),
1841  'userid' => $user->getId(),
1842  'returnToUrl' => $returnToUrl,
1843  'primary' => null,
1844  'continueRequests' => [],
1845  ];
1846 
1847  $providers = $this->getPrimaryAuthenticationProviders();
1848  foreach ( $providers as $id => $provider ) {
1849  if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1850  continue;
1851  }
1852 
1853  $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1854  switch ( $res->status ) {
1856  $this->logger->info( "Account linked to {user} by $id", [
1857  'user' => $user->getName(),
1858  ] );
1859  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1860  return $res;
1861 
1863  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1864  'user' => $user->getName(),
1865  ] );
1866  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1867  return $res;
1868 
1870  // Continue loop
1871  break;
1872 
1875  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1876  'user' => $user->getName(),
1877  ] );
1878  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1879  $state['primary'] = $id;
1880  $state['continueRequests'] = $res->neededRequests;
1881  $session->setSecret( 'AuthManager::accountLinkState', $state );
1882  $session->persist();
1883  return $res;
1884 
1885  // @codeCoverageIgnoreStart
1886  default:
1887  throw new \DomainException(
1888  get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1889  );
1890  // @codeCoverageIgnoreEnd
1891  }
1892  }
1893 
1894  $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1895  'user' => $user->getName(),
1896  ] );
1898  wfMessage( 'authmanager-link-no-primary' )
1899  );
1900  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1901  return $ret;
1902  }
1903 
1909  public function continueAccountLink( array $reqs ) {
1910  $session = $this->request->getSession();
1911  try {
1912  if ( !$this->canLinkAccounts() ) {
1913  // Caller should have called canLinkAccounts()
1914  $session->remove( 'AuthManager::accountLinkState' );
1915  throw new \LogicException( 'Account linking is not possible' );
1916  }
1917 
1918  $state = $session->getSecret( 'AuthManager::accountLinkState' );
1919  if ( !is_array( $state ) ) {
1921  wfMessage( 'authmanager-link-not-in-progress' )
1922  );
1923  }
1924  $state['continueRequests'] = [];
1925 
1926  // Step 0: Prepare and validate the input
1927 
1928  $user = User::newFromName( $state['username'], 'usable' );
1929  if ( !is_object( $user ) ) {
1930  $session->remove( 'AuthManager::accountLinkState' );
1931  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1932  }
1933  if ( $user->getId() != $state['userid'] ) {
1934  throw new \UnexpectedValueException(
1935  "User \"{$state['username']}\" is valid, but " .
1936  "ID {$user->getId()} != {$state['userid']}!"
1937  );
1938  }
1939 
1940  foreach ( $reqs as $req ) {
1941  $req->username = $state['username'];
1942  $req->returnToUrl = $state['returnToUrl'];
1943  }
1944 
1945  // Step 1: Call the primary again until it succeeds
1946 
1947  $provider = $this->getAuthenticationProvider( $state['primary'] );
1948  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1949  // Configuration changed? Force them to start over.
1950  // @codeCoverageIgnoreStart
1952  wfMessage( 'authmanager-link-not-in-progress' )
1953  );
1954  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1955  $session->remove( 'AuthManager::accountLinkState' );
1956  return $ret;
1957  // @codeCoverageIgnoreEnd
1958  }
1959  $id = $provider->getUniqueId();
1960  $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1961  switch ( $res->status ) {
1963  $this->logger->info( "Account linked to {user} by $id", [
1964  'user' => $user->getName(),
1965  ] );
1966  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1967  $session->remove( 'AuthManager::accountLinkState' );
1968  return $res;
1970  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1971  'user' => $user->getName(),
1972  ] );
1973  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1974  $session->remove( 'AuthManager::accountLinkState' );
1975  return $res;
1978  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1979  'user' => $user->getName(),
1980  ] );
1981  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1982  $state['continueRequests'] = $res->neededRequests;
1983  $session->setSecret( 'AuthManager::accountLinkState', $state );
1984  return $res;
1985  default:
1986  throw new \DomainException(
1987  get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1988  );
1989  }
1990  } catch ( \Exception $ex ) {
1991  $session->remove( 'AuthManager::accountLinkState' );
1992  throw $ex;
1993  }
1994  }
1995 
2021  public function getAuthenticationRequests( $action, User $user = null ) {
2022  $options = [];
2023  $providerAction = $action;
2024 
2025  // Figure out which providers to query
2026  switch ( $action ) {
2027  case self::ACTION_LOGIN:
2028  case self::ACTION_CREATE:
2029  $providers = $this->getPreAuthenticationProviders() +
2032  break;
2033 
2034  case self::ACTION_LOGIN_CONTINUE:
2035  $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2036  return is_array( $state ) ? $state['continueRequests'] : [];
2037 
2038  case self::ACTION_CREATE_CONTINUE:
2039  $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2040  return is_array( $state ) ? $state['continueRequests'] : [];
2041 
2042  case self::ACTION_LINK:
2043  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2044  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2045  } );
2046  break;
2047 
2048  case self::ACTION_UNLINK:
2049  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2050  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2051  } );
2052 
2053  // To providers, unlink and remove are identical.
2054  $providerAction = self::ACTION_REMOVE;
2055  break;
2056 
2057  case self::ACTION_LINK_CONTINUE:
2058  $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2059  return is_array( $state ) ? $state['continueRequests'] : [];
2060 
2061  case self::ACTION_CHANGE:
2062  case self::ACTION_REMOVE:
2063  $providers = $this->getPrimaryAuthenticationProviders() +
2065  break;
2066 
2067  // @codeCoverageIgnoreStart
2068  default:
2069  throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2070  }
2071  // @codeCoverageIgnoreEnd
2072 
2073  return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2074  }
2075 
2086  $providerAction, array $options, array $providers, User $user = null
2087  ) {
2088  $user = $user ?: \RequestContext::getMain()->getUser();
2089  $options['username'] = $user->isAnon() ? null : $user->getName();
2090 
2091  // Query them and merge results
2092  $reqs = [];
2093  foreach ( $providers as $provider ) {
2094  $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2095  foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2096  $id = $req->getUniqueId();
2097 
2098  // If a required request if from a Primary, mark it as "primary-required" instead
2099  if ( $isPrimary ) {
2100  if ( $req->required ) {
2102  }
2103  }
2104 
2105  if (
2106  !isset( $reqs[$id] )
2107  || $req->required === AuthenticationRequest::REQUIRED
2108  || $reqs[$id] === AuthenticationRequest::OPTIONAL
2109  ) {
2110  $reqs[$id] = $req;
2111  }
2112  }
2113  }
2114 
2115  // AuthManager has its own req for some actions
2116  switch ( $providerAction ) {
2117  case self::ACTION_LOGIN:
2118  $reqs[] = new RememberMeAuthenticationRequest;
2119  break;
2120 
2121  case self::ACTION_CREATE:
2122  $reqs[] = new UsernameAuthenticationRequest;
2123  $reqs[] = new UserDataAuthenticationRequest;
2124  if ( $options['username'] !== null ) {
2126  $options['username'] = null; // Don't fill in the username below
2127  }
2128  break;
2129  }
2130 
2131  // Fill in reqs data
2132  $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2133 
2134  // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2135  if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2136  $reqs = array_filter( $reqs, function ( $req ) {
2137  return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2138  } );
2139  }
2140 
2141  return array_values( $reqs );
2142  }
2143 
2151  private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2152  foreach ( $reqs as $req ) {
2153  if ( !$req->action || $forceAction ) {
2154  $req->action = $action;
2155  }
2156  if ( $req->username === null ) {
2157  $req->username = $username;
2158  }
2159  }
2160  }
2161 
2168  public function userExists( $username, $flags = User::READ_NORMAL ) {
2169  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2170  if ( $provider->testUserExists( $username, $flags ) ) {
2171  return true;
2172  }
2173  }
2174 
2175  return false;
2176  }
2177 
2189  public function allowsPropertyChange( $property ) {
2190  $providers = $this->getPrimaryAuthenticationProviders() +
2192  foreach ( $providers as $provider ) {
2193  if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2194  return false;
2195  }
2196  }
2197  return true;
2198  }
2199 
2208  public function getAuthenticationProvider( $id ) {
2209  // Fast version
2210  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2211  return $this->allAuthenticationProviders[$id];
2212  }
2213 
2214  // Slow version: instantiate each kind and check
2215  $providers = $this->getPrimaryAuthenticationProviders();
2216  if ( isset( $providers[$id] ) ) {
2217  return $providers[$id];
2218  }
2219  $providers = $this->getSecondaryAuthenticationProviders();
2220  if ( isset( $providers[$id] ) ) {
2221  return $providers[$id];
2222  }
2223  $providers = $this->getPreAuthenticationProviders();
2224  if ( isset( $providers[$id] ) ) {
2225  return $providers[$id];
2226  }
2227 
2228  return null;
2229  }
2230 
2244  public function setAuthenticationSessionData( $key, $data ) {
2245  $session = $this->request->getSession();
2246  $arr = $session->getSecret( 'authData' );
2247  if ( !is_array( $arr ) ) {
2248  $arr = [];
2249  }
2250  $arr[$key] = $data;
2251  $session->setSecret( 'authData', $arr );
2252  }
2253 
2261  public function getAuthenticationSessionData( $key, $default = null ) {
2262  $arr = $this->request->getSession()->getSecret( 'authData' );
2263  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2264  return $arr[$key];
2265  } else {
2266  return $default;
2267  }
2268  }
2269 
2275  public function removeAuthenticationSessionData( $key ) {
2276  $session = $this->request->getSession();
2277  if ( $key === null ) {
2278  $session->remove( 'authData' );
2279  } else {
2280  $arr = $session->getSecret( 'authData' );
2281  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2282  unset( $arr[$key] );
2283  $session->setSecret( 'authData', $arr );
2284  }
2285  }
2286  }
2287 
2294  protected function providerArrayFromSpecs( $class, array $specs ) {
2295  $i = 0;
2296  foreach ( $specs as &$spec ) {
2297  $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2298  }
2299  unset( $spec );
2300  // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2301  usort( $specs, function ( $a, $b ) {
2302  return $a['sort'] <=> $b['sort']
2303  ?: $a['sort2'] <=> $b['sort2'];
2304  } );
2305 
2306  $ret = [];
2307  foreach ( $specs as $spec ) {
2308  $provider = ObjectFactory::getObjectFromSpec( $spec );
2309  if ( !$provider instanceof $class ) {
2310  throw new \RuntimeException(
2311  "Expected instance of $class, got " . get_class( $provider )
2312  );
2313  }
2314  $provider->setLogger( $this->logger );
2315  $provider->setManager( $this );
2316  $provider->setConfig( $this->config );
2317  $id = $provider->getUniqueId();
2318  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2319  throw new \RuntimeException(
2320  "Duplicate specifications for id $id (classes " .
2321  get_class( $provider ) . ' and ' .
2322  get_class( $this->allAuthenticationProviders[$id] ) . ')'
2323  );
2324  }
2325  $this->allAuthenticationProviders[$id] = $provider;
2326  $ret[$id] = $provider;
2327  }
2328  return $ret;
2329  }
2330 
2335  private function getConfiguration() {
2336  return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2337  }
2338 
2343  protected function getPreAuthenticationProviders() {
2344  if ( $this->preAuthenticationProviders === null ) {
2345  $conf = $this->getConfiguration();
2346  $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2347  PreAuthenticationProvider::class, $conf['preauth']
2348  );
2349  }
2351  }
2352 
2357  protected function getPrimaryAuthenticationProviders() {
2358  if ( $this->primaryAuthenticationProviders === null ) {
2359  $conf = $this->getConfiguration();
2360  $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2361  PrimaryAuthenticationProvider::class, $conf['primaryauth']
2362  );
2363  }
2365  }
2366 
2372  if ( $this->secondaryAuthenticationProviders === null ) {
2373  $conf = $this->getConfiguration();
2374  $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2375  SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2376  );
2377  }
2379  }
2380 
2386  private function setSessionDataForUser( $user, $remember = null ) {
2387  $session = $this->request->getSession();
2388  $delay = $session->delaySave();
2389 
2390  $session->resetId();
2391  $session->resetAllTokens();
2392  if ( $session->canSetUser() ) {
2393  $session->setUser( $user );
2394  }
2395  if ( $remember !== null ) {
2396  $session->setRememberUser( $remember );
2397  }
2398  $session->set( 'AuthManager:lastAuthId', $user->getId() );
2399  $session->set( 'AuthManager:lastAuthTimestamp', time() );
2400  $session->persist();
2401 
2402  \Wikimedia\ScopedCallback::consume( $delay );
2403 
2404  \Hooks::run( 'UserLoggedIn', [ $user ] );
2405  }
2406 
2411  private function setDefaultUserOptions( User $user, $useContextLang ) {
2412  $user->setToken();
2413 
2414  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2415 
2416  $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $contLang;
2417  $user->setOption( 'language', $lang->getPreferredVariant() );
2418 
2419  if ( $contLang->hasVariants() ) {
2420  $user->setOption( 'variant', $contLang->getPreferredVariant() );
2421  }
2422  }
2423 
2429  private function callMethodOnProviders( $which, $method, array $args ) {
2430  $providers = [];
2431  if ( $which & 1 ) {
2432  $providers += $this->getPreAuthenticationProviders();
2433  }
2434  if ( $which & 2 ) {
2435  $providers += $this->getPrimaryAuthenticationProviders();
2436  }
2437  if ( $which & 4 ) {
2438  $providers += $this->getSecondaryAuthenticationProviders();
2439  }
2440  foreach ( $providers as $provider ) {
2441  $provider->$method( ...$args );
2442  }
2443  }
2444 
2449  public static function resetCache() {
2450  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2451  // @codeCoverageIgnoreStart
2452  throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2453  // @codeCoverageIgnoreEnd
2454  }
2455 
2456  self::$instance = null;
2457  }
2458 
2461 }
2462 
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
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:3947
securitySensitiveOperationStatus( $operation)
Whether security-sensitive operations should proceed.
$property
const ABSTAIN
Indicates that the authentication provider does not handle this request.
const TYPE_RANGE
Definition: Block.php:93
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:4201
setId( $v)
Set the user and reload all fields according to a given ID.
Definition: User.php:2460
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:1996
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
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 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object '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:1277
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:3037
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:904
continueAccountCreation(array $reqs)
Continue an account creation flow.
Authentication request for the reason given for account creation.
$wgAuth $wgAuth
Authentication plugin.
A helper class for throttling authentication attempts.
setOption( $oname, $val)
Set the given option for a user.
Definition: User.php:3271
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition: User.php:1992
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:2469
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:47
canCreateAccount( $username, $options=[])
Determine whether a particular account can be created.
$last
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:68
loadFromId( $flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition: User.php:457
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition: User.php:4512
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
AuthenticationProvider [] $allAuthenticationProviders
Backwards-compatibility wrapper for AuthManager via $wgAuth.
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:1116
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)
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 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:77
$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:1996
getConfiguration()
Get the configuration.
const REQUIRED
Indicates that the request is required for authentication to proceed.
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition: User.php:1041
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:83
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:82
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:95
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:990
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:785
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:608
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:2444
addToDatabase()
Add this existing user object to the database.
Definition: User.php:4390
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:88
const ACTION_REMOVE
Remove a user&#39;s credentials.
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:4562
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 this hook should only be used to add variables that depend on the current page request
Definition: hooks.txt:2173
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:93
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:276
setDefaultUserOptions(User $user, $useContextLang)
const ACTION_CREATE
Create a new user.
Definition: AuthManager.php:90
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:585
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
Definition: AuthManager.php:85
const ACTION_LINK_CONTINUE
Continue a user linking process that was interrupted by the need for user input or communication with...
Definition: AuthManager.php:98
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:1487
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.