MediaWiki REL1_33
AuthManager.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Auth;
25
28use Psr\Log\LoggerAwareInterface;
29use Psr\Log\LoggerInterface;
32use User;
34use Wikimedia\ObjectFactory;
35
84class 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
115 const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
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
310 $reqs, CreatedAccountAuthenticationRequest::class
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 ] );
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
344 $this->removeAuthenticationSessionData( null );
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
374 $reqs, CreateFromLoginAuthenticationRequest::class
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
574 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
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 ] );
691 $beginReqs, RememberMeAuthenticationRequest::class
692 );
693 $this->setSessionDataForUser( $user, $req && $req->rememberMe );
695 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
696 $session->remove( 'AuthManager::authnState' );
697 $this->removeAuthenticationSessionData( null );
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 ) {
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?
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 ) {
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 ) {
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 ) {
906 \BotPassword::invalidateAllPasswordsForUser( $req->username );
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 ) {
998 $status = Status::newGood();
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
1110 $this->removeAuthenticationSessionData( null );
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
1129 $reqs, CreateFromLoginAuthenticationRequest::class
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
1187 $cache = \ObjectCache::getLocalClusterInstance();
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...
1256 $status = Status::wrap( $status );
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() );
1450 $state['reqs'], CreationReasonAuthenticationRequest::class
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();
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' );
1528 $this->removeAuthenticationSessionData( null );
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 }
1588 $status = Status::newGood();
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
1650 $cache = \ObjectCache::getLocalClusterInstance();
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 }
1716 $status = Status::newGood();
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
1827 $this->removeAuthenticationSessionData( null );
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
2026 public function getAuthenticationRequests( $action, User $user = null ) {
2027 $options = [];
2028 $providerAction = $action;
2029
2030 // Figure out which providers to query
2031 switch ( $action ) {
2032 case self::ACTION_LOGIN:
2034 $providers = $this->getPreAuthenticationProviders() +
2037 break;
2038
2040 $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2041 return is_array( $state ) ? $state['continueRequests'] : [];
2042
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
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
2063 $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2064 return is_array( $state ) ? $state['continueRequests'] : [];
2065
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
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
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
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
wfReadOnly()
Check whether the wiki is in read-only mode.
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
if( $line===false) $args
Definition cdb.php:64
const TYPE_RANGE
Definition Block.php:98
This serves as the entry point to the authentication system.
static resetCache()
Reset the internal caching for unit testing.
canLinkAccounts()
Determine whether accounts can be linked.
const ACTION_UNLINK
Like ACTION_REMOVE but for linking providers only.
const SEC_FAIL
Security-sensitive should not be performed.
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...
setAuthenticationSessionData( $key, $data)
Store authentication in the current session.
beginAccountCreation(User $creator, array $reqs, $returnToUrl)
Start an account creation flow.
callMethodOnProviders( $which, $method, array $args)
getAuthenticationProvider( $id)
Get a provider by ID.
setSessionDataForUser( $user, $remember=null)
Log the user in.
securitySensitiveOperationStatus( $operation)
Whether security-sensitive operations should proceed.
SecondaryAuthenticationProvider[] $secondaryAuthenticationProviders
revokeAccessForUser( $username)
Revoke any authentication credentials for a user.
beginAccountLink(User $user, array $reqs, $returnToUrl)
Start an account linking flow.
const SEC_REAUTH
Security-sensitive operations should re-authenticate.
getSecondaryAuthenticationProviders()
Get the list of SecondaryAuthenticationProviders.
allowsPropertyChange( $property)
Determine whether a user property should be allowed to be changed.
const ACTION_CREATE_CONTINUE
Continue a user creation process that was interrupted by the need for user input or communication wit...
continueAccountLink(array $reqs)
Continue an account linking flow.
setLogger(LoggerInterface $logger)
allowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
beginAuthentication(array $reqs, $returnToUrl)
Start an authentication flow.
getAuthenticationSessionData( $key, $default=null)
Fetch authentication data from the current session.
canCreateAccounts()
Determine whether accounts can be created.
canCreateAccount( $username, $options=[])
Determine whether a particular account can be created.
changeAuthenticationData(AuthenticationRequest $req, $isAddition=false)
Change authentication data (e.g.
userCanAuthenticate( $username)
Determine whether a username can authenticate.
removeAuthenticationSessionData( $key)
Remove authentication data.
providerArrayFromSpecs( $class, array $specs)
Create an array of AuthenticationProviders from an array of ObjectFactory specs.
forcePrimaryAuthenticationProviders(array $providers, $why)
Force certain PrimaryAuthenticationProviders.
const AUTOCREATE_SOURCE_MAINT
Auto-creation is due to a Maintenance script.
setDefaultUserOptions(User $user, $useContextLang)
checkAccountCreatePermissions(User $creator)
Basic permissions checks on whether a user can create accounts.
static singleton()
Get the global AuthManager.
CreatedAccountAuthenticationRequest[] $createdAccountAuthenticationRequests
AuthenticationProvider[] $allAuthenticationProviders
continueAuthentication(array $reqs)
Continue an authentication flow.
const SEC_OK
Security-sensitive operations are ok.
getAuthenticationRequestsInternal( $providerAction, array $options, array $providers, User $user=null)
Internal request lookup for self::getAuthenticationRequests.
autoCreateUser(User $user, $source, $login=true)
Auto-create an account, and log into that account.
const ACTION_LINK_CONTINUE
Continue a user linking process that was interrupted by the need for user input or communication with...
const ACTION_CHANGE
Change a user's credentials.
canAuthenticateNow()
Indicate whether user authentication is possible.
static callLegacyAuthPlugin( $method, array $params, $return=null)
This used to call a legacy AuthPlugin method, if necessary.
const ACTION_REMOVE
Remove a user's credentials.
const ACTION_LINK
Link an existing user to a third-party account.
PreAuthenticationProvider[] $preAuthenticationProviders
PrimaryAuthenticationProvider[] $primaryAuthenticationProviders
getConfiguration()
Get the configuration.
const AUTOCREATE_SOURCE_SESSION
Auto-creation is due to SessionManager.
getPreAuthenticationProviders()
Get the list of PreAuthenticationProviders.
getAuthenticationRequests( $action, User $user=null)
Return the applicable list of AuthenticationRequests.
fillRequests(array &$reqs, $action, $username, $forceAction=false)
Set values in an array of requests.
userExists( $username, $flags=User::READ_NORMAL)
Determine whether a username exists.
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
__construct(WebRequest $request, Config $config)
normalizeUsername( $username)
Provide normalized versions of the username for security checks.
static AuthManager null $instance
continueAccountCreation(array $reqs)
Continue an account creation flow.
const ACTION_CREATE
Create a new user.
This is a value object for authentication requests.
const OPTIONAL
Indicates that the request is not required for authentication to proceed.
const PRIMARY_REQUIRED
Indicates that the request is required by a primary authentication provider.
const REQUIRED
Indicates that the request is required for authentication to proceed.
static getUsernameFromRequests(array $reqs)
Get the username from the set of requests.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
const FAIL
Indicates that the authentication failed.
const PASS
Indicates that the authentication succeeded.
const UI
Indicates that the authentication needs further user input of some sort.
const REDIRECT
Indicates that the authentication needs to be redirected to a third party to proceed.
const ABSTAIN
Indicates that the authentication provider does not handle this request.
This transfers state between the login and account creation flows.
Returned from account creation to allow for logging into the created account.
Authentication request for the reason given for account creation.
This is an authentication request added by AuthManager to show a "remember me" checkbox.
This represents additional user data requested on the account creation form.
AuthenticationRequest to ensure something with a username is present.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:40
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2452
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:585
static isCreatableName( $name)
Usernames which fail to pass this function will be blocked from new account registrations,...
Definition User.php:1117
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition User.php:1996
setName( $str)
Set the user name.
Definition User.php:2480
getId()
Get the user's ID.
Definition User.php:2425
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:609
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition User.php:1042
const IGNORE_USER_RIGHTS
Definition User.php:78
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition User.php:4496
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition User.php:905
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
$res
Definition database.txt:21
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
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
this hook is for auditing only $req
Definition hooks.txt:979
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1266
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:1999
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:783
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:2003
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;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:782
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:2224
return true to allow those checks to and false if checking is done & $user
Definition hooks.txt:1510
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:37
Interface for configuration instances.
Definition Config.php:28
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
An AuthenticationProvider is used by AuthManager when authenticating users.
getUniqueId()
Return a unique identifier for this instance.
A pre-authentication provider can prevent authentication early on.
A primary authentication provider is responsible for associating the submitted authentication data wi...
const TYPE_LINK
Provider can link to existing accounts elsewhere.
const TYPE_NONE
Provider cannot create or link to accounts.
A secondary provider mostly acts when the submitted authentication data has already been associated t...
$cache
Definition mcc.php:33
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))
$source
A helper class for throttling authentication attempts.
$last
$property
$params
if(!isset( $args[0])) $lang