MediaWiki REL1_30
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;
34
82class AuthManager implements LoggerAwareInterface {
84 const ACTION_LOGIN = 'login';
87 const ACTION_LOGIN_CONTINUE = 'login-continue';
89 const ACTION_CREATE = 'create';
92 const ACTION_CREATE_CONTINUE = 'create-continue';
94 const ACTION_LINK = 'link';
97 const ACTION_LINK_CONTINUE = 'link-continue';
99 const ACTION_CHANGE = 'change';
101 const ACTION_REMOVE = 'remove';
103 const ACTION_UNLINK = 'unlink';
104
106 const SEC_OK = 'ok';
108 const SEC_REAUTH = 'reauth';
110 const SEC_FAIL = 'fail';
111
113 const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
114
116 private static $instance = null;
117
119 private $request;
120
122 private $config;
123
125 private $logger;
126
129
132
135
138
141
146 public static function singleton() {
147 if ( self::$instance === null ) {
148 self::$instance = new self(
149 \RequestContext::getMain()->getRequest(),
150 MediaWikiServices::getInstance()->getMainConfig()
151 );
152 }
153 return self::$instance;
154 }
155
161 $this->request = $request;
162 $this->config = $config;
163 $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
164 }
165
169 public function setLogger( LoggerInterface $logger ) {
170 $this->logger = $logger;
171 }
172
176 public function getRequest() {
177 return $this->request;
178 }
179
186 public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
187 $this->logger->warning( "Overriding AuthManager primary authn because $why" );
188
189 if ( $this->primaryAuthenticationProviders !== null ) {
190 $this->logger->warning(
191 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
192 );
193
194 $this->allAuthenticationProviders = array_diff_key(
195 $this->allAuthenticationProviders,
196 $this->primaryAuthenticationProviders
197 );
198 $session = $this->request->getSession();
199 $session->remove( 'AuthManager::authnState' );
200 $session->remove( 'AuthManager::accountCreationState' );
201 $session->remove( 'AuthManager::accountLinkState' );
202 $this->createdAccountAuthenticationRequests = [];
203 }
204
205 $this->primaryAuthenticationProviders = [];
206 foreach ( $providers as $provider ) {
207 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
208 throw new \RuntimeException(
209 'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
210 get_class( $provider )
211 );
212 }
213 $provider->setLogger( $this->logger );
214 $provider->setManager( $this );
215 $provider->setConfig( $this->config );
216 $id = $provider->getUniqueId();
217 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
218 throw new \RuntimeException(
219 "Duplicate specifications for id $id (classes " .
220 get_class( $provider ) . ' and ' .
221 get_class( $this->allAuthenticationProviders[$id] ) . ')'
222 );
223 }
224 $this->allAuthenticationProviders[$id] = $provider;
225 $this->primaryAuthenticationProviders[$id] = $provider;
226 }
227 }
228
238 public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
240
241 if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
242 return call_user_func_array( [ $wgAuth, $method ], $params );
243 } else {
244 return $return;
245 }
246 }
247
261 public function canAuthenticateNow() {
262 return $this->request->getSession()->canSetUser();
263 }
264
283 public function beginAuthentication( array $reqs, $returnToUrl ) {
284 $session = $this->request->getSession();
285 if ( !$session->canSetUser() ) {
286 // Caller should have called canAuthenticateNow()
287 $session->remove( 'AuthManager::authnState' );
288 throw new \LogicException( 'Authentication is not possible now' );
289 }
290
291 $guessUserName = null;
292 foreach ( $reqs as $req ) {
293 $req->returnToUrl = $returnToUrl;
294 // @codeCoverageIgnoreStart
295 if ( $req->username !== null && $req->username !== '' ) {
296 if ( $guessUserName === null ) {
297 $guessUserName = $req->username;
298 } elseif ( $guessUserName !== $req->username ) {
299 $guessUserName = null;
300 break;
301 }
302 }
303 // @codeCoverageIgnoreEnd
304 }
305
306 // Check for special-case login of a just-created account
308 $reqs, CreatedAccountAuthenticationRequest::class
309 );
310 if ( $req ) {
311 if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
312 throw new \LogicException(
313 'CreatedAccountAuthenticationRequests are only valid on ' .
314 'the same AuthManager that created the account'
315 );
316 }
317
318 $user = User::newFromName( $req->username );
319 // @codeCoverageIgnoreStart
320 if ( !$user ) {
321 throw new \UnexpectedValueException(
322 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
323 );
324 } elseif ( $user->getId() != $req->id ) {
325 throw new \UnexpectedValueException(
326 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
327 );
328 }
329 // @codeCoverageIgnoreEnd
330
331 $this->logger->info( 'Logging in {user} after account creation', [
332 'user' => $user->getName(),
333 ] );
336 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
337 $session->remove( 'AuthManager::authnState' );
338 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
339 return $ret;
340 }
341
342 $this->removeAuthenticationSessionData( null );
343
344 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
345 $status = $provider->testForAuthentication( $reqs );
346 if ( !$status->isGood() ) {
347 $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
349 Status::wrap( $status )->getMessage()
350 );
351 $this->callMethodOnProviders( 7, 'postAuthentication',
352 [ User::newFromName( $guessUserName ) ?: null, $ret ]
353 );
354 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
355 return $ret;
356 }
357 }
358
359 $state = [
360 'reqs' => $reqs,
361 'returnToUrl' => $returnToUrl,
362 'guessUserName' => $guessUserName,
363 'primary' => null,
364 'primaryResponse' => null,
365 'secondary' => [],
366 'maybeLink' => [],
367 'continueRequests' => [],
368 ];
369
370 // Preserve state from a previous failed login
372 $reqs, CreateFromLoginAuthenticationRequest::class
373 );
374 if ( $req ) {
375 $state['maybeLink'] = $req->maybeLink;
376 }
377
378 $session = $this->request->getSession();
379 $session->setSecret( 'AuthManager::authnState', $state );
380 $session->persist();
381
382 return $this->continueAuthentication( $reqs );
383 }
384
407 public function continueAuthentication( array $reqs ) {
408 $session = $this->request->getSession();
409 try {
410 if ( !$session->canSetUser() ) {
411 // Caller should have called canAuthenticateNow()
412 // @codeCoverageIgnoreStart
413 throw new \LogicException( 'Authentication is not possible now' );
414 // @codeCoverageIgnoreEnd
415 }
416
417 $state = $session->getSecret( 'AuthManager::authnState' );
418 if ( !is_array( $state ) ) {
420 wfMessage( 'authmanager-authn-not-in-progress' )
421 );
422 }
423 $state['continueRequests'] = [];
424
425 $guessUserName = $state['guessUserName'];
426
427 foreach ( $reqs as $req ) {
428 $req->returnToUrl = $state['returnToUrl'];
429 }
430
431 // Step 1: Choose an primary authentication provider, and call it until it succeeds.
432
433 if ( $state['primary'] === null ) {
434 // We haven't picked a PrimaryAuthenticationProvider yet
435 // @codeCoverageIgnoreStart
436 $guessUserName = null;
437 foreach ( $reqs as $req ) {
438 if ( $req->username !== null && $req->username !== '' ) {
439 if ( $guessUserName === null ) {
440 $guessUserName = $req->username;
441 } elseif ( $guessUserName !== $req->username ) {
442 $guessUserName = null;
443 break;
444 }
445 }
446 }
447 $state['guessUserName'] = $guessUserName;
448 // @codeCoverageIgnoreEnd
449 $state['reqs'] = $reqs;
450
451 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
452 $res = $provider->beginPrimaryAuthentication( $reqs );
453 switch ( $res->status ) {
455 $state['primary'] = $id;
456 $state['primaryResponse'] = $res;
457 $this->logger->debug( "Primary login with $id succeeded" );
458 break 2;
460 $this->logger->debug( "Login failed in primary authentication by $id" );
461 if ( $res->createRequest || $state['maybeLink'] ) {
462 $res->createRequest = new CreateFromLoginAuthenticationRequest(
463 $res->createRequest, $state['maybeLink']
464 );
465 }
466 $this->callMethodOnProviders( 7, 'postAuthentication',
467 [ User::newFromName( $guessUserName ) ?: null, $res ]
468 );
469 $session->remove( 'AuthManager::authnState' );
470 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
471 return $res;
473 // Continue loop
474 break;
477 $this->logger->debug( "Primary login with $id returned $res->status" );
478 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
479 $state['primary'] = $id;
480 $state['continueRequests'] = $res->neededRequests;
481 $session->setSecret( 'AuthManager::authnState', $state );
482 return $res;
483
484 // @codeCoverageIgnoreStart
485 default:
486 throw new \DomainException(
487 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
488 );
489 // @codeCoverageIgnoreEnd
490 }
491 }
492 if ( $state['primary'] === null ) {
493 $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
495 wfMessage( 'authmanager-authn-no-primary' )
496 );
497 $this->callMethodOnProviders( 7, 'postAuthentication',
498 [ User::newFromName( $guessUserName ) ?: null, $ret ]
499 );
500 $session->remove( 'AuthManager::authnState' );
501 return $ret;
502 }
503 } elseif ( $state['primaryResponse'] === null ) {
504 $provider = $this->getAuthenticationProvider( $state['primary'] );
505 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
506 // Configuration changed? Force them to start over.
507 // @codeCoverageIgnoreStart
509 wfMessage( 'authmanager-authn-not-in-progress' )
510 );
511 $this->callMethodOnProviders( 7, 'postAuthentication',
512 [ User::newFromName( $guessUserName ) ?: null, $ret ]
513 );
514 $session->remove( 'AuthManager::authnState' );
515 return $ret;
516 // @codeCoverageIgnoreEnd
517 }
518 $id = $provider->getUniqueId();
519 $res = $provider->continuePrimaryAuthentication( $reqs );
520 switch ( $res->status ) {
522 $state['primaryResponse'] = $res;
523 $this->logger->debug( "Primary login with $id succeeded" );
524 break;
526 $this->logger->debug( "Login failed in primary authentication by $id" );
527 if ( $res->createRequest || $state['maybeLink'] ) {
528 $res->createRequest = new CreateFromLoginAuthenticationRequest(
529 $res->createRequest, $state['maybeLink']
530 );
531 }
532 $this->callMethodOnProviders( 7, 'postAuthentication',
533 [ User::newFromName( $guessUserName ) ?: null, $res ]
534 );
535 $session->remove( 'AuthManager::authnState' );
536 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
537 return $res;
540 $this->logger->debug( "Primary login with $id returned $res->status" );
541 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
542 $state['continueRequests'] = $res->neededRequests;
543 $session->setSecret( 'AuthManager::authnState', $state );
544 return $res;
545 default:
546 throw new \DomainException(
547 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
548 );
549 }
550 }
551
552 $res = $state['primaryResponse'];
553 if ( $res->username === null ) {
554 $provider = $this->getAuthenticationProvider( $state['primary'] );
555 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
556 // Configuration changed? Force them to start over.
557 // @codeCoverageIgnoreStart
559 wfMessage( 'authmanager-authn-not-in-progress' )
560 );
561 $this->callMethodOnProviders( 7, 'postAuthentication',
562 [ User::newFromName( $guessUserName ) ?: null, $ret ]
563 );
564 $session->remove( 'AuthManager::authnState' );
565 return $ret;
566 // @codeCoverageIgnoreEnd
567 }
568
569 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
570 $res->linkRequest &&
571 // don't confuse the user with an incorrect message if linking is disabled
572 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
573 ) {
574 $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
575 $msg = 'authmanager-authn-no-local-user-link';
576 } else {
577 $msg = 'authmanager-authn-no-local-user';
578 }
579 $this->logger->debug(
580 "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
581 );
583 $ret->neededRequests = $this->getAuthenticationRequestsInternal(
584 self::ACTION_LOGIN,
585 [],
587 );
588 if ( $res->createRequest || $state['maybeLink'] ) {
589 $ret->createRequest = new CreateFromLoginAuthenticationRequest(
590 $res->createRequest, $state['maybeLink']
591 );
592 $ret->neededRequests[] = $ret->createRequest;
593 }
594 $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
595 $session->setSecret( 'AuthManager::authnState', [
596 'reqs' => [], // Will be filled in later
597 'primary' => null,
598 'primaryResponse' => null,
599 'secondary' => [],
600 'continueRequests' => $ret->neededRequests,
601 ] + $state );
602 return $ret;
603 }
604
605 // Step 2: Primary authentication succeeded, create the User object
606 // (and add the user locally if necessary)
607
608 $user = User::newFromName( $res->username, 'usable' );
609 if ( !$user ) {
610 $provider = $this->getAuthenticationProvider( $state['primary'] );
611 throw new \DomainException(
612 get_class( $provider ) . " returned an invalid username: {$res->username}"
613 );
614 }
615 if ( $user->getId() === 0 ) {
616 // User doesn't exist locally. Create it.
617 $this->logger->info( 'Auto-creating {user} on login', [
618 'user' => $user->getName(),
619 ] );
620 $status = $this->autoCreateUser( $user, $state['primary'], false );
621 if ( !$status->isGood() ) {
623 Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
624 );
625 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
626 $session->remove( 'AuthManager::authnState' );
627 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
628 return $ret;
629 }
630 }
631
632 // Step 3: Iterate over all the secondary authentication providers.
633
634 $beginReqs = $state['reqs'];
635
636 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
637 if ( !isset( $state['secondary'][$id] ) ) {
638 // This provider isn't started yet, so we pass it the set
639 // of reqs from beginAuthentication instead of whatever
640 // might have been used by a previous provider in line.
641 $func = 'beginSecondaryAuthentication';
642 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
643 } elseif ( !$state['secondary'][$id] ) {
644 $func = 'continueSecondaryAuthentication';
645 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
646 } else {
647 continue;
648 }
649 switch ( $res->status ) {
651 $this->logger->debug( "Secondary login with $id succeeded" );
652 // fall through
654 $state['secondary'][$id] = true;
655 break;
657 $this->logger->debug( "Login failed in secondary authentication by $id" );
658 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
659 $session->remove( 'AuthManager::authnState' );
660 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
661 return $res;
664 $this->logger->debug( "Secondary login with $id returned " . $res->status );
665 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
666 $state['secondary'][$id] = false;
667 $state['continueRequests'] = $res->neededRequests;
668 $session->setSecret( 'AuthManager::authnState', $state );
669 return $res;
670
671 // @codeCoverageIgnoreStart
672 default:
673 throw new \DomainException(
674 get_class( $provider ) . "::{$func}() returned $res->status"
675 );
676 // @codeCoverageIgnoreEnd
677 }
678 }
679
680 // Step 4: Authentication complete! Set the user in the session and
681 // clean up.
682
683 $this->logger->info( 'Login for {user} succeeded from {clientip}', [
684 'user' => $user->getName(),
685 'clientip' => $this->request->getIP(),
686 ] );
689 $beginReqs, RememberMeAuthenticationRequest::class
690 );
691 $this->setSessionDataForUser( $user, $req && $req->rememberMe );
693 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
694 $session->remove( 'AuthManager::authnState' );
695 $this->removeAuthenticationSessionData( null );
696 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
697 return $ret;
698 } catch ( \Exception $ex ) {
699 $session->remove( 'AuthManager::authnState' );
700 throw $ex;
701 }
702 }
703
715 public function securitySensitiveOperationStatus( $operation ) {
717
718 $this->logger->debug( __METHOD__ . ": Checking $operation" );
719
720 $session = $this->request->getSession();
721 $aId = $session->getUser()->getId();
722 if ( $aId === 0 ) {
723 // User isn't authenticated. DWIM?
725 $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
726 return $status;
727 }
728
729 if ( $session->canSetUser() ) {
730 $id = $session->get( 'AuthManager:lastAuthId' );
731 $last = $session->get( 'AuthManager:lastAuthTimestamp' );
732 if ( $id !== $aId || $last === null ) {
733 $timeSinceLogin = PHP_INT_MAX; // Forever ago
734 } else {
735 $timeSinceLogin = max( 0, time() - $last );
736 }
737
738 $thresholds = $this->config->get( 'ReauthenticateTime' );
739 if ( isset( $thresholds[$operation] ) ) {
740 $threshold = $thresholds[$operation];
741 } elseif ( isset( $thresholds['default'] ) ) {
742 $threshold = $thresholds['default'];
743 } else {
744 throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
745 }
746
747 if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
749 }
750 } else {
751 $timeSinceLogin = -1;
752
753 $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
754 if ( isset( $pass[$operation] ) ) {
755 $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
756 } elseif ( isset( $pass['default'] ) ) {
757 $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
758 } else {
759 throw new \UnexpectedValueException(
760 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
761 );
762 }
763 }
764
765 \Hooks::run( 'SecuritySensitiveOperationStatus', [
766 &$status, $operation, $session, $timeSinceLogin
767 ] );
768
769 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
770 if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
772 }
773
774 $this->logger->info( __METHOD__ . ": $operation is $status" );
775
776 return $status;
777 }
778
788 public function userCanAuthenticate( $username ) {
789 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
790 if ( $provider->testUserCanAuthenticate( $username ) ) {
791 return true;
792 }
793 }
794 return false;
795 }
796
811 public function normalizeUsername( $username ) {
812 $ret = [];
813 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
814 $normalized = $provider->providerNormalizeUsername( $username );
815 if ( $normalized !== null ) {
816 $ret[$normalized] = true;
817 }
818 }
819 return array_keys( $ret );
820 }
821
836 public function revokeAccessForUser( $username ) {
837 $this->logger->info( 'Revoking access for {user}', [
838 'user' => $username,
839 ] );
840 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
841 }
842
852 public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
853 $any = false;
854 $providers = $this->getPrimaryAuthenticationProviders() +
856 foreach ( $providers as $provider ) {
857 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
858 if ( !$status->isGood() ) {
859 return Status::wrap( $status );
860 }
861 $any = $any || $status->value !== 'ignored';
862 }
863 if ( !$any ) {
864 $status = Status::newGood( 'ignored' );
865 $status->warning( 'authmanager-change-not-supported' );
866 return $status;
867 }
868 return Status::newGood();
869 }
870
886 $this->logger->info( 'Changing authentication data for {user} class {what}', [
887 'user' => is_string( $req->username ) ? $req->username : '<no name>',
888 'what' => get_class( $req ),
889 ] );
890
891 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
892
893 // When the main account's authentication data is changed, invalidate
894 // all BotPasswords too.
895 \BotPassword::invalidateAllPasswordsForUser( $req->username );
896 }
897
909 public function canCreateAccounts() {
910 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
911 switch ( $provider->accountCreationType() ) {
914 return true;
915 }
916 }
917 return false;
918 }
919
928 public function canCreateAccount( $username, $options = [] ) {
929 // Back compat
930 if ( is_int( $options ) ) {
931 $options = [ 'flags' => $options ];
932 }
933 $options += [
934 'flags' => User::READ_NORMAL,
935 'creating' => false,
936 ];
937 $flags = $options['flags'];
938
939 if ( !$this->canCreateAccounts() ) {
940 return Status::newFatal( 'authmanager-create-disabled' );
941 }
942
943 if ( $this->userExists( $username, $flags ) ) {
944 return Status::newFatal( 'userexists' );
945 }
946
947 $user = User::newFromName( $username, 'creatable' );
948 if ( !is_object( $user ) ) {
949 return Status::newFatal( 'noname' );
950 } else {
951 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
952 if ( $user->getId() !== 0 ) {
953 return Status::newFatal( 'userexists' );
954 }
955 }
956
957 // Denied by providers?
958 $providers = $this->getPreAuthenticationProviders() +
961 foreach ( $providers as $provider ) {
962 $status = $provider->testUserForCreation( $user, false, $options );
963 if ( !$status->isGood() ) {
964 return Status::wrap( $status );
965 }
966 }
967
968 return Status::newGood();
969 }
970
976 public function checkAccountCreatePermissions( User $creator ) {
977 // Wiki is read-only?
978 if ( wfReadOnly() ) {
979 return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
980 }
981
982 // This is awful, this permission check really shouldn't go through Title.
983 $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
984 ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
985 if ( $permErrors ) {
986 $status = Status::newGood();
987 foreach ( $permErrors as $args ) {
988 call_user_func_array( [ $status, 'fatal' ], $args );
989 }
990 return $status;
991 }
992
993 $block = $creator->isBlockedFromCreateAccount();
994 if ( $block ) {
995 $errorParams = [
996 $block->getTarget(),
997 $block->mReason ?: wfMessage( 'blockednoreason' )->text(),
998 $block->getByName()
999 ];
1000
1001 if ( $block->getType() === \Block::TYPE_RANGE ) {
1002 $errorMessage = 'cantcreateaccount-range-text';
1003 $errorParams[] = $this->getRequest()->getIP();
1004 } else {
1005 $errorMessage = 'cantcreateaccount-text';
1006 }
1007
1008 return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1009 }
1010
1011 $ip = $this->getRequest()->getIP();
1012 if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1013 return Status::newFatal( 'sorbs_create_account_reason' );
1014 }
1015
1016 return Status::newGood();
1017 }
1018
1038 public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1039 $session = $this->request->getSession();
1040 if ( !$this->canCreateAccounts() ) {
1041 // Caller should have called canCreateAccounts()
1042 $session->remove( 'AuthManager::accountCreationState' );
1043 throw new \LogicException( 'Account creation is not possible' );
1044 }
1045
1046 try {
1048 } catch ( \UnexpectedValueException $ex ) {
1049 $username = null;
1050 }
1051 if ( $username === null ) {
1052 $this->logger->debug( __METHOD__ . ': No username provided' );
1053 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1054 }
1055
1056 // Permissions check
1057 $status = $this->checkAccountCreatePermissions( $creator );
1058 if ( !$status->isGood() ) {
1059 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1060 'user' => $username,
1061 'creator' => $creator->getName(),
1062 'reason' => $status->getWikiText( null, null, 'en' )
1063 ] );
1064 return AuthenticationResponse::newFail( $status->getMessage() );
1065 }
1066
1067 $status = $this->canCreateAccount(
1068 $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1069 );
1070 if ( !$status->isGood() ) {
1071 $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1072 'user' => $username,
1073 'creator' => $creator->getName(),
1074 'reason' => $status->getWikiText( null, null, 'en' )
1075 ] );
1076 return AuthenticationResponse::newFail( $status->getMessage() );
1077 }
1078
1079 $user = User::newFromName( $username, 'creatable' );
1080 foreach ( $reqs as $req ) {
1081 $req->username = $username;
1082 $req->returnToUrl = $returnToUrl;
1083 if ( $req instanceof UserDataAuthenticationRequest ) {
1084 $status = $req->populateUser( $user );
1085 if ( !$status->isGood() ) {
1086 $status = Status::wrap( $status );
1087 $session->remove( 'AuthManager::accountCreationState' );
1088 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1089 'user' => $user->getName(),
1090 'creator' => $creator->getName(),
1091 'reason' => $status->getWikiText( null, null, 'en' ),
1092 ] );
1093 return AuthenticationResponse::newFail( $status->getMessage() );
1094 }
1095 }
1096 }
1097
1098 $this->removeAuthenticationSessionData( null );
1099
1100 $state = [
1101 'username' => $username,
1102 'userid' => 0,
1103 'creatorid' => $creator->getId(),
1104 'creatorname' => $creator->getName(),
1105 'reqs' => $reqs,
1106 'returnToUrl' => $returnToUrl,
1107 'primary' => null,
1108 'primaryResponse' => null,
1109 'secondary' => [],
1110 'continueRequests' => [],
1111 'maybeLink' => [],
1112 'ranPreTests' => false,
1113 ];
1114
1115 // Special case: converting a login to an account creation
1117 $reqs, CreateFromLoginAuthenticationRequest::class
1118 );
1119 if ( $req ) {
1120 $state['maybeLink'] = $req->maybeLink;
1121
1122 if ( $req->createRequest ) {
1123 $reqs[] = $req->createRequest;
1124 $state['reqs'][] = $req->createRequest;
1125 }
1126 }
1127
1128 $session->setSecret( 'AuthManager::accountCreationState', $state );
1129 $session->persist();
1130
1131 return $this->continueAccountCreation( $reqs );
1132 }
1133
1139 public function continueAccountCreation( array $reqs ) {
1140 $session = $this->request->getSession();
1141 try {
1142 if ( !$this->canCreateAccounts() ) {
1143 // Caller should have called canCreateAccounts()
1144 $session->remove( 'AuthManager::accountCreationState' );
1145 throw new \LogicException( 'Account creation is not possible' );
1146 }
1147
1148 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1149 if ( !is_array( $state ) ) {
1151 wfMessage( 'authmanager-create-not-in-progress' )
1152 );
1153 }
1154 $state['continueRequests'] = [];
1155
1156 // Step 0: Prepare and validate the input
1157
1158 $user = User::newFromName( $state['username'], 'creatable' );
1159 if ( !is_object( $user ) ) {
1160 $session->remove( 'AuthManager::accountCreationState' );
1161 $this->logger->debug( __METHOD__ . ': Invalid username', [
1162 'user' => $state['username'],
1163 ] );
1164 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1165 }
1166
1167 if ( $state['creatorid'] ) {
1168 $creator = User::newFromId( $state['creatorid'] );
1169 } else {
1170 $creator = new User;
1171 $creator->setName( $state['creatorname'] );
1172 }
1173
1174 // Avoid account creation races on double submissions
1175 $cache = \ObjectCache::getLocalClusterInstance();
1176 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1177 if ( !$lock ) {
1178 // Don't clear AuthManager::accountCreationState for this code
1179 // path because the process that won the race owns it.
1180 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1181 'user' => $user->getName(),
1182 'creator' => $creator->getName(),
1183 ] );
1184 return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1185 }
1186
1187 // Permissions check
1188 $status = $this->checkAccountCreatePermissions( $creator );
1189 if ( !$status->isGood() ) {
1190 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1191 'user' => $user->getName(),
1192 'creator' => $creator->getName(),
1193 'reason' => $status->getWikiText( null, null, 'en' )
1194 ] );
1195 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1196 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1197 $session->remove( 'AuthManager::accountCreationState' );
1198 return $ret;
1199 }
1200
1201 // Load from master for existence check
1202 $user->load( User::READ_LOCKING );
1203
1204 if ( $state['userid'] === 0 ) {
1205 if ( $user->getId() != 0 ) {
1206 $this->logger->debug( __METHOD__ . ': User exists locally', [
1207 'user' => $user->getName(),
1208 'creator' => $creator->getName(),
1209 ] );
1210 $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1211 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1212 $session->remove( 'AuthManager::accountCreationState' );
1213 return $ret;
1214 }
1215 } else {
1216 if ( $user->getId() == 0 ) {
1217 $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1218 'user' => $user->getName(),
1219 'creator' => $creator->getName(),
1220 'expected_id' => $state['userid'],
1221 ] );
1222 throw new \UnexpectedValueException(
1223 "User \"{$state['username']}\" should exist now, but doesn't!"
1224 );
1225 }
1226 if ( $user->getId() != $state['userid'] ) {
1227 $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1228 'user' => $user->getName(),
1229 'creator' => $creator->getName(),
1230 'expected_id' => $state['userid'],
1231 'actual_id' => $user->getId(),
1232 ] );
1233 throw new \UnexpectedValueException(
1234 "User \"{$state['username']}\" exists, but " .
1235 "ID {$user->getId()} != {$state['userid']}!"
1236 );
1237 }
1238 }
1239 foreach ( $state['reqs'] as $req ) {
1240 if ( $req instanceof UserDataAuthenticationRequest ) {
1241 $status = $req->populateUser( $user );
1242 if ( !$status->isGood() ) {
1243 // This should never happen...
1244 $status = Status::wrap( $status );
1245 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1246 'user' => $user->getName(),
1247 'creator' => $creator->getName(),
1248 'reason' => $status->getWikiText( null, null, 'en' ),
1249 ] );
1250 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1251 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1252 $session->remove( 'AuthManager::accountCreationState' );
1253 return $ret;
1254 }
1255 }
1256 }
1257
1258 foreach ( $reqs as $req ) {
1259 $req->returnToUrl = $state['returnToUrl'];
1260 $req->username = $state['username'];
1261 }
1262
1263 // Run pre-creation tests, if we haven't already
1264 if ( !$state['ranPreTests'] ) {
1265 $providers = $this->getPreAuthenticationProviders() +
1268 foreach ( $providers as $id => $provider ) {
1269 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1270 if ( !$status->isGood() ) {
1271 $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1272 'user' => $user->getName(),
1273 'creator' => $creator->getName(),
1274 ] );
1276 Status::wrap( $status )->getMessage()
1277 );
1278 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1279 $session->remove( 'AuthManager::accountCreationState' );
1280 return $ret;
1281 }
1282 }
1283
1284 $state['ranPreTests'] = true;
1285 }
1286
1287 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1288
1289 if ( $state['primary'] === null ) {
1290 // We haven't picked a PrimaryAuthenticationProvider yet
1291 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1292 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1293 continue;
1294 }
1295 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1296 switch ( $res->status ) {
1298 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1299 'user' => $user->getName(),
1300 'creator' => $creator->getName(),
1301 ] );
1302 $state['primary'] = $id;
1303 $state['primaryResponse'] = $res;
1304 break 2;
1306 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1307 'user' => $user->getName(),
1308 'creator' => $creator->getName(),
1309 ] );
1310 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1311 $session->remove( 'AuthManager::accountCreationState' );
1312 return $res;
1314 // Continue loop
1315 break;
1318 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1319 'user' => $user->getName(),
1320 'creator' => $creator->getName(),
1321 ] );
1322 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1323 $state['primary'] = $id;
1324 $state['continueRequests'] = $res->neededRequests;
1325 $session->setSecret( 'AuthManager::accountCreationState', $state );
1326 return $res;
1327
1328 // @codeCoverageIgnoreStart
1329 default:
1330 throw new \DomainException(
1331 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1332 );
1333 // @codeCoverageIgnoreEnd
1334 }
1335 }
1336 if ( $state['primary'] === null ) {
1337 $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1338 'user' => $user->getName(),
1339 'creator' => $creator->getName(),
1340 ] );
1342 wfMessage( 'authmanager-create-no-primary' )
1343 );
1344 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1345 $session->remove( 'AuthManager::accountCreationState' );
1346 return $ret;
1347 }
1348 } elseif ( $state['primaryResponse'] === null ) {
1349 $provider = $this->getAuthenticationProvider( $state['primary'] );
1350 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1351 // Configuration changed? Force them to start over.
1352 // @codeCoverageIgnoreStart
1354 wfMessage( 'authmanager-create-not-in-progress' )
1355 );
1356 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1357 $session->remove( 'AuthManager::accountCreationState' );
1358 return $ret;
1359 // @codeCoverageIgnoreEnd
1360 }
1361 $id = $provider->getUniqueId();
1362 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1363 switch ( $res->status ) {
1365 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1366 'user' => $user->getName(),
1367 'creator' => $creator->getName(),
1368 ] );
1369 $state['primaryResponse'] = $res;
1370 break;
1372 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1373 'user' => $user->getName(),
1374 'creator' => $creator->getName(),
1375 ] );
1376 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1377 $session->remove( 'AuthManager::accountCreationState' );
1378 return $res;
1381 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1382 'user' => $user->getName(),
1383 'creator' => $creator->getName(),
1384 ] );
1385 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1386 $state['continueRequests'] = $res->neededRequests;
1387 $session->setSecret( 'AuthManager::accountCreationState', $state );
1388 return $res;
1389 default:
1390 throw new \DomainException(
1391 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1392 );
1393 }
1394 }
1395
1396 // Step 2: Primary authentication succeeded, create the User object
1397 // and add the user locally.
1398
1399 if ( $state['userid'] === 0 ) {
1400 $this->logger->info( 'Creating user {user} during account creation', [
1401 'user' => $user->getName(),
1402 'creator' => $creator->getName(),
1403 ] );
1404 $status = $user->addToDatabase();
1405 if ( !$status->isOK() ) {
1406 // @codeCoverageIgnoreStart
1407 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1408 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1409 $session->remove( 'AuthManager::accountCreationState' );
1410 return $ret;
1411 // @codeCoverageIgnoreEnd
1412 }
1413 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1414 \Hooks::run( 'LocalUserCreated', [ $user, false ] );
1415 $user->saveSettings();
1416 $state['userid'] = $user->getId();
1417
1418 // Update user count
1419 \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1420
1421 // Watch user's userpage and talk page
1422 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1423
1424 // Inform the provider
1425 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1426
1427 // Log the creation
1428 if ( $this->config->get( 'NewUserLog' ) ) {
1429 $isAnon = $creator->isAnon();
1430 $logEntry = new \ManualLogEntry(
1431 'newusers',
1432 $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1433 );
1434 $logEntry->setPerformer( $isAnon ? $user : $creator );
1435 $logEntry->setTarget( $user->getUserPage() );
1438 $state['reqs'], CreationReasonAuthenticationRequest::class
1439 );
1440 $logEntry->setComment( $req ? $req->reason : '' );
1441 $logEntry->setParameters( [
1442 '4::userid' => $user->getId(),
1443 ] );
1444 $logid = $logEntry->insert();
1445 $logEntry->publish( $logid );
1446 }
1447 }
1448
1449 // Step 3: Iterate over all the secondary authentication providers.
1450
1451 $beginReqs = $state['reqs'];
1452
1453 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1454 if ( !isset( $state['secondary'][$id] ) ) {
1455 // This provider isn't started yet, so we pass it the set
1456 // of reqs from beginAuthentication instead of whatever
1457 // might have been used by a previous provider in line.
1458 $func = 'beginSecondaryAccountCreation';
1459 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1460 } elseif ( !$state['secondary'][$id] ) {
1461 $func = 'continueSecondaryAccountCreation';
1462 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1463 } else {
1464 continue;
1465 }
1466 switch ( $res->status ) {
1468 $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1469 'user' => $user->getName(),
1470 'creator' => $creator->getName(),
1471 ] );
1472 // fall through
1474 $state['secondary'][$id] = true;
1475 break;
1478 $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1479 'user' => $user->getName(),
1480 'creator' => $creator->getName(),
1481 ] );
1482 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1483 $state['secondary'][$id] = false;
1484 $state['continueRequests'] = $res->neededRequests;
1485 $session->setSecret( 'AuthManager::accountCreationState', $state );
1486 return $res;
1488 throw new \DomainException(
1489 get_class( $provider ) . "::{$func}() returned $res->status." .
1490 ' Secondary providers are not allowed to fail account creation, that' .
1491 ' should have been done via testForAccountCreation().'
1492 );
1493 // @codeCoverageIgnoreStart
1494 default:
1495 throw new \DomainException(
1496 get_class( $provider ) . "::{$func}() returned $res->status"
1497 );
1498 // @codeCoverageIgnoreEnd
1499 }
1500 }
1501
1502 $id = $user->getId();
1503 $name = $user->getName();
1506 $ret->loginRequest = $req;
1507 $this->createdAccountAuthenticationRequests[] = $req;
1508
1509 $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1510 'user' => $user->getName(),
1511 'creator' => $creator->getName(),
1512 ] );
1513
1514 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1515 $session->remove( 'AuthManager::accountCreationState' );
1516 $this->removeAuthenticationSessionData( null );
1517 return $ret;
1518 } catch ( \Exception $ex ) {
1519 $session->remove( 'AuthManager::accountCreationState' );
1520 throw $ex;
1521 }
1522 }
1523
1539 public function autoCreateUser( User $user, $source, $login = true ) {
1540 if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1542 ) {
1543 throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1544 }
1545
1546 $username = $user->getName();
1547
1548 // Try the local user from the replica DB
1549 $localId = User::idFromName( $username );
1550 $flags = User::READ_NORMAL;
1551
1552 // Fetch the user ID from the master, so that we don't try to create the user
1553 // when they already exist, due to replication lag
1554 // @codeCoverageIgnoreStart
1555 if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
1556 $localId = User::idFromName( $username, User::READ_LATEST );
1557 $flags = User::READ_LATEST;
1558 }
1559 // @codeCoverageIgnoreEnd
1560
1561 if ( $localId ) {
1562 $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1563 'username' => $username,
1564 ] );
1565 $user->setId( $localId );
1566 $user->loadFromId( $flags );
1567 if ( $login ) {
1568 $this->setSessionDataForUser( $user );
1569 }
1570 $status = Status::newGood();
1571 $status->warning( 'userexists' );
1572 return $status;
1573 }
1574
1575 // Wiki is read-only?
1576 if ( wfReadOnly() ) {
1577 $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1578 'username' => $username,
1579 'reason' => wfReadOnlyReason(),
1580 ] );
1581 $user->setId( 0 );
1582 $user->loadFromId();
1583 return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1584 }
1585
1586 // Check the session, if we tried to create this user already there's
1587 // no point in retrying.
1588 $session = $this->request->getSession();
1589 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1590 $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1591 'username' => $username,
1592 'sessionid' => $session->getId(),
1593 ] );
1594 $user->setId( 0 );
1595 $user->loadFromId();
1596 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1597 if ( $reason instanceof StatusValue ) {
1598 return Status::wrap( $reason );
1599 } else {
1600 return Status::newFatal( $reason );
1601 }
1602 }
1603
1604 // Is the username creatable?
1605 if ( !User::isCreatableName( $username ) ) {
1606 $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1607 'username' => $username,
1608 ] );
1609 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1610 $user->setId( 0 );
1611 $user->loadFromId();
1612 return Status::newFatal( 'noname' );
1613 }
1614
1615 // Is the IP user able to create accounts?
1616 $anon = new User;
1617 if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1618 $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1619 'username' => $username,
1620 'ip' => $anon->getName(),
1621 ] );
1622 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1623 $session->persist();
1624 $user->setId( 0 );
1625 $user->loadFromId();
1626 return Status::newFatal( 'authmanager-autocreate-noperm' );
1627 }
1628
1629 // Avoid account creation races on double submissions
1630 $cache = \ObjectCache::getLocalClusterInstance();
1631 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1632 if ( !$lock ) {
1633 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1634 'user' => $username,
1635 ] );
1636 $user->setId( 0 );
1637 $user->loadFromId();
1638 return Status::newFatal( 'usernameinprogress' );
1639 }
1640
1641 // Denied by providers?
1642 $options = [
1643 'flags' => User::READ_LATEST,
1644 'creating' => true,
1645 ];
1646 $providers = $this->getPreAuthenticationProviders() +
1649 foreach ( $providers as $provider ) {
1650 $status = $provider->testUserForCreation( $user, $source, $options );
1651 if ( !$status->isGood() ) {
1652 $ret = Status::wrap( $status );
1653 $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1654 'username' => $username,
1655 'reason' => $ret->getWikiText( null, null, 'en' ),
1656 ] );
1657 $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1658 $user->setId( 0 );
1659 $user->loadFromId();
1660 return $ret;
1661 }
1662 }
1663
1664 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1665 if ( $cache->get( $backoffKey ) ) {
1666 $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1667 'username' => $username,
1668 ] );
1669 $user->setId( 0 );
1670 $user->loadFromId();
1671 return Status::newFatal( 'authmanager-autocreate-exception' );
1672 }
1673
1674 // Checks passed, create the user...
1675 $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
1676 $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1677 'username' => $username,
1678 'from' => $from,
1679 ] );
1680
1681 // Ignore warnings about master connections/writes...hard to avoid here
1682 $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1683 $old = $trxProfiler->setSilenced( true );
1684 try {
1685 $status = $user->addToDatabase();
1686 if ( !$status->isOK() ) {
1687 // Double-check for a race condition (T70012). We make use of the fact that when
1688 // addToDatabase fails due to the user already existing, the user object gets loaded.
1689 if ( $user->getId() ) {
1690 $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1691 'username' => $username,
1692 ] );
1693 if ( $login ) {
1694 $this->setSessionDataForUser( $user );
1695 }
1696 $status = Status::newGood();
1697 $status->warning( 'userexists' );
1698 } else {
1699 $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1700 'username' => $username,
1701 'msg' => $status->getWikiText( null, null, 'en' )
1702 ] );
1703 $user->setId( 0 );
1704 $user->loadFromId();
1705 }
1706 return $status;
1707 }
1708 } catch ( \Exception $ex ) {
1709 $trxProfiler->setSilenced( $old );
1710 $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1711 'username' => $username,
1712 'exception' => $ex,
1713 ] );
1714 // Do not keep throwing errors for a while
1715 $cache->set( $backoffKey, 1, 600 );
1716 // Bubble up error; which should normally trigger DB rollbacks
1717 throw $ex;
1718 }
1719
1720 $this->setDefaultUserOptions( $user, false );
1721
1722 // Inform the providers
1723 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1724
1725 \Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1726 \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1727 $user->saveSettings();
1728
1729 // Update user count
1730 \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1731 // Watch user's userpage and talk page
1732 \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1733 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1734 } );
1735
1736 // Log the creation
1737 if ( $this->config->get( 'NewUserLog' ) ) {
1738 $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1739 $logEntry->setPerformer( $user );
1740 $logEntry->setTarget( $user->getUserPage() );
1741 $logEntry->setComment( '' );
1742 $logEntry->setParameters( [
1743 '4::userid' => $user->getId(),
1744 ] );
1745 $logEntry->insert();
1746 }
1747
1748 $trxProfiler->setSilenced( $old );
1749
1750 if ( $login ) {
1751 $this->setSessionDataForUser( $user );
1752 }
1753
1754 return Status::newGood();
1755 }
1756
1768 public function canLinkAccounts() {
1769 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1770 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1771 return true;
1772 }
1773 }
1774 return false;
1775 }
1776
1786 public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1787 $session = $this->request->getSession();
1788 $session->remove( 'AuthManager::accountLinkState' );
1789
1790 if ( !$this->canLinkAccounts() ) {
1791 // Caller should have called canLinkAccounts()
1792 throw new \LogicException( 'Account linking is not possible' );
1793 }
1794
1795 if ( $user->getId() === 0 ) {
1796 if ( !User::isUsableName( $user->getName() ) ) {
1797 $msg = wfMessage( 'noname' );
1798 } else {
1799 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1800 }
1801 return AuthenticationResponse::newFail( $msg );
1802 }
1803 foreach ( $reqs as $req ) {
1804 $req->username = $user->getName();
1805 $req->returnToUrl = $returnToUrl;
1806 }
1807
1808 $this->removeAuthenticationSessionData( null );
1809
1810 $providers = $this->getPreAuthenticationProviders();
1811 foreach ( $providers as $id => $provider ) {
1812 $status = $provider->testForAccountLink( $user );
1813 if ( !$status->isGood() ) {
1814 $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1815 'user' => $user->getName(),
1816 ] );
1818 Status::wrap( $status )->getMessage()
1819 );
1820 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1821 return $ret;
1822 }
1823 }
1824
1825 $state = [
1826 'username' => $user->getName(),
1827 'userid' => $user->getId(),
1828 'returnToUrl' => $returnToUrl,
1829 'primary' => null,
1830 'continueRequests' => [],
1831 ];
1832
1833 $providers = $this->getPrimaryAuthenticationProviders();
1834 foreach ( $providers as $id => $provider ) {
1835 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1836 continue;
1837 }
1838
1839 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1840 switch ( $res->status ) {
1842 $this->logger->info( "Account linked to {user} by $id", [
1843 'user' => $user->getName(),
1844 ] );
1845 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1846 return $res;
1847
1849 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1850 'user' => $user->getName(),
1851 ] );
1852 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1853 return $res;
1854
1856 // Continue loop
1857 break;
1858
1861 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1862 'user' => $user->getName(),
1863 ] );
1864 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1865 $state['primary'] = $id;
1866 $state['continueRequests'] = $res->neededRequests;
1867 $session->setSecret( 'AuthManager::accountLinkState', $state );
1868 $session->persist();
1869 return $res;
1870
1871 // @codeCoverageIgnoreStart
1872 default:
1873 throw new \DomainException(
1874 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1875 );
1876 // @codeCoverageIgnoreEnd
1877 }
1878 }
1879
1880 $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1881 'user' => $user->getName(),
1882 ] );
1884 wfMessage( 'authmanager-link-no-primary' )
1885 );
1886 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1887 return $ret;
1888 }
1889
1895 public function continueAccountLink( array $reqs ) {
1896 $session = $this->request->getSession();
1897 try {
1898 if ( !$this->canLinkAccounts() ) {
1899 // Caller should have called canLinkAccounts()
1900 $session->remove( 'AuthManager::accountLinkState' );
1901 throw new \LogicException( 'Account linking is not possible' );
1902 }
1903
1904 $state = $session->getSecret( 'AuthManager::accountLinkState' );
1905 if ( !is_array( $state ) ) {
1907 wfMessage( 'authmanager-link-not-in-progress' )
1908 );
1909 }
1910 $state['continueRequests'] = [];
1911
1912 // Step 0: Prepare and validate the input
1913
1914 $user = User::newFromName( $state['username'], 'usable' );
1915 if ( !is_object( $user ) ) {
1916 $session->remove( 'AuthManager::accountLinkState' );
1917 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1918 }
1919 if ( $user->getId() != $state['userid'] ) {
1920 throw new \UnexpectedValueException(
1921 "User \"{$state['username']}\" is valid, but " .
1922 "ID {$user->getId()} != {$state['userid']}!"
1923 );
1924 }
1925
1926 foreach ( $reqs as $req ) {
1927 $req->username = $state['username'];
1928 $req->returnToUrl = $state['returnToUrl'];
1929 }
1930
1931 // Step 1: Call the primary again until it succeeds
1932
1933 $provider = $this->getAuthenticationProvider( $state['primary'] );
1934 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1935 // Configuration changed? Force them to start over.
1936 // @codeCoverageIgnoreStart
1938 wfMessage( 'authmanager-link-not-in-progress' )
1939 );
1940 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1941 $session->remove( 'AuthManager::accountLinkState' );
1942 return $ret;
1943 // @codeCoverageIgnoreEnd
1944 }
1945 $id = $provider->getUniqueId();
1946 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1947 switch ( $res->status ) {
1949 $this->logger->info( "Account linked to {user} by $id", [
1950 'user' => $user->getName(),
1951 ] );
1952 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1953 $session->remove( 'AuthManager::accountLinkState' );
1954 return $res;
1956 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1957 'user' => $user->getName(),
1958 ] );
1959 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1960 $session->remove( 'AuthManager::accountLinkState' );
1961 return $res;
1964 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1965 'user' => $user->getName(),
1966 ] );
1967 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1968 $state['continueRequests'] = $res->neededRequests;
1969 $session->setSecret( 'AuthManager::accountLinkState', $state );
1970 return $res;
1971 default:
1972 throw new \DomainException(
1973 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1974 );
1975 }
1976 } catch ( \Exception $ex ) {
1977 $session->remove( 'AuthManager::accountLinkState' );
1978 throw $ex;
1979 }
1980 }
1981
2007 public function getAuthenticationRequests( $action, User $user = null ) {
2008 $options = [];
2009 $providerAction = $action;
2010
2011 // Figure out which providers to query
2012 switch ( $action ) {
2013 case self::ACTION_LOGIN:
2015 $providers = $this->getPreAuthenticationProviders() +
2018 break;
2019
2021 $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2022 return is_array( $state ) ? $state['continueRequests'] : [];
2023
2025 $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2026 return is_array( $state ) ? $state['continueRequests'] : [];
2027
2028 case self::ACTION_LINK:
2029 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2030 return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2031 } );
2032 break;
2033
2035 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2036 return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2037 } );
2038
2039 // To providers, unlink and remove are identical.
2040 $providerAction = self::ACTION_REMOVE;
2041 break;
2042
2044 $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2045 return is_array( $state ) ? $state['continueRequests'] : [];
2046
2049 $providers = $this->getPrimaryAuthenticationProviders() +
2051 break;
2052
2053 // @codeCoverageIgnoreStart
2054 default:
2055 throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2056 }
2057 // @codeCoverageIgnoreEnd
2058
2059 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2060 }
2061
2072 $providerAction, array $options, array $providers, User $user = null
2073 ) {
2074 $user = $user ?: \RequestContext::getMain()->getUser();
2075 $options['username'] = $user->isAnon() ? null : $user->getName();
2076
2077 // Query them and merge results
2078 $reqs = [];
2079 foreach ( $providers as $provider ) {
2080 $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2081 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2082 $id = $req->getUniqueId();
2083
2084 // If a required request if from a Primary, mark it as "primary-required" instead
2085 if ( $isPrimary ) {
2086 if ( $req->required ) {
2088 }
2089 }
2090
2091 if (
2092 !isset( $reqs[$id] )
2093 || $req->required === AuthenticationRequest::REQUIRED
2094 || $reqs[$id] === AuthenticationRequest::OPTIONAL
2095 ) {
2096 $reqs[$id] = $req;
2097 }
2098 }
2099 }
2100
2101 // AuthManager has its own req for some actions
2102 switch ( $providerAction ) {
2103 case self::ACTION_LOGIN:
2104 $reqs[] = new RememberMeAuthenticationRequest;
2105 break;
2106
2108 $reqs[] = new UsernameAuthenticationRequest;
2109 $reqs[] = new UserDataAuthenticationRequest;
2110 if ( $options['username'] !== null ) {
2112 $options['username'] = null; // Don't fill in the username below
2113 }
2114 break;
2115 }
2116
2117 // Fill in reqs data
2118 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2119
2120 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2121 if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2122 $reqs = array_filter( $reqs, function ( $req ) {
2123 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2124 } );
2125 }
2126
2127 return array_values( $reqs );
2128 }
2129
2137 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2138 foreach ( $reqs as $req ) {
2139 if ( !$req->action || $forceAction ) {
2140 $req->action = $action;
2141 }
2142 if ( $req->username === null ) {
2143 $req->username = $username;
2144 }
2145 }
2146 }
2147
2154 public function userExists( $username, $flags = User::READ_NORMAL ) {
2155 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2156 if ( $provider->testUserExists( $username, $flags ) ) {
2157 return true;
2158 }
2159 }
2160
2161 return false;
2162 }
2163
2175 public function allowsPropertyChange( $property ) {
2176 $providers = $this->getPrimaryAuthenticationProviders() +
2178 foreach ( $providers as $provider ) {
2179 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2180 return false;
2181 }
2182 }
2183 return true;
2184 }
2185
2194 public function getAuthenticationProvider( $id ) {
2195 // Fast version
2196 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2197 return $this->allAuthenticationProviders[$id];
2198 }
2199
2200 // Slow version: instantiate each kind and check
2201 $providers = $this->getPrimaryAuthenticationProviders();
2202 if ( isset( $providers[$id] ) ) {
2203 return $providers[$id];
2204 }
2205 $providers = $this->getSecondaryAuthenticationProviders();
2206 if ( isset( $providers[$id] ) ) {
2207 return $providers[$id];
2208 }
2209 $providers = $this->getPreAuthenticationProviders();
2210 if ( isset( $providers[$id] ) ) {
2211 return $providers[$id];
2212 }
2213
2214 return null;
2215 }
2216
2230 public function setAuthenticationSessionData( $key, $data ) {
2231 $session = $this->request->getSession();
2232 $arr = $session->getSecret( 'authData' );
2233 if ( !is_array( $arr ) ) {
2234 $arr = [];
2235 }
2236 $arr[$key] = $data;
2237 $session->setSecret( 'authData', $arr );
2238 }
2239
2247 public function getAuthenticationSessionData( $key, $default = null ) {
2248 $arr = $this->request->getSession()->getSecret( 'authData' );
2249 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2250 return $arr[$key];
2251 } else {
2252 return $default;
2253 }
2254 }
2255
2261 public function removeAuthenticationSessionData( $key ) {
2262 $session = $this->request->getSession();
2263 if ( $key === null ) {
2264 $session->remove( 'authData' );
2265 } else {
2266 $arr = $session->getSecret( 'authData' );
2267 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2268 unset( $arr[$key] );
2269 $session->setSecret( 'authData', $arr );
2270 }
2271 }
2272 }
2273
2280 protected function providerArrayFromSpecs( $class, array $specs ) {
2281 $i = 0;
2282 foreach ( $specs as &$spec ) {
2283 $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2284 }
2285 unset( $spec );
2286 usort( $specs, function ( $a, $b ) {
2287 return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2288 ?: $a['sort2'] - $b['sort2'];
2289 } );
2290
2291 $ret = [];
2292 foreach ( $specs as $spec ) {
2293 $provider = \ObjectFactory::getObjectFromSpec( $spec );
2294 if ( !$provider instanceof $class ) {
2295 throw new \RuntimeException(
2296 "Expected instance of $class, got " . get_class( $provider )
2297 );
2298 }
2299 $provider->setLogger( $this->logger );
2300 $provider->setManager( $this );
2301 $provider->setConfig( $this->config );
2302 $id = $provider->getUniqueId();
2303 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2304 throw new \RuntimeException(
2305 "Duplicate specifications for id $id (classes " .
2306 get_class( $provider ) . ' and ' .
2307 get_class( $this->allAuthenticationProviders[$id] ) . ')'
2308 );
2309 }
2310 $this->allAuthenticationProviders[$id] = $provider;
2311 $ret[$id] = $provider;
2312 }
2313 return $ret;
2314 }
2315
2320 private function getConfiguration() {
2321 return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2322 }
2323
2328 protected function getPreAuthenticationProviders() {
2329 if ( $this->preAuthenticationProviders === null ) {
2330 $conf = $this->getConfiguration();
2331 $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2332 PreAuthenticationProvider::class, $conf['preauth']
2333 );
2334 }
2336 }
2337
2343 if ( $this->primaryAuthenticationProviders === null ) {
2344 $conf = $this->getConfiguration();
2345 $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2346 PrimaryAuthenticationProvider::class, $conf['primaryauth']
2347 );
2348 }
2350 }
2351
2357 if ( $this->secondaryAuthenticationProviders === null ) {
2358 $conf = $this->getConfiguration();
2359 $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2360 SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2361 );
2362 }
2364 }
2365
2371 private function setSessionDataForUser( $user, $remember = null ) {
2372 $session = $this->request->getSession();
2373 $delay = $session->delaySave();
2374
2375 $session->resetId();
2376 $session->resetAllTokens();
2377 if ( $session->canSetUser() ) {
2378 $session->setUser( $user );
2379 }
2380 if ( $remember !== null ) {
2381 $session->setRememberUser( $remember );
2382 }
2383 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2384 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2385 $session->persist();
2386
2387 \Wikimedia\ScopedCallback::consume( $delay );
2388
2389 \Hooks::run( 'UserLoggedIn', [ $user ] );
2390 }
2391
2396 private function setDefaultUserOptions( User $user, $useContextLang ) {
2398
2399 $user->setToken();
2400
2401 $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
2402 $user->setOption( 'language', $lang->getPreferredVariant() );
2403
2404 if ( $wgContLang->hasVariants() ) {
2405 $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2406 }
2407 }
2408
2414 private function callMethodOnProviders( $which, $method, array $args ) {
2415 $providers = [];
2416 if ( $which & 1 ) {
2417 $providers += $this->getPreAuthenticationProviders();
2418 }
2419 if ( $which & 2 ) {
2420 $providers += $this->getPrimaryAuthenticationProviders();
2421 }
2422 if ( $which & 4 ) {
2423 $providers += $this->getSecondaryAuthenticationProviders();
2424 }
2425 foreach ( $providers as $provider ) {
2426 call_user_func_array( [ $provider, $method ], $args );
2427 }
2428 }
2429
2434 public static function resetCache() {
2435 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2436 // @codeCoverageIgnoreStart
2437 throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2438 // @codeCoverageIgnoreEnd
2439 }
2440
2441 self::$instance = null;
2442 }
2443
2446}
2447
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
$wgAuth $wgAuth
Authentication plugin.
wfGetLB( $wiki=false)
Get a load balancer object.
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:63
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...
changeAuthenticationData(AuthenticationRequest $req)
Change authentication data (e.g.
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.
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.
Class for handling updates to the site_stats table.
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:51
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2249
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition User.php:1787
setName( $str)
Set the user name.
Definition User.php:2276
getId()
Get the user's ID.
Definition User.php:2224
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition User.php:4273
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
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). '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:1245
this hook is for auditing only $req
Definition hooks.txt:988
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:1971
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:2194
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2805
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:1975
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:783
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
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