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