MediaWiki REL1_32
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 ) {
240 global $wgAuth;
241
242 if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
243 return $wgAuth->$method( ...$params );
244 } else {
245 return $return;
246 }
247 }
248
262 public function canAuthenticateNow() {
263 return $this->request->getSession()->canSetUser();
264 }
265
284 public function beginAuthentication( array $reqs, $returnToUrl ) {
285 $session = $this->request->getSession();
286 if ( !$session->canSetUser() ) {
287 // Caller should have called canAuthenticateNow()
288 $session->remove( 'AuthManager::authnState' );
289 throw new \LogicException( 'Authentication is not possible now' );
290 }
291
292 $guessUserName = null;
293 foreach ( $reqs as $req ) {
294 $req->returnToUrl = $returnToUrl;
295 // @codeCoverageIgnoreStart
296 if ( $req->username !== null && $req->username !== '' ) {
297 if ( $guessUserName === null ) {
298 $guessUserName = $req->username;
299 } elseif ( $guessUserName !== $req->username ) {
300 $guessUserName = null;
301 break;
302 }
303 }
304 // @codeCoverageIgnoreEnd
305 }
306
307 // Check for special-case login of a just-created account
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 for '{user}'",
776 [
777 'user' => $session->getUser()->getName(),
778 'clientip' => $this->getRequest()->getIP(),
779 ]
780 );
781
782 return $status;
783 }
784
794 public function userCanAuthenticate( $username ) {
795 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
796 if ( $provider->testUserCanAuthenticate( $username ) ) {
797 return true;
798 }
799 }
800 return false;
801 }
802
817 public function normalizeUsername( $username ) {
818 $ret = [];
819 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
820 $normalized = $provider->providerNormalizeUsername( $username );
821 if ( $normalized !== null ) {
822 $ret[$normalized] = true;
823 }
824 }
825 return array_keys( $ret );
826 }
827
842 public function revokeAccessForUser( $username ) {
843 $this->logger->info( 'Revoking access for {user}', [
844 'user' => $username,
845 ] );
846 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
847 }
848
858 public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
859 $any = false;
860 $providers = $this->getPrimaryAuthenticationProviders() +
862 foreach ( $providers as $provider ) {
863 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
864 if ( !$status->isGood() ) {
865 return Status::wrap( $status );
866 }
867 $any = $any || $status->value !== 'ignored';
868 }
869 if ( !$any ) {
870 $status = Status::newGood( 'ignored' );
871 $status->warning( 'authmanager-change-not-supported' );
872 return $status;
873 }
874 return Status::newGood();
875 }
876
894 public function changeAuthenticationData( AuthenticationRequest $req, $isAddition = false ) {
895 $this->logger->info( 'Changing authentication data for {user} class {what}', [
896 'user' => is_string( $req->username ) ? $req->username : '<no name>',
897 'what' => get_class( $req ),
898 ] );
899
900 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
901
902 // When the main account's authentication data is changed, invalidate
903 // all BotPasswords too.
904 if ( !$isAddition ) {
905 \BotPassword::invalidateAllPasswordsForUser( $req->username );
906 }
907 }
908
920 public function canCreateAccounts() {
921 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
922 switch ( $provider->accountCreationType() ) {
925 return true;
926 }
927 }
928 return false;
929 }
930
939 public function canCreateAccount( $username, $options = [] ) {
940 // Back compat
941 if ( is_int( $options ) ) {
942 $options = [ 'flags' => $options ];
943 }
944 $options += [
945 'flags' => User::READ_NORMAL,
946 'creating' => false,
947 ];
948 $flags = $options['flags'];
949
950 if ( !$this->canCreateAccounts() ) {
951 return Status::newFatal( 'authmanager-create-disabled' );
952 }
953
954 if ( $this->userExists( $username, $flags ) ) {
955 return Status::newFatal( 'userexists' );
956 }
957
958 $user = User::newFromName( $username, 'creatable' );
959 if ( !is_object( $user ) ) {
960 return Status::newFatal( 'noname' );
961 } else {
962 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
963 if ( $user->getId() !== 0 ) {
964 return Status::newFatal( 'userexists' );
965 }
966 }
967
968 // Denied by providers?
969 $providers = $this->getPreAuthenticationProviders() +
972 foreach ( $providers as $provider ) {
973 $status = $provider->testUserForCreation( $user, false, $options );
974 if ( !$status->isGood() ) {
975 return Status::wrap( $status );
976 }
977 }
978
979 return Status::newGood();
980 }
981
987 public function checkAccountCreatePermissions( User $creator ) {
988 // Wiki is read-only?
989 if ( wfReadOnly() ) {
990 return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
991 }
992
993 // This is awful, this permission check really shouldn't go through Title.
994 $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
995 ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
996 if ( $permErrors ) {
997 $status = Status::newGood();
998 foreach ( $permErrors as $args ) {
999 $status->fatal( ...$args );
1000 }
1001 return $status;
1002 }
1003
1004 $block = $creator->isBlockedFromCreateAccount();
1005 if ( $block ) {
1006 $errorParams = [
1007 $block->getTarget(),
1008 $block->mReason ?: wfMessage( 'blockednoreason' )->text(),
1009 $block->getByName()
1010 ];
1011
1012 if ( $block->getType() === \Block::TYPE_RANGE ) {
1013 $errorMessage = 'cantcreateaccount-range-text';
1014 $errorParams[] = $this->getRequest()->getIP();
1015 } else {
1016 $errorMessage = 'cantcreateaccount-text';
1017 }
1018
1019 return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1020 }
1021
1022 $ip = $this->getRequest()->getIP();
1023 if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1024 return Status::newFatal( 'sorbs_create_account_reason' );
1025 }
1026
1027 return Status::newGood();
1028 }
1029
1049 public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1050 $session = $this->request->getSession();
1051 if ( !$this->canCreateAccounts() ) {
1052 // Caller should have called canCreateAccounts()
1053 $session->remove( 'AuthManager::accountCreationState' );
1054 throw new \LogicException( 'Account creation is not possible' );
1055 }
1056
1057 try {
1059 } catch ( \UnexpectedValueException $ex ) {
1060 $username = null;
1061 }
1062 if ( $username === null ) {
1063 $this->logger->debug( __METHOD__ . ': No username provided' );
1064 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1065 }
1066
1067 // Permissions check
1068 $status = $this->checkAccountCreatePermissions( $creator );
1069 if ( !$status->isGood() ) {
1070 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1071 'user' => $username,
1072 'creator' => $creator->getName(),
1073 'reason' => $status->getWikiText( null, null, 'en' )
1074 ] );
1075 return AuthenticationResponse::newFail( $status->getMessage() );
1076 }
1077
1078 $status = $this->canCreateAccount(
1079 $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1080 );
1081 if ( !$status->isGood() ) {
1082 $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1083 'user' => $username,
1084 'creator' => $creator->getName(),
1085 'reason' => $status->getWikiText( null, null, 'en' )
1086 ] );
1087 return AuthenticationResponse::newFail( $status->getMessage() );
1088 }
1089
1090 $user = User::newFromName( $username, 'creatable' );
1091 foreach ( $reqs as $req ) {
1092 $req->username = $username;
1093 $req->returnToUrl = $returnToUrl;
1094 if ( $req instanceof UserDataAuthenticationRequest ) {
1095 $status = $req->populateUser( $user );
1096 if ( !$status->isGood() ) {
1097 $status = Status::wrap( $status );
1098 $session->remove( 'AuthManager::accountCreationState' );
1099 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1100 'user' => $user->getName(),
1101 'creator' => $creator->getName(),
1102 'reason' => $status->getWikiText( null, null, 'en' ),
1103 ] );
1104 return AuthenticationResponse::newFail( $status->getMessage() );
1105 }
1106 }
1107 }
1108
1109 $this->removeAuthenticationSessionData( null );
1110
1111 $state = [
1112 'username' => $username,
1113 'userid' => 0,
1114 'creatorid' => $creator->getId(),
1115 'creatorname' => $creator->getName(),
1116 'reqs' => $reqs,
1117 'returnToUrl' => $returnToUrl,
1118 'primary' => null,
1119 'primaryResponse' => null,
1120 'secondary' => [],
1121 'continueRequests' => [],
1122 'maybeLink' => [],
1123 'ranPreTests' => false,
1124 ];
1125
1126 // Special case: converting a login to an account creation
1128 $reqs, CreateFromLoginAuthenticationRequest::class
1129 );
1130 if ( $req ) {
1131 $state['maybeLink'] = $req->maybeLink;
1132
1133 if ( $req->createRequest ) {
1134 $reqs[] = $req->createRequest;
1135 $state['reqs'][] = $req->createRequest;
1136 }
1137 }
1138
1139 $session->setSecret( 'AuthManager::accountCreationState', $state );
1140 $session->persist();
1141
1142 return $this->continueAccountCreation( $reqs );
1143 }
1144
1150 public function continueAccountCreation( array $reqs ) {
1151 $session = $this->request->getSession();
1152 try {
1153 if ( !$this->canCreateAccounts() ) {
1154 // Caller should have called canCreateAccounts()
1155 $session->remove( 'AuthManager::accountCreationState' );
1156 throw new \LogicException( 'Account creation is not possible' );
1157 }
1158
1159 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1160 if ( !is_array( $state ) ) {
1162 wfMessage( 'authmanager-create-not-in-progress' )
1163 );
1164 }
1165 $state['continueRequests'] = [];
1166
1167 // Step 0: Prepare and validate the input
1168
1169 $user = User::newFromName( $state['username'], 'creatable' );
1170 if ( !is_object( $user ) ) {
1171 $session->remove( 'AuthManager::accountCreationState' );
1172 $this->logger->debug( __METHOD__ . ': Invalid username', [
1173 'user' => $state['username'],
1174 ] );
1175 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1176 }
1177
1178 if ( $state['creatorid'] ) {
1179 $creator = User::newFromId( $state['creatorid'] );
1180 } else {
1181 $creator = new User;
1182 $creator->setName( $state['creatorname'] );
1183 }
1184
1185 // Avoid account creation races on double submissions
1186 $cache = \ObjectCache::getLocalClusterInstance();
1187 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1188 if ( !$lock ) {
1189 // Don't clear AuthManager::accountCreationState for this code
1190 // path because the process that won the race owns it.
1191 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1192 'user' => $user->getName(),
1193 'creator' => $creator->getName(),
1194 ] );
1195 return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1196 }
1197
1198 // Permissions check
1199 $status = $this->checkAccountCreatePermissions( $creator );
1200 if ( !$status->isGood() ) {
1201 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1202 'user' => $user->getName(),
1203 'creator' => $creator->getName(),
1204 'reason' => $status->getWikiText( null, null, 'en' )
1205 ] );
1206 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1207 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1208 $session->remove( 'AuthManager::accountCreationState' );
1209 return $ret;
1210 }
1211
1212 // Load from master for existence check
1213 $user->load( User::READ_LOCKING );
1214
1215 if ( $state['userid'] === 0 ) {
1216 if ( $user->getId() != 0 ) {
1217 $this->logger->debug( __METHOD__ . ': User exists locally', [
1218 'user' => $user->getName(),
1219 'creator' => $creator->getName(),
1220 ] );
1221 $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1222 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1223 $session->remove( 'AuthManager::accountCreationState' );
1224 return $ret;
1225 }
1226 } else {
1227 if ( $user->getId() == 0 ) {
1228 $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1229 'user' => $user->getName(),
1230 'creator' => $creator->getName(),
1231 'expected_id' => $state['userid'],
1232 ] );
1233 throw new \UnexpectedValueException(
1234 "User \"{$state['username']}\" should exist now, but doesn't!"
1235 );
1236 }
1237 if ( $user->getId() != $state['userid'] ) {
1238 $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1239 'user' => $user->getName(),
1240 'creator' => $creator->getName(),
1241 'expected_id' => $state['userid'],
1242 'actual_id' => $user->getId(),
1243 ] );
1244 throw new \UnexpectedValueException(
1245 "User \"{$state['username']}\" exists, but " .
1246 "ID {$user->getId()} != {$state['userid']}!"
1247 );
1248 }
1249 }
1250 foreach ( $state['reqs'] as $req ) {
1251 if ( $req instanceof UserDataAuthenticationRequest ) {
1252 $status = $req->populateUser( $user );
1253 if ( !$status->isGood() ) {
1254 // This should never happen...
1255 $status = Status::wrap( $status );
1256 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1257 'user' => $user->getName(),
1258 'creator' => $creator->getName(),
1259 'reason' => $status->getWikiText( null, null, 'en' ),
1260 ] );
1261 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1262 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1263 $session->remove( 'AuthManager::accountCreationState' );
1264 return $ret;
1265 }
1266 }
1267 }
1268
1269 foreach ( $reqs as $req ) {
1270 $req->returnToUrl = $state['returnToUrl'];
1271 $req->username = $state['username'];
1272 }
1273
1274 // Run pre-creation tests, if we haven't already
1275 if ( !$state['ranPreTests'] ) {
1276 $providers = $this->getPreAuthenticationProviders() +
1279 foreach ( $providers as $id => $provider ) {
1280 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1281 if ( !$status->isGood() ) {
1282 $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1283 'user' => $user->getName(),
1284 'creator' => $creator->getName(),
1285 ] );
1287 Status::wrap( $status )->getMessage()
1288 );
1289 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1290 $session->remove( 'AuthManager::accountCreationState' );
1291 return $ret;
1292 }
1293 }
1294
1295 $state['ranPreTests'] = true;
1296 }
1297
1298 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1299
1300 if ( $state['primary'] === null ) {
1301 // We haven't picked a PrimaryAuthenticationProvider yet
1302 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1303 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1304 continue;
1305 }
1306 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1307 switch ( $res->status ) {
1309 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1310 'user' => $user->getName(),
1311 'creator' => $creator->getName(),
1312 ] );
1313 $state['primary'] = $id;
1314 $state['primaryResponse'] = $res;
1315 break 2;
1317 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1318 'user' => $user->getName(),
1319 'creator' => $creator->getName(),
1320 ] );
1321 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1322 $session->remove( 'AuthManager::accountCreationState' );
1323 return $res;
1325 // Continue loop
1326 break;
1329 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1330 'user' => $user->getName(),
1331 'creator' => $creator->getName(),
1332 ] );
1333 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1334 $state['primary'] = $id;
1335 $state['continueRequests'] = $res->neededRequests;
1336 $session->setSecret( 'AuthManager::accountCreationState', $state );
1337 return $res;
1338
1339 // @codeCoverageIgnoreStart
1340 default:
1341 throw new \DomainException(
1342 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1343 );
1344 // @codeCoverageIgnoreEnd
1345 }
1346 }
1347 if ( $state['primary'] === null ) {
1348 $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1349 'user' => $user->getName(),
1350 'creator' => $creator->getName(),
1351 ] );
1353 wfMessage( 'authmanager-create-no-primary' )
1354 );
1355 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1356 $session->remove( 'AuthManager::accountCreationState' );
1357 return $ret;
1358 }
1359 } elseif ( $state['primaryResponse'] === null ) {
1360 $provider = $this->getAuthenticationProvider( $state['primary'] );
1361 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1362 // Configuration changed? Force them to start over.
1363 // @codeCoverageIgnoreStart
1365 wfMessage( 'authmanager-create-not-in-progress' )
1366 );
1367 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1368 $session->remove( 'AuthManager::accountCreationState' );
1369 return $ret;
1370 // @codeCoverageIgnoreEnd
1371 }
1372 $id = $provider->getUniqueId();
1373 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1374 switch ( $res->status ) {
1376 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1377 'user' => $user->getName(),
1378 'creator' => $creator->getName(),
1379 ] );
1380 $state['primaryResponse'] = $res;
1381 break;
1383 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1384 'user' => $user->getName(),
1385 'creator' => $creator->getName(),
1386 ] );
1387 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1388 $session->remove( 'AuthManager::accountCreationState' );
1389 return $res;
1392 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1393 'user' => $user->getName(),
1394 'creator' => $creator->getName(),
1395 ] );
1396 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1397 $state['continueRequests'] = $res->neededRequests;
1398 $session->setSecret( 'AuthManager::accountCreationState', $state );
1399 return $res;
1400 default:
1401 throw new \DomainException(
1402 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1403 );
1404 }
1405 }
1406
1407 // Step 2: Primary authentication succeeded, create the User object
1408 // and add the user locally.
1409
1410 if ( $state['userid'] === 0 ) {
1411 $this->logger->info( 'Creating user {user} during account creation', [
1412 'user' => $user->getName(),
1413 'creator' => $creator->getName(),
1414 ] );
1415 $status = $user->addToDatabase();
1416 if ( !$status->isOK() ) {
1417 // @codeCoverageIgnoreStart
1418 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1419 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1420 $session->remove( 'AuthManager::accountCreationState' );
1421 return $ret;
1422 // @codeCoverageIgnoreEnd
1423 }
1424 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1425 \Hooks::run( 'LocalUserCreated', [ $user, false ] );
1426 $user->saveSettings();
1427 $state['userid'] = $user->getId();
1428
1429 // Update user count
1430 \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1431
1432 // Watch user's userpage and talk page
1433 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1434
1435 // Inform the provider
1436 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1437
1438 // Log the creation
1439 if ( $this->config->get( 'NewUserLog' ) ) {
1440 $isAnon = $creator->isAnon();
1441 $logEntry = new \ManualLogEntry(
1442 'newusers',
1443 $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1444 );
1445 $logEntry->setPerformer( $isAnon ? $user : $creator );
1446 $logEntry->setTarget( $user->getUserPage() );
1449 $state['reqs'], CreationReasonAuthenticationRequest::class
1450 );
1451 $logEntry->setComment( $req ? $req->reason : '' );
1452 $logEntry->setParameters( [
1453 '4::userid' => $user->getId(),
1454 ] );
1455 $logid = $logEntry->insert();
1456 $logEntry->publish( $logid );
1457 }
1458 }
1459
1460 // Step 3: Iterate over all the secondary authentication providers.
1461
1462 $beginReqs = $state['reqs'];
1463
1464 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1465 if ( !isset( $state['secondary'][$id] ) ) {
1466 // This provider isn't started yet, so we pass it the set
1467 // of reqs from beginAuthentication instead of whatever
1468 // might have been used by a previous provider in line.
1469 $func = 'beginSecondaryAccountCreation';
1470 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1471 } elseif ( !$state['secondary'][$id] ) {
1472 $func = 'continueSecondaryAccountCreation';
1473 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1474 } else {
1475 continue;
1476 }
1477 switch ( $res->status ) {
1479 $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1480 'user' => $user->getName(),
1481 'creator' => $creator->getName(),
1482 ] );
1483 // fall through
1485 $state['secondary'][$id] = true;
1486 break;
1489 $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1490 'user' => $user->getName(),
1491 'creator' => $creator->getName(),
1492 ] );
1493 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1494 $state['secondary'][$id] = false;
1495 $state['continueRequests'] = $res->neededRequests;
1496 $session->setSecret( 'AuthManager::accountCreationState', $state );
1497 return $res;
1499 throw new \DomainException(
1500 get_class( $provider ) . "::{$func}() returned $res->status." .
1501 ' Secondary providers are not allowed to fail account creation, that' .
1502 ' should have been done via testForAccountCreation().'
1503 );
1504 // @codeCoverageIgnoreStart
1505 default:
1506 throw new \DomainException(
1507 get_class( $provider ) . "::{$func}() returned $res->status"
1508 );
1509 // @codeCoverageIgnoreEnd
1510 }
1511 }
1512
1513 $id = $user->getId();
1514 $name = $user->getName();
1517 $ret->loginRequest = $req;
1518 $this->createdAccountAuthenticationRequests[] = $req;
1519
1520 $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1521 'user' => $user->getName(),
1522 'creator' => $creator->getName(),
1523 ] );
1524
1525 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1526 $session->remove( 'AuthManager::accountCreationState' );
1527 $this->removeAuthenticationSessionData( null );
1528 return $ret;
1529 } catch ( \Exception $ex ) {
1530 $session->remove( 'AuthManager::accountCreationState' );
1531 throw $ex;
1532 }
1533 }
1534
1550 public function autoCreateUser( User $user, $source, $login = true ) {
1551 if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1553 ) {
1554 throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1555 }
1556
1557 $username = $user->getName();
1558
1559 // Try the local user from the replica DB
1560 $localId = User::idFromName( $username );
1561 $flags = User::READ_NORMAL;
1562
1563 // Fetch the user ID from the master, so that we don't try to create the user
1564 // when they already exist, due to replication lag
1565 // @codeCoverageIgnoreStart
1566 if (
1567 !$localId &&
1568 MediaWikiServices::getInstance()->getDBLoadBalancer()->getReaderIndex() != 0
1569 ) {
1570 $localId = User::idFromName( $username, User::READ_LATEST );
1571 $flags = User::READ_LATEST;
1572 }
1573 // @codeCoverageIgnoreEnd
1574
1575 if ( $localId ) {
1576 $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1577 'username' => $username,
1578 ] );
1579 $user->setId( $localId );
1580 $user->loadFromId( $flags );
1581 if ( $login ) {
1582 $this->setSessionDataForUser( $user );
1583 }
1584 $status = Status::newGood();
1585 $status->warning( 'userexists' );
1586 return $status;
1587 }
1588
1589 // Wiki is read-only?
1590 if ( wfReadOnly() ) {
1591 $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1592 'username' => $username,
1593 'reason' => wfReadOnlyReason(),
1594 ] );
1595 $user->setId( 0 );
1596 $user->loadFromId();
1597 return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1598 }
1599
1600 // Check the session, if we tried to create this user already there's
1601 // no point in retrying.
1602 $session = $this->request->getSession();
1603 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1604 $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1605 'username' => $username,
1606 'sessionid' => $session->getId(),
1607 ] );
1608 $user->setId( 0 );
1609 $user->loadFromId();
1610 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1611 if ( $reason instanceof StatusValue ) {
1612 return Status::wrap( $reason );
1613 } else {
1614 return Status::newFatal( $reason );
1615 }
1616 }
1617
1618 // Is the username creatable?
1619 if ( !User::isCreatableName( $username ) ) {
1620 $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1621 'username' => $username,
1622 ] );
1623 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1624 $user->setId( 0 );
1625 $user->loadFromId();
1626 return Status::newFatal( 'noname' );
1627 }
1628
1629 // Is the IP user able to create accounts?
1630 $anon = new User;
1631 if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1632 $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1633 'username' => $username,
1634 'ip' => $anon->getName(),
1635 ] );
1636 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1637 $session->persist();
1638 $user->setId( 0 );
1639 $user->loadFromId();
1640 return Status::newFatal( 'authmanager-autocreate-noperm' );
1641 }
1642
1643 // Avoid account creation races on double submissions
1644 $cache = \ObjectCache::getLocalClusterInstance();
1645 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1646 if ( !$lock ) {
1647 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1648 'user' => $username,
1649 ] );
1650 $user->setId( 0 );
1651 $user->loadFromId();
1652 return Status::newFatal( 'usernameinprogress' );
1653 }
1654
1655 // Denied by providers?
1656 $options = [
1657 'flags' => User::READ_LATEST,
1658 'creating' => true,
1659 ];
1660 $providers = $this->getPreAuthenticationProviders() +
1663 foreach ( $providers as $provider ) {
1664 $status = $provider->testUserForCreation( $user, $source, $options );
1665 if ( !$status->isGood() ) {
1666 $ret = Status::wrap( $status );
1667 $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1668 'username' => $username,
1669 'reason' => $ret->getWikiText( null, null, 'en' ),
1670 ] );
1671 $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1672 $user->setId( 0 );
1673 $user->loadFromId();
1674 return $ret;
1675 }
1676 }
1677
1678 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1679 if ( $cache->get( $backoffKey ) ) {
1680 $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1681 'username' => $username,
1682 ] );
1683 $user->setId( 0 );
1684 $user->loadFromId();
1685 return Status::newFatal( 'authmanager-autocreate-exception' );
1686 }
1687
1688 // Checks passed, create the user...
1689 $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
1690 $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1691 'username' => $username,
1692 'from' => $from,
1693 ] );
1694
1695 // Ignore warnings about master connections/writes...hard to avoid here
1696 $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1697 $old = $trxProfiler->setSilenced( true );
1698 try {
1699 $status = $user->addToDatabase();
1700 if ( !$status->isOK() ) {
1701 // Double-check for a race condition (T70012). We make use of the fact that when
1702 // addToDatabase fails due to the user already existing, the user object gets loaded.
1703 if ( $user->getId() ) {
1704 $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1705 'username' => $username,
1706 ] );
1707 if ( $login ) {
1708 $this->setSessionDataForUser( $user );
1709 }
1710 $status = Status::newGood();
1711 $status->warning( 'userexists' );
1712 } else {
1713 $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1714 'username' => $username,
1715 'msg' => $status->getWikiText( null, null, 'en' )
1716 ] );
1717 $user->setId( 0 );
1718 $user->loadFromId();
1719 }
1720 return $status;
1721 }
1722 } catch ( \Exception $ex ) {
1723 $trxProfiler->setSilenced( $old );
1724 $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1725 'username' => $username,
1726 'exception' => $ex,
1727 ] );
1728 // Do not keep throwing errors for a while
1729 $cache->set( $backoffKey, 1, 600 );
1730 // Bubble up error; which should normally trigger DB rollbacks
1731 throw $ex;
1732 }
1733
1734 $this->setDefaultUserOptions( $user, false );
1735
1736 // Inform the providers
1737 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1738
1739 \Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1740 \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1741 $user->saveSettings();
1742
1743 // Update user count
1744 \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1745 // Watch user's userpage and talk page
1746 \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1747 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1748 } );
1749
1750 // Log the creation
1751 if ( $this->config->get( 'NewUserLog' ) ) {
1752 $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1753 $logEntry->setPerformer( $user );
1754 $logEntry->setTarget( $user->getUserPage() );
1755 $logEntry->setComment( '' );
1756 $logEntry->setParameters( [
1757 '4::userid' => $user->getId(),
1758 ] );
1759 $logEntry->insert();
1760 }
1761
1762 $trxProfiler->setSilenced( $old );
1763
1764 if ( $login ) {
1765 $this->setSessionDataForUser( $user );
1766 }
1767
1768 return Status::newGood();
1769 }
1770
1782 public function canLinkAccounts() {
1783 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1784 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1785 return true;
1786 }
1787 }
1788 return false;
1789 }
1790
1800 public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1801 $session = $this->request->getSession();
1802 $session->remove( 'AuthManager::accountLinkState' );
1803
1804 if ( !$this->canLinkAccounts() ) {
1805 // Caller should have called canLinkAccounts()
1806 throw new \LogicException( 'Account linking is not possible' );
1807 }
1808
1809 if ( $user->getId() === 0 ) {
1810 if ( !User::isUsableName( $user->getName() ) ) {
1811 $msg = wfMessage( 'noname' );
1812 } else {
1813 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1814 }
1815 return AuthenticationResponse::newFail( $msg );
1816 }
1817 foreach ( $reqs as $req ) {
1818 $req->username = $user->getName();
1819 $req->returnToUrl = $returnToUrl;
1820 }
1821
1822 $this->removeAuthenticationSessionData( null );
1823
1824 $providers = $this->getPreAuthenticationProviders();
1825 foreach ( $providers as $id => $provider ) {
1826 $status = $provider->testForAccountLink( $user );
1827 if ( !$status->isGood() ) {
1828 $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1829 'user' => $user->getName(),
1830 ] );
1832 Status::wrap( $status )->getMessage()
1833 );
1834 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1835 return $ret;
1836 }
1837 }
1838
1839 $state = [
1840 'username' => $user->getName(),
1841 'userid' => $user->getId(),
1842 'returnToUrl' => $returnToUrl,
1843 'primary' => null,
1844 'continueRequests' => [],
1845 ];
1846
1847 $providers = $this->getPrimaryAuthenticationProviders();
1848 foreach ( $providers as $id => $provider ) {
1849 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1850 continue;
1851 }
1852
1853 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1854 switch ( $res->status ) {
1856 $this->logger->info( "Account linked to {user} by $id", [
1857 'user' => $user->getName(),
1858 ] );
1859 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1860 return $res;
1861
1863 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1864 'user' => $user->getName(),
1865 ] );
1866 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1867 return $res;
1868
1870 // Continue loop
1871 break;
1872
1875 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1876 'user' => $user->getName(),
1877 ] );
1878 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1879 $state['primary'] = $id;
1880 $state['continueRequests'] = $res->neededRequests;
1881 $session->setSecret( 'AuthManager::accountLinkState', $state );
1882 $session->persist();
1883 return $res;
1884
1885 // @codeCoverageIgnoreStart
1886 default:
1887 throw new \DomainException(
1888 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1889 );
1890 // @codeCoverageIgnoreEnd
1891 }
1892 }
1893
1894 $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1895 'user' => $user->getName(),
1896 ] );
1898 wfMessage( 'authmanager-link-no-primary' )
1899 );
1900 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1901 return $ret;
1902 }
1903
1909 public function continueAccountLink( array $reqs ) {
1910 $session = $this->request->getSession();
1911 try {
1912 if ( !$this->canLinkAccounts() ) {
1913 // Caller should have called canLinkAccounts()
1914 $session->remove( 'AuthManager::accountLinkState' );
1915 throw new \LogicException( 'Account linking is not possible' );
1916 }
1917
1918 $state = $session->getSecret( 'AuthManager::accountLinkState' );
1919 if ( !is_array( $state ) ) {
1921 wfMessage( 'authmanager-link-not-in-progress' )
1922 );
1923 }
1924 $state['continueRequests'] = [];
1925
1926 // Step 0: Prepare and validate the input
1927
1928 $user = User::newFromName( $state['username'], 'usable' );
1929 if ( !is_object( $user ) ) {
1930 $session->remove( 'AuthManager::accountLinkState' );
1931 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1932 }
1933 if ( $user->getId() != $state['userid'] ) {
1934 throw new \UnexpectedValueException(
1935 "User \"{$state['username']}\" is valid, but " .
1936 "ID {$user->getId()} != {$state['userid']}!"
1937 );
1938 }
1939
1940 foreach ( $reqs as $req ) {
1941 $req->username = $state['username'];
1942 $req->returnToUrl = $state['returnToUrl'];
1943 }
1944
1945 // Step 1: Call the primary again until it succeeds
1946
1947 $provider = $this->getAuthenticationProvider( $state['primary'] );
1948 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1949 // Configuration changed? Force them to start over.
1950 // @codeCoverageIgnoreStart
1952 wfMessage( 'authmanager-link-not-in-progress' )
1953 );
1954 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1955 $session->remove( 'AuthManager::accountLinkState' );
1956 return $ret;
1957 // @codeCoverageIgnoreEnd
1958 }
1959 $id = $provider->getUniqueId();
1960 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1961 switch ( $res->status ) {
1963 $this->logger->info( "Account linked to {user} by $id", [
1964 'user' => $user->getName(),
1965 ] );
1966 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1967 $session->remove( 'AuthManager::accountLinkState' );
1968 return $res;
1970 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1971 'user' => $user->getName(),
1972 ] );
1973 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1974 $session->remove( 'AuthManager::accountLinkState' );
1975 return $res;
1978 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1979 'user' => $user->getName(),
1980 ] );
1981 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1982 $state['continueRequests'] = $res->neededRequests;
1983 $session->setSecret( 'AuthManager::accountLinkState', $state );
1984 return $res;
1985 default:
1986 throw new \DomainException(
1987 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1988 );
1989 }
1990 } catch ( \Exception $ex ) {
1991 $session->remove( 'AuthManager::accountLinkState' );
1992 throw $ex;
1993 }
1994 }
1995
2021 public function getAuthenticationRequests( $action, User $user = null ) {
2022 $options = [];
2023 $providerAction = $action;
2024
2025 // Figure out which providers to query
2026 switch ( $action ) {
2027 case self::ACTION_LOGIN:
2029 $providers = $this->getPreAuthenticationProviders() +
2032 break;
2033
2035 $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2036 return is_array( $state ) ? $state['continueRequests'] : [];
2037
2039 $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2040 return is_array( $state ) ? $state['continueRequests'] : [];
2041
2042 case self::ACTION_LINK:
2043 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2044 return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2045 } );
2046 break;
2047
2049 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2050 return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2051 } );
2052
2053 // To providers, unlink and remove are identical.
2054 $providerAction = self::ACTION_REMOVE;
2055 break;
2056
2058 $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2059 return is_array( $state ) ? $state['continueRequests'] : [];
2060
2063 $providers = $this->getPrimaryAuthenticationProviders() +
2065 break;
2066
2067 // @codeCoverageIgnoreStart
2068 default:
2069 throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2070 }
2071 // @codeCoverageIgnoreEnd
2072
2073 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2074 }
2075
2086 $providerAction, array $options, array $providers, User $user = null
2087 ) {
2088 $user = $user ?: \RequestContext::getMain()->getUser();
2089 $options['username'] = $user->isAnon() ? null : $user->getName();
2090
2091 // Query them and merge results
2092 $reqs = [];
2093 foreach ( $providers as $provider ) {
2094 $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2095 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2096 $id = $req->getUniqueId();
2097
2098 // If a required request if from a Primary, mark it as "primary-required" instead
2099 if ( $isPrimary ) {
2100 if ( $req->required ) {
2102 }
2103 }
2104
2105 if (
2106 !isset( $reqs[$id] )
2107 || $req->required === AuthenticationRequest::REQUIRED
2108 || $reqs[$id] === AuthenticationRequest::OPTIONAL
2109 ) {
2110 $reqs[$id] = $req;
2111 }
2112 }
2113 }
2114
2115 // AuthManager has its own req for some actions
2116 switch ( $providerAction ) {
2117 case self::ACTION_LOGIN:
2118 $reqs[] = new RememberMeAuthenticationRequest;
2119 break;
2120
2122 $reqs[] = new UsernameAuthenticationRequest;
2123 $reqs[] = new UserDataAuthenticationRequest;
2124 if ( $options['username'] !== null ) {
2126 $options['username'] = null; // Don't fill in the username below
2127 }
2128 break;
2129 }
2130
2131 // Fill in reqs data
2132 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2133
2134 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2135 if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2136 $reqs = array_filter( $reqs, function ( $req ) {
2137 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2138 } );
2139 }
2140
2141 return array_values( $reqs );
2142 }
2143
2151 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2152 foreach ( $reqs as $req ) {
2153 if ( !$req->action || $forceAction ) {
2154 $req->action = $action;
2155 }
2156 if ( $req->username === null ) {
2157 $req->username = $username;
2158 }
2159 }
2160 }
2161
2168 public function userExists( $username, $flags = User::READ_NORMAL ) {
2169 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2170 if ( $provider->testUserExists( $username, $flags ) ) {
2171 return true;
2172 }
2173 }
2174
2175 return false;
2176 }
2177
2189 public function allowsPropertyChange( $property ) {
2190 $providers = $this->getPrimaryAuthenticationProviders() +
2192 foreach ( $providers as $provider ) {
2193 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2194 return false;
2195 }
2196 }
2197 return true;
2198 }
2199
2208 public function getAuthenticationProvider( $id ) {
2209 // Fast version
2210 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2211 return $this->allAuthenticationProviders[$id];
2212 }
2213
2214 // Slow version: instantiate each kind and check
2215 $providers = $this->getPrimaryAuthenticationProviders();
2216 if ( isset( $providers[$id] ) ) {
2217 return $providers[$id];
2218 }
2219 $providers = $this->getSecondaryAuthenticationProviders();
2220 if ( isset( $providers[$id] ) ) {
2221 return $providers[$id];
2222 }
2223 $providers = $this->getPreAuthenticationProviders();
2224 if ( isset( $providers[$id] ) ) {
2225 return $providers[$id];
2226 }
2227
2228 return null;
2229 }
2230
2244 public function setAuthenticationSessionData( $key, $data ) {
2245 $session = $this->request->getSession();
2246 $arr = $session->getSecret( 'authData' );
2247 if ( !is_array( $arr ) ) {
2248 $arr = [];
2249 }
2250 $arr[$key] = $data;
2251 $session->setSecret( 'authData', $arr );
2252 }
2253
2261 public function getAuthenticationSessionData( $key, $default = null ) {
2262 $arr = $this->request->getSession()->getSecret( 'authData' );
2263 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2264 return $arr[$key];
2265 } else {
2266 return $default;
2267 }
2268 }
2269
2275 public function removeAuthenticationSessionData( $key ) {
2276 $session = $this->request->getSession();
2277 if ( $key === null ) {
2278 $session->remove( 'authData' );
2279 } else {
2280 $arr = $session->getSecret( 'authData' );
2281 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2282 unset( $arr[$key] );
2283 $session->setSecret( 'authData', $arr );
2284 }
2285 }
2286 }
2287
2294 protected function providerArrayFromSpecs( $class, array $specs ) {
2295 $i = 0;
2296 foreach ( $specs as &$spec ) {
2297 $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2298 }
2299 unset( $spec );
2300 // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2301 usort( $specs, function ( $a, $b ) {
2302 return $a['sort'] <=> $b['sort']
2303 ?: $a['sort2'] <=> $b['sort2'];
2304 } );
2305
2306 $ret = [];
2307 foreach ( $specs as $spec ) {
2308 $provider = ObjectFactory::getObjectFromSpec( $spec );
2309 if ( !$provider instanceof $class ) {
2310 throw new \RuntimeException(
2311 "Expected instance of $class, got " . get_class( $provider )
2312 );
2313 }
2314 $provider->setLogger( $this->logger );
2315 $provider->setManager( $this );
2316 $provider->setConfig( $this->config );
2317 $id = $provider->getUniqueId();
2318 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2319 throw new \RuntimeException(
2320 "Duplicate specifications for id $id (classes " .
2321 get_class( $provider ) . ' and ' .
2322 get_class( $this->allAuthenticationProviders[$id] ) . ')'
2323 );
2324 }
2325 $this->allAuthenticationProviders[$id] = $provider;
2326 $ret[$id] = $provider;
2327 }
2328 return $ret;
2329 }
2330
2335 private function getConfiguration() {
2336 return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2337 }
2338
2343 protected function getPreAuthenticationProviders() {
2344 if ( $this->preAuthenticationProviders === null ) {
2345 $conf = $this->getConfiguration();
2346 $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2347 PreAuthenticationProvider::class, $conf['preauth']
2348 );
2349 }
2351 }
2352
2358 if ( $this->primaryAuthenticationProviders === null ) {
2359 $conf = $this->getConfiguration();
2360 $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2361 PrimaryAuthenticationProvider::class, $conf['primaryauth']
2362 );
2363 }
2365 }
2366
2372 if ( $this->secondaryAuthenticationProviders === null ) {
2373 $conf = $this->getConfiguration();
2374 $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2375 SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2376 );
2377 }
2379 }
2380
2386 private function setSessionDataForUser( $user, $remember = null ) {
2387 $session = $this->request->getSession();
2388 $delay = $session->delaySave();
2389
2390 $session->resetId();
2391 $session->resetAllTokens();
2392 if ( $session->canSetUser() ) {
2393 $session->setUser( $user );
2394 }
2395 if ( $remember !== null ) {
2396 $session->setRememberUser( $remember );
2397 }
2398 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2399 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2400 $session->persist();
2401
2402 \Wikimedia\ScopedCallback::consume( $delay );
2403
2404 \Hooks::run( 'UserLoggedIn', [ $user ] );
2405 }
2406
2411 private function setDefaultUserOptions( User $user, $useContextLang ) {
2412 $user->setToken();
2413
2414 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2415
2416 $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $contLang;
2417 $user->setOption( 'language', $lang->getPreferredVariant() );
2418
2419 if ( $contLang->hasVariants() ) {
2420 $user->setOption( 'variant', $contLang->getPreferredVariant() );
2421 }
2422 }
2423
2429 private function callMethodOnProviders( $which, $method, array $args ) {
2430 $providers = [];
2431 if ( $which & 1 ) {
2432 $providers += $this->getPreAuthenticationProviders();
2433 }
2434 if ( $which & 2 ) {
2435 $providers += $this->getPrimaryAuthenticationProviders();
2436 }
2437 if ( $which & 4 ) {
2438 $providers += $this->getSecondaryAuthenticationProviders();
2439 }
2440 foreach ( $providers as $provider ) {
2441 $provider->$method( ...$args );
2442 }
2443 }
2444
2449 public static function resetCache() {
2450 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2451 // @codeCoverageIgnoreStart
2452 throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2453 // @codeCoverageIgnoreEnd
2454 }
2455
2456 self::$instance = null;
2457 }
2458
2461}
2462
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.
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:47
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2462
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:592
static isCreatableName( $name)
Usernames which fail to pass this function will be blocked from new account registrations,...
Definition User.php:1121
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition User.php:2000
setName( $str)
Set the user name.
Definition User.php:2489
getId()
Get the user's ID.
Definition User.php:2437
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:615
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition User.php:1046
const IGNORE_USER_RIGHTS
Definition User.php:84
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition User.php:4499
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition User.php:911
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
this hook is for auditing only $req
Definition hooks.txt:1018
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:1305
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:2050
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:2274
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:2054
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:302
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:815
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
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