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 
245  public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
246  wfDeprecated( __METHOD__, '1.33' );
247  return $return;
248  }
249 
263  public function canAuthenticateNow() {
264  return $this->request->getSession()->canSetUser();
265  }
266 
285  public function beginAuthentication( array $reqs, $returnToUrl ) {
286  $session = $this->request->getSession();
287  if ( !$session->canSetUser() ) {
288  // Caller should have called canAuthenticateNow()
289  $session->remove( 'AuthManager::authnState' );
290  throw new \LogicException( 'Authentication is not possible now' );
291  }
292 
293  $guessUserName = null;
294  foreach ( $reqs as $req ) {
295  $req->returnToUrl = $returnToUrl;
296  // @codeCoverageIgnoreStart
297  if ( $req->username !== null && $req->username !== '' ) {
298  if ( $guessUserName === null ) {
299  $guessUserName = $req->username;
300  } elseif ( $guessUserName !== $req->username ) {
301  $guessUserName = null;
302  break;
303  }
304  }
305  // @codeCoverageIgnoreEnd
306  }
307 
308  // Check for special-case login of a just-created account
311  );
312  if ( $req ) {
313  if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
314  throw new \LogicException(
315  'CreatedAccountAuthenticationRequests are only valid on ' .
316  'the same AuthManager that created the account'
317  );
318  }
319 
320  $user = User::newFromName( $req->username );
321  // @codeCoverageIgnoreStart
322  if ( !$user ) {
323  throw new \UnexpectedValueException(
324  "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
325  );
326  } elseif ( $user->getId() != $req->id ) {
327  throw new \UnexpectedValueException(
328  "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
329  );
330  }
331  // @codeCoverageIgnoreEnd
332 
333  $this->logger->info( 'Logging in {user} after account creation', [
334  'user' => $user->getName(),
335  ] );
337  $this->setSessionDataForUser( $user );
338  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
339  $session->remove( 'AuthManager::authnState' );
340  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
341  return $ret;
342  }
343 
345 
346  foreach ( $this->getPreAuthenticationProviders() as $provider ) {
347  $status = $provider->testForAuthentication( $reqs );
348  if ( !$status->isGood() ) {
349  $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
351  Status::wrap( $status )->getMessage()
352  );
353  $this->callMethodOnProviders( 7, 'postAuthentication',
354  [ User::newFromName( $guessUserName ) ?: null, $ret ]
355  );
356  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName, [] ] );
357  return $ret;
358  }
359  }
360 
361  $state = [
362  'reqs' => $reqs,
363  'returnToUrl' => $returnToUrl,
364  'guessUserName' => $guessUserName,
365  'primary' => null,
366  'primaryResponse' => null,
367  'secondary' => [],
368  'maybeLink' => [],
369  'continueRequests' => [],
370  ];
371 
372  // Preserve state from a previous failed login
375  );
376  if ( $req ) {
377  $state['maybeLink'] = $req->maybeLink;
378  }
379 
380  $session = $this->request->getSession();
381  $session->setSecret( 'AuthManager::authnState', $state );
382  $session->persist();
383 
384  return $this->continueAuthentication( $reqs );
385  }
386 
409  public function continueAuthentication( array $reqs ) {
410  $session = $this->request->getSession();
411  try {
412  if ( !$session->canSetUser() ) {
413  // Caller should have called canAuthenticateNow()
414  // @codeCoverageIgnoreStart
415  throw new \LogicException( 'Authentication is not possible now' );
416  // @codeCoverageIgnoreEnd
417  }
418 
419  $state = $session->getSecret( 'AuthManager::authnState' );
420  if ( !is_array( $state ) ) {
422  wfMessage( 'authmanager-authn-not-in-progress' )
423  );
424  }
425  $state['continueRequests'] = [];
426 
427  $guessUserName = $state['guessUserName'];
428 
429  foreach ( $reqs as $req ) {
430  $req->returnToUrl = $state['returnToUrl'];
431  }
432 
433  // Step 1: Choose an primary authentication provider, and call it until it succeeds.
434 
435  if ( $state['primary'] === null ) {
436  // We haven't picked a PrimaryAuthenticationProvider yet
437  // @codeCoverageIgnoreStart
438  $guessUserName = null;
439  foreach ( $reqs as $req ) {
440  if ( $req->username !== null && $req->username !== '' ) {
441  if ( $guessUserName === null ) {
442  $guessUserName = $req->username;
443  } elseif ( $guessUserName !== $req->username ) {
444  $guessUserName = null;
445  break;
446  }
447  }
448  }
449  $state['guessUserName'] = $guessUserName;
450  // @codeCoverageIgnoreEnd
451  $state['reqs'] = $reqs;
452 
453  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
454  $res = $provider->beginPrimaryAuthentication( $reqs );
455  switch ( $res->status ) {
457  $state['primary'] = $id;
458  $state['primaryResponse'] = $res;
459  $this->logger->debug( "Primary login with $id succeeded" );
460  break 2;
462  $this->logger->debug( "Login failed in primary authentication by $id" );
463  if ( $res->createRequest || $state['maybeLink'] ) {
464  $res->createRequest = new CreateFromLoginAuthenticationRequest(
465  $res->createRequest, $state['maybeLink']
466  );
467  }
468  $this->callMethodOnProviders( 7, 'postAuthentication',
469  [ User::newFromName( $guessUserName ) ?: null, $res ]
470  );
471  $session->remove( 'AuthManager::authnState' );
472  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] );
473  return $res;
475  // Continue loop
476  break;
479  $this->logger->debug( "Primary login with $id returned $res->status" );
480  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
481  $state['primary'] = $id;
482  $state['continueRequests'] = $res->neededRequests;
483  $session->setSecret( 'AuthManager::authnState', $state );
484  return $res;
485 
486  // @codeCoverageIgnoreStart
487  default:
488  throw new \DomainException(
489  get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
490  );
491  // @codeCoverageIgnoreEnd
492  }
493  }
494  if ( $state['primary'] === null ) {
495  $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
497  wfMessage( 'authmanager-authn-no-primary' )
498  );
499  $this->callMethodOnProviders( 7, 'postAuthentication',
500  [ User::newFromName( $guessUserName ) ?: null, $ret ]
501  );
502  $session->remove( 'AuthManager::authnState' );
503  return $ret;
504  }
505  } elseif ( $state['primaryResponse'] === null ) {
506  $provider = $this->getAuthenticationProvider( $state['primary'] );
507  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
508  // Configuration changed? Force them to start over.
509  // @codeCoverageIgnoreStart
511  wfMessage( 'authmanager-authn-not-in-progress' )
512  );
513  $this->callMethodOnProviders( 7, 'postAuthentication',
514  [ User::newFromName( $guessUserName ) ?: null, $ret ]
515  );
516  $session->remove( 'AuthManager::authnState' );
517  return $ret;
518  // @codeCoverageIgnoreEnd
519  }
520  $id = $provider->getUniqueId();
521  $res = $provider->continuePrimaryAuthentication( $reqs );
522  switch ( $res->status ) {
524  $state['primaryResponse'] = $res;
525  $this->logger->debug( "Primary login with $id succeeded" );
526  break;
528  $this->logger->debug( "Login failed in primary authentication by $id" );
529  if ( $res->createRequest || $state['maybeLink'] ) {
530  $res->createRequest = new CreateFromLoginAuthenticationRequest(
531  $res->createRequest, $state['maybeLink']
532  );
533  }
534  $this->callMethodOnProviders( 7, 'postAuthentication',
535  [ User::newFromName( $guessUserName ) ?: null, $res ]
536  );
537  $session->remove( 'AuthManager::authnState' );
538  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] );
539  return $res;
542  $this->logger->debug( "Primary login with $id returned $res->status" );
543  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
544  $state['continueRequests'] = $res->neededRequests;
545  $session->setSecret( 'AuthManager::authnState', $state );
546  return $res;
547  default:
548  throw new \DomainException(
549  get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
550  );
551  }
552  }
553 
554  $res = $state['primaryResponse'];
555  if ( $res->username === null ) {
556  $provider = $this->getAuthenticationProvider( $state['primary'] );
557  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
558  // Configuration changed? Force them to start over.
559  // @codeCoverageIgnoreStart
561  wfMessage( 'authmanager-authn-not-in-progress' )
562  );
563  $this->callMethodOnProviders( 7, 'postAuthentication',
564  [ User::newFromName( $guessUserName ) ?: null, $ret ]
565  );
566  $session->remove( 'AuthManager::authnState' );
567  return $ret;
568  // @codeCoverageIgnoreEnd
569  }
570 
571  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
572  $res->linkRequest &&
573  // don't confuse the user with an incorrect message if linking is disabled
575  ) {
576  $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
577  $msg = 'authmanager-authn-no-local-user-link';
578  } else {
579  $msg = 'authmanager-authn-no-local-user';
580  }
581  $this->logger->debug(
582  "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
583  );
585  $ret->neededRequests = $this->getAuthenticationRequestsInternal(
586  self::ACTION_LOGIN,
587  [],
589  );
590  if ( $res->createRequest || $state['maybeLink'] ) {
591  $ret->createRequest = new CreateFromLoginAuthenticationRequest(
592  $res->createRequest, $state['maybeLink']
593  );
594  $ret->neededRequests[] = $ret->createRequest;
595  }
596  $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
597  $session->setSecret( 'AuthManager::authnState', [
598  'reqs' => [], // Will be filled in later
599  'primary' => null,
600  'primaryResponse' => null,
601  'secondary' => [],
602  'continueRequests' => $ret->neededRequests,
603  ] + $state );
604  return $ret;
605  }
606 
607  // Step 2: Primary authentication succeeded, create the User object
608  // (and add the user locally if necessary)
609 
610  $user = User::newFromName( $res->username, 'usable' );
611  if ( !$user ) {
612  $provider = $this->getAuthenticationProvider( $state['primary'] );
613  throw new \DomainException(
614  get_class( $provider ) . " returned an invalid username: {$res->username}"
615  );
616  }
617  if ( $user->getId() === 0 ) {
618  // User doesn't exist locally. Create it.
619  $this->logger->info( 'Auto-creating {user} on login', [
620  'user' => $user->getName(),
621  ] );
622  $status = $this->autoCreateUser( $user, $state['primary'], false );
623  if ( !$status->isGood() ) {
625  Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
626  );
627  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
628  $session->remove( 'AuthManager::authnState' );
629  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
630  return $ret;
631  }
632  }
633 
634  // Step 3: Iterate over all the secondary authentication providers.
635 
636  $beginReqs = $state['reqs'];
637 
638  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
639  if ( !isset( $state['secondary'][$id] ) ) {
640  // This provider isn't started yet, so we pass it the set
641  // of reqs from beginAuthentication instead of whatever
642  // might have been used by a previous provider in line.
643  $func = 'beginSecondaryAuthentication';
644  $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
645  } elseif ( !$state['secondary'][$id] ) {
646  $func = 'continueSecondaryAuthentication';
647  $res = $provider->continueSecondaryAuthentication( $user, $reqs );
648  } else {
649  continue;
650  }
651  switch ( $res->status ) {
653  $this->logger->debug( "Secondary login with $id succeeded" );
654  // fall through
656  $state['secondary'][$id] = true;
657  break;
659  $this->logger->debug( "Login failed in secondary authentication by $id" );
660  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
661  $session->remove( 'AuthManager::authnState' );
662  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName(), [] ] );
663  return $res;
666  $this->logger->debug( "Secondary login with $id returned " . $res->status );
667  $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
668  $state['secondary'][$id] = false;
669  $state['continueRequests'] = $res->neededRequests;
670  $session->setSecret( 'AuthManager::authnState', $state );
671  return $res;
672 
673  // @codeCoverageIgnoreStart
674  default:
675  throw new \DomainException(
676  get_class( $provider ) . "::{$func}() returned $res->status"
677  );
678  // @codeCoverageIgnoreEnd
679  }
680  }
681 
682  // Step 4: Authentication complete! Set the user in the session and
683  // clean up.
684 
685  $this->logger->info( 'Login for {user} succeeded from {clientip}', [
686  'user' => $user->getName(),
687  'clientip' => $this->request->getIP(),
688  ] );
692  );
693  $this->setSessionDataForUser( $user, $req && $req->rememberMe );
695  $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
696  $session->remove( 'AuthManager::authnState' );
698  \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
699  return $ret;
700  } catch ( \Exception $ex ) {
701  $session->remove( 'AuthManager::authnState' );
702  throw $ex;
703  }
704  }
705 
717  public function securitySensitiveOperationStatus( $operation ) {
718  $status = self::SEC_OK;
719 
720  $this->logger->debug( __METHOD__ . ": Checking $operation" );
721 
722  $session = $this->request->getSession();
723  $aId = $session->getUser()->getId();
724  if ( $aId === 0 ) {
725  // User isn't authenticated. DWIM?
726  $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
727  $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
728  return $status;
729  }
730 
731  if ( $session->canSetUser() ) {
732  $id = $session->get( 'AuthManager:lastAuthId' );
733  $last = $session->get( 'AuthManager:lastAuthTimestamp' );
734  if ( $id !== $aId || $last === null ) {
735  $timeSinceLogin = PHP_INT_MAX; // Forever ago
736  } else {
737  $timeSinceLogin = max( 0, time() - $last );
738  }
739 
740  $thresholds = $this->config->get( 'ReauthenticateTime' );
741  if ( isset( $thresholds[$operation] ) ) {
742  $threshold = $thresholds[$operation];
743  } elseif ( isset( $thresholds['default'] ) ) {
744  $threshold = $thresholds['default'];
745  } else {
746  throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
747  }
748 
749  if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
750  $status = self::SEC_REAUTH;
751  }
752  } else {
753  $timeSinceLogin = -1;
754 
755  $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
756  if ( isset( $pass[$operation] ) ) {
757  $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
758  } elseif ( isset( $pass['default'] ) ) {
759  $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
760  } else {
761  throw new \UnexpectedValueException(
762  '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
763  );
764  }
765  }
766 
767  \Hooks::run( 'SecuritySensitiveOperationStatus', [
768  &$status, $operation, $session, $timeSinceLogin
769  ] );
770 
771  // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
772  if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
773  $status = self::SEC_FAIL;
774  }
775 
776  $this->logger->info( __METHOD__ . ": $operation is $status for '{user}'",
777  [
778  'user' => $session->getUser()->getName(),
779  'clientip' => $this->getRequest()->getIP(),
780  ]
781  );
782 
783  return $status;
784  }
785 
795  public function userCanAuthenticate( $username ) {
796  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
797  if ( $provider->testUserCanAuthenticate( $username ) ) {
798  return true;
799  }
800  }
801  return false;
802  }
803 
818  public function normalizeUsername( $username ) {
819  $ret = [];
820  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
821  $normalized = $provider->providerNormalizeUsername( $username );
822  if ( $normalized !== null ) {
823  $ret[$normalized] = true;
824  }
825  }
826  return array_keys( $ret );
827  }
828 
843  public function revokeAccessForUser( $username ) {
844  $this->logger->info( 'Revoking access for {user}', [
845  'user' => $username,
846  ] );
847  $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
848  }
849 
859  public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
860  $any = false;
861  $providers = $this->getPrimaryAuthenticationProviders() +
863  foreach ( $providers as $provider ) {
864  $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
865  if ( !$status->isGood() ) {
866  return Status::wrap( $status );
867  }
868  $any = $any || $status->value !== 'ignored';
869  }
870  if ( !$any ) {
871  $status = Status::newGood( 'ignored' );
872  $status->warning( 'authmanager-change-not-supported' );
873  return $status;
874  }
875  return Status::newGood();
876  }
877 
895  public function changeAuthenticationData( AuthenticationRequest $req, $isAddition = false ) {
896  $this->logger->info( 'Changing authentication data for {user} class {what}', [
897  'user' => is_string( $req->username ) ? $req->username : '<no name>',
898  'what' => get_class( $req ),
899  ] );
900 
901  $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
902 
903  // When the main account's authentication data is changed, invalidate
904  // all BotPasswords too.
905  if ( !$isAddition ) {
907  }
908  }
909 
921  public function canCreateAccounts() {
922  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
923  switch ( $provider->accountCreationType() ) {
926  return true;
927  }
928  }
929  return false;
930  }
931 
940  public function canCreateAccount( $username, $options = [] ) {
941  // Back compat
942  if ( is_int( $options ) ) {
943  $options = [ 'flags' => $options ];
944  }
945  $options += [
946  'flags' => User::READ_NORMAL,
947  'creating' => false,
948  ];
949  $flags = $options['flags'];
950 
951  if ( !$this->canCreateAccounts() ) {
952  return Status::newFatal( 'authmanager-create-disabled' );
953  }
954 
955  if ( $this->userExists( $username, $flags ) ) {
956  return Status::newFatal( 'userexists' );
957  }
958 
959  $user = User::newFromName( $username, 'creatable' );
960  if ( !is_object( $user ) ) {
961  return Status::newFatal( 'noname' );
962  } else {
963  $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
964  if ( $user->getId() !== 0 ) {
965  return Status::newFatal( 'userexists' );
966  }
967  }
968 
969  // Denied by providers?
970  $providers = $this->getPreAuthenticationProviders() +
973  foreach ( $providers as $provider ) {
974  $status = $provider->testUserForCreation( $user, false, $options );
975  if ( !$status->isGood() ) {
976  return Status::wrap( $status );
977  }
978  }
979 
980  return Status::newGood();
981  }
982 
988  public function checkAccountCreatePermissions( User $creator ) {
989  // Wiki is read-only?
990  if ( wfReadOnly() ) {
991  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
992  }
993 
994  // This is awful, this permission check really shouldn't go through Title.
995  $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
996  ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
997  if ( $permErrors ) {
999  foreach ( $permErrors as $args ) {
1000  $status->fatal( ...$args );
1001  }
1002  return $status;
1003  }
1004 
1005  $block = $creator->isBlockedFromCreateAccount();
1006  if ( $block ) {
1007  $errorParams = [
1008  $block->getTarget(),
1009  $block->getReason() ?: wfMessage( 'blockednoreason' )->text(),
1010  $block->getByName()
1011  ];
1012 
1013  if ( $block->getType() === \Block::TYPE_RANGE ) {
1014  $errorMessage = 'cantcreateaccount-range-text';
1015  $errorParams[] = $this->getRequest()->getIP();
1016  } else {
1017  $errorMessage = 'cantcreateaccount-text';
1018  }
1019 
1020  return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1021  }
1022 
1023  $ip = $this->getRequest()->getIP();
1024  if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1025  return Status::newFatal( 'sorbs_create_account_reason' );
1026  }
1027 
1028  return Status::newGood();
1029  }
1030 
1050  public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1051  $session = $this->request->getSession();
1052  if ( !$this->canCreateAccounts() ) {
1053  // Caller should have called canCreateAccounts()
1054  $session->remove( 'AuthManager::accountCreationState' );
1055  throw new \LogicException( 'Account creation is not possible' );
1056  }
1057 
1058  try {
1060  } catch ( \UnexpectedValueException $ex ) {
1061  $username = null;
1062  }
1063  if ( $username === null ) {
1064  $this->logger->debug( __METHOD__ . ': No username provided' );
1065  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1066  }
1067 
1068  // Permissions check
1069  $status = $this->checkAccountCreatePermissions( $creator );
1070  if ( !$status->isGood() ) {
1071  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1072  'user' => $username,
1073  'creator' => $creator->getName(),
1074  'reason' => $status->getWikiText( null, null, 'en' )
1075  ] );
1076  return AuthenticationResponse::newFail( $status->getMessage() );
1077  }
1078 
1079  $status = $this->canCreateAccount(
1080  $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1081  );
1082  if ( !$status->isGood() ) {
1083  $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1084  'user' => $username,
1085  'creator' => $creator->getName(),
1086  'reason' => $status->getWikiText( null, null, 'en' )
1087  ] );
1088  return AuthenticationResponse::newFail( $status->getMessage() );
1089  }
1090 
1091  $user = User::newFromName( $username, 'creatable' );
1092  foreach ( $reqs as $req ) {
1093  $req->username = $username;
1094  $req->returnToUrl = $returnToUrl;
1095  if ( $req instanceof UserDataAuthenticationRequest ) {
1096  $status = $req->populateUser( $user );
1097  if ( !$status->isGood() ) {
1098  $status = Status::wrap( $status );
1099  $session->remove( 'AuthManager::accountCreationState' );
1100  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1101  'user' => $user->getName(),
1102  'creator' => $creator->getName(),
1103  'reason' => $status->getWikiText( null, null, 'en' ),
1104  ] );
1105  return AuthenticationResponse::newFail( $status->getMessage() );
1106  }
1107  }
1108  }
1109 
1111 
1112  $state = [
1113  'username' => $username,
1114  'userid' => 0,
1115  'creatorid' => $creator->getId(),
1116  'creatorname' => $creator->getName(),
1117  'reqs' => $reqs,
1118  'returnToUrl' => $returnToUrl,
1119  'primary' => null,
1120  'primaryResponse' => null,
1121  'secondary' => [],
1122  'continueRequests' => [],
1123  'maybeLink' => [],
1124  'ranPreTests' => false,
1125  ];
1126 
1127  // Special case: converting a login to an account creation
1130  );
1131  if ( $req ) {
1132  $state['maybeLink'] = $req->maybeLink;
1133 
1134  if ( $req->createRequest ) {
1135  $reqs[] = $req->createRequest;
1136  $state['reqs'][] = $req->createRequest;
1137  }
1138  }
1139 
1140  $session->setSecret( 'AuthManager::accountCreationState', $state );
1141  $session->persist();
1142 
1143  return $this->continueAccountCreation( $reqs );
1144  }
1145 
1151  public function continueAccountCreation( array $reqs ) {
1152  $session = $this->request->getSession();
1153  try {
1154  if ( !$this->canCreateAccounts() ) {
1155  // Caller should have called canCreateAccounts()
1156  $session->remove( 'AuthManager::accountCreationState' );
1157  throw new \LogicException( 'Account creation is not possible' );
1158  }
1159 
1160  $state = $session->getSecret( 'AuthManager::accountCreationState' );
1161  if ( !is_array( $state ) ) {
1163  wfMessage( 'authmanager-create-not-in-progress' )
1164  );
1165  }
1166  $state['continueRequests'] = [];
1167 
1168  // Step 0: Prepare and validate the input
1169 
1170  $user = User::newFromName( $state['username'], 'creatable' );
1171  if ( !is_object( $user ) ) {
1172  $session->remove( 'AuthManager::accountCreationState' );
1173  $this->logger->debug( __METHOD__ . ': Invalid username', [
1174  'user' => $state['username'],
1175  ] );
1176  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1177  }
1178 
1179  if ( $state['creatorid'] ) {
1180  $creator = User::newFromId( $state['creatorid'] );
1181  } else {
1182  $creator = new User;
1183  $creator->setName( $state['creatorname'] );
1184  }
1185 
1186  // Avoid account creation races on double submissions
1188  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1189  if ( !$lock ) {
1190  // Don't clear AuthManager::accountCreationState for this code
1191  // path because the process that won the race owns it.
1192  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1193  'user' => $user->getName(),
1194  'creator' => $creator->getName(),
1195  ] );
1196  return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1197  }
1198 
1199  // Permissions check
1200  $status = $this->checkAccountCreatePermissions( $creator );
1201  if ( !$status->isGood() ) {
1202  $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1203  'user' => $user->getName(),
1204  'creator' => $creator->getName(),
1205  'reason' => $status->getWikiText( null, null, 'en' )
1206  ] );
1207  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1208  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1209  $session->remove( 'AuthManager::accountCreationState' );
1210  return $ret;
1211  }
1212 
1213  // Load from master for existence check
1214  $user->load( User::READ_LOCKING );
1215 
1216  if ( $state['userid'] === 0 ) {
1217  if ( $user->getId() !== 0 ) {
1218  $this->logger->debug( __METHOD__ . ': User exists locally', [
1219  'user' => $user->getName(),
1220  'creator' => $creator->getName(),
1221  ] );
1222  $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1223  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1224  $session->remove( 'AuthManager::accountCreationState' );
1225  return $ret;
1226  }
1227  } else {
1228  if ( $user->getId() === 0 ) {
1229  $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1230  'user' => $user->getName(),
1231  'creator' => $creator->getName(),
1232  'expected_id' => $state['userid'],
1233  ] );
1234  throw new \UnexpectedValueException(
1235  "User \"{$state['username']}\" should exist now, but doesn't!"
1236  );
1237  }
1238  if ( $user->getId() !== $state['userid'] ) {
1239  $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1240  'user' => $user->getName(),
1241  'creator' => $creator->getName(),
1242  'expected_id' => $state['userid'],
1243  'actual_id' => $user->getId(),
1244  ] );
1245  throw new \UnexpectedValueException(
1246  "User \"{$state['username']}\" exists, but " .
1247  "ID {$user->getId()} !== {$state['userid']}!"
1248  );
1249  }
1250  }
1251  foreach ( $state['reqs'] as $req ) {
1252  if ( $req instanceof UserDataAuthenticationRequest ) {
1253  $status = $req->populateUser( $user );
1254  if ( !$status->isGood() ) {
1255  // This should never happen...
1257  $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1258  'user' => $user->getName(),
1259  'creator' => $creator->getName(),
1260  'reason' => $status->getWikiText( null, null, 'en' ),
1261  ] );
1262  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1263  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1264  $session->remove( 'AuthManager::accountCreationState' );
1265  return $ret;
1266  }
1267  }
1268  }
1269 
1270  foreach ( $reqs as $req ) {
1271  $req->returnToUrl = $state['returnToUrl'];
1272  $req->username = $state['username'];
1273  }
1274 
1275  // Run pre-creation tests, if we haven't already
1276  if ( !$state['ranPreTests'] ) {
1277  $providers = $this->getPreAuthenticationProviders() +
1280  foreach ( $providers as $id => $provider ) {
1281  $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1282  if ( !$status->isGood() ) {
1283  $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1284  'user' => $user->getName(),
1285  'creator' => $creator->getName(),
1286  ] );
1288  Status::wrap( $status )->getMessage()
1289  );
1290  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1291  $session->remove( 'AuthManager::accountCreationState' );
1292  return $ret;
1293  }
1294  }
1295 
1296  $state['ranPreTests'] = true;
1297  }
1298 
1299  // Step 1: Choose a primary authentication provider and call it until it succeeds.
1300 
1301  if ( $state['primary'] === null ) {
1302  // We haven't picked a PrimaryAuthenticationProvider yet
1303  foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1304  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1305  continue;
1306  }
1307  $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1308  switch ( $res->status ) {
1310  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1311  'user' => $user->getName(),
1312  'creator' => $creator->getName(),
1313  ] );
1314  $state['primary'] = $id;
1315  $state['primaryResponse'] = $res;
1316  break 2;
1318  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1319  'user' => $user->getName(),
1320  'creator' => $creator->getName(),
1321  ] );
1322  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1323  $session->remove( 'AuthManager::accountCreationState' );
1324  return $res;
1326  // Continue loop
1327  break;
1330  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1331  'user' => $user->getName(),
1332  'creator' => $creator->getName(),
1333  ] );
1334  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1335  $state['primary'] = $id;
1336  $state['continueRequests'] = $res->neededRequests;
1337  $session->setSecret( 'AuthManager::accountCreationState', $state );
1338  return $res;
1339 
1340  // @codeCoverageIgnoreStart
1341  default:
1342  throw new \DomainException(
1343  get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1344  );
1345  // @codeCoverageIgnoreEnd
1346  }
1347  }
1348  if ( $state['primary'] === null ) {
1349  $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1350  'user' => $user->getName(),
1351  'creator' => $creator->getName(),
1352  ] );
1354  wfMessage( 'authmanager-create-no-primary' )
1355  );
1356  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1357  $session->remove( 'AuthManager::accountCreationState' );
1358  return $ret;
1359  }
1360  } elseif ( $state['primaryResponse'] === null ) {
1361  $provider = $this->getAuthenticationProvider( $state['primary'] );
1362  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1363  // Configuration changed? Force them to start over.
1364  // @codeCoverageIgnoreStart
1366  wfMessage( 'authmanager-create-not-in-progress' )
1367  );
1368  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1369  $session->remove( 'AuthManager::accountCreationState' );
1370  return $ret;
1371  // @codeCoverageIgnoreEnd
1372  }
1373  $id = $provider->getUniqueId();
1374  $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1375  switch ( $res->status ) {
1377  $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1378  'user' => $user->getName(),
1379  'creator' => $creator->getName(),
1380  ] );
1381  $state['primaryResponse'] = $res;
1382  break;
1384  $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1385  'user' => $user->getName(),
1386  'creator' => $creator->getName(),
1387  ] );
1388  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1389  $session->remove( 'AuthManager::accountCreationState' );
1390  return $res;
1393  $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1394  'user' => $user->getName(),
1395  'creator' => $creator->getName(),
1396  ] );
1397  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1398  $state['continueRequests'] = $res->neededRequests;
1399  $session->setSecret( 'AuthManager::accountCreationState', $state );
1400  return $res;
1401  default:
1402  throw new \DomainException(
1403  get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1404  );
1405  }
1406  }
1407 
1408  // Step 2: Primary authentication succeeded, create the User object
1409  // and add the user locally.
1410 
1411  if ( $state['userid'] === 0 ) {
1412  $this->logger->info( 'Creating user {user} during account creation', [
1413  'user' => $user->getName(),
1414  'creator' => $creator->getName(),
1415  ] );
1416  $status = $user->addToDatabase();
1417  if ( !$status->isOK() ) {
1418  // @codeCoverageIgnoreStart
1419  $ret = AuthenticationResponse::newFail( $status->getMessage() );
1420  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1421  $session->remove( 'AuthManager::accountCreationState' );
1422  return $ret;
1423  // @codeCoverageIgnoreEnd
1424  }
1425  $this->setDefaultUserOptions( $user, $creator->isAnon() );
1426  \Hooks::runWithoutAbort( 'LocalUserCreated', [ $user, false ] );
1427  $user->saveSettings();
1428  $state['userid'] = $user->getId();
1429 
1430  // Update user count
1431  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1432 
1433  // Watch user's userpage and talk page
1434  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1435 
1436  // Inform the provider
1437  $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1438 
1439  // Log the creation
1440  if ( $this->config->get( 'NewUserLog' ) ) {
1441  $isAnon = $creator->isAnon();
1442  $logEntry = new \ManualLogEntry(
1443  'newusers',
1444  $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1445  );
1446  $logEntry->setPerformer( $isAnon ? $user : $creator );
1447  $logEntry->setTarget( $user->getUserPage() );
1451  );
1452  $logEntry->setComment( $req ? $req->reason : '' );
1453  $logEntry->setParameters( [
1454  '4::userid' => $user->getId(),
1455  ] );
1456  $logid = $logEntry->insert();
1457  $logEntry->publish( $logid );
1458  }
1459  }
1460 
1461  // Step 3: Iterate over all the secondary authentication providers.
1462 
1463  $beginReqs = $state['reqs'];
1464 
1465  foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1466  if ( !isset( $state['secondary'][$id] ) ) {
1467  // This provider isn't started yet, so we pass it the set
1468  // of reqs from beginAuthentication instead of whatever
1469  // might have been used by a previous provider in line.
1470  $func = 'beginSecondaryAccountCreation';
1471  $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1472  } elseif ( !$state['secondary'][$id] ) {
1473  $func = 'continueSecondaryAccountCreation';
1474  $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1475  } else {
1476  continue;
1477  }
1478  switch ( $res->status ) {
1480  $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1481  'user' => $user->getName(),
1482  'creator' => $creator->getName(),
1483  ] );
1484  // fall through
1486  $state['secondary'][$id] = true;
1487  break;
1490  $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1491  'user' => $user->getName(),
1492  'creator' => $creator->getName(),
1493  ] );
1494  $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1495  $state['secondary'][$id] = false;
1496  $state['continueRequests'] = $res->neededRequests;
1497  $session->setSecret( 'AuthManager::accountCreationState', $state );
1498  return $res;
1500  throw new \DomainException(
1501  get_class( $provider ) . "::{$func}() returned $res->status." .
1502  ' Secondary providers are not allowed to fail account creation, that' .
1503  ' should have been done via testForAccountCreation().'
1504  );
1505  // @codeCoverageIgnoreStart
1506  default:
1507  throw new \DomainException(
1508  get_class( $provider ) . "::{$func}() returned $res->status"
1509  );
1510  // @codeCoverageIgnoreEnd
1511  }
1512  }
1513 
1514  $id = $user->getId();
1515  $name = $user->getName();
1516  $req = new CreatedAccountAuthenticationRequest( $id, $name );
1518  $ret->loginRequest = $req;
1519  $this->createdAccountAuthenticationRequests[] = $req;
1520 
1521  $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1522  'user' => $user->getName(),
1523  'creator' => $creator->getName(),
1524  ] );
1525 
1526  $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1527  $session->remove( 'AuthManager::accountCreationState' );
1529  return $ret;
1530  } catch ( \Exception $ex ) {
1531  $session->remove( 'AuthManager::accountCreationState' );
1532  throw $ex;
1533  }
1534  }
1535 
1553  public function autoCreateUser( User $user, $source, $login = true ) {
1554  if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1555  $source !== self::AUTOCREATE_SOURCE_MAINT &&
1557  ) {
1558  throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1559  }
1560 
1561  $username = $user->getName();
1562 
1563  // Try the local user from the replica DB
1564  $localId = User::idFromName( $username );
1565  $flags = User::READ_NORMAL;
1566 
1567  // Fetch the user ID from the master, so that we don't try to create the user
1568  // when they already exist, due to replication lag
1569  // @codeCoverageIgnoreStart
1570  if (
1571  !$localId &&
1572  MediaWikiServices::getInstance()->getDBLoadBalancer()->getReaderIndex() !== 0
1573  ) {
1574  $localId = User::idFromName( $username, User::READ_LATEST );
1575  $flags = User::READ_LATEST;
1576  }
1577  // @codeCoverageIgnoreEnd
1578 
1579  if ( $localId ) {
1580  $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1581  'username' => $username,
1582  ] );
1583  $user->setId( $localId );
1584  $user->loadFromId( $flags );
1585  if ( $login ) {
1586  $this->setSessionDataForUser( $user );
1587  }
1589  $status->warning( 'userexists' );
1590  return $status;
1591  }
1592 
1593  // Wiki is read-only?
1594  if ( wfReadOnly() ) {
1595  $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1596  'username' => $username,
1597  'reason' => wfReadOnlyReason(),
1598  ] );
1599  $user->setId( 0 );
1600  $user->loadFromId();
1601  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1602  }
1603 
1604  // Check the session, if we tried to create this user already there's
1605  // no point in retrying.
1606  $session = $this->request->getSession();
1607  if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1608  $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1609  'username' => $username,
1610  'sessionid' => $session->getId(),
1611  ] );
1612  $user->setId( 0 );
1613  $user->loadFromId();
1614  $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1615  if ( $reason instanceof StatusValue ) {
1616  return Status::wrap( $reason );
1617  } else {
1618  return Status::newFatal( $reason );
1619  }
1620  }
1621 
1622  // Is the username creatable?
1623  if ( !User::isCreatableName( $username ) ) {
1624  $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1625  'username' => $username,
1626  ] );
1627  $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1628  $user->setId( 0 );
1629  $user->loadFromId();
1630  return Status::newFatal( 'noname' );
1631  }
1632 
1633  // Is the IP user able to create accounts?
1634  $anon = new User;
1635  if ( $source !== self::AUTOCREATE_SOURCE_MAINT &&
1636  !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
1637  ) {
1638  $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1639  'username' => $username,
1640  'ip' => $anon->getName(),
1641  ] );
1642  $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1643  $session->persist();
1644  $user->setId( 0 );
1645  $user->loadFromId();
1646  return Status::newFatal( 'authmanager-autocreate-noperm' );
1647  }
1648 
1649  // Avoid account creation races on double submissions
1651  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1652  if ( !$lock ) {
1653  $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1654  'user' => $username,
1655  ] );
1656  $user->setId( 0 );
1657  $user->loadFromId();
1658  return Status::newFatal( 'usernameinprogress' );
1659  }
1660 
1661  // Denied by providers?
1662  $options = [
1663  'flags' => User::READ_LATEST,
1664  'creating' => true,
1665  ];
1666  $providers = $this->getPreAuthenticationProviders() +
1669  foreach ( $providers as $provider ) {
1670  $status = $provider->testUserForCreation( $user, $source, $options );
1671  if ( !$status->isGood() ) {
1672  $ret = Status::wrap( $status );
1673  $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1674  'username' => $username,
1675  'reason' => $ret->getWikiText( null, null, 'en' ),
1676  ] );
1677  $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1678  $user->setId( 0 );
1679  $user->loadFromId();
1680  return $ret;
1681  }
1682  }
1683 
1684  $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1685  if ( $cache->get( $backoffKey ) ) {
1686  $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1687  'username' => $username,
1688  ] );
1689  $user->setId( 0 );
1690  $user->loadFromId();
1691  return Status::newFatal( 'authmanager-autocreate-exception' );
1692  }
1693 
1694  // Checks passed, create the user...
1695  $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
1696  $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1697  'username' => $username,
1698  'from' => $from,
1699  ] );
1700 
1701  // Ignore warnings about master connections/writes...hard to avoid here
1702  $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1703  $old = $trxProfiler->setSilenced( true );
1704  try {
1705  $status = $user->addToDatabase();
1706  if ( !$status->isOK() ) {
1707  // Double-check for a race condition (T70012). We make use of the fact that when
1708  // addToDatabase fails due to the user already existing, the user object gets loaded.
1709  if ( $user->getId() ) {
1710  $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1711  'username' => $username,
1712  ] );
1713  if ( $login ) {
1714  $this->setSessionDataForUser( $user );
1715  }
1717  $status->warning( 'userexists' );
1718  } else {
1719  $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1720  'username' => $username,
1721  'msg' => $status->getWikiText( null, null, 'en' )
1722  ] );
1723  $user->setId( 0 );
1724  $user->loadFromId();
1725  }
1726  return $status;
1727  }
1728  } catch ( \Exception $ex ) {
1729  $trxProfiler->setSilenced( $old );
1730  $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1731  'username' => $username,
1732  'exception' => $ex,
1733  ] );
1734  // Do not keep throwing errors for a while
1735  $cache->set( $backoffKey, 1, 600 );
1736  // Bubble up error; which should normally trigger DB rollbacks
1737  throw $ex;
1738  }
1739 
1740  $this->setDefaultUserOptions( $user, false );
1741 
1742  // Inform the providers
1743  $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1744 
1745  \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1746  $user->saveSettings();
1747 
1748  // Update user count
1749  \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1750  // Watch user's userpage and talk page
1751  \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1752  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1753  } );
1754 
1755  // Log the creation
1756  if ( $this->config->get( 'NewUserLog' ) ) {
1757  $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1758  $logEntry->setPerformer( $user );
1759  $logEntry->setTarget( $user->getUserPage() );
1760  $logEntry->setComment( '' );
1761  $logEntry->setParameters( [
1762  '4::userid' => $user->getId(),
1763  ] );
1764  $logEntry->insert();
1765  }
1766 
1767  $trxProfiler->setSilenced( $old );
1768 
1769  if ( $login ) {
1770  $this->setSessionDataForUser( $user );
1771  }
1772 
1773  return Status::newGood();
1774  }
1775 
1787  public function canLinkAccounts() {
1788  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1789  if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1790  return true;
1791  }
1792  }
1793  return false;
1794  }
1795 
1805  public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1806  $session = $this->request->getSession();
1807  $session->remove( 'AuthManager::accountLinkState' );
1808 
1809  if ( !$this->canLinkAccounts() ) {
1810  // Caller should have called canLinkAccounts()
1811  throw new \LogicException( 'Account linking is not possible' );
1812  }
1813 
1814  if ( $user->getId() === 0 ) {
1815  if ( !User::isUsableName( $user->getName() ) ) {
1816  $msg = wfMessage( 'noname' );
1817  } else {
1818  $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1819  }
1820  return AuthenticationResponse::newFail( $msg );
1821  }
1822  foreach ( $reqs as $req ) {
1823  $req->username = $user->getName();
1824  $req->returnToUrl = $returnToUrl;
1825  }
1826 
1828 
1829  $providers = $this->getPreAuthenticationProviders();
1830  foreach ( $providers as $id => $provider ) {
1831  $status = $provider->testForAccountLink( $user );
1832  if ( !$status->isGood() ) {
1833  $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1834  'user' => $user->getName(),
1835  ] );
1837  Status::wrap( $status )->getMessage()
1838  );
1839  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1840  return $ret;
1841  }
1842  }
1843 
1844  $state = [
1845  'username' => $user->getName(),
1846  'userid' => $user->getId(),
1847  'returnToUrl' => $returnToUrl,
1848  'primary' => null,
1849  'continueRequests' => [],
1850  ];
1851 
1852  $providers = $this->getPrimaryAuthenticationProviders();
1853  foreach ( $providers as $id => $provider ) {
1854  if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1855  continue;
1856  }
1857 
1858  $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1859  switch ( $res->status ) {
1861  $this->logger->info( "Account linked to {user} by $id", [
1862  'user' => $user->getName(),
1863  ] );
1864  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1865  return $res;
1866 
1868  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1869  'user' => $user->getName(),
1870  ] );
1871  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1872  return $res;
1873 
1875  // Continue loop
1876  break;
1877 
1880  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1881  'user' => $user->getName(),
1882  ] );
1883  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1884  $state['primary'] = $id;
1885  $state['continueRequests'] = $res->neededRequests;
1886  $session->setSecret( 'AuthManager::accountLinkState', $state );
1887  $session->persist();
1888  return $res;
1889 
1890  // @codeCoverageIgnoreStart
1891  default:
1892  throw new \DomainException(
1893  get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1894  );
1895  // @codeCoverageIgnoreEnd
1896  }
1897  }
1898 
1899  $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1900  'user' => $user->getName(),
1901  ] );
1903  wfMessage( 'authmanager-link-no-primary' )
1904  );
1905  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1906  return $ret;
1907  }
1908 
1914  public function continueAccountLink( array $reqs ) {
1915  $session = $this->request->getSession();
1916  try {
1917  if ( !$this->canLinkAccounts() ) {
1918  // Caller should have called canLinkAccounts()
1919  $session->remove( 'AuthManager::accountLinkState' );
1920  throw new \LogicException( 'Account linking is not possible' );
1921  }
1922 
1923  $state = $session->getSecret( 'AuthManager::accountLinkState' );
1924  if ( !is_array( $state ) ) {
1926  wfMessage( 'authmanager-link-not-in-progress' )
1927  );
1928  }
1929  $state['continueRequests'] = [];
1930 
1931  // Step 0: Prepare and validate the input
1932 
1933  $user = User::newFromName( $state['username'], 'usable' );
1934  if ( !is_object( $user ) ) {
1935  $session->remove( 'AuthManager::accountLinkState' );
1936  return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1937  }
1938  if ( $user->getId() !== $state['userid'] ) {
1939  throw new \UnexpectedValueException(
1940  "User \"{$state['username']}\" is valid, but " .
1941  "ID {$user->getId()} !== {$state['userid']}!"
1942  );
1943  }
1944 
1945  foreach ( $reqs as $req ) {
1946  $req->username = $state['username'];
1947  $req->returnToUrl = $state['returnToUrl'];
1948  }
1949 
1950  // Step 1: Call the primary again until it succeeds
1951 
1952  $provider = $this->getAuthenticationProvider( $state['primary'] );
1953  if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1954  // Configuration changed? Force them to start over.
1955  // @codeCoverageIgnoreStart
1957  wfMessage( 'authmanager-link-not-in-progress' )
1958  );
1959  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1960  $session->remove( 'AuthManager::accountLinkState' );
1961  return $ret;
1962  // @codeCoverageIgnoreEnd
1963  }
1964  $id = $provider->getUniqueId();
1965  $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1966  switch ( $res->status ) {
1968  $this->logger->info( "Account linked to {user} by $id", [
1969  'user' => $user->getName(),
1970  ] );
1971  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1972  $session->remove( 'AuthManager::accountLinkState' );
1973  return $res;
1975  $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1976  'user' => $user->getName(),
1977  ] );
1978  $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1979  $session->remove( 'AuthManager::accountLinkState' );
1980  return $res;
1983  $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1984  'user' => $user->getName(),
1985  ] );
1986  $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1987  $state['continueRequests'] = $res->neededRequests;
1988  $session->setSecret( 'AuthManager::accountLinkState', $state );
1989  return $res;
1990  default:
1991  throw new \DomainException(
1992  get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1993  );
1994  }
1995  } catch ( \Exception $ex ) {
1996  $session->remove( 'AuthManager::accountLinkState' );
1997  throw $ex;
1998  }
1999  }
2000 
2027  $options = [];
2028  $providerAction = $action;
2029 
2030  // Figure out which providers to query
2031  switch ( $action ) {
2032  case self::ACTION_LOGIN:
2033  case self::ACTION_CREATE:
2034  $providers = $this->getPreAuthenticationProviders() +
2037  break;
2038 
2039  case self::ACTION_LOGIN_CONTINUE:
2040  $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2041  return is_array( $state ) ? $state['continueRequests'] : [];
2042 
2043  case self::ACTION_CREATE_CONTINUE:
2044  $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2045  return is_array( $state ) ? $state['continueRequests'] : [];
2046 
2047  case self::ACTION_LINK:
2048  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2049  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2050  } );
2051  break;
2052 
2053  case self::ACTION_UNLINK:
2054  $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2055  return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2056  } );
2057 
2058  // To providers, unlink and remove are identical.
2059  $providerAction = self::ACTION_REMOVE;
2060  break;
2061 
2062  case self::ACTION_LINK_CONTINUE:
2063  $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2064  return is_array( $state ) ? $state['continueRequests'] : [];
2065 
2066  case self::ACTION_CHANGE:
2067  case self::ACTION_REMOVE:
2068  $providers = $this->getPrimaryAuthenticationProviders() +
2070  break;
2071 
2072  // @codeCoverageIgnoreStart
2073  default:
2074  throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2075  }
2076  // @codeCoverageIgnoreEnd
2077 
2078  return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2079  }
2080 
2091  $providerAction, array $options, array $providers, User $user = null
2092  ) {
2093  $user = $user ?: \RequestContext::getMain()->getUser();
2094  $options['username'] = $user->isAnon() ? null : $user->getName();
2095 
2096  // Query them and merge results
2097  $reqs = [];
2098  foreach ( $providers as $provider ) {
2099  $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2100  foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2101  $id = $req->getUniqueId();
2102 
2103  // If a required request if from a Primary, mark it as "primary-required" instead
2104  if ( $isPrimary && $req->required ) {
2106  }
2107 
2108  if (
2109  !isset( $reqs[$id] )
2110  || $req->required === AuthenticationRequest::REQUIRED
2111  || $reqs[$id] === AuthenticationRequest::OPTIONAL
2112  ) {
2113  $reqs[$id] = $req;
2114  }
2115  }
2116  }
2117 
2118  // AuthManager has its own req for some actions
2119  switch ( $providerAction ) {
2120  case self::ACTION_LOGIN:
2121  $reqs[] = new RememberMeAuthenticationRequest;
2122  break;
2123 
2124  case self::ACTION_CREATE:
2125  $reqs[] = new UsernameAuthenticationRequest;
2126  $reqs[] = new UserDataAuthenticationRequest;
2127  if ( $options['username'] !== null ) {
2129  $options['username'] = null; // Don't fill in the username below
2130  }
2131  break;
2132  }
2133 
2134  // Fill in reqs data
2135  $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2136 
2137  // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2138  if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2139  $reqs = array_filter( $reqs, function ( $req ) {
2140  return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2141  } );
2142  }
2143 
2144  return array_values( $reqs );
2145  }
2146 
2154  private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2155  foreach ( $reqs as $req ) {
2156  if ( !$req->action || $forceAction ) {
2157  $req->action = $action;
2158  }
2159  if ( $req->username === null ) {
2160  $req->username = $username;
2161  }
2162  }
2163  }
2164 
2171  public function userExists( $username, $flags = User::READ_NORMAL ) {
2172  foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2173  if ( $provider->testUserExists( $username, $flags ) ) {
2174  return true;
2175  }
2176  }
2177 
2178  return false;
2179  }
2180 
2192  public function allowsPropertyChange( $property ) {
2193  $providers = $this->getPrimaryAuthenticationProviders() +
2195  foreach ( $providers as $provider ) {
2196  if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2197  return false;
2198  }
2199  }
2200  return true;
2201  }
2202 
2211  public function getAuthenticationProvider( $id ) {
2212  // Fast version
2213  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2214  return $this->allAuthenticationProviders[$id];
2215  }
2216 
2217  // Slow version: instantiate each kind and check
2218  $providers = $this->getPrimaryAuthenticationProviders();
2219  if ( isset( $providers[$id] ) ) {
2220  return $providers[$id];
2221  }
2222  $providers = $this->getSecondaryAuthenticationProviders();
2223  if ( isset( $providers[$id] ) ) {
2224  return $providers[$id];
2225  }
2226  $providers = $this->getPreAuthenticationProviders();
2227  if ( isset( $providers[$id] ) ) {
2228  return $providers[$id];
2229  }
2230 
2231  return null;
2232  }
2233 
2247  public function setAuthenticationSessionData( $key, $data ) {
2248  $session = $this->request->getSession();
2249  $arr = $session->getSecret( 'authData' );
2250  if ( !is_array( $arr ) ) {
2251  $arr = [];
2252  }
2253  $arr[$key] = $data;
2254  $session->setSecret( 'authData', $arr );
2255  }
2256 
2264  public function getAuthenticationSessionData( $key, $default = null ) {
2265  $arr = $this->request->getSession()->getSecret( 'authData' );
2266  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2267  return $arr[$key];
2268  } else {
2269  return $default;
2270  }
2271  }
2272 
2278  public function removeAuthenticationSessionData( $key ) {
2279  $session = $this->request->getSession();
2280  if ( $key === null ) {
2281  $session->remove( 'authData' );
2282  } else {
2283  $arr = $session->getSecret( 'authData' );
2284  if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2285  unset( $arr[$key] );
2286  $session->setSecret( 'authData', $arr );
2287  }
2288  }
2289  }
2290 
2297  protected function providerArrayFromSpecs( $class, array $specs ) {
2298  $i = 0;
2299  foreach ( $specs as &$spec ) {
2300  $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2301  }
2302  unset( $spec );
2303  // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2304  usort( $specs, function ( $a, $b ) {
2305  return $a['sort'] <=> $b['sort']
2306  ?: $a['sort2'] <=> $b['sort2'];
2307  } );
2308 
2309  $ret = [];
2310  foreach ( $specs as $spec ) {
2311  $provider = ObjectFactory::getObjectFromSpec( $spec );
2312  if ( !$provider instanceof $class ) {
2313  throw new \RuntimeException(
2314  "Expected instance of $class, got " . get_class( $provider )
2315  );
2316  }
2317  $provider->setLogger( $this->logger );
2318  $provider->setManager( $this );
2319  $provider->setConfig( $this->config );
2320  $id = $provider->getUniqueId();
2321  if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2322  throw new \RuntimeException(
2323  "Duplicate specifications for id $id (classes " .
2324  get_class( $provider ) . ' and ' .
2325  get_class( $this->allAuthenticationProviders[$id] ) . ')'
2326  );
2327  }
2328  $this->allAuthenticationProviders[$id] = $provider;
2329  $ret[$id] = $provider;
2330  }
2331  return $ret;
2332  }
2333 
2338  private function getConfiguration() {
2339  return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2340  }
2341 
2346  protected function getPreAuthenticationProviders() {
2347  if ( $this->preAuthenticationProviders === null ) {
2348  $conf = $this->getConfiguration();
2349  $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2350  PreAuthenticationProvider::class, $conf['preauth']
2351  );
2352  }
2354  }
2355 
2360  protected function getPrimaryAuthenticationProviders() {
2361  if ( $this->primaryAuthenticationProviders === null ) {
2362  $conf = $this->getConfiguration();
2363  $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2364  PrimaryAuthenticationProvider::class, $conf['primaryauth']
2365  );
2366  }
2368  }
2369 
2375  if ( $this->secondaryAuthenticationProviders === null ) {
2376  $conf = $this->getConfiguration();
2377  $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2378  SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2379  );
2380  }
2382  }
2383 
2389  private function setSessionDataForUser( $user, $remember = null ) {
2390  $session = $this->request->getSession();
2391  $delay = $session->delaySave();
2392 
2393  $session->resetId();
2394  $session->resetAllTokens();
2395  if ( $session->canSetUser() ) {
2396  $session->setUser( $user );
2397  }
2398  if ( $remember !== null ) {
2399  $session->setRememberUser( $remember );
2400  }
2401  $session->set( 'AuthManager:lastAuthId', $user->getId() );
2402  $session->set( 'AuthManager:lastAuthTimestamp', time() );
2403  $session->persist();
2404 
2405  \Wikimedia\ScopedCallback::consume( $delay );
2406 
2407  \Hooks::run( 'UserLoggedIn', [ $user ] );
2408  }
2409 
2414  private function setDefaultUserOptions( User $user, $useContextLang ) {
2415  $user->setToken();
2416 
2417  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2418 
2419  $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $contLang;
2420  $user->setOption( 'language', $lang->getPreferredVariant() );
2421 
2422  if ( $contLang->hasVariants() ) {
2423  $user->setOption( 'variant', $contLang->getPreferredVariant() );
2424  }
2425  }
2426 
2432  private function callMethodOnProviders( $which, $method, array $args ) {
2433  $providers = [];
2434  if ( $which & 1 ) {
2435  $providers += $this->getPreAuthenticationProviders();
2436  }
2437  if ( $which & 2 ) {
2438  $providers += $this->getPrimaryAuthenticationProviders();
2439  }
2440  if ( $which & 4 ) {
2441  $providers += $this->getSecondaryAuthenticationProviders();
2442  }
2443  foreach ( $providers as $provider ) {
2444  $provider->$method( ...$args );
2445  }
2446  }
2447 
2452  public static function resetCache() {
2453  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2454  // @codeCoverageIgnoreStart
2455  throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2456  // @codeCoverageIgnoreEnd
2457  }
2458 
2459  self::$instance = null;
2460  }
2461 
2464 }
2465 
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.
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
addWatch( $title, $checkRights=self::CHECK_USER_RIGHTS)
Watch an article.
Definition: User.php:3925
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:4178
setId( $v)
Set the user and reload all fields according to a given ID.
Definition: User.php:2434
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1982
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
static instance()
Singleton.
Definition: Profiler.php:62
if(!isset( $args[0])) $lang
const ACTION_UNLINK
Like ACTION_REMOVE but for linking providers only.
removeAuthenticationSessionData( $key)
Remove authentication data.
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
$source
static getUsernameFromRequests(array $reqs)
Get the username from the set of requests.
allowsPropertyChange( $property)
Determine whether a user property should be allowed to be changed.
static getLocalClusterInstance()
Get the main cluster-local cache object.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
static getInstance( $channel)
Get a named logger instance from the currently configured logger factory.
string $action
Cache what action this request is.
Definition: MediaWiki.php:48
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition: User.php:3015
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:907
continueAccountCreation(array $reqs)
Continue an account creation flow.
Authentication request for the reason given for account creation.
A helper class for throttling authentication attempts.
setOption( $oname, $val)
Set the given option for a user.
Definition: User.php:3247
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition: User.php:1987
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:2443
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:48
canCreateAccount( $username, $options=[])
Determine whether a particular account can be created.
$last
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:68
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition: hooks.txt:1263
loadFromId( $flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition: User.php:460
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition: User.php:4487
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
AuthenticationProvider [] $allAuthenticationProviders
wfReadOnly()
Check whether the wiki is in read-only mode.
static resetCache()
Reset the internal caching for unit testing.
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
static getMain()
Get the RequestContext object associated with the main request.
static isCreatableName( $name)
Usernames which fail to pass this function will be blocked from new account registrations, but may be used internally either by batch processes or by user accounts which have already been created.
Definition: User.php:1119
canLinkAccounts()
Determine whether accounts can be linked.
Interface for configuration instances.
Definition: Config.php:28
This represents additional user data requested on the account creation form.
const FAIL
Indicates that the authentication failed.
const TYPE_LINK
Provider can link to existing accounts elsewhere.
static callLegacyAuthPlugin( $method, array $params, $return=null)
This used to call a legacy AuthPlugin method, if necessary.
static factory(array $deltas)
static AuthManager null $instance
PrimaryAuthenticationProvider [] $primaryAuthenticationProviders
getAuthenticationProvider( $id)
Get a provider by ID.
static singleton()
Get the global AuthManager.
getAuthenticationRequestsInternal( $providerAction, array $options, array $providers, User $user=null)
Internal request lookup for self::getAuthenticationRequests.
$res
Definition: database.txt:21
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
userExists( $username, $flags=User::READ_NORMAL)
Determine whether a username exists.
const ACTION_CHANGE
Change a user&#39;s credentials.
const SEC_FAIL
Security-sensitive should not be performed.
const AUTOCREATE_SOURCE_MAINT
Auto-creation is due to a Maintenance script.
const SEC_REAUTH
Security-sensitive operations should re-authenticate.
const AUTOCREATE_SOURCE_SESSION
Auto-creation is due to SessionManager.
const OPTIONAL
Indicates that the request is not required for authentication to proceed.
$cache
Definition: mcc.php:33
const IGNORE_USER_RIGHTS
Definition: User.php:80
$params
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1982
getConfiguration()
Get the configuration.
const REQUIRED
Indicates that the request is required for authentication to proceed.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition: User.php:1044
static runWithoutAbort( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:231
Returned from account creation to allow for logging into the created account.
This is an authentication request added by AuthManager to show a "remember me" checkbox.
const PASS
Indicates that the authentication succeeded.
This serves as the entry point to the authentication system.
Definition: AuthManager.php:84
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging a wrapping ErrorException create2 Corresponds to logging log_action database field and which is displayed in the UI similar to $comment or false if none Defaults to false if not set multiOccurrence Can this option be passed multiple times Defaults to false if not set this hook should only be used to add variables that depend on the current page request
Definition: hooks.txt:2159
canAuthenticateNow()
Indicate whether user authentication is possible.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php: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:979
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:780
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:611
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:2416
addToDatabase()
Add this existing user object to the database.
Definition: User.php:4367
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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
autoCreateUser(User $user, $source, $login=true)
Auto-create an account, and log into that account.
getUserPage()
Get this user&#39;s personal page title.
Definition: User.php:4538
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:587
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:1473
This is a value object for authentication requests.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
normalizeUsername( $username)
Provide normalized versions of the username for security checks.
getSecondaryAuthenticationProviders()
Get the list of SecondaryAuthenticationProviders.
const UI
Indicates that the authentication needs further user input of some sort.