MediaWiki REL1_31
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
83class AuthManager implements LoggerAwareInterface {
85 const ACTION_LOGIN = 'login';
88 const ACTION_LOGIN_CONTINUE = 'login-continue';
90 const ACTION_CREATE = 'create';
93 const ACTION_CREATE_CONTINUE = 'create-continue';
95 const ACTION_LINK = 'link';
98 const ACTION_LINK_CONTINUE = 'link-continue';
100 const ACTION_CHANGE = 'change';
102 const ACTION_REMOVE = 'remove';
104 const ACTION_UNLINK = 'unlink';
105
107 const SEC_OK = 'ok';
109 const SEC_REAUTH = 'reauth';
111 const SEC_FAIL = 'fail';
112
114 const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
115
117 private static $instance = null;
118
120 private $request;
121
123 private $config;
124
126 private $logger;
127
130
133
136
139
142
147 public static function singleton() {
148 if ( self::$instance === null ) {
149 self::$instance = new self(
150 \RequestContext::getMain()->getRequest(),
151 MediaWikiServices::getInstance()->getMainConfig()
152 );
153 }
154 return self::$instance;
155 }
156
162 $this->request = $request;
163 $this->config = $config;
164 $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
165 }
166
170 public function setLogger( LoggerInterface $logger ) {
171 $this->logger = $logger;
172 }
173
177 public function getRequest() {
178 return $this->request;
179 }
180
187 public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
188 $this->logger->warning( "Overriding AuthManager primary authn because $why" );
189
190 if ( $this->primaryAuthenticationProviders !== null ) {
191 $this->logger->warning(
192 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
193 );
194
195 $this->allAuthenticationProviders = array_diff_key(
196 $this->allAuthenticationProviders,
197 $this->primaryAuthenticationProviders
198 );
199 $session = $this->request->getSession();
200 $session->remove( 'AuthManager::authnState' );
201 $session->remove( 'AuthManager::accountCreationState' );
202 $session->remove( 'AuthManager::accountLinkState' );
203 $this->createdAccountAuthenticationRequests = [];
204 }
205
206 $this->primaryAuthenticationProviders = [];
207 foreach ( $providers as $provider ) {
208 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
209 throw new \RuntimeException(
210 'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
211 get_class( $provider )
212 );
213 }
214 $provider->setLogger( $this->logger );
215 $provider->setManager( $this );
216 $provider->setConfig( $this->config );
217 $id = $provider->getUniqueId();
218 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
219 throw new \RuntimeException(
220 "Duplicate specifications for id $id (classes " .
221 get_class( $provider ) . ' and ' .
222 get_class( $this->allAuthenticationProviders[$id] ) . ')'
223 );
224 }
225 $this->allAuthenticationProviders[$id] = $provider;
226 $this->primaryAuthenticationProviders[$id] = $provider;
227 }
228 }
229
239 public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
241
242 if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
243 return call_user_func_array( [ $wgAuth, $method ], $params );
244 } else {
245 return $return;
246 }
247 }
248
262 public function canAuthenticateNow() {
263 return $this->request->getSession()->canSetUser();
264 }
265
284 public function beginAuthentication( array $reqs, $returnToUrl ) {
285 $session = $this->request->getSession();
286 if ( !$session->canSetUser() ) {
287 // Caller should have called canAuthenticateNow()
288 $session->remove( 'AuthManager::authnState' );
289 throw new \LogicException( 'Authentication is not possible now' );
290 }
291
292 $guessUserName = null;
293 foreach ( $reqs as $req ) {
294 $req->returnToUrl = $returnToUrl;
295 // @codeCoverageIgnoreStart
296 if ( $req->username !== null && $req->username !== '' ) {
297 if ( $guessUserName === null ) {
298 $guessUserName = $req->username;
299 } elseif ( $guessUserName !== $req->username ) {
300 $guessUserName = null;
301 break;
302 }
303 }
304 // @codeCoverageIgnoreEnd
305 }
306
307 // Check for special-case login of a just-created account
309 $reqs, CreatedAccountAuthenticationRequest::class
310 );
311 if ( $req ) {
312 if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
313 throw new \LogicException(
314 'CreatedAccountAuthenticationRequests are only valid on ' .
315 'the same AuthManager that created the account'
316 );
317 }
318
319 $user = User::newFromName( $req->username );
320 // @codeCoverageIgnoreStart
321 if ( !$user ) {
322 throw new \UnexpectedValueException(
323 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
324 );
325 } elseif ( $user->getId() != $req->id ) {
326 throw new \UnexpectedValueException(
327 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
328 );
329 }
330 // @codeCoverageIgnoreEnd
331
332 $this->logger->info( 'Logging in {user} after account creation', [
333 'user' => $user->getName(),
334 ] );
337 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
338 $session->remove( 'AuthManager::authnState' );
339 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
340 return $ret;
341 }
342
343 $this->removeAuthenticationSessionData( null );
344
345 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
346 $status = $provider->testForAuthentication( $reqs );
347 if ( !$status->isGood() ) {
348 $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
350 Status::wrap( $status )->getMessage()
351 );
352 $this->callMethodOnProviders( 7, 'postAuthentication',
353 [ User::newFromName( $guessUserName ) ?: null, $ret ]
354 );
355 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
356 return $ret;
357 }
358 }
359
360 $state = [
361 'reqs' => $reqs,
362 'returnToUrl' => $returnToUrl,
363 'guessUserName' => $guessUserName,
364 'primary' => null,
365 'primaryResponse' => null,
366 'secondary' => [],
367 'maybeLink' => [],
368 'continueRequests' => [],
369 ];
370
371 // Preserve state from a previous failed login
373 $reqs, CreateFromLoginAuthenticationRequest::class
374 );
375 if ( $req ) {
376 $state['maybeLink'] = $req->maybeLink;
377 }
378
379 $session = $this->request->getSession();
380 $session->setSecret( 'AuthManager::authnState', $state );
381 $session->persist();
382
383 return $this->continueAuthentication( $reqs );
384 }
385
408 public function continueAuthentication( array $reqs ) {
409 $session = $this->request->getSession();
410 try {
411 if ( !$session->canSetUser() ) {
412 // Caller should have called canAuthenticateNow()
413 // @codeCoverageIgnoreStart
414 throw new \LogicException( 'Authentication is not possible now' );
415 // @codeCoverageIgnoreEnd
416 }
417
418 $state = $session->getSecret( 'AuthManager::authnState' );
419 if ( !is_array( $state ) ) {
421 wfMessage( 'authmanager-authn-not-in-progress' )
422 );
423 }
424 $state['continueRequests'] = [];
425
426 $guessUserName = $state['guessUserName'];
427
428 foreach ( $reqs as $req ) {
429 $req->returnToUrl = $state['returnToUrl'];
430 }
431
432 // Step 1: Choose an primary authentication provider, and call it until it succeeds.
433
434 if ( $state['primary'] === null ) {
435 // We haven't picked a PrimaryAuthenticationProvider yet
436 // @codeCoverageIgnoreStart
437 $guessUserName = null;
438 foreach ( $reqs as $req ) {
439 if ( $req->username !== null && $req->username !== '' ) {
440 if ( $guessUserName === null ) {
441 $guessUserName = $req->username;
442 } elseif ( $guessUserName !== $req->username ) {
443 $guessUserName = null;
444 break;
445 }
446 }
447 }
448 $state['guessUserName'] = $guessUserName;
449 // @codeCoverageIgnoreEnd
450 $state['reqs'] = $reqs;
451
452 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
453 $res = $provider->beginPrimaryAuthentication( $reqs );
454 switch ( $res->status ) {
456 $state['primary'] = $id;
457 $state['primaryResponse'] = $res;
458 $this->logger->debug( "Primary login with $id succeeded" );
459 break 2;
461 $this->logger->debug( "Login failed in primary authentication by $id" );
462 if ( $res->createRequest || $state['maybeLink'] ) {
463 $res->createRequest = new CreateFromLoginAuthenticationRequest(
464 $res->createRequest, $state['maybeLink']
465 );
466 }
467 $this->callMethodOnProviders( 7, 'postAuthentication',
468 [ User::newFromName( $guessUserName ) ?: null, $res ]
469 );
470 $session->remove( 'AuthManager::authnState' );
471 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
472 return $res;
474 // Continue loop
475 break;
478 $this->logger->debug( "Primary login with $id returned $res->status" );
479 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
480 $state['primary'] = $id;
481 $state['continueRequests'] = $res->neededRequests;
482 $session->setSecret( 'AuthManager::authnState', $state );
483 return $res;
484
485 // @codeCoverageIgnoreStart
486 default:
487 throw new \DomainException(
488 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
489 );
490 // @codeCoverageIgnoreEnd
491 }
492 }
493 if ( $state['primary'] === null ) {
494 $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
496 wfMessage( 'authmanager-authn-no-primary' )
497 );
498 $this->callMethodOnProviders( 7, 'postAuthentication',
499 [ User::newFromName( $guessUserName ) ?: null, $ret ]
500 );
501 $session->remove( 'AuthManager::authnState' );
502 return $ret;
503 }
504 } elseif ( $state['primaryResponse'] === null ) {
505 $provider = $this->getAuthenticationProvider( $state['primary'] );
506 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
507 // Configuration changed? Force them to start over.
508 // @codeCoverageIgnoreStart
510 wfMessage( 'authmanager-authn-not-in-progress' )
511 );
512 $this->callMethodOnProviders( 7, 'postAuthentication',
513 [ User::newFromName( $guessUserName ) ?: null, $ret ]
514 );
515 $session->remove( 'AuthManager::authnState' );
516 return $ret;
517 // @codeCoverageIgnoreEnd
518 }
519 $id = $provider->getUniqueId();
520 $res = $provider->continuePrimaryAuthentication( $reqs );
521 switch ( $res->status ) {
523 $state['primaryResponse'] = $res;
524 $this->logger->debug( "Primary login with $id succeeded" );
525 break;
527 $this->logger->debug( "Login failed in primary authentication by $id" );
528 if ( $res->createRequest || $state['maybeLink'] ) {
529 $res->createRequest = new CreateFromLoginAuthenticationRequest(
530 $res->createRequest, $state['maybeLink']
531 );
532 }
533 $this->callMethodOnProviders( 7, 'postAuthentication',
534 [ User::newFromName( $guessUserName ) ?: null, $res ]
535 );
536 $session->remove( 'AuthManager::authnState' );
537 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
538 return $res;
541 $this->logger->debug( "Primary login with $id returned $res->status" );
542 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
543 $state['continueRequests'] = $res->neededRequests;
544 $session->setSecret( 'AuthManager::authnState', $state );
545 return $res;
546 default:
547 throw new \DomainException(
548 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
549 );
550 }
551 }
552
553 $res = $state['primaryResponse'];
554 if ( $res->username === null ) {
555 $provider = $this->getAuthenticationProvider( $state['primary'] );
556 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
557 // Configuration changed? Force them to start over.
558 // @codeCoverageIgnoreStart
560 wfMessage( 'authmanager-authn-not-in-progress' )
561 );
562 $this->callMethodOnProviders( 7, 'postAuthentication',
563 [ User::newFromName( $guessUserName ) ?: null, $ret ]
564 );
565 $session->remove( 'AuthManager::authnState' );
566 return $ret;
567 // @codeCoverageIgnoreEnd
568 }
569
570 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
571 $res->linkRequest &&
572 // don't confuse the user with an incorrect message if linking is disabled
573 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
574 ) {
575 $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
576 $msg = 'authmanager-authn-no-local-user-link';
577 } else {
578 $msg = 'authmanager-authn-no-local-user';
579 }
580 $this->logger->debug(
581 "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
582 );
584 $ret->neededRequests = $this->getAuthenticationRequestsInternal(
585 self::ACTION_LOGIN,
586 [],
588 );
589 if ( $res->createRequest || $state['maybeLink'] ) {
590 $ret->createRequest = new CreateFromLoginAuthenticationRequest(
591 $res->createRequest, $state['maybeLink']
592 );
593 $ret->neededRequests[] = $ret->createRequest;
594 }
595 $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
596 $session->setSecret( 'AuthManager::authnState', [
597 'reqs' => [], // Will be filled in later
598 'primary' => null,
599 'primaryResponse' => null,
600 'secondary' => [],
601 'continueRequests' => $ret->neededRequests,
602 ] + $state );
603 return $ret;
604 }
605
606 // Step 2: Primary authentication succeeded, create the User object
607 // (and add the user locally if necessary)
608
609 $user = User::newFromName( $res->username, 'usable' );
610 if ( !$user ) {
611 $provider = $this->getAuthenticationProvider( $state['primary'] );
612 throw new \DomainException(
613 get_class( $provider ) . " returned an invalid username: {$res->username}"
614 );
615 }
616 if ( $user->getId() === 0 ) {
617 // User doesn't exist locally. Create it.
618 $this->logger->info( 'Auto-creating {user} on login', [
619 'user' => $user->getName(),
620 ] );
621 $status = $this->autoCreateUser( $user, $state['primary'], false );
622 if ( !$status->isGood() ) {
624 Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
625 );
626 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
627 $session->remove( 'AuthManager::authnState' );
628 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
629 return $ret;
630 }
631 }
632
633 // Step 3: Iterate over all the secondary authentication providers.
634
635 $beginReqs = $state['reqs'];
636
637 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
638 if ( !isset( $state['secondary'][$id] ) ) {
639 // This provider isn't started yet, so we pass it the set
640 // of reqs from beginAuthentication instead of whatever
641 // might have been used by a previous provider in line.
642 $func = 'beginSecondaryAuthentication';
643 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
644 } elseif ( !$state['secondary'][$id] ) {
645 $func = 'continueSecondaryAuthentication';
646 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
647 } else {
648 continue;
649 }
650 switch ( $res->status ) {
652 $this->logger->debug( "Secondary login with $id succeeded" );
653 // fall through
655 $state['secondary'][$id] = true;
656 break;
658 $this->logger->debug( "Login failed in secondary authentication by $id" );
659 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
660 $session->remove( 'AuthManager::authnState' );
661 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
662 return $res;
665 $this->logger->debug( "Secondary login with $id returned " . $res->status );
666 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
667 $state['secondary'][$id] = false;
668 $state['continueRequests'] = $res->neededRequests;
669 $session->setSecret( 'AuthManager::authnState', $state );
670 return $res;
671
672 // @codeCoverageIgnoreStart
673 default:
674 throw new \DomainException(
675 get_class( $provider ) . "::{$func}() returned $res->status"
676 );
677 // @codeCoverageIgnoreEnd
678 }
679 }
680
681 // Step 4: Authentication complete! Set the user in the session and
682 // clean up.
683
684 $this->logger->info( 'Login for {user} succeeded from {clientip}', [
685 'user' => $user->getName(),
686 'clientip' => $this->request->getIP(),
687 ] );
690 $beginReqs, RememberMeAuthenticationRequest::class
691 );
692 $this->setSessionDataForUser( $user, $req && $req->rememberMe );
694 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
695 $session->remove( 'AuthManager::authnState' );
696 $this->removeAuthenticationSessionData( null );
697 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
698 return $ret;
699 } catch ( \Exception $ex ) {
700 $session->remove( 'AuthManager::authnState' );
701 throw $ex;
702 }
703 }
704
716 public function securitySensitiveOperationStatus( $operation ) {
718
719 $this->logger->debug( __METHOD__ . ": Checking $operation" );
720
721 $session = $this->request->getSession();
722 $aId = $session->getUser()->getId();
723 if ( $aId === 0 ) {
724 // User isn't authenticated. DWIM?
726 $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
727 return $status;
728 }
729
730 if ( $session->canSetUser() ) {
731 $id = $session->get( 'AuthManager:lastAuthId' );
732 $last = $session->get( 'AuthManager:lastAuthTimestamp' );
733 if ( $id !== $aId || $last === null ) {
734 $timeSinceLogin = PHP_INT_MAX; // Forever ago
735 } else {
736 $timeSinceLogin = max( 0, time() - $last );
737 }
738
739 $thresholds = $this->config->get( 'ReauthenticateTime' );
740 if ( isset( $thresholds[$operation] ) ) {
741 $threshold = $thresholds[$operation];
742 } elseif ( isset( $thresholds['default'] ) ) {
743 $threshold = $thresholds['default'];
744 } else {
745 throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
746 }
747
748 if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
750 }
751 } else {
752 $timeSinceLogin = -1;
753
754 $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
755 if ( isset( $pass[$operation] ) ) {
756 $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
757 } elseif ( isset( $pass['default'] ) ) {
758 $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
759 } else {
760 throw new \UnexpectedValueException(
761 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
762 );
763 }
764 }
765
766 \Hooks::run( 'SecuritySensitiveOperationStatus', [
767 &$status, $operation, $session, $timeSinceLogin
768 ] );
769
770 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
771 if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
773 }
774
775 $this->logger->info( __METHOD__ . ": $operation is $status" );
776
777 return $status;
778 }
779
789 public function userCanAuthenticate( $username ) {
790 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
791 if ( $provider->testUserCanAuthenticate( $username ) ) {
792 return true;
793 }
794 }
795 return false;
796 }
797
812 public function normalizeUsername( $username ) {
813 $ret = [];
814 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
815 $normalized = $provider->providerNormalizeUsername( $username );
816 if ( $normalized !== null ) {
817 $ret[$normalized] = true;
818 }
819 }
820 return array_keys( $ret );
821 }
822
837 public function revokeAccessForUser( $username ) {
838 $this->logger->info( 'Revoking access for {user}', [
839 'user' => $username,
840 ] );
841 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
842 }
843
853 public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
854 $any = false;
855 $providers = $this->getPrimaryAuthenticationProviders() +
857 foreach ( $providers as $provider ) {
858 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
859 if ( !$status->isGood() ) {
860 return Status::wrap( $status );
861 }
862 $any = $any || $status->value !== 'ignored';
863 }
864 if ( !$any ) {
865 $status = Status::newGood( 'ignored' );
866 $status->warning( 'authmanager-change-not-supported' );
867 return $status;
868 }
869 return Status::newGood();
870 }
871
889 public function changeAuthenticationData( AuthenticationRequest $req, $isAddition = false ) {
890 $this->logger->info( 'Changing authentication data for {user} class {what}', [
891 'user' => is_string( $req->username ) ? $req->username : '<no name>',
892 'what' => get_class( $req ),
893 ] );
894
895 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
896
897 // When the main account's authentication data is changed, invalidate
898 // all BotPasswords too.
899 if ( !$isAddition ) {
900 \BotPassword::invalidateAllPasswordsForUser( $req->username );
901 }
902 }
903
915 public function canCreateAccounts() {
916 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
917 switch ( $provider->accountCreationType() ) {
920 return true;
921 }
922 }
923 return false;
924 }
925
934 public function canCreateAccount( $username, $options = [] ) {
935 // Back compat
936 if ( is_int( $options ) ) {
937 $options = [ 'flags' => $options ];
938 }
939 $options += [
940 'flags' => User::READ_NORMAL,
941 'creating' => false,
942 ];
943 $flags = $options['flags'];
944
945 if ( !$this->canCreateAccounts() ) {
946 return Status::newFatal( 'authmanager-create-disabled' );
947 }
948
949 if ( $this->userExists( $username, $flags ) ) {
950 return Status::newFatal( 'userexists' );
951 }
952
953 $user = User::newFromName( $username, 'creatable' );
954 if ( !is_object( $user ) ) {
955 return Status::newFatal( 'noname' );
956 } else {
957 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
958 if ( $user->getId() !== 0 ) {
959 return Status::newFatal( 'userexists' );
960 }
961 }
962
963 // Denied by providers?
964 $providers = $this->getPreAuthenticationProviders() +
967 foreach ( $providers as $provider ) {
968 $status = $provider->testUserForCreation( $user, false, $options );
969 if ( !$status->isGood() ) {
970 return Status::wrap( $status );
971 }
972 }
973
974 return Status::newGood();
975 }
976
982 public function checkAccountCreatePermissions( User $creator ) {
983 // Wiki is read-only?
984 if ( wfReadOnly() ) {
985 return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
986 }
987
988 // This is awful, this permission check really shouldn't go through Title.
989 $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
990 ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
991 if ( $permErrors ) {
992 $status = Status::newGood();
993 foreach ( $permErrors as $args ) {
994 call_user_func_array( [ $status, 'fatal' ], $args );
995 }
996 return $status;
997 }
998
999 $block = $creator->isBlockedFromCreateAccount();
1000 if ( $block ) {
1001 $errorParams = [
1002 $block->getTarget(),
1003 $block->mReason ?: wfMessage( 'blockednoreason' )->text(),
1004 $block->getByName()
1005 ];
1006
1007 if ( $block->getType() === \Block::TYPE_RANGE ) {
1008 $errorMessage = 'cantcreateaccount-range-text';
1009 $errorParams[] = $this->getRequest()->getIP();
1010 } else {
1011 $errorMessage = 'cantcreateaccount-text';
1012 }
1013
1014 return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1015 }
1016
1017 $ip = $this->getRequest()->getIP();
1018 if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1019 return Status::newFatal( 'sorbs_create_account_reason' );
1020 }
1021
1022 return Status::newGood();
1023 }
1024
1044 public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1045 $session = $this->request->getSession();
1046 if ( !$this->canCreateAccounts() ) {
1047 // Caller should have called canCreateAccounts()
1048 $session->remove( 'AuthManager::accountCreationState' );
1049 throw new \LogicException( 'Account creation is not possible' );
1050 }
1051
1052 try {
1054 } catch ( \UnexpectedValueException $ex ) {
1055 $username = null;
1056 }
1057 if ( $username === null ) {
1058 $this->logger->debug( __METHOD__ . ': No username provided' );
1059 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1060 }
1061
1062 // Permissions check
1063 $status = $this->checkAccountCreatePermissions( $creator );
1064 if ( !$status->isGood() ) {
1065 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1066 'user' => $username,
1067 'creator' => $creator->getName(),
1068 'reason' => $status->getWikiText( null, null, 'en' )
1069 ] );
1070 return AuthenticationResponse::newFail( $status->getMessage() );
1071 }
1072
1073 $status = $this->canCreateAccount(
1074 $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1075 );
1076 if ( !$status->isGood() ) {
1077 $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1078 'user' => $username,
1079 'creator' => $creator->getName(),
1080 'reason' => $status->getWikiText( null, null, 'en' )
1081 ] );
1082 return AuthenticationResponse::newFail( $status->getMessage() );
1083 }
1084
1085 $user = User::newFromName( $username, 'creatable' );
1086 foreach ( $reqs as $req ) {
1087 $req->username = $username;
1088 $req->returnToUrl = $returnToUrl;
1089 if ( $req instanceof UserDataAuthenticationRequest ) {
1090 $status = $req->populateUser( $user );
1091 if ( !$status->isGood() ) {
1092 $status = Status::wrap( $status );
1093 $session->remove( 'AuthManager::accountCreationState' );
1094 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1095 'user' => $user->getName(),
1096 'creator' => $creator->getName(),
1097 'reason' => $status->getWikiText( null, null, 'en' ),
1098 ] );
1099 return AuthenticationResponse::newFail( $status->getMessage() );
1100 }
1101 }
1102 }
1103
1104 $this->removeAuthenticationSessionData( null );
1105
1106 $state = [
1107 'username' => $username,
1108 'userid' => 0,
1109 'creatorid' => $creator->getId(),
1110 'creatorname' => $creator->getName(),
1111 'reqs' => $reqs,
1112 'returnToUrl' => $returnToUrl,
1113 'primary' => null,
1114 'primaryResponse' => null,
1115 'secondary' => [],
1116 'continueRequests' => [],
1117 'maybeLink' => [],
1118 'ranPreTests' => false,
1119 ];
1120
1121 // Special case: converting a login to an account creation
1123 $reqs, CreateFromLoginAuthenticationRequest::class
1124 );
1125 if ( $req ) {
1126 $state['maybeLink'] = $req->maybeLink;
1127
1128 if ( $req->createRequest ) {
1129 $reqs[] = $req->createRequest;
1130 $state['reqs'][] = $req->createRequest;
1131 }
1132 }
1133
1134 $session->setSecret( 'AuthManager::accountCreationState', $state );
1135 $session->persist();
1136
1137 return $this->continueAccountCreation( $reqs );
1138 }
1139
1145 public function continueAccountCreation( array $reqs ) {
1146 $session = $this->request->getSession();
1147 try {
1148 if ( !$this->canCreateAccounts() ) {
1149 // Caller should have called canCreateAccounts()
1150 $session->remove( 'AuthManager::accountCreationState' );
1151 throw new \LogicException( 'Account creation is not possible' );
1152 }
1153
1154 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1155 if ( !is_array( $state ) ) {
1157 wfMessage( 'authmanager-create-not-in-progress' )
1158 );
1159 }
1160 $state['continueRequests'] = [];
1161
1162 // Step 0: Prepare and validate the input
1163
1164 $user = User::newFromName( $state['username'], 'creatable' );
1165 if ( !is_object( $user ) ) {
1166 $session->remove( 'AuthManager::accountCreationState' );
1167 $this->logger->debug( __METHOD__ . ': Invalid username', [
1168 'user' => $state['username'],
1169 ] );
1170 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1171 }
1172
1173 if ( $state['creatorid'] ) {
1174 $creator = User::newFromId( $state['creatorid'] );
1175 } else {
1176 $creator = new User;
1177 $creator->setName( $state['creatorname'] );
1178 }
1179
1180 // Avoid account creation races on double submissions
1181 $cache = \ObjectCache::getLocalClusterInstance();
1182 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1183 if ( !$lock ) {
1184 // Don't clear AuthManager::accountCreationState for this code
1185 // path because the process that won the race owns it.
1186 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1187 'user' => $user->getName(),
1188 'creator' => $creator->getName(),
1189 ] );
1190 return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1191 }
1192
1193 // Permissions check
1194 $status = $this->checkAccountCreatePermissions( $creator );
1195 if ( !$status->isGood() ) {
1196 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1197 'user' => $user->getName(),
1198 'creator' => $creator->getName(),
1199 'reason' => $status->getWikiText( null, null, 'en' )
1200 ] );
1201 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1202 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1203 $session->remove( 'AuthManager::accountCreationState' );
1204 return $ret;
1205 }
1206
1207 // Load from master for existence check
1208 $user->load( User::READ_LOCKING );
1209
1210 if ( $state['userid'] === 0 ) {
1211 if ( $user->getId() != 0 ) {
1212 $this->logger->debug( __METHOD__ . ': User exists locally', [
1213 'user' => $user->getName(),
1214 'creator' => $creator->getName(),
1215 ] );
1216 $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1217 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1218 $session->remove( 'AuthManager::accountCreationState' );
1219 return $ret;
1220 }
1221 } else {
1222 if ( $user->getId() == 0 ) {
1223 $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1224 'user' => $user->getName(),
1225 'creator' => $creator->getName(),
1226 'expected_id' => $state['userid'],
1227 ] );
1228 throw new \UnexpectedValueException(
1229 "User \"{$state['username']}\" should exist now, but doesn't!"
1230 );
1231 }
1232 if ( $user->getId() != $state['userid'] ) {
1233 $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1234 'user' => $user->getName(),
1235 'creator' => $creator->getName(),
1236 'expected_id' => $state['userid'],
1237 'actual_id' => $user->getId(),
1238 ] );
1239 throw new \UnexpectedValueException(
1240 "User \"{$state['username']}\" exists, but " .
1241 "ID {$user->getId()} != {$state['userid']}!"
1242 );
1243 }
1244 }
1245 foreach ( $state['reqs'] as $req ) {
1246 if ( $req instanceof UserDataAuthenticationRequest ) {
1247 $status = $req->populateUser( $user );
1248 if ( !$status->isGood() ) {
1249 // This should never happen...
1250 $status = Status::wrap( $status );
1251 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1252 'user' => $user->getName(),
1253 'creator' => $creator->getName(),
1254 'reason' => $status->getWikiText( null, null, 'en' ),
1255 ] );
1256 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1257 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1258 $session->remove( 'AuthManager::accountCreationState' );
1259 return $ret;
1260 }
1261 }
1262 }
1263
1264 foreach ( $reqs as $req ) {
1265 $req->returnToUrl = $state['returnToUrl'];
1266 $req->username = $state['username'];
1267 }
1268
1269 // Run pre-creation tests, if we haven't already
1270 if ( !$state['ranPreTests'] ) {
1271 $providers = $this->getPreAuthenticationProviders() +
1274 foreach ( $providers as $id => $provider ) {
1275 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1276 if ( !$status->isGood() ) {
1277 $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1278 'user' => $user->getName(),
1279 'creator' => $creator->getName(),
1280 ] );
1282 Status::wrap( $status )->getMessage()
1283 );
1284 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1285 $session->remove( 'AuthManager::accountCreationState' );
1286 return $ret;
1287 }
1288 }
1289
1290 $state['ranPreTests'] = true;
1291 }
1292
1293 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1294
1295 if ( $state['primary'] === null ) {
1296 // We haven't picked a PrimaryAuthenticationProvider yet
1297 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1298 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1299 continue;
1300 }
1301 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1302 switch ( $res->status ) {
1304 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1305 'user' => $user->getName(),
1306 'creator' => $creator->getName(),
1307 ] );
1308 $state['primary'] = $id;
1309 $state['primaryResponse'] = $res;
1310 break 2;
1312 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1313 'user' => $user->getName(),
1314 'creator' => $creator->getName(),
1315 ] );
1316 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1317 $session->remove( 'AuthManager::accountCreationState' );
1318 return $res;
1320 // Continue loop
1321 break;
1324 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1325 'user' => $user->getName(),
1326 'creator' => $creator->getName(),
1327 ] );
1328 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1329 $state['primary'] = $id;
1330 $state['continueRequests'] = $res->neededRequests;
1331 $session->setSecret( 'AuthManager::accountCreationState', $state );
1332 return $res;
1333
1334 // @codeCoverageIgnoreStart
1335 default:
1336 throw new \DomainException(
1337 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1338 );
1339 // @codeCoverageIgnoreEnd
1340 }
1341 }
1342 if ( $state['primary'] === null ) {
1343 $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1344 'user' => $user->getName(),
1345 'creator' => $creator->getName(),
1346 ] );
1348 wfMessage( 'authmanager-create-no-primary' )
1349 );
1350 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1351 $session->remove( 'AuthManager::accountCreationState' );
1352 return $ret;
1353 }
1354 } elseif ( $state['primaryResponse'] === null ) {
1355 $provider = $this->getAuthenticationProvider( $state['primary'] );
1356 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1357 // Configuration changed? Force them to start over.
1358 // @codeCoverageIgnoreStart
1360 wfMessage( 'authmanager-create-not-in-progress' )
1361 );
1362 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1363 $session->remove( 'AuthManager::accountCreationState' );
1364 return $ret;
1365 // @codeCoverageIgnoreEnd
1366 }
1367 $id = $provider->getUniqueId();
1368 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1369 switch ( $res->status ) {
1371 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1372 'user' => $user->getName(),
1373 'creator' => $creator->getName(),
1374 ] );
1375 $state['primaryResponse'] = $res;
1376 break;
1378 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1379 'user' => $user->getName(),
1380 'creator' => $creator->getName(),
1381 ] );
1382 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1383 $session->remove( 'AuthManager::accountCreationState' );
1384 return $res;
1387 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1388 'user' => $user->getName(),
1389 'creator' => $creator->getName(),
1390 ] );
1391 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1392 $state['continueRequests'] = $res->neededRequests;
1393 $session->setSecret( 'AuthManager::accountCreationState', $state );
1394 return $res;
1395 default:
1396 throw new \DomainException(
1397 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1398 );
1399 }
1400 }
1401
1402 // Step 2: Primary authentication succeeded, create the User object
1403 // and add the user locally.
1404
1405 if ( $state['userid'] === 0 ) {
1406 $this->logger->info( 'Creating user {user} during account creation', [
1407 'user' => $user->getName(),
1408 'creator' => $creator->getName(),
1409 ] );
1410 $status = $user->addToDatabase();
1411 if ( !$status->isOK() ) {
1412 // @codeCoverageIgnoreStart
1413 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1414 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1415 $session->remove( 'AuthManager::accountCreationState' );
1416 return $ret;
1417 // @codeCoverageIgnoreEnd
1418 }
1419 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1420 \Hooks::run( 'LocalUserCreated', [ $user, false ] );
1421 $user->saveSettings();
1422 $state['userid'] = $user->getId();
1423
1424 // Update user count
1425 \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1426
1427 // Watch user's userpage and talk page
1428 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1429
1430 // Inform the provider
1431 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1432
1433 // Log the creation
1434 if ( $this->config->get( 'NewUserLog' ) ) {
1435 $isAnon = $creator->isAnon();
1436 $logEntry = new \ManualLogEntry(
1437 'newusers',
1438 $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1439 );
1440 $logEntry->setPerformer( $isAnon ? $user : $creator );
1441 $logEntry->setTarget( $user->getUserPage() );
1444 $state['reqs'], CreationReasonAuthenticationRequest::class
1445 );
1446 $logEntry->setComment( $req ? $req->reason : '' );
1447 $logEntry->setParameters( [
1448 '4::userid' => $user->getId(),
1449 ] );
1450 $logid = $logEntry->insert();
1451 $logEntry->publish( $logid );
1452 }
1453 }
1454
1455 // Step 3: Iterate over all the secondary authentication providers.
1456
1457 $beginReqs = $state['reqs'];
1458
1459 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1460 if ( !isset( $state['secondary'][$id] ) ) {
1461 // This provider isn't started yet, so we pass it the set
1462 // of reqs from beginAuthentication instead of whatever
1463 // might have been used by a previous provider in line.
1464 $func = 'beginSecondaryAccountCreation';
1465 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1466 } elseif ( !$state['secondary'][$id] ) {
1467 $func = 'continueSecondaryAccountCreation';
1468 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1469 } else {
1470 continue;
1471 }
1472 switch ( $res->status ) {
1474 $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1475 'user' => $user->getName(),
1476 'creator' => $creator->getName(),
1477 ] );
1478 // fall through
1480 $state['secondary'][$id] = true;
1481 break;
1484 $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1485 'user' => $user->getName(),
1486 'creator' => $creator->getName(),
1487 ] );
1488 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1489 $state['secondary'][$id] = false;
1490 $state['continueRequests'] = $res->neededRequests;
1491 $session->setSecret( 'AuthManager::accountCreationState', $state );
1492 return $res;
1494 throw new \DomainException(
1495 get_class( $provider ) . "::{$func}() returned $res->status." .
1496 ' Secondary providers are not allowed to fail account creation, that' .
1497 ' should have been done via testForAccountCreation().'
1498 );
1499 // @codeCoverageIgnoreStart
1500 default:
1501 throw new \DomainException(
1502 get_class( $provider ) . "::{$func}() returned $res->status"
1503 );
1504 // @codeCoverageIgnoreEnd
1505 }
1506 }
1507
1508 $id = $user->getId();
1509 $name = $user->getName();
1512 $ret->loginRequest = $req;
1513 $this->createdAccountAuthenticationRequests[] = $req;
1514
1515 $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1516 'user' => $user->getName(),
1517 'creator' => $creator->getName(),
1518 ] );
1519
1520 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1521 $session->remove( 'AuthManager::accountCreationState' );
1522 $this->removeAuthenticationSessionData( null );
1523 return $ret;
1524 } catch ( \Exception $ex ) {
1525 $session->remove( 'AuthManager::accountCreationState' );
1526 throw $ex;
1527 }
1528 }
1529
1545 public function autoCreateUser( User $user, $source, $login = true ) {
1546 if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1548 ) {
1549 throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1550 }
1551
1552 $username = $user->getName();
1553
1554 // Try the local user from the replica DB
1555 $localId = User::idFromName( $username );
1556 $flags = User::READ_NORMAL;
1557
1558 // Fetch the user ID from the master, so that we don't try to create the user
1559 // when they already exist, due to replication lag
1560 // @codeCoverageIgnoreStart
1561 if (
1562 !$localId &&
1563 MediaWikiServices::getInstance()->getDBLoadBalancer()->getReaderIndex() != 0
1564 ) {
1565 $localId = User::idFromName( $username, User::READ_LATEST );
1566 $flags = User::READ_LATEST;
1567 }
1568 // @codeCoverageIgnoreEnd
1569
1570 if ( $localId ) {
1571 $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1572 'username' => $username,
1573 ] );
1574 $user->setId( $localId );
1575 $user->loadFromId( $flags );
1576 if ( $login ) {
1577 $this->setSessionDataForUser( $user );
1578 }
1579 $status = Status::newGood();
1580 $status->warning( 'userexists' );
1581 return $status;
1582 }
1583
1584 // Wiki is read-only?
1585 if ( wfReadOnly() ) {
1586 $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1587 'username' => $username,
1588 'reason' => wfReadOnlyReason(),
1589 ] );
1590 $user->setId( 0 );
1591 $user->loadFromId();
1592 return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1593 }
1594
1595 // Check the session, if we tried to create this user already there's
1596 // no point in retrying.
1597 $session = $this->request->getSession();
1598 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1599 $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1600 'username' => $username,
1601 'sessionid' => $session->getId(),
1602 ] );
1603 $user->setId( 0 );
1604 $user->loadFromId();
1605 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1606 if ( $reason instanceof StatusValue ) {
1607 return Status::wrap( $reason );
1608 } else {
1609 return Status::newFatal( $reason );
1610 }
1611 }
1612
1613 // Is the username creatable?
1614 if ( !User::isCreatableName( $username ) ) {
1615 $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1616 'username' => $username,
1617 ] );
1618 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1619 $user->setId( 0 );
1620 $user->loadFromId();
1621 return Status::newFatal( 'noname' );
1622 }
1623
1624 // Is the IP user able to create accounts?
1625 $anon = new User;
1626 if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1627 $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1628 'username' => $username,
1629 'ip' => $anon->getName(),
1630 ] );
1631 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1632 $session->persist();
1633 $user->setId( 0 );
1634 $user->loadFromId();
1635 return Status::newFatal( 'authmanager-autocreate-noperm' );
1636 }
1637
1638 // Avoid account creation races on double submissions
1639 $cache = \ObjectCache::getLocalClusterInstance();
1640 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1641 if ( !$lock ) {
1642 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1643 'user' => $username,
1644 ] );
1645 $user->setId( 0 );
1646 $user->loadFromId();
1647 return Status::newFatal( 'usernameinprogress' );
1648 }
1649
1650 // Denied by providers?
1651 $options = [
1652 'flags' => User::READ_LATEST,
1653 'creating' => true,
1654 ];
1655 $providers = $this->getPreAuthenticationProviders() +
1658 foreach ( $providers as $provider ) {
1659 $status = $provider->testUserForCreation( $user, $source, $options );
1660 if ( !$status->isGood() ) {
1661 $ret = Status::wrap( $status );
1662 $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1663 'username' => $username,
1664 'reason' => $ret->getWikiText( null, null, 'en' ),
1665 ] );
1666 $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1667 $user->setId( 0 );
1668 $user->loadFromId();
1669 return $ret;
1670 }
1671 }
1672
1673 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1674 if ( $cache->get( $backoffKey ) ) {
1675 $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1676 'username' => $username,
1677 ] );
1678 $user->setId( 0 );
1679 $user->loadFromId();
1680 return Status::newFatal( 'authmanager-autocreate-exception' );
1681 }
1682
1683 // Checks passed, create the user...
1684 $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
1685 $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1686 'username' => $username,
1687 'from' => $from,
1688 ] );
1689
1690 // Ignore warnings about master connections/writes...hard to avoid here
1691 $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1692 $old = $trxProfiler->setSilenced( true );
1693 try {
1694 $status = $user->addToDatabase();
1695 if ( !$status->isOK() ) {
1696 // Double-check for a race condition (T70012). We make use of the fact that when
1697 // addToDatabase fails due to the user already existing, the user object gets loaded.
1698 if ( $user->getId() ) {
1699 $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1700 'username' => $username,
1701 ] );
1702 if ( $login ) {
1703 $this->setSessionDataForUser( $user );
1704 }
1705 $status = Status::newGood();
1706 $status->warning( 'userexists' );
1707 } else {
1708 $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1709 'username' => $username,
1710 'msg' => $status->getWikiText( null, null, 'en' )
1711 ] );
1712 $user->setId( 0 );
1713 $user->loadFromId();
1714 }
1715 return $status;
1716 }
1717 } catch ( \Exception $ex ) {
1718 $trxProfiler->setSilenced( $old );
1719 $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1720 'username' => $username,
1721 'exception' => $ex,
1722 ] );
1723 // Do not keep throwing errors for a while
1724 $cache->set( $backoffKey, 1, 600 );
1725 // Bubble up error; which should normally trigger DB rollbacks
1726 throw $ex;
1727 }
1728
1729 $this->setDefaultUserOptions( $user, false );
1730
1731 // Inform the providers
1732 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1733
1734 \Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1735 \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1736 $user->saveSettings();
1737
1738 // Update user count
1739 \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1740 // Watch user's userpage and talk page
1741 \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1742 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1743 } );
1744
1745 // Log the creation
1746 if ( $this->config->get( 'NewUserLog' ) ) {
1747 $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1748 $logEntry->setPerformer( $user );
1749 $logEntry->setTarget( $user->getUserPage() );
1750 $logEntry->setComment( '' );
1751 $logEntry->setParameters( [
1752 '4::userid' => $user->getId(),
1753 ] );
1754 $logEntry->insert();
1755 }
1756
1757 $trxProfiler->setSilenced( $old );
1758
1759 if ( $login ) {
1760 $this->setSessionDataForUser( $user );
1761 }
1762
1763 return Status::newGood();
1764 }
1765
1777 public function canLinkAccounts() {
1778 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1779 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1780 return true;
1781 }
1782 }
1783 return false;
1784 }
1785
1795 public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1796 $session = $this->request->getSession();
1797 $session->remove( 'AuthManager::accountLinkState' );
1798
1799 if ( !$this->canLinkAccounts() ) {
1800 // Caller should have called canLinkAccounts()
1801 throw new \LogicException( 'Account linking is not possible' );
1802 }
1803
1804 if ( $user->getId() === 0 ) {
1805 if ( !User::isUsableName( $user->getName() ) ) {
1806 $msg = wfMessage( 'noname' );
1807 } else {
1808 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1809 }
1810 return AuthenticationResponse::newFail( $msg );
1811 }
1812 foreach ( $reqs as $req ) {
1813 $req->username = $user->getName();
1814 $req->returnToUrl = $returnToUrl;
1815 }
1816
1817 $this->removeAuthenticationSessionData( null );
1818
1819 $providers = $this->getPreAuthenticationProviders();
1820 foreach ( $providers as $id => $provider ) {
1821 $status = $provider->testForAccountLink( $user );
1822 if ( !$status->isGood() ) {
1823 $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1824 'user' => $user->getName(),
1825 ] );
1827 Status::wrap( $status )->getMessage()
1828 );
1829 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1830 return $ret;
1831 }
1832 }
1833
1834 $state = [
1835 'username' => $user->getName(),
1836 'userid' => $user->getId(),
1837 'returnToUrl' => $returnToUrl,
1838 'primary' => null,
1839 'continueRequests' => [],
1840 ];
1841
1842 $providers = $this->getPrimaryAuthenticationProviders();
1843 foreach ( $providers as $id => $provider ) {
1844 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1845 continue;
1846 }
1847
1848 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1849 switch ( $res->status ) {
1851 $this->logger->info( "Account linked to {user} by $id", [
1852 'user' => $user->getName(),
1853 ] );
1854 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1855 return $res;
1856
1858 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1859 'user' => $user->getName(),
1860 ] );
1861 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1862 return $res;
1863
1865 // Continue loop
1866 break;
1867
1870 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1871 'user' => $user->getName(),
1872 ] );
1873 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1874 $state['primary'] = $id;
1875 $state['continueRequests'] = $res->neededRequests;
1876 $session->setSecret( 'AuthManager::accountLinkState', $state );
1877 $session->persist();
1878 return $res;
1879
1880 // @codeCoverageIgnoreStart
1881 default:
1882 throw new \DomainException(
1883 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1884 );
1885 // @codeCoverageIgnoreEnd
1886 }
1887 }
1888
1889 $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1890 'user' => $user->getName(),
1891 ] );
1893 wfMessage( 'authmanager-link-no-primary' )
1894 );
1895 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1896 return $ret;
1897 }
1898
1904 public function continueAccountLink( array $reqs ) {
1905 $session = $this->request->getSession();
1906 try {
1907 if ( !$this->canLinkAccounts() ) {
1908 // Caller should have called canLinkAccounts()
1909 $session->remove( 'AuthManager::accountLinkState' );
1910 throw new \LogicException( 'Account linking is not possible' );
1911 }
1912
1913 $state = $session->getSecret( 'AuthManager::accountLinkState' );
1914 if ( !is_array( $state ) ) {
1916 wfMessage( 'authmanager-link-not-in-progress' )
1917 );
1918 }
1919 $state['continueRequests'] = [];
1920
1921 // Step 0: Prepare and validate the input
1922
1923 $user = User::newFromName( $state['username'], 'usable' );
1924 if ( !is_object( $user ) ) {
1925 $session->remove( 'AuthManager::accountLinkState' );
1926 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1927 }
1928 if ( $user->getId() != $state['userid'] ) {
1929 throw new \UnexpectedValueException(
1930 "User \"{$state['username']}\" is valid, but " .
1931 "ID {$user->getId()} != {$state['userid']}!"
1932 );
1933 }
1934
1935 foreach ( $reqs as $req ) {
1936 $req->username = $state['username'];
1937 $req->returnToUrl = $state['returnToUrl'];
1938 }
1939
1940 // Step 1: Call the primary again until it succeeds
1941
1942 $provider = $this->getAuthenticationProvider( $state['primary'] );
1943 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1944 // Configuration changed? Force them to start over.
1945 // @codeCoverageIgnoreStart
1947 wfMessage( 'authmanager-link-not-in-progress' )
1948 );
1949 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1950 $session->remove( 'AuthManager::accountLinkState' );
1951 return $ret;
1952 // @codeCoverageIgnoreEnd
1953 }
1954 $id = $provider->getUniqueId();
1955 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1956 switch ( $res->status ) {
1958 $this->logger->info( "Account linked to {user} by $id", [
1959 'user' => $user->getName(),
1960 ] );
1961 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1962 $session->remove( 'AuthManager::accountLinkState' );
1963 return $res;
1965 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1966 'user' => $user->getName(),
1967 ] );
1968 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1969 $session->remove( 'AuthManager::accountLinkState' );
1970 return $res;
1973 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1974 'user' => $user->getName(),
1975 ] );
1976 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1977 $state['continueRequests'] = $res->neededRequests;
1978 $session->setSecret( 'AuthManager::accountLinkState', $state );
1979 return $res;
1980 default:
1981 throw new \DomainException(
1982 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1983 );
1984 }
1985 } catch ( \Exception $ex ) {
1986 $session->remove( 'AuthManager::accountLinkState' );
1987 throw $ex;
1988 }
1989 }
1990
2016 public function getAuthenticationRequests( $action, User $user = null ) {
2017 $options = [];
2018 $providerAction = $action;
2019
2020 // Figure out which providers to query
2021 switch ( $action ) {
2022 case self::ACTION_LOGIN:
2024 $providers = $this->getPreAuthenticationProviders() +
2027 break;
2028
2030 $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2031 return is_array( $state ) ? $state['continueRequests'] : [];
2032
2034 $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2035 return is_array( $state ) ? $state['continueRequests'] : [];
2036
2037 case self::ACTION_LINK:
2038 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2039 return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2040 } );
2041 break;
2042
2044 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2045 return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2046 } );
2047
2048 // To providers, unlink and remove are identical.
2049 $providerAction = self::ACTION_REMOVE;
2050 break;
2051
2053 $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2054 return is_array( $state ) ? $state['continueRequests'] : [];
2055
2058 $providers = $this->getPrimaryAuthenticationProviders() +
2060 break;
2061
2062 // @codeCoverageIgnoreStart
2063 default:
2064 throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2065 }
2066 // @codeCoverageIgnoreEnd
2067
2068 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2069 }
2070
2081 $providerAction, array $options, array $providers, User $user = null
2082 ) {
2083 $user = $user ?: \RequestContext::getMain()->getUser();
2084 $options['username'] = $user->isAnon() ? null : $user->getName();
2085
2086 // Query them and merge results
2087 $reqs = [];
2088 foreach ( $providers as $provider ) {
2089 $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2090 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2091 $id = $req->getUniqueId();
2092
2093 // If a required request if from a Primary, mark it as "primary-required" instead
2094 if ( $isPrimary ) {
2095 if ( $req->required ) {
2097 }
2098 }
2099
2100 if (
2101 !isset( $reqs[$id] )
2102 || $req->required === AuthenticationRequest::REQUIRED
2103 || $reqs[$id] === AuthenticationRequest::OPTIONAL
2104 ) {
2105 $reqs[$id] = $req;
2106 }
2107 }
2108 }
2109
2110 // AuthManager has its own req for some actions
2111 switch ( $providerAction ) {
2112 case self::ACTION_LOGIN:
2113 $reqs[] = new RememberMeAuthenticationRequest;
2114 break;
2115
2117 $reqs[] = new UsernameAuthenticationRequest;
2118 $reqs[] = new UserDataAuthenticationRequest;
2119 if ( $options['username'] !== null ) {
2121 $options['username'] = null; // Don't fill in the username below
2122 }
2123 break;
2124 }
2125
2126 // Fill in reqs data
2127 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2128
2129 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2130 if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2131 $reqs = array_filter( $reqs, function ( $req ) {
2132 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2133 } );
2134 }
2135
2136 return array_values( $reqs );
2137 }
2138
2146 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2147 foreach ( $reqs as $req ) {
2148 if ( !$req->action || $forceAction ) {
2149 $req->action = $action;
2150 }
2151 if ( $req->username === null ) {
2152 $req->username = $username;
2153 }
2154 }
2155 }
2156
2163 public function userExists( $username, $flags = User::READ_NORMAL ) {
2164 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2165 if ( $provider->testUserExists( $username, $flags ) ) {
2166 return true;
2167 }
2168 }
2169
2170 return false;
2171 }
2172
2184 public function allowsPropertyChange( $property ) {
2185 $providers = $this->getPrimaryAuthenticationProviders() +
2187 foreach ( $providers as $provider ) {
2188 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2189 return false;
2190 }
2191 }
2192 return true;
2193 }
2194
2203 public function getAuthenticationProvider( $id ) {
2204 // Fast version
2205 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2206 return $this->allAuthenticationProviders[$id];
2207 }
2208
2209 // Slow version: instantiate each kind and check
2210 $providers = $this->getPrimaryAuthenticationProviders();
2211 if ( isset( $providers[$id] ) ) {
2212 return $providers[$id];
2213 }
2214 $providers = $this->getSecondaryAuthenticationProviders();
2215 if ( isset( $providers[$id] ) ) {
2216 return $providers[$id];
2217 }
2218 $providers = $this->getPreAuthenticationProviders();
2219 if ( isset( $providers[$id] ) ) {
2220 return $providers[$id];
2221 }
2222
2223 return null;
2224 }
2225
2239 public function setAuthenticationSessionData( $key, $data ) {
2240 $session = $this->request->getSession();
2241 $arr = $session->getSecret( 'authData' );
2242 if ( !is_array( $arr ) ) {
2243 $arr = [];
2244 }
2245 $arr[$key] = $data;
2246 $session->setSecret( 'authData', $arr );
2247 }
2248
2256 public function getAuthenticationSessionData( $key, $default = null ) {
2257 $arr = $this->request->getSession()->getSecret( 'authData' );
2258 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2259 return $arr[$key];
2260 } else {
2261 return $default;
2262 }
2263 }
2264
2270 public function removeAuthenticationSessionData( $key ) {
2271 $session = $this->request->getSession();
2272 if ( $key === null ) {
2273 $session->remove( 'authData' );
2274 } else {
2275 $arr = $session->getSecret( 'authData' );
2276 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2277 unset( $arr[$key] );
2278 $session->setSecret( 'authData', $arr );
2279 }
2280 }
2281 }
2282
2289 protected function providerArrayFromSpecs( $class, array $specs ) {
2290 $i = 0;
2291 foreach ( $specs as &$spec ) {
2292 $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2293 }
2294 unset( $spec );
2295 usort( $specs, function ( $a, $b ) {
2296 return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2297 ?: $a['sort2'] - $b['sort2'];
2298 } );
2299
2300 $ret = [];
2301 foreach ( $specs as $spec ) {
2302 $provider = ObjectFactory::getObjectFromSpec( $spec );
2303 if ( !$provider instanceof $class ) {
2304 throw new \RuntimeException(
2305 "Expected instance of $class, got " . get_class( $provider )
2306 );
2307 }
2308 $provider->setLogger( $this->logger );
2309 $provider->setManager( $this );
2310 $provider->setConfig( $this->config );
2311 $id = $provider->getUniqueId();
2312 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2313 throw new \RuntimeException(
2314 "Duplicate specifications for id $id (classes " .
2315 get_class( $provider ) . ' and ' .
2316 get_class( $this->allAuthenticationProviders[$id] ) . ')'
2317 );
2318 }
2319 $this->allAuthenticationProviders[$id] = $provider;
2320 $ret[$id] = $provider;
2321 }
2322 return $ret;
2323 }
2324
2329 private function getConfiguration() {
2330 return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2331 }
2332
2337 protected function getPreAuthenticationProviders() {
2338 if ( $this->preAuthenticationProviders === null ) {
2339 $conf = $this->getConfiguration();
2340 $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2341 PreAuthenticationProvider::class, $conf['preauth']
2342 );
2343 }
2345 }
2346
2352 if ( $this->primaryAuthenticationProviders === null ) {
2353 $conf = $this->getConfiguration();
2354 $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2355 PrimaryAuthenticationProvider::class, $conf['primaryauth']
2356 );
2357 }
2359 }
2360
2366 if ( $this->secondaryAuthenticationProviders === null ) {
2367 $conf = $this->getConfiguration();
2368 $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2369 SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2370 );
2371 }
2373 }
2374
2380 private function setSessionDataForUser( $user, $remember = null ) {
2381 $session = $this->request->getSession();
2382 $delay = $session->delaySave();
2383
2384 $session->resetId();
2385 $session->resetAllTokens();
2386 if ( $session->canSetUser() ) {
2387 $session->setUser( $user );
2388 }
2389 if ( $remember !== null ) {
2390 $session->setRememberUser( $remember );
2391 }
2392 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2393 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2394 $session->persist();
2395
2396 \Wikimedia\ScopedCallback::consume( $delay );
2397
2398 \Hooks::run( 'UserLoggedIn', [ $user ] );
2399 }
2400
2405 private function setDefaultUserOptions( User $user, $useContextLang ) {
2407
2408 $user->setToken();
2409
2410 $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
2411 $user->setOption( 'language', $lang->getPreferredVariant() );
2412
2413 if ( $wgContLang->hasVariants() ) {
2414 $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2415 }
2416 }
2417
2423 private function callMethodOnProviders( $which, $method, array $args ) {
2424 $providers = [];
2425 if ( $which & 1 ) {
2426 $providers += $this->getPreAuthenticationProviders();
2427 }
2428 if ( $which & 2 ) {
2429 $providers += $this->getPrimaryAuthenticationProviders();
2430 }
2431 if ( $which & 4 ) {
2432 $providers += $this->getSecondaryAuthenticationProviders();
2433 }
2434 foreach ( $providers as $provider ) {
2435 call_user_func_array( [ $provider, $method ], $args );
2436 }
2437 }
2438
2443 public static function resetCache() {
2444 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2445 // @codeCoverageIgnoreStart
2446 throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2447 // @codeCoverageIgnoreEnd
2448 }
2449
2450 self::$instance = null;
2451 }
2452
2455}
2456
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
$wgAuth $wgAuth
Authentication plugin.
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.
if( $line===false) $args
Definition cdb.php:64
const TYPE_RANGE
Definition Block.php:85
Backwards-compatibility wrapper for AuthManager via $wgAuth.
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.
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)
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.
static factory(array $deltas)
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:53
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2482
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:591
static isCreatableName( $name)
Usernames which fail to pass this function will be blocked from new account registrations,...
Definition User.php:1093
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition User.php:1932
setName( $str)
Set the user name.
Definition User.php:2509
getId()
Get the user's ID.
Definition User.php:2457
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:614
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition User.php:1018
const IGNORE_USER_RIGHTS
Definition User.php:90
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition User.php:4494
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition User.php:883
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
$res
Definition database.txt:21
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
when a variable name is used in a it is silently declared as a new local masking the global
Definition design.txt:95
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
this hook is for auditing only $req
Definition hooks.txt:990
the array() calling protocol came about after MediaWiki 1.4rc1.
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:2001
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 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
returning false will NOT prevent logging a wrapping ErrorException instead of letting the login form give the generic error message that the account does not exist For when the account has been renamed or deleted or an array to pass a message key and parameters create2 Corresponds to logging log_action database field and which is displayed in the UI similar to $comment this hook should only be used to add variables that depend on the current page request
Definition hooks.txt:2224
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:2005
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:785
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. '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:1255
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
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
$source
A helper class for throttling authentication attempts.
$last
$property
$params
if(!isset( $args[0])) $lang