MediaWiki master
AuthManager.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Auth;
8
9use DomainException;
10use Exception;
11use InvalidArgumentException;
12use LogicException;
52use Psr\Log\LoggerAwareInterface;
53use Psr\Log\LoggerInterface;
54use Psr\Log\NullLogger;
55use RuntimeException;
56use StatusValue;
57use UnexpectedValueException;
58use Wikimedia\NormalizedException\NormalizedException;
59use Wikimedia\ObjectFactory\ObjectFactory;
63
112class AuthManager implements LoggerAwareInterface {
117 public const AUTHN_STATE = 'AuthManager::authnState';
118
123 public const ACCOUNT_CREATION_STATE = 'AuthManager::accountCreationState';
124
129 public const ACCOUNT_LINK_STATE = 'AuthManager::accountLinkState';
130
132 public const ACTION_LOGIN = 'login';
136 public const ACTION_LOGIN_CONTINUE = 'login-continue';
138 public const ACTION_CREATE = 'create';
142 public const ACTION_CREATE_CONTINUE = 'create-continue';
144 public const ACTION_LINK = 'link';
148 public const ACTION_LINK_CONTINUE = 'link-continue';
150 public const ACTION_CHANGE = 'change';
152 public const ACTION_REMOVE = 'remove';
154 public const ACTION_UNLINK = 'unlink';
155
157 public const string SEC_OK = 'ok';
159 public const string SEC_REAUTH = 'reauth';
161 public const string SEC_FAIL = 'fail';
162
164 public const AUTOCREATE_SOURCE_SESSION = SessionManager::class;
165
167 public const AUTOCREATE_SOURCE_MAINT = '::Maintenance::';
168
170 public const AUTOCREATE_SOURCE_TEMP = TempUserCreator::class;
171
176 public const REMEMBER_ME = 'rememberMe';
177
185 public const LOGIN_WAS_INTERACTIVE = 'loginWasInteractive';
186
188 private const CALL_PRE = 1;
189
191 private const CALL_PRIMARY = 2;
192
194 private const CALL_SECONDARY = 4;
195
197 private const CALL_ALL = self::CALL_PRE | self::CALL_PRIMARY | self::CALL_SECONDARY;
198
200 private $allAuthenticationProviders = [];
201
203 private $preAuthenticationProviders = null;
204
206 private $primaryAuthenticationProviders = null;
207
209 private $secondaryAuthenticationProviders = null;
210
212 private $createdAccountAuthenticationRequests = [];
213
214 private LoggerInterface $logger;
215 private LoggerInterface $authEventsLogger;
216 private HookRunner $hookRunner;
217
218 public function __construct(
219 private readonly WebRequest $request,
220 private readonly Config $config,
221 private readonly ChangeTagsStore $changeTagsStore,
222 private readonly ObjectFactory $objectFactory,
223 private readonly ObjectCacheFactory $objectCacheFactory,
224 private readonly HookContainer $hookContainer,
225 private readonly ReadOnlyMode $readOnlyMode,
226 private readonly UserNameUtils $userNameUtils,
227 private readonly BlockManager $blockManager,
228 private readonly WatchlistManager $watchlistManager,
229 private readonly ILoadBalancer $loadBalancer,
230 private readonly Language $contentLanguage,
231 private readonly LanguageConverterFactory $languageConverterFactory,
232 private readonly BotPasswordStore $botPasswordStore,
233 private readonly UserFactory $userFactory,
234 private readonly UserIdentityLookup $userIdentityLookup,
235 private readonly UserIdentityUtils $identityUtils,
236 private readonly UserOptionsManager $userOptionsManager,
237 private readonly NotificationService $notificationService,
238 private readonly SessionManagerInterface $sessionManager,
239 ) {
240 $this->hookRunner = new HookRunner( $hookContainer );
241 $this->setLogger( new NullLogger() );
242 $this->setAuthEventsLogger( new NullLogger() );
243 }
244
245 public function setLogger( LoggerInterface $logger ): void {
246 $this->logger = $logger;
247 }
248
249 public function setAuthEventsLogger( LoggerInterface $authEventsLogger ): void {
250 $this->authEventsLogger = $authEventsLogger;
251 }
252
256 public function getRequest() {
257 return $this->request;
258 }
259
260 /***************************************************************************/
261 // region Authentication
272 public function canAuthenticateNow() {
273 return $this->request->getSession()->canSetUser();
274 }
275
294 public function beginAuthentication( array $reqs, $returnToUrl ) {
295 $session = $this->request->getSession();
296 if ( !$session->canSetUser() ) {
297 // Caller should have called canAuthenticateNow()
298 $session->remove( self::AUTHN_STATE );
299 throw new LogicException( 'Authentication is not possible now' );
300 }
301
302 $status = Status::newGood();
303 $guessUserName = null;
304 foreach ( $reqs as $req ) {
305 $req->returnToUrl = $returnToUrl;
306 $status->merge( $req->validate() );
307 // @codeCoverageIgnoreStart
308 if ( $req->username !== null && $req->username !== '' ) {
309 if ( $guessUserName === null ) {
310 $guessUserName = $req->username;
311 } elseif ( $guessUserName !== $req->username ) {
312 $guessUserName = null;
313 break;
314 }
315 }
316 // @codeCoverageIgnoreEnd
317 }
318 if ( !$status->isOK() ) {
319 $this->logger->debug( "Login failed at AuthRequest validation", [
320 'user' => $guessUserName,
321 'reason' => $status->getWikiText( false, false, 'en' ),
322 ] );
323 $res = AuthenticationResponse::newFail( $status->getMessage() );
324 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
325 [ $this->userFactory->newFromName( (string)$guessUserName ) ?: null, $res ]
326 );
327 $session->remove( self::AUTHN_STATE );
328 $this->callLoginAuditHook( $reqs, $res, $guessUserName );
329 return $res;
330 }
331
332 // Check for special-case login of a just-created account
333 $req = AuthenticationRequest::getRequestByClass(
334 $reqs, CreatedAccountAuthenticationRequest::class
335 );
336 if ( $req ) {
337 if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
338 throw new LogicException(
339 'CreatedAccountAuthenticationRequests are only valid on ' .
340 'the same AuthManager that created the account'
341 );
342 }
343
344 $user = $this->userFactory->newFromName( (string)$req->username );
345 // @codeCoverageIgnoreStart
346 if ( !$user ) {
347 throw new UnexpectedValueException(
348 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
349 );
350 } elseif ( $user->getId() != $req->id ) {
351 throw new UnexpectedValueException(
352 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
353 );
354 }
355 // @codeCoverageIgnoreEnd
356
357 $this->logger->info( 'Logging in {user} after account creation', [
358 'user' => $user->getName(),
359 ] );
360 $ret = AuthenticationResponse::newPass( $user->getName() );
361 $this->setSessionDataForUser( $user );
362 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $ret ] );
363 $session->remove( self::AUTHN_STATE );
364 $this->callLoginAuditHook( $reqs, $ret, $user );
365 return $ret;
366 }
367
368 $this->removeAuthenticationSessionData( null );
369
370 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
371 $status = $provider->testForAuthentication( $reqs );
372 if ( !$status->isGood() ) {
373 $this->logger->debug( 'Login failed in pre-authentication by {providerUniqueId}', [
374 'providerUniqueId' => $provider->getUniqueId(),
375 ] );
376 $ret = AuthenticationResponse::newFail(
377 Status::wrap( $status )->getMessage()
378 );
379 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
380 [ $this->userFactory->newFromName( (string)$guessUserName ), $ret ]
381 );
382 $this->callLoginAuditHook( $reqs, $ret, $guessUserName );
383 return $ret;
384 }
385 }
386
387 $state = [
388 'reqs' => $reqs,
389 'returnToUrl' => $returnToUrl,
390 'guessUserName' => $guessUserName,
391 'providerIds' => $this->getProviderIds(),
392 'primary' => null,
393 'primaryResponse' => null,
394 'secondary' => [],
395 'maybeLink' => [],
396 'continueRequests' => [],
397 ];
398
399 // Preserve state from a previous failed login
400 $req = AuthenticationRequest::getRequestByClass(
401 $reqs, CreateFromLoginAuthenticationRequest::class
402 );
403 if ( $req ) {
404 $state['maybeLink'] = $req->maybeLink;
405 }
406
407 $session = $this->request->getSession();
408 $session->setSecret( self::AUTHN_STATE, $state );
409 $session->persist();
410
411 return $this->continueAuthentication( $reqs );
412 }
413
436 public function continueAuthentication( array $reqs ) {
437 $session = $this->request->getSession();
438 try {
439 if ( !$session->canSetUser() ) {
440 // Caller should have called canAuthenticateNow()
441 // @codeCoverageIgnoreStart
442 throw new LogicException( 'Authentication is not possible now' );
443 // @codeCoverageIgnoreEnd
444 }
445
446 $state = $session->getSecret( self::AUTHN_STATE );
447 if ( !is_array( $state ) ) {
448 return AuthenticationResponse::newFail(
449 wfMessage( 'authmanager-authn-not-in-progress' )
450 );
451 }
452 if ( $state['providerIds'] !== $this->getProviderIds() ) {
453 // An inconsistent AuthManagerFilterProviders hook, or site configuration changed
454 // while the user was in the middle of authentication. The first is a bug, the
455 // second is rare but expected when deploying a config change. Try handle in a way
456 // that's useful for both cases.
457 // @codeCoverageIgnoreStart
458 MWExceptionHandler::logException( new NormalizedException(
459 'Authentication failed because of inconsistent provider array',
460 [ 'old' => json_encode( $state['providerIds'] ), 'new' => json_encode( $this->getProviderIds() ) ]
461 ) );
462 $response = AuthenticationResponse::newFail(
463 wfMessage( 'authmanager-authn-not-in-progress' )
464 );
465 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
466 [ $this->userFactory->newFromName( (string)$state['guessUserName'] ), $response ]
467 );
468 $session->remove( self::AUTHN_STATE );
469 return $response;
470 // @codeCoverageIgnoreEnd
471 }
472 $state['continueRequests'] = [];
473
474 $guessUserName = $state['guessUserName'];
475
476 $elevatedSecurityReq = AuthenticationRequest::getRequestByClass(
477 $state['reqs'], ElevatedSecurityAuthenticationRequest::class
478 );
479
480 $status = Status::newGood();
481 foreach ( $reqs as $req ) {
482 $req->returnToUrl = $state['returnToUrl'];
483 $status->merge( $req->validate() );
484 }
485 if ( !$status->isOK() ) {
486 $this->logger->debug( "Login failed at AuthRequest validation", [
487 'user' => $guessUserName,
488 'reason' => $status->getWikiText( false, false, 'en' ),
489 ] );
490 $res = AuthenticationResponse::newFail( $status->getMessage() );
491 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
492 [ $this->userFactory->newFromName( (string)$guessUserName ), $res ]
493 );
494 $session->remove( self::AUTHN_STATE );
495 $this->callLoginAuditHook( $state['reqs'], $res, $guessUserName );
496 return $res;
497 }
498
499 // Step 1: Choose a primary authentication provider, and call it until it succeeds.
500
501 if ( $state['primary'] === null ) {
502 // We haven't picked a PrimaryAuthenticationProvider yet
503 // @codeCoverageIgnoreStart
504 $guessUserName = null;
505 foreach ( $reqs as $req ) {
506 if ( $req->username !== null && $req->username !== '' ) {
507 if ( $guessUserName === null ) {
508 $guessUserName = $req->username;
509 } elseif ( $guessUserName !== $req->username ) {
510 $guessUserName = null;
511 break;
512 }
513 }
514 }
515 $state['guessUserName'] = $guessUserName;
516 // @codeCoverageIgnoreEnd
517 $state['reqs'] = $reqs;
518
519 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
520 $res = $provider->beginPrimaryAuthentication( $reqs );
521 switch ( $res->status ) {
522 case AuthenticationResponse::PASS:
523 $state['primary'] = $id;
524 $state['primaryResponse'] = $res;
525 $this->logger->debug( 'Primary login with {id} succeeded', [
526 'id' => $id,
527 ] );
528 break 2;
529 case AuthenticationResponse::FAIL:
530 $this->logger->debug( 'Login failed in primary authentication by {id}', [
531 'id' => $id,
532 ] );
533 if ( $res->createRequest || $state['maybeLink'] ) {
534 $res->createRequest = new CreateFromLoginAuthenticationRequest(
535 $res->createRequest, $state['maybeLink']
536 );
537 }
538 $this->callMethodOnProviders(
539 self::CALL_ALL,
540 'postAuthentication',
541 [
542 $this->userFactory->newFromName( (string)$guessUserName ),
543 $res
544 ]
545 );
546 $session->remove( self::AUTHN_STATE );
547 $this->callLoginAuditHook( $state['reqs'], $res, $guessUserName );
548 return $res;
549 case AuthenticationResponse::ABSTAIN:
550 // Continue loop
551 break;
552 case AuthenticationResponse::REDIRECT:
553 case AuthenticationResponse::UI:
554 $this->logger->debug( 'Primary login with {id} returned {status}', [
555 'id' => $id,
556 'status' => $res->status,
557 ] );
558 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
559 $state['primary'] = $id;
560 $state['continueRequests'] = $res->neededRequests;
561 $session->setSecret( self::AUTHN_STATE, $state );
562 return $res;
563
564 // @codeCoverageIgnoreStart
565 default:
566 throw new DomainException(
567 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
568 );
569 // @codeCoverageIgnoreEnd
570 }
571 }
572 if ( $state['primary'] === null ) {
573 $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
574 $response = AuthenticationResponse::newFail(
575 wfMessage( 'authmanager-authn-no-primary' )
576 );
577 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
578 [ $this->userFactory->newFromName( (string)$guessUserName ), $response ]
579 );
580 $session->remove( self::AUTHN_STATE );
581 return $response;
582 }
583 } elseif ( $state['primaryResponse'] === null ) {
584 $provider = $this->getAuthenticationProvider( $state['primary'] );
585 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
586 // Configuration changed? Force them to start over.
587 // @codeCoverageIgnoreStart
588 $response = AuthenticationResponse::newFail(
589 wfMessage( 'authmanager-authn-not-in-progress' )
590 );
591 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
592 [ $this->userFactory->newFromName( (string)$guessUserName ), $response ]
593 );
594 $session->remove( self::AUTHN_STATE );
595 return $response;
596 // @codeCoverageIgnoreEnd
597 }
598 $id = $provider->getUniqueId();
599 $res = $provider->continuePrimaryAuthentication( $reqs );
600 switch ( $res->status ) {
601 case AuthenticationResponse::PASS:
602 $state['primaryResponse'] = $res;
603 $this->logger->debug( 'Primary login with {id} succeeded', [
604 'id' => $id,
605 ] );
606 break;
607 case AuthenticationResponse::FAIL:
608 $this->logger->debug( 'Login failed in primary authentication by {id}', [
609 'id' => $id,
610 ] );
611 if ( $res->createRequest || $state['maybeLink'] ) {
612 $res->createRequest = new CreateFromLoginAuthenticationRequest(
613 $res->createRequest, $state['maybeLink']
614 );
615 }
616 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
617 [ $this->userFactory->newFromName( (string)$guessUserName ), $res ]
618 );
619 $session->remove( self::AUTHN_STATE );
620 $this->callLoginAuditHook( $state['reqs'], $res, $guessUserName );
621 return $res;
622 case AuthenticationResponse::REDIRECT:
623 case AuthenticationResponse::UI:
624 $this->logger->debug( 'Primary login with {id} returned {status}', [
625 'id' => $id,
626 'status' => $res->status,
627 ] );
628 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
629 $state['continueRequests'] = $res->neededRequests;
630 $session->setSecret( self::AUTHN_STATE, $state );
631 return $res;
632 default:
633 throw new DomainException(
634 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
635 );
636 }
637 }
638
639 $res = $state['primaryResponse'];
640 if ( $res->username === null ) {
641 // The user was authenticated successfully but had no wiki account (neither local
642 // nor central). This can happen when using a third-party identity provider. End
643 // this login attempt, but provide a way for the user to reuse this identity for
644 // signup or account linking.
645
646 $provider = $this->getAuthenticationProvider( $state['primary'] );
647 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
648 // Configuration changed? Force them to start over.
649 // @codeCoverageIgnoreStart
650 $response = AuthenticationResponse::newFail(
651 wfMessage( 'authmanager-authn-not-in-progress' )
652 );
653 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
654 [ $this->userFactory->newFromName( (string)$guessUserName ), $response ]
655 );
656 $session->remove( self::AUTHN_STATE );
657 $this->callLoginAuditHook( $state['reqs'], $res, $guessUserName );
658 return $response;
659 // @codeCoverageIgnoreEnd
660 }
661
662 if ( $elevatedSecurityReq ) {
663 // The user was asked to reauthenticate for elevated security but authenticated
664 // as a different user. Does not make sense, maybe some kind of phishing attempt?
665 $this->logger->info( 'Reauthentication failed because of user mismatch', [
666 'oldUserId' => $elevatedSecurityReq->userId,
667 'newUserName' => '<no user>',
668 ] );
669 $ret = AuthenticationResponse::newFail( wfMessage( 'authmanager-authn-reauth-switch' ) );
670 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ null, $ret ] );
671 $session->remove( self::AUTHN_STATE );
672 return $ret;
673 }
674
675 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
676 $res->linkRequest &&
677 // don't confuse the user with an incorrect message if linking is disabled
678 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
679 ) {
680 $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
681 $msg = 'authmanager-authn-no-local-user-link';
682 } else {
683 $msg = 'authmanager-authn-no-local-user';
684 }
685 $this->logger->debug(
686 'Primary login with {providerUniqueId} succeeded, but returned no user',
687 [ 'providerUniqueId' => $provider->getUniqueId() ]
688 );
689 $response = AuthenticationResponse::newRestart( wfMessage( $msg ) );
690 $response->neededRequests = $this->getAuthenticationRequestsInternal(
691 self::ACTION_LOGIN,
692 [],
693 $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
694 );
695 if ( $res->createRequest || $state['maybeLink'] ) {
696 $response->createRequest = new CreateFromLoginAuthenticationRequest(
697 $res->createRequest, $state['maybeLink']
698 );
699 $response->neededRequests[] = $response->createRequest;
700 }
701 $this->fillRequests( $response->neededRequests, self::ACTION_LOGIN, null, true );
702 $session->setSecret( self::AUTHN_STATE, [
703 'reqs' => [], // Will be filled in later
704 'primary' => null,
705 'primaryResponse' => null,
706 'secondary' => [],
707 'continueRequests' => $response->neededRequests,
708 ] + $state );
709
710 // Give the AuthManagerVerifyAuthentication hook a chance to interrupt - even though
711 // RESTART does not immediately result in a successful login, the response and session
712 // state can hold information identifying a (remote) user, and that could be turned
713 // into access to that user's account in a follow-up request.
714 if ( !$this->runVerifyHook( self::ACTION_LOGIN, null, $response, $state['primary'] ) ) {
715 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ null, $response ] );
716 $session->remove( self::AUTHN_STATE );
717 $this->callLoginAuditHook( $state['reqs'], $response, null );
718 return $response;
719 }
720
721 return $response;
722 }
723
724 // Step 2: Primary authentication succeeded, create the User object
725 // (and add the user locally if necessary)
726
727 $user = $this->userFactory->newFromName(
728 (string)$res->username,
729 UserRigorOptions::RIGOR_USABLE
730 );
731 if ( !$user ) {
732 $provider = $this->getAuthenticationProvider( $state['primary'] );
733 throw new DomainException(
734 get_class( $provider ) . " returned an invalid username: {$res->username}"
735 );
736 }
737
738 if ( $elevatedSecurityReq && $elevatedSecurityReq->userId !== $user->getId() ) {
739 // The user was asked to reauthenticate for elevated security but authenticated
740 // as a different user. Does not make sense, maybe some kind of phishing attempt?
741 $this->logger->info( 'Reauthentication failed because of user mismatch', [
742 'oldUserId' => $elevatedSecurityReq->userId,
743 'newUserName' => $res->username,
744 ] );
745 $ret = AuthenticationResponse::newFail( wfMessage( 'authmanager-authn-reauth-switch' ) );
746 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ null, $ret ] );
747 $session->remove( self::AUTHN_STATE );
748 $this->callLoginAuditHook( $state['reqs'], $ret, null );
749 return $ret;
750 }
751
752 if ( !$user->isRegistered() ) {
753 // User doesn't exist locally. Create it.
754 $this->logger->info( 'Auto-creating {user} on login', [
755 'user' => $user->getName(),
756 ] );
757 // Also use $user as performer, because the performer will be used for permission
758 // checks and global rights extensions might add rights based on the username,
759 // even if the user doesn't exist at this point.
760 $status = $this->autoCreateUser( $user, $state['primary'], false, true, $user );
761 if ( !$status->isGood() ) {
762 $response = AuthenticationResponse::newFail(
763 Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
764 );
765 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $response ] );
766 $session->remove( self::AUTHN_STATE );
767
768 // T390051: Don't use the $user provided to ::autoCreateUser for the "user being authenticated
769 // against" for the user provided in the AuthManagerLoginAuthenticateAudit hook run, as
770 // ::autoCreateUser may reset $user to an anon user.
771 $userForHook = $this->userFactory->newFromName(
772 (string)$res->username, UserRigorOptions::RIGOR_USABLE
773 );
774 $this->callLoginAuditHook( $state['reqs'], $response, $userForHook );
775 return $response;
776 }
777 }
778
779 // Step 3: Iterate over all the secondary authentication providers.
780
781 $beginReqs = $state['reqs'];
782
783 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
784 if ( !isset( $state['secondary'][$id] ) ) {
785 // This provider isn't started yet, so we pass it the set
786 // of reqs from beginAuthentication instead of whatever
787 // might have been used by a previous provider in line.
788 $func = 'beginSecondaryAuthentication';
789 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
790 } elseif ( !$state['secondary'][$id] ) {
791 $func = 'continueSecondaryAuthentication';
792 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
793 } else {
794 continue;
795 }
796 switch ( $res->status ) {
797 case AuthenticationResponse::PASS:
798 $this->logger->debug( 'Secondary login with {id} succeeded', [
799 'id' => $id,
800 ] );
801 // fall through
802 case AuthenticationResponse::ABSTAIN:
803 $state['secondary'][$id] = true;
804 break;
805 case AuthenticationResponse::FAIL:
806 $this->logger->debug( 'Login failed in secondary authentication by {id}', [
807 'id' => $id,
808 ] );
809 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $res ] );
810 $session->remove( self::AUTHN_STATE );
811 $this->callLoginAuditHook( $state['reqs'], $res, $user );
812 return $res;
813 case AuthenticationResponse::REDIRECT:
814 case AuthenticationResponse::UI:
815 $this->logger->debug( 'Secondary login with {id} returned {status}', [
816 'id' => $id,
817 'status' => $res->status,
818 ] );
819 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
820 $state['secondary'][$id] = false;
821 $state['continueRequests'] = $res->neededRequests;
822 $session->setSecret( self::AUTHN_STATE, $state );
823 return $res;
824
825 // @codeCoverageIgnoreStart
826 default:
827 throw new DomainException(
828 get_class( $provider ) . "::{$func}() returned $res->status"
829 );
830 // @codeCoverageIgnoreEnd
831 }
832 }
833
834 // Step 4: Authentication complete! Give hook handlers a chance to interrupt, then
835 // set the user in the session and clean up.
836
837 $response = AuthenticationResponse::newPass( $user->getName() );
838 if ( !$this->runVerifyHook( self::ACTION_LOGIN, $user, $response, $state['primary'] ) ) {
839 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $response ] );
840 $session->remove( self::AUTHN_STATE );
841 $this->callLoginAuditHook( $state['reqs'], $response, $user );
842 return $response;
843 }
844 $this->logger->info( 'Login for {user} succeeded from {clientIp}',
845 $this->request->getSecurityLogContext( $user ) );
846
847 // Determine whether to remember the user's login
848 // For reauths, the "remember me" checkbox isn't shown, so don't modify the rememberMe state
849 if ( $elevatedSecurityReq ) {
850 $rememberMe = null;
851 } else {
852 $rememberMeConfig = $this->config->get( MainConfigNames::RememberMe );
853 if ( $rememberMeConfig === RememberMeAuthenticationRequest::ALWAYS_REMEMBER ) {
854 $rememberMe = true;
855 } elseif ( $rememberMeConfig === RememberMeAuthenticationRequest::NEVER_REMEMBER ) {
856 $rememberMe = false;
857 } else {
859 $req = AuthenticationRequest::getRequestByClass(
860 $beginReqs, RememberMeAuthenticationRequest::class
861 );
862
863 // T369668: Before we conclude, let's make sure the user hasn't specified
864 // that they want their login remembered elsewhere like in the central domain.
865 // If the user clicked "remember me" in the central domain, then we should
866 // prioritise that when we call continuePrimaryAuthentication() in the provider
867 // that makes calls continuePrimaryAuthentication(). NOTE: It is the responsibility
868 // of the provider to refresh the "remember me" state that will be applied to
869 // the local wiki.
870 $rememberMe = ( $req && $req->rememberMe ) ||
871 $this->getAuthenticationSessionData( self::REMEMBER_ME );
872 }
873 }
874
875 $loginWasInteractive = $this->getAuthenticationSessionData( self::LOGIN_WAS_INTERACTIVE, true );
876 // If the login was not interactive, don't set the securityLevel in the session
877 // This ensures that only interactive reauthentications count
878 $securityLevel = $loginWasInteractive ? $elevatedSecurityReq?->securityLevel : null;
879
880 $performer = $session->getUser();
881 // If the session is associated with a temporary account user, invalidate its
882 // session and remove the TempUser:name property from the session
883 // This is necessary in order to ensure that the temporary account session is exited
884 // when the user transitions to a logged-in named account
885 if ( $session->getUser()->isTemp() ) {
886 $this->sessionManager->invalidateSessionsForUser( $session->getUser() );
887 $session->remove( 'TempUser:name' );
888 $performer = new User();
889 }
890
891 $this->setSessionDataForUser( $user, $rememberMe, $securityLevel );
892 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $response ] );
893 $session->remove( self::AUTHN_STATE );
894 $this->removeAuthenticationSessionData( null );
895 $this->callLoginAuditHook( $state['reqs'], $response, $user, [
896 'performer' => $performer,
897 'securityLevel' => $securityLevel
898 ] );
899 return $response;
900 } catch ( Exception $ex ) {
901 $session->remove( self::AUTHN_STATE );
902 throw $ex;
903 }
904 }
905
921 public function securitySensitiveOperationStatus( $operation ) {
922 $status = self::SEC_OK;
923
924 $this->logger->debug( __METHOD__ . ': Checking {operation}', [
925 'operation' => $operation,
926 ] );
927
928 $session = $this->request->getSession();
929 $aId = $session->getUser()->getId();
930 if ( $aId === 0 ) {
931 // User isn't authenticated. DWIM?
932 $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
933 $this->logger->info( __METHOD__ . ': Not logged in! {operation} is {status}', [
934 'operation' => $operation,
935 'status' => $status,
936 ] );
937
938 return $status;
939 }
940
941 if ( $session->canSetUser() ) {
942 $id = $session->get( 'AuthManager:lastAuthId' );
943 $lastAuthTimestamps = $session->get( 'AuthManager:lastAuthTimestamps', [] );
944 $last = $lastAuthTimestamps[$operation] ?? null;
945 if ( $id !== $aId || $last === null ) {
946 // Forever ago
947 $timeSinceAuth = PHP_INT_MAX;
948 } else {
949 $timeSinceAuth = max( 0, time() - $last );
950 }
951
952 $thresholds = $this->config->get( MainConfigNames::ReauthenticateTime );
953 if ( isset( $thresholds[$operation] ) ) {
954 $threshold = $thresholds[$operation];
955 } elseif ( isset( $thresholds['default'] ) ) {
956 $threshold = $thresholds['default'];
957 } else {
958 throw new UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
959 }
960
961 if ( $threshold >= 0 && $timeSinceAuth > $threshold ) {
962 $status = self::SEC_REAUTH;
963 }
964 } else {
965 $timeSinceAuth = -1;
966
967 $pass = $this->config->get(
968 MainConfigNames::AllowSecuritySensitiveOperationIfCannotReauthenticate
969 );
970 if ( isset( $pass[$operation] ) ) {
971 $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
972 } elseif ( isset( $pass['default'] ) ) {
973 $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
974 } else {
975 throw new UnexpectedValueException(
976 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
977 );
978 }
979 }
980
981 $oldStatus = $status;
982
983 $this->getHookRunner()->onSecuritySensitiveOperationStatus(
984 $status,
985 $operation,
986 $session,
987 $timeSinceAuth
988 );
989
990 if ( $oldStatus === self::SEC_OK && $status !== self::SEC_OK ) {
991 $this->logger->info(
992 __METHOD__ .
993 ': {operation} escalated from {oldstatus} to {status} for {user} in ' .
994 'SecuritySensitiveOperationStatusHook hook',
995 [
996 'operation' => $operation,
997 'oldstatus' => $oldStatus,
998 'status' => $status,
999 ] + $this->getRequest()->getSecurityLogContext( $session->getUser() )
1000 );
1001 } elseif ( $oldStatus !== self::SEC_OK && $status === self::SEC_OK ) {
1002 $this->logger->info(
1003 __METHOD__ .
1004 ': {operation} downgraded from {oldstatus} to {status} for {user} in ' .
1005 'SecuritySensitiveOperationStatusHook hook',
1006 [
1007 'operation' => $operation,
1008 'oldstatus' => $oldStatus,
1009 'status' => $status,
1010 ] + $this->getRequest()->getSecurityLogContext( $session->getUser() )
1011 );
1012 }
1013
1014 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
1015 if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
1016 $status = self::SEC_FAIL;
1017 }
1018
1019 $this->logger->info( __METHOD__ . ': {operation} is {status} for {user}',
1020 [
1021 'operation' => $operation,
1022 'status' => $status,
1023 ] + $this->getRequest()->getSecurityLogContext( $session->getUser() )
1024 );
1025
1026 return $status;
1027 }
1028
1038 public function userCanAuthenticate( $username ) {
1039 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1040 if ( $provider->testUserCanAuthenticate( $username ) ) {
1041 return true;
1042 }
1043 }
1044 return false;
1045 }
1046
1061 public function normalizeUsername( $username ) {
1062 $ret = [];
1063 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1064 $normalized = $provider->providerNormalizeUsername( $username );
1065 if ( $normalized !== null ) {
1066 $ret[$normalized] = true;
1067 }
1068 }
1069 return array_keys( $ret );
1070 }
1071
1089 $context = RequestContext::getMain();
1090 $user = $context->getRequest()->getSession()->getUser();
1091
1092 $context->setUser( $user );
1093
1094 // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
1095 global $wgLang;
1096 // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
1097 $wgLang = $context->getLanguage();
1098 }
1099
1100 // endregion -- end of Authentication
1101
1102 /***************************************************************************/
1103 // region Authentication data changing
1113 public function revokeAccessForUser( $username ) {
1114 $this->logger->info( 'Revoking access for {user}', [
1115 'user' => $username,
1116 ] );
1117 $this->callMethodOnProviders( self::CALL_PRIMARY | self::CALL_SECONDARY, 'providerRevokeAccessForUser',
1118 [ $username ]
1119 );
1120 }
1121
1131 public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
1132 if ( $checkData ) {
1133 $status = Status::wrap( $req->validate() );
1134 if ( !$status->isOK() ) {
1135 $this->logger->debug( "Auth data change failed at AuthRequest validation", [
1136 'user' => $req->username,
1137 'reason' => $status->getWikiText( false, false, 'en' ),
1138 ] );
1139 return $status;
1140 }
1141 }
1142
1143 $any = false;
1144 $providers = $this->getPrimaryAuthenticationProviders() +
1145 $this->getSecondaryAuthenticationProviders();
1146
1147 foreach ( $providers as $provider ) {
1148 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
1149 if ( !$status->isGood() ) {
1150 // If status is not good because reset email password last attempt was within
1151 // $wgPasswordReminderResendTime then return good status with throttled-mailpassword value;
1152 // otherwise, return the $status wrapped.
1153 return $status->hasMessage( 'throttled-mailpassword' )
1154 ? Status::newGood( 'throttled-mailpassword' )
1155 : Status::wrap( $status );
1156 }
1157 $any = $any || $status->value !== 'ignored';
1158 }
1159 if ( !$any ) {
1160 return Status::newGood( 'ignored' )
1161 ->warning( 'authmanager-change-not-supported' );
1162 }
1163 return Status::newGood();
1164 }
1165
1183 public function changeAuthenticationData( AuthenticationRequest $req, $isAddition = false ) {
1184 $status = Status::wrap( $req->validate() );
1185 if ( !$status->isOK() ) {
1186 // Caller should have tried with allowsAuthenticationDataChange() first.
1187 throw new LogicException( "Invalid auth data submitted for change for '{$req->username}': "
1188 . $status->getWikiText( false, false, 'en' ) );
1189 }
1190
1191 $this->logger->info( 'Changing authentication data for {user} class {what}', [
1192 'user' => is_string( $req->username ) ? $req->username : '<no name>',
1193 'what' => get_class( $req ),
1194 ] );
1195
1196 $this->callMethodOnProviders( self::CALL_PRIMARY | self::CALL_SECONDARY, 'providerChangeAuthenticationData',
1197 [ $req ]
1198 );
1199
1200 // When the main account's authentication data is changed, invalidate
1201 // all BotPasswords too.
1202 if ( !$isAddition ) {
1203 $this->botPasswordStore->invalidateUserPasswords( (string)$req->username );
1204 }
1205 }
1206
1207 // endregion -- end of Authentication data changing
1208
1209 /***************************************************************************/
1210 // region Account creation
1217 public function canCreateAccounts() {
1218 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1219 switch ( $provider->accountCreationType() ) {
1220 case PrimaryAuthenticationProvider::TYPE_CREATE:
1221 case PrimaryAuthenticationProvider::TYPE_LINK:
1222 return true;
1223 }
1224 }
1225 return false;
1226 }
1227
1236 public function canCreateAccount( $username, $options = [] ) {
1237 // Back compat
1238 if ( is_int( $options ) ) {
1239 $options = [ 'flags' => $options ];
1240 }
1241 $options += [
1242 'flags' => IDBAccessObject::READ_NORMAL,
1243 'creating' => false,
1244 ];
1245 $flags = $options['flags'];
1246
1247 if ( !$this->canCreateAccounts() ) {
1248 return Status::newFatal( 'authmanager-create-disabled' );
1249 }
1250
1251 if ( $this->userExists( $username, $flags ) ) {
1252 return Status::newFatal( 'userexists' );
1253 }
1254
1255 $user = $this->userFactory->newFromName( (string)$username, UserRigorOptions::RIGOR_CREATABLE );
1256 if ( !is_object( $user ) ) {
1257 return Status::newFatal( 'noname' );
1258 } else {
1259 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
1260 if ( $user->isRegistered() ) {
1261 return Status::newFatal( 'userexists' );
1262 }
1263 }
1264
1265 // Denied by providers?
1266 $providers = $this->getPreAuthenticationProviders() +
1267 $this->getPrimaryAuthenticationProviders() +
1268 $this->getSecondaryAuthenticationProviders();
1269 foreach ( $providers as $provider ) {
1270 $status = $provider->testUserForCreation( $user, false, $options );
1271 if ( !$status->isGood() ) {
1272 return Status::wrap( $status );
1273 }
1274 }
1275
1276 return Status::newGood();
1277 }
1278
1284 private function authorizeInternal(
1285 callable $authorizer,
1286 string $action
1287 ): StatusValue {
1288 // Wiki is read-only?
1289 if ( $this->readOnlyMode->isReadOnly() ) {
1290 return StatusValue::newFatal( 'readonlytext', $this->readOnlyMode->getReason() );
1291 }
1292
1293 $permStatus = new PermissionStatus();
1294 if ( !$authorizer(
1295 $action,
1296 SpecialPage::getTitleFor( 'CreateAccount' ),
1297 $permStatus
1298 ) ) {
1299 return $permStatus;
1300 }
1301
1302 $ip = $this->getRequest()->getIP();
1303 if ( $this->blockManager->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1304 return StatusValue::newFatal( 'sorbs_create_account_reason' );
1305 }
1306
1307 return StatusValue::newGood();
1308 }
1309
1321 public function probablyCanCreateAccount( Authority $creator ): StatusValue {
1322 return $this->authorizeInternal(
1323 static function (
1324 string $action,
1325 PageIdentity $target,
1326 PermissionStatus $status
1327 ) use ( $creator ) {
1328 return $creator->probablyCan( $action, $target, $status );
1329 },
1330 'createaccount'
1331 );
1332 }
1333
1345 public function authorizeCreateAccount( Authority $creator ): StatusValue {
1346 return $this->authorizeInternal(
1347 static function (
1348 string $action,
1349 PageIdentity $target,
1350 PermissionStatus $status
1351 ) use ( $creator ) {
1352 return $creator->authorizeWrite( $action, $target, $status );
1353 },
1354 'createaccount'
1355 );
1356 }
1357
1377 public function beginAccountCreation( Authority $creator, array $reqs, $returnToUrl ) {
1378 $session = $this->request->getSession();
1379 if ( $creator->isTemp() ) {
1380 // For a temp account creating a permanent account, we do not want the temporary
1381 // account to be associated with the created permanent account. To avoid this,
1382 // invalidate their sessions, set the session user to a new anonymous user, save it,
1383 // set the request context from the new session user account. (T393628)
1384 $creatorUser = $this->userFactory->newFromUserIdentity( $creator->getUser() );
1385 $this->sessionManager->invalidateSessionsForUser( $creatorUser );
1386 $creator = $this->userFactory->newAnonymous();
1387 $session->setUser( $creator );
1388 // Ensure the temporary account username is also cleared from the session, this is set
1389 // in TempUserCreator::acquireAndStashName
1390 $session->remove( 'TempUser:name' );
1391 $session->save();
1392 $this->setRequestContextUserFromSessionUser();
1393 }
1394 if ( !$this->canCreateAccounts() ) {
1395 // Caller should have called canCreateAccounts()
1396 $session->remove( self::ACCOUNT_CREATION_STATE );
1397 throw new LogicException( 'Account creation is not possible' );
1398 }
1399
1400 try {
1401 $username = AuthenticationRequest::getUsernameFromRequests( $reqs );
1402 } catch ( UnexpectedValueException ) {
1403 $username = null;
1404 }
1405 if ( $username === null ) {
1406 $this->logger->debug( __METHOD__ . ': No username provided' );
1407 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1408 }
1409
1410 // Permissions check
1411 $status = Status::wrap( $this->authorizeCreateAccount( $creator ) );
1412 if ( !$status->isGood() ) {
1413 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1414 'user' => $username,
1415 'creator' => $creator->getUser()->getName(),
1416 'reason' => $status->getWikiText( false, false, 'en' )
1417 ] );
1418 return AuthenticationResponse::newFail( $status->getMessage() );
1419 }
1420
1421 // Avoid deadlocks by placing no shared or exclusive gap locks (T199393)
1422 // As defense in-depth, PrimaryAuthenticationProvider::testUserExists only
1423 // supports READ_NORMAL/READ_LATEST (no support for recency query flags).
1424 $status = $this->canCreateAccount(
1425 $username, [ 'flags' => IDBAccessObject::READ_LATEST, 'creating' => true ]
1426 );
1427 if ( !$status->isGood() ) {
1428 $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1429 'user' => $username,
1430 'creator' => $creator->getUser()->getName(),
1431 'reason' => $status->getWikiText( false, false, 'en' )
1432 ] );
1433 return AuthenticationResponse::newFail( $status->getMessage() );
1434 }
1435
1436 $user = $this->userFactory->newFromName( (string)$username, UserRigorOptions::RIGOR_CREATABLE );
1437 foreach ( $reqs as $req ) {
1438 $req->username = $username;
1439 $req->returnToUrl = $returnToUrl;
1440 if ( $req instanceof UserDataAuthenticationRequest ) {
1441 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable user should be checked and valid here
1442 $status = $req->populateUser( $user );
1443 if ( !$status->isGood() ) {
1444 $status = Status::wrap( $status );
1445 $session->remove( self::ACCOUNT_CREATION_STATE );
1446 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1447 'user' => $user->getName(),
1448 'creator' => $creator->getUser()->getName(),
1449 'reason' => $status->getWikiText( false, false, 'en' ),
1450 ] );
1451 return AuthenticationResponse::newFail( $status->getMessage() );
1452 }
1453 }
1454 }
1455
1456 $this->removeAuthenticationSessionData( null );
1457
1458 $state = [
1459 'username' => $username,
1460 'userid' => 0,
1461 'creatorid' => $creator->getUser()->getId(),
1462 'creatorname' => $creator->getUser()->getName(),
1463 'reqs' => $reqs,
1464 'returnToUrl' => $returnToUrl,
1465 'providerIds' => $this->getProviderIds(),
1466 'primary' => null,
1467 'primaryResponse' => null,
1468 'secondary' => [],
1469 'continueRequests' => [],
1470 'maybeLink' => [],
1471 'ranPreTests' => false,
1472 ];
1473
1474 // Special case: converting a login to an account creation
1475 $req = AuthenticationRequest::getRequestByClass(
1476 $reqs, CreateFromLoginAuthenticationRequest::class
1477 );
1478 if ( $req ) {
1479 $state['maybeLink'] = $req->maybeLink;
1480
1481 if ( $req->createRequest ) {
1482 $reqs[] = $req->createRequest;
1483 $state['reqs'][] = $req->createRequest;
1484 }
1485 }
1486
1487 $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1488 $session->persist();
1489 $this->logger->debug( __METHOD__ . ': Proceeding with account creation for {username} by {creator}', [
1490 'username' => $user->getName(),
1491 'creator' => $creator->getUser()->getName(),
1492 ] );
1493
1494 return $this->continueAccountCreation( $reqs );
1495 }
1496
1502 public function continueAccountCreation( array $reqs ) {
1503 $session = $this->request->getSession();
1504 try {
1505 if ( !$this->canCreateAccounts() ) {
1506 // Caller should have called canCreateAccounts()
1507 $session->remove( self::ACCOUNT_CREATION_STATE );
1508 throw new LogicException( 'Account creation is not possible' );
1509 }
1510
1511 $state = $session->getSecret( self::ACCOUNT_CREATION_STATE );
1512 if ( !is_array( $state ) ) {
1513 return AuthenticationResponse::newFail(
1514 wfMessage( 'authmanager-create-not-in-progress' )
1515 );
1516 }
1517 $state['continueRequests'] = [];
1518
1519 // Step 0: Prepare and validate the input
1520
1521 $user = $this->userFactory->newFromName(
1522 (string)$state['username'],
1523 UserRigorOptions::RIGOR_CREATABLE
1524 );
1525 if ( !is_object( $user ) ) {
1526 $session->remove( self::ACCOUNT_CREATION_STATE );
1527 $this->logger->debug( __METHOD__ . ': Invalid username', [
1528 'user' => $state['username'],
1529 ] );
1530 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1531 }
1532
1533 if ( $state['creatorid'] ) {
1534 $creator = $this->userFactory->newFromId( (int)$state['creatorid'] );
1535 } else {
1536 $creator = $this->userFactory->newAnonymous();
1537 $creator->setName( $state['creatorname'] );
1538 }
1539
1540 if ( $state['providerIds'] !== $this->getProviderIds() ) {
1541 // An inconsistent AuthManagerFilterProviders hook, or site configuration changed
1542 // while the user was in the middle of authentication. The first is a bug, the
1543 // second is rare but expected when deploying a config change. Try handle in a way
1544 // that's useful for both cases.
1545 // @codeCoverageIgnoreStart
1546 MWExceptionHandler::logException( new NormalizedException(
1547 'Authentication failed because of inconsistent provider array',
1548 [ 'old' => json_encode( $state['providerIds'] ), 'new' => json_encode( $this->getProviderIds() ) ]
1549 ) );
1550 $ret = AuthenticationResponse::newFail(
1551 wfMessage( 'authmanager-create-not-in-progress' )
1552 );
1553 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1554 $session->remove( self::ACCOUNT_CREATION_STATE );
1555 return $ret;
1556 // @codeCoverageIgnoreEnd
1557 }
1558
1559 // Avoid account creation races on double submissions
1560 $cache = $this->objectCacheFactory->getLocalClusterInstance();
1561 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1562 if ( !$lock ) {
1563 // Don't clear account creation state for this code path because the process that won the race owns it.
1564 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1565 'user' => $user->getName(),
1566 'creator' => $creator->getName(),
1567 ] );
1568 return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1569 }
1570
1571 // Permissions check
1572 $status = Status::wrap( $this->authorizeCreateAccount( $creator ) );
1573 if ( !$status->isGood() ) {
1574 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1575 'user' => $user->getName(),
1576 'creator' => $creator->getName(),
1577 'reason' => $status->getWikiText( false, false, 'en' )
1578 ] );
1579 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1580 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1581 $session->remove( self::ACCOUNT_CREATION_STATE );
1582 return $ret;
1583 }
1584
1585 // Load from primary DB for existence check
1586 $user->load( IDBAccessObject::READ_LATEST );
1587
1588 if ( $state['userid'] === 0 ) {
1589 if ( $user->isRegistered() ) {
1590 $this->logger->debug( __METHOD__ . ': User exists locally', [
1591 'user' => $user->getName(),
1592 'creator' => $creator->getName(),
1593 ] );
1594 $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1595 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1596 $session->remove( self::ACCOUNT_CREATION_STATE );
1597 return $ret;
1598 }
1599 } else {
1600 if ( !$user->isRegistered() ) {
1601 $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1602 'user' => $user->getName(),
1603 'creator' => $creator->getName(),
1604 'expected_id' => $state['userid'],
1605 ] );
1606 throw new UnexpectedValueException(
1607 "User \"{$state['username']}\" should exist now, but doesn't!"
1608 );
1609 }
1610 if ( $user->getId() !== $state['userid'] ) {
1611 $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1612 'user' => $user->getName(),
1613 'creator' => $creator->getName(),
1614 'expected_id' => $state['userid'],
1615 'actual_id' => $user->getId(),
1616 ] );
1617 throw new UnexpectedValueException(
1618 "User \"{$state['username']}\" exists, but " .
1619 "ID {$user->getId()} !== {$state['userid']}!"
1620 );
1621 }
1622 }
1623 foreach ( $state['reqs'] as $req ) {
1624 if ( $req instanceof UserDataAuthenticationRequest ) {
1625 $status = $req->populateUser( $user );
1626 if ( !$status->isGood() ) {
1627 // This should never happen...
1628 $status = Status::wrap( $status );
1629 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1630 'user' => $user->getName(),
1631 'creator' => $creator->getName(),
1632 'reason' => $status->getWikiText( false, false, 'en' ),
1633 ] );
1634 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1635 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1636 [ $user, $creator, $ret ]
1637 );
1638 $session->remove( self::ACCOUNT_CREATION_STATE );
1639 return $ret;
1640 }
1641 }
1642 }
1643
1644 $status = Status::newGood();
1645 foreach ( $reqs as $req ) {
1646 $req->returnToUrl = $state['returnToUrl'];
1647 $req->username = $state['username'];
1648 $status->merge( $req->validate() );
1649 }
1650 if ( !$status->isOK() ) {
1651 $this->logger->debug( "Account creation failed at AuthRequest validation", [
1652 'user' => $user->getName(),
1653 'creator' => $creator->getName(),
1654 'reason' => $status->getWikiText( false, false, 'en' ),
1655 ] );
1656 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1657 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1658 $session->remove( self::ACCOUNT_CREATION_STATE );
1659 return $ret;
1660 }
1661
1662 // Run pre-creation tests, if we haven't already
1663 if ( !$state['ranPreTests'] ) {
1664 $providers = $this->getPreAuthenticationProviders() +
1665 $this->getPrimaryAuthenticationProviders() +
1666 $this->getSecondaryAuthenticationProviders();
1667 foreach ( $providers as $id => $provider ) {
1668 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1669 if ( !$status->isGood() ) {
1670 $this->logger->debug( __METHOD__ . ': Fail in pre-authentication by {id}', [
1671 'id' => $id,
1672 'user' => $user->getName(),
1673 'creator' => $creator->getName(),
1674 ] );
1675 $ret = AuthenticationResponse::newFail(
1676 Status::wrap( $status )->getMessage()
1677 );
1678 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1679 [ $user, $creator, $ret ]
1680 );
1681 $session->remove( self::ACCOUNT_CREATION_STATE );
1682 return $ret;
1683 }
1684 }
1685
1686 $state['ranPreTests'] = true;
1687 }
1688
1689 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1690
1691 if ( $state['primary'] === null ) {
1692 // We haven't picked a PrimaryAuthenticationProvider yet
1693 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1694 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1695 continue;
1696 }
1697 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1698 switch ( $res->status ) {
1699 case AuthenticationResponse::PASS:
1700 $this->logger->debug( __METHOD__ . ': Primary creation passed by {id}', [
1701 'id' => $id,
1702 'user' => $user->getName(),
1703 'creator' => $creator->getName(),
1704 ] );
1705 $state['primary'] = $id;
1706 $state['primaryResponse'] = $res;
1707 break 2;
1708 case AuthenticationResponse::FAIL:
1709 $this->logger->debug( __METHOD__ . ': Primary creation failed by {id}', [
1710 'id' => $id,
1711 'user' => $user->getName(),
1712 'creator' => $creator->getName(),
1713 ] );
1714 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1715 [ $user, $creator, $res ]
1716 );
1717 $session->remove( self::ACCOUNT_CREATION_STATE );
1718 return $res;
1719 case AuthenticationResponse::ABSTAIN:
1720 // Continue loop
1721 break;
1722 case AuthenticationResponse::REDIRECT:
1723 case AuthenticationResponse::UI:
1724 $this->logger->debug( __METHOD__ . ': Primary creation {status} by {id}', [
1725 'status' => $res->status,
1726 'id' => $id,
1727 'user' => $user->getName(),
1728 'creator' => $creator->getName(),
1729 ] );
1730 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1731 $state['primary'] = $id;
1732 $state['continueRequests'] = $res->neededRequests;
1733 $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1734 return $res;
1735
1736 // @codeCoverageIgnoreStart
1737 default:
1738 throw new DomainException(
1739 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1740 );
1741 // @codeCoverageIgnoreEnd
1742 }
1743 }
1744 if ( $state['primary'] === null ) {
1745 $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1746 'user' => $user->getName(),
1747 'creator' => $creator->getName(),
1748 ] );
1749 $ret = AuthenticationResponse::newFail(
1750 wfMessage( 'authmanager-create-no-primary' )
1751 );
1752 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1753 $session->remove( self::ACCOUNT_CREATION_STATE );
1754 return $ret;
1755 }
1756 } elseif ( $state['primaryResponse'] === null ) {
1757 $provider = $this->getAuthenticationProvider( $state['primary'] );
1758 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1759 // Configuration changed? Force them to start over.
1760 // @codeCoverageIgnoreStart
1761 $ret = AuthenticationResponse::newFail(
1762 wfMessage( 'authmanager-create-not-in-progress' )
1763 );
1764 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1765 $session->remove( self::ACCOUNT_CREATION_STATE );
1766 return $ret;
1767 // @codeCoverageIgnoreEnd
1768 }
1769 $id = $provider->getUniqueId();
1770 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1771 switch ( $res->status ) {
1772 case AuthenticationResponse::PASS:
1773 $this->logger->debug( __METHOD__ . ': Primary creation passed by {id}', [
1774 'id' => $id,
1775 'user' => $user->getName(),
1776 'creator' => $creator->getName(),
1777 ] );
1778 $state['primaryResponse'] = $res;
1779 break;
1780 case AuthenticationResponse::FAIL:
1781 $this->logger->debug( __METHOD__ . ': Primary creation failed by {id}', [
1782 'id' => $id,
1783 'user' => $user->getName(),
1784 'creator' => $creator->getName(),
1785 ] );
1786 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1787 [ $user, $creator, $res ]
1788 );
1789 $session->remove( self::ACCOUNT_CREATION_STATE );
1790 return $res;
1791 case AuthenticationResponse::REDIRECT:
1792 case AuthenticationResponse::UI:
1793 $this->logger->debug( __METHOD__ . ': Primary creation {status} by {id}', [
1794 'status' => $res->status,
1795 'id' => $id,
1796 'user' => $user->getName(),
1797 'creator' => $creator->getName(),
1798 ] );
1799 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1800 $state['continueRequests'] = $res->neededRequests;
1801 $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1802 return $res;
1803 default:
1804 throw new DomainException(
1805 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1806 );
1807 }
1808 }
1809
1810 // Step 2: Primary authentication succeeded. Give hook handlers a chance to interrupt,
1811 // then create the User object and add the user locally.
1812
1813 if ( $state['userid'] === 0 ) {
1814 $response = $state['primaryResponse'];
1815 if ( !$this->runVerifyHook( self::ACTION_CREATE, $user, $response, $state['primary'] ) ) {
1816 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1817 [ $user, $creator, $response ]
1818 );
1819 $session->remove( self::ACCOUNT_CREATION_STATE );
1820 return $response;
1821 }
1822 $this->logger->info( 'Creating user {user} during account creation', [
1823 'user' => $user->getName(),
1824 'creator' => $creator->getName(),
1825 ] );
1826 $status = $user->addToDatabase();
1827 if ( !$status->isOK() ) {
1828 // @codeCoverageIgnoreStart
1829 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1830 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1831 $session->remove( self::ACCOUNT_CREATION_STATE );
1832 return $ret;
1833 // @codeCoverageIgnoreEnd
1834 }
1835 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1836 $this->getHookRunner()->onLocalUserCreated( $user, false );
1837 $this->notificationService->notify(
1838 new WelcomeNotification( $user ),
1839 new RecipientSet( [ $user ] )
1840 );
1841 $user->saveSettings();
1842 $state['userid'] = $user->getId();
1843
1844 // Update user count
1845 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1846
1847 // Watch user's userpage and talk page
1848 $this->watchlistManager->addWatchIgnoringRights( $user, $user->getUserPage() );
1849
1850 // Inform the provider
1851 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable
1852 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1853
1854 // Log the creation
1855 if ( $this->config->get( MainConfigNames::NewUserLog ) ) {
1856 $isNamed = $creator->isNamed();
1857 $logEntry = new ManualLogEntry(
1858 'newusers',
1859 $logSubtype ?: ( $isNamed ? 'create2' : 'create' )
1860 );
1861 $logEntry->setPerformer( $isNamed ? $creator : $user );
1862 $logEntry->setTarget( $user->getUserPage() );
1864 $req = AuthenticationRequest::getRequestByClass(
1865 $state['reqs'], CreationReasonAuthenticationRequest::class
1866 );
1867 $logEntry->setComment( $req ? $req->reason : '' );
1868 $logEntry->setParameters( [
1869 '4::userid' => $user->getId(),
1870 ] );
1871 $logid = $logEntry->insert();
1872 $logEntry->publish( $logid );
1873 }
1874 }
1875
1876 // Step 3: Iterate over all the secondary authentication providers.
1877
1878 $beginReqs = $state['reqs'];
1879
1880 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1881 if ( !isset( $state['secondary'][$id] ) ) {
1882 // This provider isn't started yet, so we pass it the set
1883 // of reqs from beginAuthentication instead of whatever
1884 // might have been used by a previous provider in line.
1885 $func = 'beginSecondaryAccountCreation';
1886 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1887 } elseif ( !$state['secondary'][$id] ) {
1888 $func = 'continueSecondaryAccountCreation';
1889 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1890 } else {
1891 continue;
1892 }
1893 switch ( $res->status ) {
1894 case AuthenticationResponse::PASS:
1895 $this->logger->debug( __METHOD__ . ': Secondary creation passed by {id}', [
1896 'id' => $id,
1897 'user' => $user->getName(),
1898 'creator' => $creator->getName(),
1899 ] );
1900 // fall through
1901 case AuthenticationResponse::ABSTAIN:
1902 $state['secondary'][$id] = true;
1903 break;
1904 case AuthenticationResponse::REDIRECT:
1905 case AuthenticationResponse::UI:
1906 $this->logger->debug( __METHOD__ . ': Secondary creation {status} by {id}', [
1907 'status' => $res->status,
1908 'id' => $id,
1909 'user' => $user->getName(),
1910 'creator' => $creator->getName(),
1911 ] );
1912 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1913 $state['secondary'][$id] = false;
1914 $state['continueRequests'] = $res->neededRequests;
1915 $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1916 return $res;
1917 case AuthenticationResponse::FAIL:
1918 throw new DomainException(
1919 get_class( $provider ) . "::{$func}() returned $res->status." .
1920 ' Secondary providers are not allowed to fail account creation, that' .
1921 ' should have been done via testForAccountCreation().'
1922 );
1923 // @codeCoverageIgnoreStart
1924 default:
1925 throw new DomainException(
1926 get_class( $provider ) . "::{$func}() returned $res->status"
1927 );
1928 // @codeCoverageIgnoreEnd
1929 }
1930 }
1931
1932 $id = $user->getId();
1933 $name = $user->getName();
1934 $req = new CreatedAccountAuthenticationRequest( $id, $name );
1935 $ret = AuthenticationResponse::newPass( $name );
1936 $ret->loginRequest = $req;
1937 $this->createdAccountAuthenticationRequests[] = $req;
1938
1939 $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1940 'user' => $user->getName(),
1941 'creator' => $creator->getName(),
1942 ] );
1943
1944 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1945 $session->remove( self::ACCOUNT_CREATION_STATE );
1946 $this->removeAuthenticationSessionData( null );
1947 return $ret;
1948 } catch ( Exception $ex ) {
1949 $session->remove( self::ACCOUNT_CREATION_STATE );
1950 throw $ex;
1951 }
1952 }
1953
1961 private function logAutocreationAttempt( Status $status, User $targetUser, $source, $login ) {
1962 if ( $status->isOK() && !$status->isGood() ) {
1963 return; // user already existed, no need to log
1964 }
1965
1966 $firstMessage = $status->getMessages( 'error' )[0] ?? $status->getMessages( 'warning' )[0] ?? null;
1967
1968 $this->authEventsLogger->info( 'Autocreation attempt', [
1969 'event' => 'autocreate',
1970 'successful' => $status->isGood(),
1971 'status' => $firstMessage ? $firstMessage->getKey() : '-',
1972 'accountType' => $this->identityUtils->getShortUserTypeInternal( $targetUser ),
1973 'source' => $source,
1974 'login' => $login,
1975 ] );
1976 }
1977
1988 private function autocreatingTempUserToAppealBlock(
1989 StatusValue $status,
1990 string $source,
1991 User $performer
1992 ): bool {
1993 $block = $status instanceof PermissionStatus ? $status->getBlock() : null;
1994 if ( !( $block instanceof AbstractBlock ) ) {
1995 return false;
1996 }
1997 $title = RequestContext::getMain()->getTitle();
1998 return $title && $title->isSpecial( 'Mytalk' ) &&
1999 $source === self::AUTOCREATE_SOURCE_TEMP &&
2000 $performer->isAnon() &&
2001 count( $status->getErrors() ) === 1 &&
2002 !$block->appliesToUsertalk( $performer->getTalkPage() );
2003 }
2004
2030 public function autoCreateUser(
2031 User $user,
2032 $source,
2033 $login = true,
2034 $log = true,
2035 ?Authority $performer = null,
2036 array $tags = []
2037 ) {
2038 $validSources = [
2039 self::AUTOCREATE_SOURCE_SESSION,
2040 self::AUTOCREATE_SOURCE_MAINT,
2041 self::AUTOCREATE_SOURCE_TEMP
2042 ];
2043 if ( !in_array( $source, $validSources, true )
2044 && !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
2045 ) {
2046 throw new InvalidArgumentException( "Unknown auto-creation source: $source" );
2047 }
2048
2049 $username = $user->getName();
2050
2051 // Try the local user from the replica DB, then fall back to the primary.
2052 $localUserIdentity = $this->userIdentityLookup->getUserIdentityByName( $username );
2053 // @codeCoverageIgnoreStart
2054 if ( ( !$localUserIdentity || !$localUserIdentity->isRegistered() )
2055 && $this->loadBalancer->getReaderIndex() !== 0
2056 ) {
2057 $localUserIdentity = $this->userIdentityLookup->getUserIdentityByName(
2058 $username, IDBAccessObject::READ_LATEST
2059 );
2060 }
2061 // @codeCoverageIgnoreEnd
2062 $localId = ( $localUserIdentity && $localUserIdentity->isRegistered() )
2063 ? $localUserIdentity->getId()
2064 : null;
2065
2066 if ( $localId ) {
2067 $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
2068 'username' => $username,
2069 ] );
2070 $user->setId( $localId );
2071
2072 // Can't rely on a replica read, not even when getUserIdentityByName() used
2073 // READ_NORMAL, because that method has an in-process cache not shared
2074 // with loadFromId.
2075 $user->loadFromId( IDBAccessObject::READ_LATEST );
2076 if ( $login ) {
2077 $remember = $source === self::AUTOCREATE_SOURCE_TEMP;
2078 $this->setSessionDataForUser( $user, $remember, null );
2079 }
2080 return Status::newGood()->warning( 'userexists' );
2081 }
2082
2083 // Wiki is read-only?
2084 if ( $this->readOnlyMode->isReadOnly() ) {
2085 $reason = $this->readOnlyMode->getReason();
2086 $this->logger->debug( __METHOD__ . ': denied because of read only mode: {reason}', [
2087 'username' => $username,
2088 'reason' => $reason,
2089 ] );
2090 $user->setId( 0 );
2091 $user->loadFromId();
2092 $fatalStatus = Status::newFatal( 'readonlytext', $reason );
2093 $this->logAutocreationAttempt( $fatalStatus, $user, $source, $login );
2094 return $fatalStatus;
2095 }
2096
2097 // If there is a non-anonymous performer, don't use their session
2098 $session = null;
2099 $performer ??= $user;
2100 if ( !$performer->isRegistered() || $performer->getUser()->equals( $user ) ) {
2101 // $performer is anonymous, or refers to the same user as $user (i.e., this isn't
2102 // an autocreation attempt via Special:CreateLocalAccount or by the maintenance script)
2103 $session = $this->request->getSession();
2104 }
2105
2106 // Is the username usable? (Previously isCreatable() was checked here but
2107 // that doesn't work with auto-creation of TempUser accounts by CentralAuth)
2108 if ( !$this->userNameUtils->isUsable( $username ) ) {
2109 $this->logger->debug( __METHOD__ . ': name "{username}" is not usable', [
2110 'username' => $username,
2111 ] );
2112 $user->setId( 0 );
2113 $user->loadFromId();
2114 $fatalStatus = Status::newFatal( 'noname' );
2115 $this->logAutocreationAttempt( $fatalStatus, $user, $source, $login );
2116 return $fatalStatus;
2117 }
2118
2119 // Is the IP user able to create accounts?
2120 $bypassAuthorization = $session && $session->getProvider()->canAlwaysAutocreate();
2121 if ( $source !== self::AUTOCREATE_SOURCE_MAINT && !$bypassAuthorization ) {
2122 $status = $this->authorizeAutoCreateAccount( $performer );
2123 if ( !$status->isOk() ) {
2124 if ( $this->autocreatingTempUserToAppealBlock( $status, $source, $performer ) ) {
2125 $this->logger->info( __METHOD__ . ': autocreating temporary user to appeal a block', [
2126 'username' => $username,
2127 'creator' => $performer->getUser()->getName(),
2128 ] );
2129 } else {
2130 $this->logger->debug( __METHOD__ . ': cannot create or autocreate accounts', [
2131 'username' => $username,
2132 'creator' => $performer->getUser()->getName(),
2133 ] );
2134 $user->setId( 0 );
2135 $user->loadFromId();
2136 $statusWrapped = Status::wrap( $status );
2137 $this->logAutocreationAttempt( $statusWrapped, $user, $source, $login );
2138 return $statusWrapped;
2139 }
2140 }
2141 }
2142
2143 // Avoid account creation races on double submissions
2144 $cache = $this->objectCacheFactory->getLocalClusterInstance();
2145 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2146 if ( !$lock ) {
2147 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
2148 'user' => $username,
2149 ] );
2150 $user->setId( 0 );
2151 $user->loadFromId();
2152 $status = Status::newFatal( 'usernameinprogress' );
2153 $this->logAutocreationAttempt( $status, $user, $source, $login );
2154 return $status;
2155 }
2156
2157 // Denied by providers?
2158 $options = [
2159 'flags' => IDBAccessObject::READ_LATEST,
2160 'creating' => true,
2161 'canAlwaysAutocreate' => $session && $session->getProvider()->canAlwaysAutocreate(),
2162 'performer' => $performer,
2163 ];
2164 $providers = $this->getPreAuthenticationProviders() +
2165 $this->getPrimaryAuthenticationProviders() +
2166 $this->getSecondaryAuthenticationProviders();
2167 foreach ( $providers as $provider ) {
2168 $status = $provider->testUserForCreation( $user, $source, $options );
2169 if ( !$status->isGood() ) {
2170 $ret = Status::wrap( $status );
2171 $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
2172 'username' => $username,
2173 'reason' => $ret->getWikiText( false, false, 'en' ),
2174 ] );
2175 $user->setId( 0 );
2176 $user->loadFromId();
2177 $this->logAutocreationAttempt( $ret, $user, $source, $login );
2178 return $ret;
2179 }
2180 }
2181
2182 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2183 if ( $cache->get( $backoffKey ) ) {
2184 $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
2185 'username' => $username,
2186 ] );
2187 $user->setId( 0 );
2188 $user->loadFromId();
2189 $status = Status::newFatal( 'authmanager-autocreate-exception' );
2190 $this->logAutocreationAttempt( $status, $user, $source, $login );
2191 return $status;
2192
2193 }
2194
2195 // Checks passed, create the user...
2196 $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
2197 $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
2198 'username' => $username,
2199 'from' => $from
2200 ] + $this->request->getSecurityLogContext( $performer->getUser() )
2201 );
2202
2203 // Ignore warnings about primary connections/writes...hard to avoid here
2204 $fname = __METHOD__;
2205 $trxLimits = $this->config->get( MainConfigNames::TrxProfilerLimits );
2206 $trxProfiler = Profiler::instance()->getTransactionProfiler();
2207 $trxProfiler->redefineExpectations( $trxLimits['POST'], $fname );
2208 DeferredUpdates::addCallableUpdate( static function () use ( $trxProfiler, $trxLimits, $fname ) {
2209 $trxProfiler->redefineExpectations( $trxLimits['PostSend-POST'], $fname );
2210 } );
2211
2212 try {
2213 $status = $user->addToDatabase();
2214 if ( !$status->isOK() ) {
2215 // Double-check for a race condition (T70012). We make use of the fact that when
2216 // addToDatabase fails due to the user already existing, the user object gets loaded.
2217 if ( $user->getId() ) {
2218 $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
2219 'username' => $username,
2220 ] );
2221 if ( $login ) {
2222 $remember = $source === self::AUTOCREATE_SOURCE_TEMP;
2223 $this->setSessionDataForUser( $user, $remember, null );
2224 }
2225 $status = Status::newGood()->warning( 'userexists' );
2226 } else {
2227 $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
2228 'username' => $username,
2229 'msg' => $status->getWikiText( false, false, 'en' )
2230 ] );
2231 $user->setId( 0 );
2232 $user->loadFromId();
2233 }
2234 $this->logAutocreationAttempt( $status, $user, $source, $login );
2235 return $status;
2236 }
2237 } catch ( Exception $ex ) {
2238 $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
2239 'username' => $username,
2240 'exception' => $ex,
2241 ] );
2242 // Do not keep throwing errors for a while
2243 $cache->set( $backoffKey, 1, 600 );
2244 // Bubble up error; which should normally trigger DB rollbacks
2245 throw $ex;
2246 }
2247
2248 $this->setDefaultUserOptions( $user, false );
2249
2250 // Inform the providers
2251 $this->callMethodOnProviders( self::CALL_PRIMARY | self::CALL_SECONDARY, 'autoCreatedAccount',
2252 [ $user, $source ]
2253 );
2254
2255 $this->getHookRunner()->onLocalUserCreated( $user, true );
2256 $user->saveSettings();
2257
2258 // Update user count
2259 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
2260 // Watch user's userpage and talk page (except temp users)
2261 if ( $source !== self::AUTOCREATE_SOURCE_TEMP ) {
2262 DeferredUpdates::addCallableUpdate( function () use ( $user ) {
2263 $this->watchlistManager->addWatchIgnoringRights( $user, $user->getUserPage() );
2264 } );
2265 }
2266
2267 // Log the creation
2268 if ( $this->config->get( MainConfigNames::NewUserLog ) && $log ) {
2269 $logEntry = new ManualLogEntry( 'newusers', 'autocreate' );
2270 $logEntry->setPerformer( $user );
2271 $logEntry->setTarget( $user->getUserPage() );
2272 $logEntry->setComment( '' );
2273 $logEntry->setParameters( [
2274 '4::userid' => $user->getId(),
2275 ] );
2276 $logid = $logEntry->insert();
2277
2278 if ( $tags !== [] ) {
2279 // ManualLogEntry::insert doesn't insert tags
2280 $this->changeTagsStore->addTags( $tags, null, null, $logid );
2281 }
2282 }
2283
2284 if ( $login ) {
2285 $remember = $source === self::AUTOCREATE_SOURCE_TEMP;
2286 $this->setSessionDataForUser( $user, $remember, null );
2287 }
2288 $retStatus = Status::newGood();
2289 $this->logAutocreationAttempt( $retStatus, $user, $source, $login );
2290 return $retStatus;
2291 }
2292
2300 private function authorizeAutoCreateAccount( Authority $creator ) {
2301 return $this->authorizeInternal(
2302 static function (
2303 string $action,
2304 PageIdentity $target,
2305 PermissionStatus $status
2306 ) use ( $creator ) {
2307 return $creator->authorizeWrite( $action, $target, $status );
2308 },
2309 'autocreateaccount'
2310 );
2311 }
2312
2313 // endregion -- end of Account creation
2314
2315 /***************************************************************************/
2316 // region Account linking
2323 public function canLinkAccounts() {
2324 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2325 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2326 return true;
2327 }
2328 }
2329 return false;
2330 }
2331
2341 public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
2342 $session = $this->request->getSession();
2343 $session->remove( self::ACCOUNT_LINK_STATE );
2344
2345 if ( !$this->canLinkAccounts() ) {
2346 // Caller should have called canLinkAccounts()
2347 throw new LogicException( 'Account linking is not possible' );
2348 }
2349
2350 if ( !$user->isRegistered() ) {
2351 if ( !$this->userNameUtils->isUsable( $user->getName() ) ) {
2352 $msg = wfMessage( 'noname' );
2353 } else {
2354 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
2355 }
2356 return AuthenticationResponse::newFail( $msg );
2357 }
2358
2359 $status = Status::newGood();
2360 foreach ( $reqs as $req ) {
2361 $req->username = $user->getName();
2362 $req->returnToUrl = $returnToUrl;
2363 $status->merge( $req->validate() );
2364 }
2365 if ( !$status->isOK() ) {
2366 $this->logger->debug( "Account linking failed at AuthRequest validation", [
2367 'user' => $user->getName(),
2368 'reason' => $status->getWikiText( false, false, 'en' ),
2369 ] );
2370 $ret = AuthenticationResponse::newFail( $status->getMessage() );
2371 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2372 return $ret;
2373 }
2374
2375 $this->removeAuthenticationSessionData( null );
2376
2377 $providers = $this->getPreAuthenticationProviders();
2378 foreach ( $providers as $id => $provider ) {
2379 $status = $provider->testForAccountLink( $user );
2380 if ( !$status->isGood() ) {
2381 $this->logger->debug( __METHOD__ . ': Account linking pre-check failed by {id}', [
2382 'id' => $id,
2383 'user' => $user->getName(),
2384 ] );
2385 $ret = AuthenticationResponse::newFail(
2386 Status::wrap( $status )->getMessage()
2387 );
2388 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2389 return $ret;
2390 }
2391 }
2392
2393 $state = [
2394 'username' => $user->getName(),
2395 'userid' => $user->getId(),
2396 'returnToUrl' => $returnToUrl,
2397 'providerIds' => $this->getProviderIds(),
2398 'primary' => null,
2399 'continueRequests' => [],
2400 ];
2401
2402 $providers = $this->getPrimaryAuthenticationProviders();
2403 foreach ( $providers as $id => $provider ) {
2404 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
2405 continue;
2406 }
2407
2408 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
2409 switch ( $res->status ) {
2410 case AuthenticationResponse::PASS:
2411 $this->logger->info( 'Account linked to {user} by {id}', [
2412 'id' => $id,
2413 'user' => $user->getName(),
2414 ] );
2415 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2416 [ $user, $res ]
2417 );
2418 return $res;
2419
2420 case AuthenticationResponse::FAIL:
2421 $this->logger->debug( __METHOD__ . ': Account linking failed by {id}', [
2422 'id' => $id,
2423 'user' => $user->getName(),
2424 ] );
2425 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2426 [ $user, $res ]
2427 );
2428 return $res;
2429
2430 case AuthenticationResponse::ABSTAIN:
2431 // Continue loop
2432 break;
2433
2434 case AuthenticationResponse::REDIRECT:
2435 case AuthenticationResponse::UI:
2436 $this->logger->debug( __METHOD__ . ': Account linking {status} by {id}', [
2437 'status' => $res->status,
2438 'id' => $id,
2439 'user' => $user->getName(),
2440 ] );
2441 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
2442 $state['primary'] = $id;
2443 $state['continueRequests'] = $res->neededRequests;
2444 $session->setSecret( self::ACCOUNT_LINK_STATE, $state );
2445 $session->persist();
2446 return $res;
2447
2448 // @codeCoverageIgnoreStart
2449 default:
2450 throw new DomainException(
2451 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
2452 );
2453 // @codeCoverageIgnoreEnd
2454 }
2455 }
2456
2457 $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
2458 'user' => $user->getName(),
2459 ] );
2460 $ret = AuthenticationResponse::newFail(
2461 wfMessage( 'authmanager-link-no-primary' )
2462 );
2463 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2464 return $ret;
2465 }
2466
2472 public function continueAccountLink( array $reqs ) {
2473 $session = $this->request->getSession();
2474 try {
2475 if ( !$this->canLinkAccounts() ) {
2476 // Caller should have called canLinkAccounts()
2477 $session->remove( self::ACCOUNT_LINK_STATE );
2478 throw new LogicException( 'Account linking is not possible' );
2479 }
2480
2481 $state = $session->getSecret( self::ACCOUNT_LINK_STATE );
2482 if ( !is_array( $state ) ) {
2483 return AuthenticationResponse::newFail(
2484 wfMessage( 'authmanager-link-not-in-progress' )
2485 );
2486 }
2487 $state['continueRequests'] = [];
2488
2489 // Step 0: Prepare and validate the input
2490
2491 $user = $this->userFactory->newFromName(
2492 (string)$state['username'],
2493 UserRigorOptions::RIGOR_USABLE
2494 );
2495 if ( !is_object( $user ) ) {
2496 $session->remove( self::ACCOUNT_LINK_STATE );
2497 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
2498 }
2499 if ( $user->getId() !== $state['userid'] ) {
2500 throw new UnexpectedValueException(
2501 "User \"{$state['username']}\" is valid, but " .
2502 "ID {$user->getId()} !== {$state['userid']}!"
2503 );
2504 }
2505
2506 if ( $state['providerIds'] !== $this->getProviderIds() ) {
2507 // An inconsistent AuthManagerFilterProviders hook, or site configuration changed
2508 // while the user was in the middle of authentication. The first is a bug, the
2509 // second is rare but expected when deploying a config change. Try handle in a way
2510 // that's useful for both cases.
2511 // @codeCoverageIgnoreStart
2512 MWExceptionHandler::logException( new NormalizedException(
2513 'Authentication failed because of inconsistent provider array',
2514 [ 'old' => json_encode( $state['providerIds'] ), 'new' => json_encode( $this->getProviderIds() ) ]
2515 ) );
2516 $ret = AuthenticationResponse::newFail(
2517 wfMessage( 'authmanager-link-not-in-progress' )
2518 );
2519 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountLink', [ $user, $ret ] );
2520 $session->remove( self::ACCOUNT_LINK_STATE );
2521 return $ret;
2522 // @codeCoverageIgnoreEnd
2523 }
2524
2525 $status = Status::newGood();
2526 foreach ( $reqs as $req ) {
2527 $req->username = $state['username'];
2528 $req->returnToUrl = $state['returnToUrl'];
2529 $status->merge( $req->validate() );
2530 }
2531 if ( !$status->isOK() ) {
2532 $this->logger->debug( "Account linking failed at AuthRequest validation", [
2533 'user' => $user->getName(),
2534 'reason' => $status->getWikiText( false, false, 'en' ),
2535 ] );
2536 $ret = AuthenticationResponse::newFail( $status->getMessage() );
2537 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2538 $session->remove( self::ACCOUNT_LINK_STATE );
2539 return $ret;
2540 }
2541
2542 // Step 1: Call the primary again until it succeeds
2543
2544 $provider = $this->getAuthenticationProvider( $state['primary'] );
2545 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
2546 // Configuration changed? Force them to start over.
2547 // @codeCoverageIgnoreStart
2548 $ret = AuthenticationResponse::newFail(
2549 wfMessage( 'authmanager-link-not-in-progress' )
2550 );
2551 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2552 $session->remove( self::ACCOUNT_LINK_STATE );
2553 return $ret;
2554 // @codeCoverageIgnoreEnd
2555 }
2556 $id = $provider->getUniqueId();
2557 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
2558 switch ( $res->status ) {
2559 case AuthenticationResponse::PASS:
2560 $this->logger->info( 'Account linked to {user} by {id}', [
2561 'id' => $id,
2562 'user' => $user->getName(),
2563 ] );
2564 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2565 [ $user, $res ]
2566 );
2567 $session->remove( self::ACCOUNT_LINK_STATE );
2568 return $res;
2569 case AuthenticationResponse::FAIL:
2570 $this->logger->debug( __METHOD__ . ': Account linking failed by {id}', [
2571 'id' => $id,
2572 'user' => $user->getName(),
2573 ] );
2574 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2575 [ $user, $res ]
2576 );
2577 $session->remove( self::ACCOUNT_LINK_STATE );
2578 return $res;
2579 case AuthenticationResponse::REDIRECT:
2580 case AuthenticationResponse::UI:
2581 $this->logger->debug( __METHOD__ . ': Account linking {status} by {id}', [
2582 'status' => $res->status,
2583 'id' => $id,
2584 'user' => $user->getName(),
2585 ] );
2586 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
2587 $state['continueRequests'] = $res->neededRequests;
2588 $session->setSecret( self::ACCOUNT_LINK_STATE, $state );
2589 return $res;
2590 default:
2591 throw new DomainException(
2592 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
2593 );
2594 }
2595 } catch ( Exception $ex ) {
2596 $session->remove( self::ACCOUNT_LINK_STATE );
2597 throw $ex;
2598 }
2599 }
2600
2601 // endregion -- end of Account linking
2602
2603 /***************************************************************************/
2604 // region Information methods
2629 public function getAuthenticationRequests( $action, ?UserIdentity $user = null, array $options = [] ) {
2630 $options = [ 'securityLevel' => $options['securityLevel'] ?? null ];
2631 $providerAction = $action;
2632
2633 if ( $options['securityLevel'] !== null && $action !== self::ACTION_LOGIN ) {
2634 throw new InvalidArgumentException( "The 'securityLevel' option can only be used for the "
2635 . "'login' action, not '$action'" );
2636 } elseif ( $options['securityLevel'] !== null
2637 && $this->getRequest()->getSession()->getUser()->isAnon()
2638 ) {
2639 throw new InvalidArgumentException( "The 'securityLevel' option can only be used when the "
2640 . 'current user is logged in' );
2641 }
2642
2643 // Figure out which providers to query
2644 switch ( $action ) {
2645 case self::ACTION_LOGIN:
2646 case self::ACTION_CREATE:
2647 $providers = $this->getPreAuthenticationProviders() +
2648 $this->getPrimaryAuthenticationProviders() +
2649 $this->getSecondaryAuthenticationProviders();
2650 break;
2651
2652 case self::ACTION_LOGIN_CONTINUE:
2653 $state = $this->request->getSession()->getSecret( self::AUTHN_STATE );
2654 return is_array( $state ) ? $state['continueRequests'] : [];
2655
2656 case self::ACTION_CREATE_CONTINUE:
2657 $state = $this->request->getSession()->getSecret( self::ACCOUNT_CREATION_STATE );
2658 return is_array( $state ) ? $state['continueRequests'] : [];
2659
2660 case self::ACTION_LINK:
2661 $providers = [];
2662 foreach ( $this->getPrimaryAuthenticationProviders() as $p ) {
2663 if ( $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2664 $providers[] = $p;
2665 }
2666 }
2667 break;
2668
2669 case self::ACTION_UNLINK:
2670 $providers = [];
2671 foreach ( $this->getPrimaryAuthenticationProviders() as $p ) {
2672 if ( $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2673 $providers[] = $p;
2674 }
2675 }
2676
2677 // To providers, unlink and remove are identical.
2678 $providerAction = self::ACTION_REMOVE;
2679 break;
2680
2681 case self::ACTION_LINK_CONTINUE:
2682 $state = $this->request->getSession()->getSecret( self::ACCOUNT_LINK_STATE );
2683 return is_array( $state ) ? $state['continueRequests'] : [];
2684
2685 case self::ACTION_CHANGE:
2686 case self::ACTION_REMOVE:
2687 $providers = $this->getPrimaryAuthenticationProviders() +
2688 $this->getSecondaryAuthenticationProviders();
2689 break;
2690
2691 // @codeCoverageIgnoreStart
2692 default:
2693 throw new DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2694 }
2695 // @codeCoverageIgnoreEnd
2696
2697 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2698 }
2699
2709 private function getAuthenticationRequestsInternal(
2710 $providerAction, array $options, array $providers, ?UserIdentity $user = null
2711 ) {
2712 $user = $user ?: RequestContext::getMain()->getUser();
2713 $options['username'] = $user->isRegistered() ? $user->getName() : null;
2714 $options += [ 'securityLevel' => null ];
2715
2716 // Query them and merge results
2717 $reqs = [];
2718 foreach ( $providers as $provider ) {
2719 $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2720 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2721 $id = $req->getUniqueId();
2722
2723 // If a required request if from a Primary, mark it as "primary-required" instead
2724 if ( $isPrimary && $req->required ) {
2725 $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2726 }
2727
2728 if (
2729 !isset( $reqs[$id] )
2730 || $req->required === AuthenticationRequest::REQUIRED
2731 || $reqs[$id]->required === AuthenticationRequest::OPTIONAL
2732 ) {
2733 $reqs[$id] = $req;
2734 }
2735 }
2736 }
2737
2738 // AuthManager has its own req for some actions
2739 switch ( $providerAction ) {
2740 case self::ACTION_LOGIN:
2741 $options['username'] = null; // Don't fill in the username below
2742 if ( $options['securityLevel'] !== null ) {
2743 $reqs[] = ElevatedSecurityAuthenticationRequest::create(
2744 $this->getRequest()->getSession(), $options['securityLevel'] );
2745 } else {
2746 $reqs[] = new RememberMeAuthenticationRequest(
2747 $this->config->get( MainConfigNames::RememberMe )
2748 );
2749 }
2750 break;
2751
2752 case self::ACTION_CREATE:
2753 $reqs[] = new UsernameAuthenticationRequest;
2754 $reqs[] = new UserDataAuthenticationRequest;
2755
2756 // Registered users should be prompted to provide a rationale for account creations,
2757 // except for the case of a temporary user registering a full account (T328718).
2758 if (
2759 $options['username'] !== null &&
2760 !$this->userNameUtils->isTemp( $options['username'] )
2761 ) {
2762 $reqs[] = new CreationReasonAuthenticationRequest;
2763 $options['username'] = null; // Don't fill in the username below
2764 }
2765 break;
2766 }
2767
2768 // Fill in reqs data
2769 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2770
2771 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2772 if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2773 $reqs = array_filter( $reqs, function ( $req ) {
2774 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2775 } );
2776 }
2777
2778 return array_values( $reqs );
2779 }
2780
2788 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2789 foreach ( $reqs as $req ) {
2790 if ( !$req->action || $forceAction ) {
2791 $req->action = $action;
2792 }
2793 $req->username ??= $username;
2794 }
2795 }
2796
2803 public function userExists( $username, $flags = IDBAccessObject::READ_NORMAL ) {
2804 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2805 if ( $provider->testUserExists( $username, $flags ) ) {
2806 return true;
2807 }
2808 }
2809
2810 return false;
2811 }
2812
2824 public function allowsPropertyChange( $property ) {
2825 $providers = $this->getPrimaryAuthenticationProviders() +
2826 $this->getSecondaryAuthenticationProviders();
2827 foreach ( $providers as $provider ) {
2828 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2829 return false;
2830 }
2831 }
2832 return true;
2833 }
2834
2843 public function getAuthenticationProvider( $id ) {
2844 // Fast version
2845 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2846 return $this->allAuthenticationProviders[$id];
2847 }
2848
2849 // Slow version: instantiate each kind and check
2850 $providers = $this->getPrimaryAuthenticationProviders();
2851 if ( isset( $providers[$id] ) ) {
2852 return $providers[$id];
2853 }
2854 $providers = $this->getSecondaryAuthenticationProviders();
2855 if ( isset( $providers[$id] ) ) {
2856 return $providers[$id];
2857 }
2858 $providers = $this->getPreAuthenticationProviders();
2859 if ( isset( $providers[$id] ) ) {
2860 return $providers[$id];
2861 }
2862
2863 return null;
2864 }
2865
2866 // endregion -- end of Information methods
2867
2868 /***************************************************************************/
2869 // region Internal methods
2878 public function setAuthenticationSessionData( $key, $data ) {
2879 $session = $this->request->getSession();
2880 $arr = $session->getSecret( 'authData' );
2881 if ( !is_array( $arr ) ) {
2882 $arr = [];
2883 }
2884 $arr[$key] = $data;
2885 $session->setSecret( 'authData', $arr );
2886 }
2887
2895 public function getAuthenticationSessionData( $key, $default = null ) {
2896 $arr = $this->request->getSession()->getSecret( 'authData' );
2897 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2898 return $arr[$key];
2899 } else {
2900 return $default;
2901 }
2902 }
2903
2909 public function removeAuthenticationSessionData( $key ) {
2910 $session = $this->request->getSession();
2911 if ( $key === null ) {
2912 $session->remove( 'authData' );
2913 } else {
2914 $arr = $session->getSecret( 'authData' );
2915 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2916 unset( $arr[$key] );
2917 $session->setSecret( 'authData', $arr );
2918 }
2919 }
2920 }
2921
2929 protected function providerArrayFromSpecs( $class, array $specs ) {
2930 $i = 0;
2931 foreach ( $specs as &$spec ) {
2932 $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2933 }
2934 unset( $spec );
2935 // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2936 usort( $specs, static function ( $a, $b ) {
2937 return $a['sort'] <=> $b['sort']
2938 ?: $a['sort2'] <=> $b['sort2'];
2939 } );
2940
2941 $ret = [];
2942 foreach ( $specs as $spec ) {
2944 $provider = $this->objectFactory->createObject( $spec, [ 'assertClass' => $class ] );
2945 $provider->init( $this->logger, $this, $this->getHookContainer(), $this->config, $this->userNameUtils );
2946 $id = $provider->getUniqueId();
2947 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2948 throw new RuntimeException(
2949 "Duplicate specifications for id $id (classes " .
2950 get_class( $provider ) . ' and ' .
2951 get_class( $this->allAuthenticationProviders[$id] ) . ')'
2952 );
2953 }
2954 // @phan-suppress-next-line PhanTypeMismatchProperty
2955 $this->allAuthenticationProviders[$id] = $provider;
2956 $ret[$id] = $provider;
2957 }
2958 return $ret;
2959 }
2960
2965 protected function getPreAuthenticationProviders() {
2966 if ( $this->preAuthenticationProviders === null ) {
2967 $this->initializeAuthenticationProviders();
2968 }
2969 return $this->preAuthenticationProviders;
2970 }
2971
2977 if ( $this->primaryAuthenticationProviders === null ) {
2978 $this->initializeAuthenticationProviders();
2979 }
2980 return $this->primaryAuthenticationProviders;
2981 }
2982
2988 if ( $this->secondaryAuthenticationProviders === null ) {
2989 $this->initializeAuthenticationProviders();
2990 }
2991 return $this->secondaryAuthenticationProviders;
2992 }
2993
2994 private function getProviderIds(): array {
2995 return [
2996 'preauth' => array_keys( $this->getPreAuthenticationProviders() ),
2997 'primaryauth' => array_keys( $this->getPrimaryAuthenticationProviders() ),
2998 'secondaryauth' => array_keys( $this->getSecondaryAuthenticationProviders() ),
2999 ];
3000 }
3001
3002 private function initializeAuthenticationProviders() {
3003 $conf = $this->config->get( MainConfigNames::AuthManagerConfig )
3004 ?: $this->config->get( MainConfigNames::AuthManagerAutoConfig );
3005
3006 $providers = array_map( static fn ( $stepConf ) => array_fill_keys( array_keys( $stepConf ), true ), $conf );
3007 $this->getHookRunner()->onAuthManagerFilterProviders( $providers );
3008 foreach ( $conf as $step => $stepConf ) {
3009 $conf[$step] = array_intersect_key( $stepConf, array_filter( $providers[$step] ) );
3010 }
3011
3012 $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
3013 PreAuthenticationProvider::class, $conf['preauth']
3014 );
3015 $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
3016 PrimaryAuthenticationProvider::class, $conf['primaryauth']
3017 );
3018 $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
3019 SecondaryAuthenticationProvider::class, $conf['secondaryauth']
3020 );
3021 }
3022
3030 private function setSessionDataForUser( $user, $remember = null, $securityLevel = null ) {
3031 $session = $this->request->getSession();
3032 $delay = $session->delaySave();
3033
3034 // If the user just logged into this account, they should not have elevated security.
3035 if ( !$user->equals( $session->getUser() ) ) {
3036 $session->set( 'AuthManager:lastAuthTimestamps', [] );
3037 $securityLevel = null;
3038 }
3039
3040 $session->resetId();
3041 $session->resetAllTokens();
3042 if ( $session->canSetUser() ) {
3043 $session->setUser( $user );
3044 }
3045 if ( $remember !== null ) {
3046 $session->setRememberUser( $remember );
3047 }
3048
3049 $session->set( 'AuthManager:lastAuthId', $user->getId() );
3050 if ( $securityLevel !== null ) {
3051 $lastAuthTimestamps = $session->get( 'AuthManager:lastAuthTimestamps', [] );
3052 $lastAuthTimestamps[$securityLevel] = time();
3053 $session->set( 'AuthManager:lastAuthTimestamps', $lastAuthTimestamps );
3054 }
3055
3056 $session->persist();
3057 \Wikimedia\ScopedCallback::consume( $delay );
3058
3059 $this->getHookRunner()->onUserLoggedIn( $user );
3060 }
3061
3066 private function setDefaultUserOptions( User $user, $useContextLang ) {
3067 $user->setToken();
3068
3069 $lang = $useContextLang ? RequestContext::getMain()->getLanguage() : $this->contentLanguage;
3070 $this->userOptionsManager->setOption(
3071 $user,
3072 'language',
3073 $this->languageConverterFactory->getLanguageConverter( $lang )->getPreferredVariant()
3074 );
3075
3076 $contLangConverter = $this->languageConverterFactory->getLanguageConverter( $this->contentLanguage );
3077 if ( $contLangConverter->hasVariants() ) {
3078 $this->userOptionsManager->setOption(
3079 $user,
3080 'variant',
3081 $contLangConverter->getPreferredVariant()
3082 );
3083 }
3084 }
3085
3089 private function runVerifyHook(
3090 string $action,
3091 ?UserIdentity $user,
3092 AuthenticationResponse &$response,
3093 string $primaryId
3094 ): bool {
3095 $oldResponse = $response;
3096 $info = [
3097 'action' => $action,
3098 'primaryId' => $primaryId,
3099 ];
3100 $proceed = $this->getHookRunner()->onAuthManagerVerifyAuthentication( $user, $response, $this, $info );
3101 if ( !( $response instanceof AuthenticationResponse ) ) {
3102 throw new LogicException( '$response must be an AuthenticationResponse' );
3103 } elseif ( $proceed && $response !== $oldResponse ) {
3104 throw new LogicException(
3105 'AuthManagerVerifyAuthenticationHook must not modify the response unless it returns false' );
3106 } elseif ( !$proceed && $response->status !== AuthenticationResponse::FAIL ) {
3107 throw new LogicException(
3108 'AuthManagerVerifyAuthenticationHook must set the response to FAIL if it returns false' );
3109 }
3110 if ( !$proceed ) {
3111 $this->logger->info(
3112 $action . ' action for {user} from {clientIp} prevented by '
3113 . 'AuthManagerVerifyAuthentication hook: {reason}',
3114 [
3115 'user' => $user ? $user->getName() : '<null>',
3116 'reason' => $response->message->getKey(),
3117 'primaryId' => $primaryId,
3118 ] + $this->request->getSecurityLogContext( $user )
3119 );
3120 }
3121 return $proceed;
3122 }
3123
3129 private function callMethodOnProviders( $which, $method, array $args ) {
3130 $providers = [];
3131 if ( $which & self::CALL_PRE ) {
3132 $providers += $this->getPreAuthenticationProviders();
3133 }
3134 if ( $which & self::CALL_PRIMARY ) {
3135 $providers += $this->getPrimaryAuthenticationProviders();
3136 }
3137 if ( $which & self::CALL_SECONDARY ) {
3138 $providers += $this->getSecondaryAuthenticationProviders();
3139 }
3140 foreach ( $providers as $provider ) {
3141 $provider->$method( ...$args );
3142 }
3143 }
3144
3157 private function callLoginAuditHook(
3158 array $reqs,
3159 AuthenticationResponse $res,
3160 $user,
3161 array $options = []
3162 ) {
3163 if ( $user instanceof User ) {
3164 $guessUserName = $user->getName();
3165 } else {
3166 $guessUserName = $user;
3167 $user = null;
3168 }
3169
3170 $derivedOptions = [
3171 'performer' => $this->request->getSession()->getUser()
3172 ];
3173 $elevatedSecurityReq = AuthenticationRequest::getRequestByClass(
3174 $reqs, ElevatedSecurityAuthenticationRequest::class
3175 );
3176 // Check that the ElevatedSecurityAuthenticationRequest is valid before trusting it
3177 if ( $elevatedSecurityReq && $elevatedSecurityReq->validate()->isOK() ) {
3178 $derivedOptions['securityLevel'] = $elevatedSecurityReq->securityLevel;
3179 }
3180
3181 $hookOptions = $options + $derivedOptions;
3182 if ( array_key_exists( 'securityLevel', $options ) && $options['securityLevel'] === null ) {
3183 // Special case: if the caller explicitly sets 'securityLevel' to null, don't pass it to the hook
3184 unset( $hookOptions[ 'securityLevel' ] );
3185 }
3186 $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit( $res, $user, $guessUserName, $hookOptions );
3187 }
3188
3192 private function getHookContainer() {
3193 return $this->hookContainer;
3194 }
3195
3199 private function getHookRunner() {
3200 return $this->hookRunner;
3201 }
3202
3203 // endregion -- end of Internal methods
3204
3205}
3206
3207/*
3208 * This file uses VisualStudio style region/endregion fold markers which are
3209 * recognised by PHPStorm. If modelines are enabled, the following editor
3210 * configuration will also enable folding in vim, if it is in the last 5 lines
3211 * of the file. We also use "@name" which creates sections in Doxygen.
3212 *
3213 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
3214 */
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$wgLang
Definition Setup.php:566
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
AuthManager is the authentication system in MediaWiki and serves entry point for authentication.
canLinkAccounts()
Determine whether accounts can be linked.
const ACTION_UNLINK
Like ACTION_REMOVE but for linking providers only.
getPrimaryAuthenticationProviders()
Get the list of PrimaryAuthenticationProviders.
const string SEC_REAUTH
Security-sensitive operations should re-authenticate.
const ACTION_LOGIN_CONTINUE
Continue a login process that was interrupted by the need for user input or communication with an ext...
autoCreateUser(User $user, $source, $login=true, $log=true, ?Authority $performer=null, array $tags=[])
Auto-create an account and optionally log into that account.
setAuthenticationSessionData( $key, $data)
Store authentication in the current session.
getAuthenticationProvider( $id)
Get a provider by ID.
securitySensitiveOperationStatus( $operation)
Whether security-sensitive operations should proceed.
revokeAccessForUser( $username)
Revoke any authentication credentials for a user.
beginAccountLink(User $user, array $reqs, $returnToUrl)
Start an account linking flow.
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...
const AUTOCREATE_SOURCE_TEMP
Auto-creation is due to temporary account creation on page save.
continueAccountLink(array $reqs)
Continue an account linking flow.
setLogger(LoggerInterface $logger)
userExists( $username, $flags=IDBAccessObject::READ_NORMAL)
Determine whether a username exists.
allowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
beginAuthentication(array $reqs, $returnToUrl)
Start an authentication flow.
probablyCanCreateAccount(Authority $creator)
Check whether $creator can create accounts.
const string SEC_FAIL
Security-sensitive should not be performed.
getAuthenticationSessionData( $key, $default=null)
Fetch authentication data from the current session.
beginAccountCreation(Authority $creator, array $reqs, $returnToUrl)
Start an account creation flow.
setAuthEventsLogger(LoggerInterface $authEventsLogger)
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 @template T of Authen...
const AUTOCREATE_SOURCE_MAINT
Auto-creation is due to a Maintenance script.
continueAuthentication(array $reqs)
Continue an authentication flow.
setRequestContextUserFromSessionUser()
Call this method to set the request context user for the current request from the context session use...
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.
const ACTION_REMOVE
Remove a user's credentials.
const ACTION_LINK
Link an existing user to a third-party account.
authorizeCreateAccount(Authority $creator)
Authorize the account creation by $creator.
getAuthenticationRequests( $action, ?UserIdentity $user=null, array $options=[])
Return the applicable list of AuthenticationRequests.
const string SEC_OK
Security-sensitive operations are ok.
__construct(private readonly WebRequest $request, private readonly Config $config, private readonly ChangeTagsStore $changeTagsStore, private readonly ObjectFactory $objectFactory, private readonly ObjectCacheFactory $objectCacheFactory, private readonly HookContainer $hookContainer, private readonly ReadOnlyMode $readOnlyMode, private readonly UserNameUtils $userNameUtils, private readonly BlockManager $blockManager, private readonly WatchlistManager $watchlistManager, private readonly ILoadBalancer $loadBalancer, private readonly Language $contentLanguage, private readonly LanguageConverterFactory $languageConverterFactory, private readonly BotPasswordStore $botPasswordStore, private readonly UserFactory $userFactory, private readonly UserIdentityLookup $userIdentityLookup, private readonly UserIdentityUtils $identityUtils, private readonly UserOptionsManager $userOptionsManager, private readonly NotificationService $notificationService, private readonly SessionManagerInterface $sessionManager,)
const AUTOCREATE_SOURCE_SESSION
Auto-creation is due to SessionManager.
getPreAuthenticationProviders()
Get the list of PreAuthenticationProviders.
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
normalizeUsername( $username)
Provide normalized versions of the username for security checks.
continueAccountCreation(array $reqs)
Continue an account creation flow.
const ACTION_CREATE
Create a new user.
This is a value object for authentication requests.
This transfers state between the login and account creation flows.
Returned from account creation to allow for logging into the created account.
This represents additional user data requested on the account creation form.
A service class for checking blocks.
Read-write access to the change_tags table.
Group all the pieces relevant to the context of a request into one instance.
Defer callable updates to run later in the PHP process.
Class for handling updates to the site_stats table.
Handler class for MWExceptions.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
An interface for creating language converters.
Base class for language-specific code.
Definition Language.php:65
Class for creating new log entries and inserting them into the database.
A class containing constants representing the names of configuration variables.
Notify users about things occurring.
Factory for cache objects as configured in the ObjectCaches setting.
A StatusValue for permission errors.
Profiler base class that defines the interface and some shared functionality.
Definition Profiler.php:26
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
This serves as the entry point to the MediaWiki session handling system.
Parent class for all special pages.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
BotPassword interaction with databases.
A service class to control user options.
Service for temporary user creation.
Create User objects.
Convenience functions for interpreting UserIdentity objects using additional services or config.
UserNameUtils service.
User class for the MediaWiki software.
Definition User.php:130
getId( $wikiId=self::LOCAL)
Get the user's ID.
Definition User.php:1492
getUserPage()
Get this user's personal page title.
Definition User.php:2710
addToDatabase()
Add this existing user object to the database.
Definition User.php:2541
loadFromId( $flags=IDBAccessObject::READ_NORMAL)
Load user table data, given mId has already been set.
Definition User.php:486
setId( $v)
Set the user and reload all fields according to a given ID.
Definition User.php:1516
saveSettings()
Save this user's settings into the database.
Definition User.php:2335
isRegistered()
Get whether the user is registered.
Definition User.php:2091
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1525
Generic operation result class Has warning/error list, boolean status and arbitrary value.
getErrors()
Get the list of errors.
getMessages(?string $type=null)
Returns a list of error messages, optionally only those of the given type.
hasMessage(string $message)
Returns true if the specified message is present as a warning or error.
isOK()
Returns whether the operation completed.
merge( $other, $overwriteValue=false)
Merge another status object into this one.
error( $message,... $parameters)
Add an error, do not set fatal flag This can be used for non-fatal errors.
warning( $message,... $parameters)
Add a new warning.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
static newGood( $value=null)
Factory function for good results.
Determine whether a site is currently in read-only mode.
return[ 'config-schema-inverse'=>['default'=>['ConfigRegistry'=>['main'=> 'MediaWiki\\Config\\GlobalVarConfig::newInstance',], 'Sitename'=> 'MediaWiki', 'Server'=> false, 'CanonicalServer'=> false, 'ServerName'=> false, 'AssumeProxiesUseDefaultProtocolPorts'=> true, 'HttpsPort'=> 443, 'ForceHTTPS'=> false, 'ScriptPath'=> '/wiki', 'UsePathInfo'=> null, 'Script'=> false, 'LoadScript'=> false, 'RestPath'=> false, 'StylePath'=> false, 'LocalStylePath'=> false, 'ExtensionAssetsPath'=> false, 'ExtensionDirectory'=> null, 'StyleDirectory'=> null, 'ArticlePath'=> false, 'UploadPath'=> false, 'ImgAuthPath'=> false, 'ThumbPath'=> false, 'UploadDirectory'=> false, 'FileCacheDirectory'=> false, 'Logo'=> false, 'Logos'=> false, 'Favicon'=> '/favicon.ico', 'AppleTouchIcon'=> false, 'ReferrerPolicy'=> false, 'TmpDirectory'=> false, 'UploadBaseUrl'=> '', 'UploadStashScalerBaseUrl'=> false, 'ActionPaths'=>[], 'MainPageIsDomainRoot'=> false, 'EnableUploads'=> false, 'UploadStashMaxAge'=> 21600, 'EnableAsyncUploads'=> false, 'EnableAsyncUploadsByURL'=> false, 'UploadMaintenance'=> false, 'IllegalFileChars'=> ':\\/\\\\', 'DeletedDirectory'=> false, 'ImgAuthDetails'=> false, 'ImgAuthUrlPathMap'=>[], 'LocalFileRepo'=>['class'=> 'MediaWiki\\FileRepo\\LocalRepo', 'name'=> 'local', 'directory'=> null, 'scriptDirUrl'=> null, 'favicon'=> null, 'url'=> null, 'hashLevels'=> null, 'thumbScriptUrl'=> null, 'transformVia404'=> null, 'deletedDir'=> null, 'deletedHashLevels'=> null, 'updateCompatibleMetadata'=> null, 'reserializeMetadata'=> null,], 'ForeignFileRepos'=>[], 'UseInstantCommons'=> false, 'UseSharedUploads'=> false, 'SharedUploadDirectory'=> null, 'SharedUploadPath'=> null, 'HashedSharedUploadDirectory'=> true, 'RepositoryBaseUrl'=> 'https:'FetchCommonsDescriptions'=> false, 'SharedUploadDBname'=> false, 'SharedUploadDBprefix'=> '', 'CacheSharedUploads'=> true, 'ForeignUploadTargets'=>['local',], 'UploadDialog'=>['fields'=>['description'=> true, 'date'=> false, 'categories'=> false,], 'licensemessages'=>['local'=> 'generic-local', 'foreign'=> 'generic-foreign',], 'comment'=>['local'=> '', 'foreign'=> '',], 'format'=>['filepage'=> ' $DESCRIPTION', 'description'=> ' $TEXT', 'ownwork'=> '', 'license'=> '', 'uncategorized'=> '',],], 'FileBackends'=>[], 'LockManagers'=>[], 'ShowEXIF'=> null, 'UpdateCompatibleMetadata'=> false, 'AllowCopyUploads'=> false, 'CopyUploadsDomains'=>[], 'CopyUploadsFromSpecialUpload'=> false, 'CopyUploadProxy'=> false, 'CopyUploadTimeout'=> false, 'CopyUploadAllowOnWikiDomainConfig'=> false, 'MaxUploadSize'=> 104857600, 'MinUploadChunkSize'=> 1024, 'UploadNavigationUrl'=> false, 'UploadMissingFileUrl'=> false, 'ThumbnailScriptPath'=> false, 'SharedThumbnailScriptPath'=> false, 'HashedUploadDirectory'=> true, 'CSPUploadEntryPoint'=> true, 'FileExtensions'=>['png', 'gif', 'jpg', 'jpeg', 'webp',], 'ProhibitedFileExtensions'=>['html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht', 'php', 'phtml', 'php3', 'php4', 'php5', 'phps', 'phar', 'shtml', 'jhtml', 'pl', 'py', 'cgi', 'exe', 'scr', 'dll', 'msi', 'vbs', 'bat', 'com', 'pif', 'cmd', 'vxd', 'cpl', 'xml',], 'MimeTypeExclusions'=>['text/html', 'application/javascript', 'text/javascript', 'text/x-javascript', 'application/x-shellscript', 'application/x-php', 'text/x-php', 'text/x-python', 'text/x-perl', 'text/x-bash', 'text/x-sh', 'text/x-csh', 'text/scriptlet', 'application/x-msdownload', 'application/x-msmetafile', 'application/java', 'application/xml', 'text/xml',], 'CheckFileExtensions'=> true, 'StrictFileExtensions'=> true, 'DisableUploadScriptChecks'=> false, 'UploadSizeWarning'=> false, 'TrustedMediaFormats'=>['BITMAP', 'AUDIO', 'VIDEO', 'image/svg+xml', 'application/pdf',], 'MediaHandlers'=>[], 'NativeImageLazyLoading'=> false, 'ParserTestMediaHandlers'=>['image/jpeg'=> 'MockBitmapHandler', 'image/png'=> 'MockBitmapHandler', 'image/gif'=> 'MockBitmapHandler', 'image/tiff'=> 'MockBitmapHandler', 'image/webp'=> 'MockBitmapHandler', 'image/x-ms-bmp'=> 'MockBitmapHandler', 'image/x-bmp'=> 'MockBitmapHandler', 'image/x-xcf'=> 'MockBitmapHandler', 'image/svg+xml'=> 'MockSvgHandler', 'image/vnd.djvu'=> 'MockDjVuHandler',], 'UseImageResize'=> true, 'UseImageMagick'=> false, 'ImageMagickConvertCommand'=> '/usr/bin/convert', 'MaxInterlacingAreas'=>[], 'SharpenParameter'=> '0x0.4', 'SharpenReductionThreshold'=> 0.85, 'ImageMagickTempDir'=> false, 'CustomConvertCommand'=> false, 'JpegTran'=> '/usr/bin/jpegtran', 'JpegPixelFormat'=> 'yuv420', 'JpegQuality'=> 80, 'Exiv2Command'=> '/usr/bin/exiv2', 'Exiftool'=> '/usr/bin/exiftool', 'SVGConverters'=>['ImageMagick'=> ' $path/convert -background "#ffffff00" -thumbnail $widthx$height\\! $input PNG:$output', 'inkscape'=> ' $path/inkscape -w $width -o $output $input', 'batik'=> 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', 'rsvg'=> ' $path/rsvg-convert -w $width -h $height -o $output $input', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> true, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, 'TiffThumbnailType'=>[], 'ThumbnailEpoch'=> '20030516000000', 'AttemptFailureEpoch'=> 1, 'IgnoreImageErrors'=> false, 'GenerateThumbnailOnParse'=> true, 'ShowArchiveThumbnails'=> true, 'EnableAutoRotation'=> null, 'Antivirus'=> null, 'AntivirusSetup'=>['clamav'=>['command'=> 'clamscan --no-summary ', 'codemap'=>[0=> 0, 1=> 1, 52=> -1, ' *'=> false,], 'messagepattern'=> '/.*?:(.*)/sim',],], 'AntivirusRequired'=> true, 'VerifyMimeType'=> true, 'MimeTypeFile'=> 'internal', 'MimeInfoFile'=> 'internal', 'MimeDetectorCommand'=> null, 'TrivialMimeDetection'=> false, 'XMLMimeTypes'=>['http:'svg'=> 'image/svg+xml', 'http:'http:'html'=> 'text/html',], 'ImageLimits'=>[[320, 240,], [640, 480,], [800, 600,], [1024, 768,], [1280, 1024,], [2560, 2048,],], 'ThumbLimits'=>[120, 150, 180, 200, 220, 250, 300, 400,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> false, 'TrackMediaRequestProvenance'=> false, 'DjvuUseBoxedCommand'=> false, 'DjvuDump'=> null, 'DjvuRenderer'=> null, 'DjvuTxt'=> null, 'DjvuPostProcessor'=> 'pnmtojpeg', 'DjvuOutputExtension'=> 'jpg', 'EmergencyContact'=> false, 'PasswordSender'=> false, 'NoReplyAddress'=> false, 'EnableEmail'=> true, 'EnableUserEmail'=> true, 'UserEmailUseReplyTo'=> true, 'PasswordReminderResendTime'=> 24, 'NewPasswordExpiry'=> 604800, 'UserEmailConfirmationTokenExpiry'=> 604800, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, 'EmailConfirmationBanner'=> false, 'EnotifWatchlist'=> false, 'EnotifUserTalk'=> false, 'EnotifRevealEditorAddress'=> false, 'EnotifMinorEdits'=> true, 'EnotifUseRealName'=> false, 'UsersNotifiedOnAllChanges'=>[], 'DBname'=> 'my_wiki', 'DBmwschema'=> null, 'DBprefix'=> '', 'DBserver'=> 'localhost', 'DBport'=> 5432, 'DBuser'=> 'wikiuser', 'DBpassword'=> '', 'DBtype'=> 'mysql', 'DBssl'=> false, 'DBcompress'=> false, 'DBStrictWarnings'=> false, 'DBadminuser'=> null, 'DBadminpassword'=> null, 'SearchType'=> null, 'SearchTypeAlternatives'=> null, 'DBTableOptions'=> 'ENGINE=InnoDB, DEFAULT CHARSET=binary', 'SQLMode'=> '', 'SQLiteDataDir'=> '', 'SharedDB'=> null, 'SharedPrefix'=> false, 'SharedTables'=>['user', 'user_properties', 'user_autocreate_serial',], 'SharedSchema'=> false, 'DBservers'=> false, 'LBFactoryConf'=>['class'=> 'Wikimedia\\Rdbms\\LBFactorySimple',], 'DataCenterUpdateStickTTL'=> 10, 'DBerrorLog'=> false, 'DBerrorLogTZ'=> false, 'LocalDatabases'=>[], 'DatabaseReplicaLagWarning'=> 10, 'DatabaseReplicaLagCritical'=> 30, 'MaxExecutionTimeForExpensiveQueries'=> 0, 'VirtualDomainsMapping'=>[], 'FileSchemaMigrationStage'=> 3, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'CodeHighlighter',],], 'text'=> 'MediaWiki\\Content\\TextContentHandler', 'unknown'=> 'MediaWiki\\Content\\FallbackContentHandler',], 'NamespaceContentModels'=>[], 'TextModelsToParse'=>['wikitext', 'javascript', 'css',], 'CompressRevisions'=> false, 'ExternalStores'=>[], 'ExternalServers'=>[], 'DefaultExternalStore'=> false, 'RevisionCacheExpiry'=> 604800, 'PageLanguageUseDB'=> false, 'DiffEngine'=> null, 'ExternalDiffEngine'=> false, 'Wikidiff2Options'=>[], 'RequestTimeLimit'=> null, 'TransactionalTimeLimit'=> 120, 'CriticalSectionTimeLimit'=> 180.0, 'MiserMode'=> false, 'DisableQueryPages'=> false, 'QueryCacheLimit'=> 1000, 'WantedPagesThreshold'=> 1, 'AllowSlowParserFunctions'=> false, 'AllowSchemaUpdates'=> true, 'MaxArticleSize'=> 2048, 'MemoryLimit'=> '50M', 'PoolCounterConf'=> null, 'PoolCountClientConf'=>['servers'=>['127.0.0.1',], 'timeout'=> 0.1,], 'MaxUserDBWriteDuration'=> false, 'MaxJobDBWriteDuration'=> false, 'LinkHolderBatchSize'=> 1000, 'MaximumMovedPages'=> 100, 'ForceDeferredUpdatesPreSend'=> false, 'MultiShardSiteStats'=> false, 'CacheDirectory'=> false, 'MainCacheType'=> 0, 'MessageCacheType'=> -1, 'ParserCacheType'=> -1, 'SessionCacheType'=> -1, 'AnonSessionCacheType'=> false, 'LanguageConverterCacheType'=> -1, 'ObjectCaches'=>[0=>['class'=> 'Wikimedia\\ObjectCache\\EmptyBagOStuff', 'reportDupes'=> false,], 1=>['class'=> 'MediaWiki\\ObjectCache\\SqlBagOStuff', 'loggroup'=> 'SQLBagOStuff',], 'memcached-php'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPhpBagOStuff', 'loggroup'=> 'memcached',], 'memcached-pecl'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPeclBagOStuff', 'loggroup'=> 'memcached',], 'hash'=>['class'=> 'Wikimedia\\ObjectCache\\HashBagOStuff', 'reportDupes'=> false,], 'apc'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,], 'apcu'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,],], 'WANObjectCache'=>[], 'MicroStashType'=> -1, 'MainStash'=> 1, 'ParsoidCacheConfig'=>['StashType'=> null, 'StashDuration'=> 86400, 'WarmParsoidParserCache'=> false,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'ParserCacheFilterConfig'=>['pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'PHPSessionHandling'=> 'warn', 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, 'JwtSessionCookieIssuer'=> null, 'MemCachedServers'=>['127.0.0.1:11211',], 'MemCachedPersistent'=> false, 'MemCachedTimeout'=> 500000, 'UseLocalMessageCache'=> false, 'AdaptiveMessageCache'=> false, 'LocalisationCacheConf'=>['class'=> 'MediaWiki\\Language\\LocalisationCache', 'store'=> 'detect', 'storeClass'=> false, 'storeDirectory'=> false, 'storeServer'=>[], 'forceRecache'=> false, 'manualRecache'=> false,], 'CachePages'=> true, 'CacheEpoch'=> '20030516000000', 'GitInfoCacheDirectory'=> false, 'UseFileCache'=> false, 'FileCacheDepth'=> 2, 'RenderHashAppend'=> '', 'EnableSidebarCache'=> false, 'SidebarCacheExpiry'=> 86400, 'UseGzip'=> false, 'InvalidateCacheOnLocalSettingsChange'=> true, 'ExtensionInfoMTime'=> false, 'EnableRemoteBagOStuffTests'=> false, 'UseCdn'=> false, 'VaryOnXFP'=> false, 'InternalServer'=> false, 'CdnMaxAge'=> 18000, 'CdnMaxageLagged'=> 30, 'CdnMaxageStale'=> 10, 'CdnReboundPurgeDelay'=> 0, 'CdnMaxageSubstitute'=> 60, 'ForcedRawSMaxage'=> 300, 'CdnServers'=>[], 'CdnServersNoPurge'=>[], 'HTCPRouting'=>[], 'HTCPMulticastTTL'=> 1, 'UsePrivateIPs'=> false, 'CdnMatchParameterOrder'=> true, 'LanguageCode'=> 'en', 'GrammarForms'=>[], 'InterwikiMagic'=> true, 'HideInterlanguageLinks'=> false, 'ExtraInterlanguageLinkPrefixes'=>[], 'InterlanguageLinkCodeMap'=>[], 'ExtraLanguageNames'=>[], 'ExtraLanguageCodes'=>['bh'=> 'bho', 'no'=> 'nb', 'simple'=> 'en',], 'DummyLanguageCodes'=>[], 'AllUnicodeFixes'=> false, 'LegacyEncoding'=> false, 'AmericanDates'=> false, 'TranslateNumerals'=> true, 'UseDatabaseMessages'=> true, 'MaxMsgCacheEntrySize'=> 10000, 'DisableLangConversion'=> false, 'DisableTitleConversion'=> false, 'DefaultLanguageVariant'=> false, 'UsePigLatinVariant'=> false, 'DisabledVariants'=>[], 'VariantArticlePath'=> false, 'UseXssLanguage'=> false, 'LoginLanguageSelector'=> false, 'ForceUIMsgAsContentMsg'=>[], 'RawHtmlMessages'=>[], 'Localtimezone'=> null, 'LocalTZoffset'=> null, 'OverrideUcfirstCharacters'=>[], 'MimeType'=> 'text/html', 'Html5Version'=> null, 'EditSubmitButtonLabelPublish'=> false, 'XhtmlNamespaces'=>[], 'SiteNotice'=> '', 'BrowserFormatDetection'=> 'telephone=no', 'SkinMetaTags'=>[], 'DefaultSkin'=> 'vector-2022', 'FallbackSkin'=> 'fallback', 'SkipSkins'=>[], 'DisableOutputCompression'=> false, 'FragmentMode'=>['html5', 'legacy',], 'ExternalInterwikiFragmentMode'=> 'legacy', 'FooterIcons'=>['copyright'=>['copyright'=>[],], 'poweredby'=>['mediawiki'=>['src'=> null, 'url'=> 'https:'alt'=> 'Powered by MediaWiki', 'lang'=> 'en',],],], 'UseCombinedLoginLink'=> false, 'Edititis'=> false, 'Send404Code'=> true, 'ShowRollbackEditCount'=> 10, 'EnableCanonicalServerLink'=> false, 'InterwikiLogoOverride'=>[], 'ResourceModules'=>[], 'ResourceModuleSkinStyles'=>[], 'ResourceLoaderSources'=>[], 'ResourceBasePath'=> null, 'ResourceLoaderMaxage'=>[], 'ResourceLoaderDebug'=> false, 'ResourceLoaderMaxQueryLength'=> false, 'ResourceLoaderValidateJS'=> true, 'ResourceLoaderEnableJSProfiler'=> false, 'ResourceLoaderStorageEnabled'=> true, 'ResourceLoaderStorageVersion'=> 1, 'ResourceLoaderEnableSourceMapLinks'=> true, 'AllowSiteCSSOnRestrictedPages'=> false, 'VueDevelopmentMode'=> false, 'CodexDevelopmentDir'=> null, 'MetaNamespace'=> false, 'MetaNamespaceTalk'=> false, 'CanonicalNamespaceNames'=>[-2=> 'Media', -1=> 'Special', 0=> '', 1=> 'Talk', 2=> 'User', 3=> 'User_talk', 4=> 'Project', 5=> 'Project_talk', 6=> 'File', 7=> 'File_talk', 8=> 'MediaWiki', 9=> 'MediaWiki_talk', 10=> 'Template', 11=> 'Template_talk', 12=> 'Help', 13=> 'Help_talk', 14=> 'Category', 15=> 'Category_talk',], 'ExtraNamespaces'=>[], 'ExtraGenderNamespaces'=>[], 'NamespaceAliases'=>[], 'LegalTitleChars'=> ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', 'CapitalLinks' => true, 'CapitalLinkOverrides' => [ ], 'NamespacesWithSubpages' => [ 1 => true, 2 => true, 3 => true, 4 => true, 5 => true, 7 => true, 8 => true, 9 => true, 10 => true, 11 => true, 12 => true, 13 => true, 15 => true, ], 'NamespacesWithoutAutoSummaries' => [ ], 'ContentNamespaces' => [ 0, ], 'ShortPagesNamespaceExclusions' => [ ], 'ExtraSignatureNamespaces' => [ ], 'InvalidRedirectTargets' => [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect', 'Mylog', ], 'DisableHardRedirects' => false, 'FixDoubleRedirects' => false, 'LocalInterwikis' => [ ], 'InterwikiExpiry' => 10800, 'InterwikiCache' => false, 'InterwikiScopes' => 3, 'InterwikiFallbackSite' => 'wiki', 'RedirectSources' => false, 'SiteTypes' => [ 'mediawiki' => 'MediaWiki\\Site\\MediaWikiSite', ], 'MaxTocLevel' => 999, 'MaxPPNodeCount' => 1000000, 'MaxTemplateDepth' => 100, 'MaxPPExpandDepth' => 100, 'UrlProtocols' => [ 'bitcoin:', 'ftp: 'ftps: 'geo:', 'git: 'gopher: 'http: 'https: 'irc: 'ircs: 'magnet:', 'mailto:', 'matrix:', 'mms: 'news:', 'nntp: 'redis: 'sftp: 'sip:', 'sips:', 'sms:', 'ssh: 'svn: 'tel:', 'telnet: 'urn:', 'wikipedia: 'worldwind: 'xmpp:', ' ], 'CleanSignatures' => true, 'AllowExternalImages' => false, 'AllowExternalImagesFrom' => '', 'EnableImageWhitelist' => false, 'TidyConfig' => [ ], 'ParsoidSettings' => [ 'useSelser' => true, ], 'ParsoidExperimentalParserFunctionOutput' => false, 'RawHtml' => false, 'ExternalLinkTarget' => false, 'NoFollowLinks' => true, 'NoFollowNsExceptions' => [ ], 'NoFollowDomainExceptions' => [ 'mediawiki.org', ], 'RegisterInternalExternals' => false, 'ExternalLinksIgnoreDomains' => [ ], 'AllowDisplayTitle' => true, 'RestrictDisplayTitle' => true, 'ExpensiveParserFunctionLimit' => 100, 'PreprocessorCacheThreshold' => 1000, 'EnableScaryTranscluding' => false, 'TranscludeCacheExpiry' => 3600, 'EnableMagicLinks' => [ 'ISBN' => false, 'PMID' => false, 'RFC' => false, ], 'ParserEnableUserLanguage' => false, 'ArticleCountMethod' => 'link', 'ActiveUserDays' => 30, 'LearnerEdits' => 10, 'LearnerMemberSince' => 4, 'ExperiencedUserEdits' => 500, 'ExperiencedUserMemberSince' => 30, 'ManualRevertSearchRadius' => 15, 'RevertedTagMaxDepth' => 15, 'CentralIdLookupProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\CentralId\\LocalIdLookup', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', 'HideUserUtils', ], ], ], 'CentralIdLookupProvider' => 'local', 'UserRegistrationProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\Registration\\LocalUserRegistrationProvider', 'services' => [ 'ConnectionProvider', ], ], ], 'PasswordPolicy' => [ 'policies' => [ 'bureaucrat' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'sysop' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'interface-admin' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'bot' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'default' => [ 'MinimalPasswordLength' => [ 'value' => 8, 'suggestChangeOnLogin' => true, ], 'PasswordCannotBeSubstringInUsername' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'PasswordCannotMatchDefaults' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'MaximalPasswordLength' => [ 'value' => 4096, 'suggestChangeOnLogin' => true, ], 'PasswordNotInCommonList' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], ], ], 'checks' => [ 'MinimalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimalPasswordLength', ], 'MinimumPasswordLengthToLogin' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimumPasswordLengthToLogin', ], 'PasswordCannotBeSubstringInUsername' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotBeSubstringInUsername', ], 'PasswordCannotMatchDefaults' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotMatchDefaults', ], 'MaximalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMaximalPasswordLength', ], 'PasswordNotInCommonList' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordNotInCommonList', ], ], ], 'AuthManagerConfig' => null, 'AuthManagerAutoConfig' => [ 'preauth' => [ 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider', 'services' => [ 'ConnectionProvider', 'UserFactory', ], 'sort' => 0, ], ], 'primaryauth' => [ 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', 'UserOptionsLookup', ], 'args' => [ [ 'authoritative' => false, ], ], 'sort' => 0, ], 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'args' => [ [ 'authoritative' => true, ], ], 'sort' => 100, ], ], 'secondaryauth' => [ 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider', 'sort' => 100, ], 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'sort' => 200, ], ], ], 'RememberMe' => 'choose', 'ReauthenticateTime' => [ 'default' => 3600, ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'default' => true, ], 'ChangeCredentialsBlacklist' => [ 'MediaWiki\\Auth\\TemporaryPasswordAuthenticationRequest', ], 'RemoveCredentialsBlacklist' => [ 'MediaWiki\\Auth\\PasswordAuthenticationRequest', ], 'InvalidPasswordReset' => true, 'PasswordDefault' => 'pbkdf2', 'PasswordConfig' => [ 'A' => [ 'class' => 'MediaWiki\\Password\\MWOldPassword', ], 'B' => [ 'class' => 'MediaWiki\\Password\\MWSaltedPassword', ], 'pbkdf2-legacyA' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'A', 'pbkdf2', ], ], 'pbkdf2-legacyB' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'B', 'pbkdf2', ], ], 'bcrypt' => [ 'class' => 'MediaWiki\\Password\\BcryptPassword', 'cost' => 9, ], 'pbkdf2' => [ 'class' => 'MediaWiki\\Password\\Pbkdf2PasswordUsingOpenSSL', 'algo' => 'sha512', 'cost' => '30000', 'length' => '64', ], 'argon2' => [ 'class' => 'MediaWiki\\Password\\Argon2Password', 'algo' => 'auto', ], ], 'PasswordResetRoutes' => [ 'username' => true, 'email' => true, ], 'MaxSigChars' => 255, 'SignatureValidation' => 'warning', 'SignatureAllowedLintErrors' => [ 'obsolete-tag', ], 'MaxNameChars' => 255, 'ReservedUsernames' => [ 'MediaWiki default', 'Conversion script', 'Maintenance script', 'Template namespace initialisation script', 'ScriptImporter', 'Delete page script', 'Move page script', 'Command line script', 'Unknown user', 'msg:double-redirect-fixer', 'msg:usermessage-editor', 'msg:proxyblocker', 'msg:sorbs', 'msg:spambot_username', 'msg:autochange-username', ], 'DefaultUserOptions' => [ 'ccmeonemails' => 0, 'date' => 'default', 'diffonly' => 0, 'diff-type' => 'table', 'disablemail' => 0, 'editfont' => 'monospace', 'editondblclick' => 0, 'editrecovery' => 0, 'editsectiononrightclick' => 0, 'email-allow-new-users' => 1, 'enotifminoredits' => 0, 'enotifrevealaddr' => 0, 'enotifusertalkpages' => 1, 'enotifwatchlistpages' => 1, 'extendwatchlist' => 1, 'fancysig' => 0, 'forceeditsummary' => 0, 'forcesafemode' => 0, 'gender' => 'unknown', 'hidecategorization' => 1, 'hideminor' => 0, 'hidepatrolled' => 0, 'imagesize' => 2, 'minordefault' => 0, 'newpageshidepatrolled' => 0, 'nickname' => '', 'norollbackdiff' => 0, 'prefershttps' => 1, 'previewonfirst' => 0, 'previewontop' => 1, 'pst-cssjs' => 1, 'rcdays' => 7, 'rcenhancedfilters-disable' => 0, 'rclimit' => 50, 'requireemail' => 0, 'search-match-redirect' => true, 'search-special-page' => 'Search', 'search-thumbnail-extra-namespaces' => true, 'searchlimit' => 20, 'showhiddencats' => 0, 'shownumberswatching' => 1, 'showrollbackconfirmation' => 0, 'skin' => false, 'skin-responsive' => 1, 'thumbsize' => 5, 'underline' => 2, 'useeditwarning' => 1, 'uselivepreview' => 0, 'usenewrc' => 1, 'watchcreations' => 1, 'watchcreations-expiry' => 'infinite', 'watchdefault' => 1, 'watchdefault-expiry' => 'infinite', 'watchdeletion' => 0, 'watchlistdays' => 7, 'watchlisthideanons' => 0, 'watchlisthidebots' => 0, 'watchlisthidecategorization' => 1, 'watchlisthideliu' => 0, 'watchlisthideminor' => 0, 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, 'watchlistreloadautomatically' => 0, 'watchlistunwatchlinks' => 0, 'watchmoves' => 0, 'watchrollback' => 0, 'watchuploads' => 1, 'watchrollback-expiry' => 'infinite', 'watchstar-expiry' => 'infinite', 'wlenhancedfilters-disable' => 0, 'wllimit' => 250, ], 'ConditionalUserOptions' => [ ], 'HiddenPrefs' => [ ], 'UserJsPrefLimit' => 100, 'InvalidUsernameCharacters' => '@:>=', 'UserrightsInterwikiDelimiter' => '@', 'SecureLogin' => false, 'AuthenticationTokenVersion' => null, 'SessionProviders' => [ 'MediaWiki\\Session\\CookieSessionProvider' => [ 'class' => 'MediaWiki\\Session\\CookieSessionProvider', 'args' => [ [ 'priority' => 30, ], ], 'services' => [ 'JwtCodec', 'UrlUtils', ], ], 'MediaWiki\\Session\\BotPasswordSessionProvider' => [ 'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider', 'args' => [ [ 'priority' => 75, ], ], 'services' => [ 'GrantsInfo', ], ], ], 'AutoCreateTempUser' => [ 'known' => false, 'enabled' => false, 'actions' => [ 'edit', ], 'genPattern' => '~$1', 'matchPattern' => null, 'reservedPattern' => '~$1', 'serialProvider' => [ 'type' => 'local', 'useYear' => true, ], 'serialMapping' => [ 'type' => 'readable-numeric', ], 'expireAfterDays' => 90, 'notifyBeforeExpirationDays' => 10, ], 'AutoblockExemptions' => [ ], 'AutoblockExpiry' => 86400, 'BlockAllowsUTEdit' => true, 'BlockCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 19, ], 'BlockDisablesLogin' => false, 'EnableMultiBlocks' => false, 'WhitelistRead' => false, 'WhitelistReadRegexp' => false, 'EmailConfirmToEdit' => false, 'HideIdentifiableRedirects' => true, 'GroupPermissions' => [ '*' => [ 'createaccount' => true, 'autocreateaccount' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'viewmyprivateinfo' => true, 'editmyprivateinfo' => true, 'editmyoptions' => true, ], 'user' => [ 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'movefile' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'minoredit' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, 'editmyuserjsredirect' => true, 'sendemail' => true, 'applychangetags' => true, 'changetags' => true, 'viewmywatchlist' => true, 'editmywatchlist' => true, 'createwithcontentmodel' => true, 'logout' => true, ], 'autoconfirmed' => [ 'autoconfirmed' => true, 'editsemiprotected' => true, ], 'bot' => [ 'bot' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'nominornewtalk' => true, 'autopatrol' => true, 'suppressredirect' => true, 'apihighlimits' => true, ], 'sysop' => [ 'block' => true, 'createaccount' => true, 'createpreviouslyrenamedaccount' => true, 'delete' => true, 'bigdelete' => true, 'deletedhistory' => true, 'deletedtext' => true, 'undelete' => true, 'editcontentmodel' => true, 'editinterface' => true, 'editsitejson' => true, 'edituserjson' => true, 'import' => true, 'importupload' => true, 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'patrol' => true, 'autopatrol' => true, 'protect' => true, 'editprotected' => true, 'rollback' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'unwatchedpages' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'blockemail' => true, 'markbotedits' => true, 'apihighlimits' => true, 'browsearchive' => true, 'noratelimit' => true, 'movefile' => true, 'unblockself' => true, 'suppressredirect' => true, 'mergehistory' => true, 'managechangetags' => true, 'deletechangetags' => true, ], 'interface-admin' => [ 'editinterface' => true, 'editsitecss' => true, 'editsitejson' => true, 'editsitejs' => true, 'editusercss' => true, 'edituserjson' => true, 'edituserjs' => true, ], 'bureaucrat' => [ 'userrights' => true, 'noratelimit' => true, 'renameuser' => true, ], 'suppress' => [ 'hideuser' => true, 'suppressrevision' => true, 'viewsuppressed' => true, 'suppressionlog' => true, 'deleterevision' => true, 'deletelogentry' => true, ], ], 'PrivilegedGroups' => [ 'bureaucrat', 'interface-admin', 'suppress', 'sysop', ], 'RevokePermissions' => [ ], 'GroupInheritsPermissions' => [ ], 'ImplicitGroups' => [ '*', 'user', 'autoconfirmed', ], 'GroupsAddToSelf' => [ ], 'GroupsRemoveFromSelf' => [ ], 'RestrictedGroups' => [ ], 'UserRequirementsPrivateConditions' => [ ], 'RestrictionTypes' => [ 'create', 'edit', 'move', 'upload', ], 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', ], 'CascadingRestrictionLevels' => [ 'sysop', ], 'SemiprotectedRestrictionLevels' => [ 'autoconfirmed', ], 'NamespaceProtection' => [ ], 'NonincludableNamespaces' => [ ], 'AutoConfirmAge' => 0, 'AutoConfirmCount' => 0, 'Autopromote' => [ 'autoconfirmed' => [ '&', [ 1, null, ], [ 2, null, ], ], ], 'AutopromoteOnce' => [ 'onEdit' => [ ], ], 'AutopromoteOnceLogInRC' => true, 'AutopromoteOnceRCExcludedGroups' => [ ], 'AddGroups' => [ ], 'RemoveGroups' => [ ], 'AvailableRights' => [ ], 'ImplicitRights' => [ ], 'DeleteRevisionsLimit' => 0, 'DeleteRevisionsBatchSize' => 1000, 'HideUserContribLimit' => 1000, 'AccountCreationThrottle' => [ [ 'count' => 0, 'seconds' => 86400, ], ], 'TempAccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 600, ], [ 'count' => 6, 'seconds' => 86400, ], ], 'TempAccountNameAcquisitionThrottle' => [ [ 'count' => 60, 'seconds' => 86400, ], ], 'SpamRegex' => [ ], 'SummarySpamRegex' => [ ], 'EnableDnsBlacklist' => false, 'DnsBlacklistUrls' => [ ], 'ProxyList' => [ ], 'ProxyWhitelist' => [ ], 'SoftBlockRanges' => [ ], 'ApplyIpBlocksToXff' => false, 'RateLimits' => [ 'edit' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], 'user' => [ 90, 60, ], ], 'move' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], 'upload' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'rollback' => [ 'user' => [ 10, 60, ], 'newbie' => [ 5, 120, ], ], 'mailpassword' => [ 'ip' => [ 5, 3600, ], ], 'sendemail' => [ 'ip' => [ 5, 86400, ], 'newbie' => [ 5, 86400, ], 'user' => [ 20, 86400, ], ], 'changeemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'confirmemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'purge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'linkpurge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'renderfile' => [ 'ip' => [ 700, 30, ], 'user' => [ 700, 30, ], ], 'renderfile-nonstandard' => [ 'ip' => [ 70, 30, ], 'user' => [ 70, 30, ], ], 'stashedit' => [ 'ip' => [ 30, 60, ], 'newbie' => [ 30, 60, ], ], 'stashbasehtml' => [ 'ip' => [ 5, 60, ], 'newbie' => [ 5, 60, ], ], 'changetags' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'editcontentmodel' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], ], 'RateLimitsExcludedIPs' => [ ], 'PutIPinRC' => true, 'QueryPageDefaultLimit' => 50, 'ExternalQuerySources' => [ ], 'PasswordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300, ], [ 'count' => 150, 'seconds' => 172800, ], ], 'GrantPermissions' => [ 'basic' => [ 'autocreateaccount' => true, 'autoconfirmed' => true, 'autopatrol' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'nominornewtalk' => true, 'patrolmarks' => true, 'read' => true, 'unwatchedpages' => true, ], 'highvolume' => [ 'bot' => true, 'apihighlimits' => true, 'noratelimit' => true, 'markbotedits' => true, ], 'import' => [ 'import' => true, 'importupload' => true, ], 'editpage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, 'editusercss' => true, 'edituserjs' => true, 'editsitecss' => true, 'editsitejs' => true, ], 'createeditmovepage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'createpage' => true, 'createtalk' => true, 'delete-redirect' => true, 'move' => true, 'move-rootuserpages' => true, 'move-subpages' => true, 'move-categorypages' => true, 'suppressredirect' => true, ], 'uploadfile' => [ 'upload' => true, 'reupload-own' => true, ], 'uploadeditmovefile' => [ 'upload' => true, 'reupload-own' => true, 'reupload' => true, 'reupload-shared' => true, 'upload_by_url' => true, 'movefile' => true, 'suppressredirect' => true, ], 'patrol' => [ 'patrol' => true, ], 'rollback' => [ 'rollback' => true, ], 'blockusers' => [ 'block' => true, 'blockemail' => true, ], 'viewdeleted' => [ 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, ], 'viewrestrictedlogs' => [ 'suppressionlog' => true, ], 'delete' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, 'delete' => true, 'bigdelete' => true, 'deletelogentry' => true, 'deleterevision' => true, 'undelete' => true, ], 'oversight' => [ 'suppressrevision' => true, 'viewsuppressed' => true, ], 'protect' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => true, ], 'managesessions' => [ 'logout' => true, ], ], 'GrantPermissionGroups' => [ 'basic' => 'hidden', 'editpage' => 'page-interaction', 'createeditmovepage' => 'page-interaction', 'editprotected' => 'page-interaction', 'patrol' => 'page-interaction', 'uploadfile' => 'file-interaction', 'uploadeditmovefile' => 'file-interaction', 'sendemail' => 'email', 'viewmywatchlist' => 'watchlist-interaction', 'editviewmywatchlist' => 'watchlist-interaction', 'editmycssjs' => 'customization', 'editmyoptions' => 'customization', 'editinterface' => 'administration', 'editsiteconfig' => 'administration', 'rollback' => 'administration', 'blockusers' => 'administration', 'delete' => 'administration', 'viewdeleted' => 'administration', 'viewrestrictedlogs' => 'administration', 'protect' => 'administration', 'oversight' => 'administration', 'createaccount' => 'administration', 'mergehistory' => 'administration', 'import' => 'administration', 'highvolume' => 'high-volume', 'privateinfo' => 'private-information', 'managesessions' => 'private-information', ], 'GrantRiskGroups' => [ 'basic' => 'low', 'editpage' => 'low', 'createeditmovepage' => 'low', 'editprotected' => 'vandalism', 'patrol' => 'low', 'uploadfile' => 'low', 'uploadeditmovefile' => 'low', 'sendemail' => 'security', 'viewmywatchlist' => 'low', 'editviewmywatchlist' => 'low', 'editmycssjs' => 'security', 'editmyoptions' => 'security', 'editinterface' => 'vandalism', 'editsiteconfig' => 'security', 'rollback' => 'low', 'blockusers' => 'vandalism', 'delete' => 'vandalism', 'viewdeleted' => 'vandalism', 'viewrestrictedlogs' => 'security', 'protect' => 'vandalism', 'oversight' => 'security', 'createaccount' => 'low', 'mergehistory' => 'vandalism', 'import' => 'security', 'highvolume' => 'low', 'privateinfo' => 'low', ], 'EnableBotPasswords' => true, 'BotPasswordsCluster' => false, 'BotPasswordsDatabase' => false, 'BotPasswordsLimit' => 100, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, 'AllowUserCss' => false, 'AllowUserCssPrefs' => true, 'UseSiteJs' => true, 'UseSiteCss' => true, 'BreakFrames' => false, 'EditPageFrameOptions' => 'DENY', 'ApiFrameOptions' => 'DENY', 'CSPHeader' => false, 'CSPReportOnlyHeader' => false, 'CSPUseReportURIDirective' => false, 'CSPFalsePositiveUrls' => [ 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'chrome-extension' => true, ], 'AllowCrossOrigin' => false, 'RestAllowCrossOriginCookieAuth' => false, 'SessionSecret' => false, 'CookieExpiration' => 2592000, 'ExtendedLoginCookieExpiration' => 15552000, 'SessionCookieJwtExpiration' => 14400, 'CookieDomain' => '', 'CookiePath' => '/', 'CookieSecure' => 'detect', 'CookiePrefix' => false, 'CookieHttpOnly' => true, 'CookieSameSite' => null, 'CacheVaryCookies' => [ ], 'SessionName' => false, 'CookieSetOnAutoblock' => true, 'CookieSetOnIpBlock' => true, 'DebugLogFile' => '', 'DebugLogPrefix' => '', 'DebugRedirects' => false, 'DebugRawPage' => false, 'DebugComments' => false, 'DebugDumpSql' => false, 'TrxProfilerLimits' => [ 'GET' => [ 'masterConns' => 0, 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'POST-nonwrite' => [ 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'PostSend-GET' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 10000, 'maxAffected' => 1000, 'masterConns' => 0, 'writes' => 0, ], 'PostSend-POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'JobRunner' => [ 'readQueryTime' => 30, 'writeQueryTime' => 5, 'readQueryRows' => 100000, 'maxAffected' => 500, ], 'Maintenance' => [ 'writeQueryTime' => 5, 'maxAffected' => 1000, ], ], 'DebugLogGroups' => [ ], 'MWLoggerDefaultSpi' => [ 'class' => 'MediaWiki\\Logger\\LegacySpi', ], 'ShowDebug' => false, 'SpecialVersionShowHooks' => false, 'ShowExceptionDetails' => false, 'LogExceptionBacktrace' => true, 'PropagateErrors' => true, 'ShowHostnames' => false, 'OverrideHostname' => false, 'DevelopmentWarnings' => false, 'DeprecationReleaseLimit' => false, 'Profiler' => [ ], 'StatsdServer' => false, 'StatsdMetricPrefix' => 'MediaWiki', 'StatsTarget' => null, 'StatsFormat' => null, 'StatsPrefix' => 'mediawiki', 'OpenTelemetryConfig' => null, 'PageInfoTransclusionLimit' => 50, 'EnableJavaScriptTest' => false, 'CachePrefix' => false, 'DebugToolbar' => false, 'ApiClientErrorSampleRate' => 1.0, 'DisableTextSearch' => false, 'AdvancedSearchHighlighting' => false, 'SearchHighlightBoundaries' => '[\\p{Z}\\p{P}\\p{C}]', 'OpenSearchTemplates' => [ 'application/x-suggestions+json' => false, 'application/x-suggestions+xml' => false, ], 'OpenSearchDefaultLimit' => 10, 'OpenSearchDescriptionLength' => 100, 'SearchSuggestCacheExpiry' => 1200, 'DisableSearchUpdate' => false, 'NamespacesToBeSearchedDefault' => [ true, ], 'DisableInternalSearch' => false, 'SearchForwardUrl' => null, 'SitemapNamespaces' => false, 'SitemapNamespacesPriorities' => false, 'SitemapApiConfig' => [ ], 'SpecialSearchFormOptions' => [ ], 'SearchMatchRedirectPreference' => false, 'SearchRunSuggestedQuery' => true, 'Diff3' => '/usr/bin/diff3', 'Diff' => '/usr/bin/diff', 'PreviewOnOpenNamespaces' => [ 14 => true, ], 'UniversalEditButton' => true, 'UseAutomaticEditSummaries' => true, 'CommandLineDarkBg' => false, 'ReadOnly' => null, 'ReadOnlyWatchedItemStore' => false, 'ReadOnlyFile' => false, 'UpgradeKey' => false, 'GitBin' => '/usr/bin/git', 'GitRepositoryViewers' => [ 'https: 'ssh: 'https: 'git@github\\.com:(.*?)(\\.git)?' => 'https: ], 'InstallerInitialPages' => [ [ 'titlemsg' => 'mainpage', 'text' => '{{subst:int:mainpagetext}}{{subst:int:mainpagedocfooter}}', ], ], 'RCMaxAge' => 7776000, 'WatchersMaxAge' => 15552000, 'UnwatchedPageSecret' => 1, 'RCFilterByAge' => false, 'RCLinkLimits' => [ 50, 100, 250, 500, ], 'RCLinkDays' => [ 1, 3, 7, 14, 30, ], 'RCFeeds' => [ ], 'RCWatchCategoryMembership' => false, 'UseRCPatrol' => true, 'StructuredChangeFiltersLiveUpdatePollingRate' => 3, 'UseNPPatrol' => true, 'UseFilePatrol' => true, 'Feed' => true, 'FeedLimit' => 50, 'FeedCacheTimeout' => 60, 'FeedDiffCutoff' => 32768, 'OverrideSiteFeed' => [ ], 'FeedClasses' => [ 'rss' => 'MediaWiki\\Feed\\RSSFeed', 'atom' => 'MediaWiki\\Feed\\AtomFeed', ], 'AdvertisedFeedTypes' => [ 'atom', ], 'RCShowWatchingUsers' => false, 'RCShowChangedSize' => true, 'RCChangedSizeThreshold' => 500, 'ShowUpdatedMarker' => true, 'DisableAnonTalk' => false, 'UseTagFilter' => true, 'SoftwareTags' => [ 'mw-contentmodelchange' => true, 'mw-new-redirect' => true, 'mw-removed-redirect' => true, 'mw-changed-redirect-target' => true, 'mw-blank' => true, 'mw-replace' => true, 'mw-recreated' => true, 'mw-rollback' => true, 'mw-undo' => true, 'mw-manual-revert' => true, 'mw-reverted' => true, 'mw-server-side-upload' => true, 'mw-ipblock-appeal' => true, 'mw-edited-other-users-js' => true, 'mw-edited-other-users-css' => true, ], 'UnwatchedPageThreshold' => false, 'RecentChangesFlags' => [ 'newpage' => [ 'letter' => 'newpageletter', 'title' => 'recentchanges-label-newpage', 'legend' => 'recentchanges-legend-newpage', 'grouping' => 'any', ], 'minor' => [ 'letter' => 'minoreditletter', 'title' => 'recentchanges-label-minor', 'legend' => 'recentchanges-legend-minor', 'class' => 'minoredit', 'grouping' => 'all', ], 'bot' => [ 'letter' => 'boteditletter', 'title' => 'recentchanges-label-bot', 'legend' => 'recentchanges-legend-bot', 'class' => 'botedit', 'grouping' => 'all', ], 'unpatrolled' => [ 'letter' => 'unpatrolledletter', 'title' => 'recentchanges-label-unpatrolled', 'legend' => 'recentchanges-legend-unpatrolled', 'grouping' => 'any', ], ], 'WatchlistExpiry' => false, 'EnableWatchstarPopover' => false, 'EnableWatchlistLabels' => false, 'WatchlistLabelsMaxPerUser' => 100, 'WatchlistPurgeRate' => 0.1, 'WatchlistExpiryMaxDuration' => '1 year', 'EnableChangesListQueryPartitioning' => false, 'RightsPage' => null, 'RightsUrl' => null, 'RightsText' => null, 'RightsIcon' => null, 'UseCopyrightUpload' => false, 'MaxCredits' => 0, 'ShowCreditsIfMax' => true, 'ImportSources' => [ ], 'ImportTargetNamespace' => null, 'ExportAllowHistory' => true, 'ExportMaxHistory' => 0, 'ExportAllowListContributors' => false, 'ExportMaxLinkDepth' => 0, 'ExportFromNamespaces' => false, 'ExportAllowAll' => false, 'ExportPagelistLimit' => 5000, 'XmlDumpSchemaVersion' => '0.11', 'WikiFarmSettingsDirectory' => null, 'WikiFarmSettingsExtension' => 'yaml', 'ExtensionFunctions' => [ ], 'ExtensionMessagesFiles' => [ ], 'MessagesDirs' => [ ], 'TranslationAliasesDirs' => [ ], 'ExtensionEntryPointListFiles' => [ ], 'EnableParserLimitReporting' => true, 'ValidSkinNames' => [ ], 'SpecialPages' => [ ], 'ExtensionCredits' => [ ], 'Hooks' => [ ], 'ServiceWiringFiles' => [ ], 'JobClasses' => [ 'deletePage' => 'MediaWiki\\Page\\DeletePageJob', 'refreshLinks' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'deleteLinks' => 'MediaWiki\\Page\\DeleteLinksJob', 'htmlCacheUpdate' => 'MediaWiki\\JobQueue\\Jobs\\HTMLCacheUpdateJob', 'sendMail' => [ 'class' => 'MediaWiki\\Mail\\EmaillingJob', 'services' => [ 'Emailer', ], ], 'enotifNotify' => [ 'class' => 'MediaWiki\\RecentChanges\\RecentChangeNotifyJob', 'services' => [ 'RecentChangeLookup', ], ], 'fixDoubleRedirect' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\DoubleRedirectJob', 'services' => [ 'RevisionLookup', 'MagicWordFactory', 'WikiPageFactory', ], 'needsPage' => true, ], 'AssembleUploadChunks' => 'MediaWiki\\JobQueue\\Jobs\\AssembleUploadChunksJob', 'PublishStashedFile' => 'MediaWiki\\JobQueue\\Jobs\\PublishStashedFileJob', 'ThumbnailRender' => 'MediaWiki\\JobQueue\\Jobs\\ThumbnailRenderJob', 'UploadFromUrl' => 'MediaWiki\\JobQueue\\Jobs\\UploadFromUrlJob', 'recentChangesUpdate' => 'MediaWiki\\RecentChanges\\RecentChangesUpdateJob', 'refreshLinksPrioritized' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'refreshLinksDynamic' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'activityUpdateJob' => 'MediaWiki\\Watchlist\\ActivityUpdateJob', 'categoryMembershipChange' => [ 'class' => 'MediaWiki\\RecentChanges\\CategoryMembershipChangeJob', 'services' => [ 'RecentChangeFactory', ], ], 'CategoryCountUpdateJob' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryCountUpdateJob', 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ], ], 'clearUserWatchlist' => 'MediaWiki\\Watchlist\\ClearUserWatchlistJob', 'watchlistExpiry' => 'MediaWiki\\Watchlist\\WatchlistExpiryJob', 'cdnPurge' => 'MediaWiki\\JobQueue\\Jobs\\CdnPurgeJob', 'userGroupExpiry' => 'MediaWiki\\User\\UserGroupExpiryJob', 'clearWatchlistNotifications' => 'MediaWiki\\Watchlist\\ClearWatchlistNotificationsJob', 'userOptionsUpdate' => 'MediaWiki\\User\\Options\\UserOptionsUpdateJob', 'revertedTagUpdate' => 'MediaWiki\\JobQueue\\Jobs\\RevertedTagUpdateJob', 'null' => 'MediaWiki\\JobQueue\\Jobs\\NullJob', 'userEditCountInit' => 'MediaWiki\\User\\UserEditCountInitJob', 'parsoidCachePrewarm' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\ParsoidCachePrewarmJob', 'services' => [ 'ParserOutputAccess', 'PageStore', 'RevisionLookup', 'ParsoidSiteConfig', ], 'needsPage' => false, ], 'renameUserTable' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], 'renameUserDerived' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserDerivedJob', 'services' => [ 'RenameUserFactory', 'UserFactory', ], ], 'renameUser' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], ], 'JobTypesExcludedFromDefaultQueue' => [ 'AssembleUploadChunks', 'PublishStashedFile', 'UploadFromUrl', ], 'JobBackoffThrottling' => [ ], 'JobTypeConf' => [ 'default' => [ 'class' => 'MediaWiki\\JobQueue\\JobQueueDB', 'order' => 'random', 'claimTTL' => 3600, ], ], 'JobQueueIncludeInMaxLagFactor' => false, 'SpecialPageCacheUpdates' => [ 'Statistics' => [ 'MediaWiki\\Deferred\\SiteStatsUpdate', 'cacheUpdate', ], ], 'PagePropLinkInvalidations' => [ 'hiddencat' => 'categorylinks', ], 'CategoryMagicGallery' => true, 'CategoryPagingLimit' => 200, 'CategoryCollation' => 'uppercase', 'TempCategoryCollations' => [ ], 'SortedCategories' => false, 'TrackingCategories' => [ ], 'LogTypes' => [ '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'import', 'interwiki', 'patrol', 'merge', 'suppress', 'tag', 'managetags', 'contentmodel', 'renameuser', ], 'LogRestrictions' => [ 'suppress' => 'suppressionlog', ], 'FilterLogTypes' => [ 'patrol' => true, 'tag' => true, 'newusers' => false, ], 'LogNames' => [ '' => 'all-logs-page', 'block' => 'blocklogpage', 'protect' => 'protectlogpage', 'rights' => 'rightslog', 'delete' => 'dellogpage', 'upload' => 'uploadlogpage', 'move' => 'movelogpage', 'import' => 'importlogpage', 'patrol' => 'patrol-log-page', 'merge' => 'mergelog', 'suppress' => 'suppressionlog', ], 'LogHeaders' => [ '' => 'alllogstext', 'block' => 'blocklogtext', 'delete' => 'dellogpagetext', 'import' => 'importlogpagetext', 'merge' => 'mergelogpagetext', 'move' => 'movelogpagetext', 'patrol' => 'patrol-log-header', 'protect' => 'protectlogtext', 'rights' => 'rightslogtext', 'suppress' => 'suppressionlogtext', 'upload' => 'uploadlogpagetext', ], 'LogActions' => [ ], 'LogActionsHandlers' => [ 'block/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/unblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'contentmodel/change' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'contentmodel/new' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'delete/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir2' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/restore' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'import/interwiki' => 'MediaWiki\\Logging\\ImportLogFormatter', 'import/upload' => 'MediaWiki\\Logging\\ImportLogFormatter', 'interwiki/iw_add' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_delete' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_edit' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'managetags/activate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/create' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/deactivate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/delete' => 'MediaWiki\\Logging\\LogFormatter', 'merge/merge' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'merge/merge-into' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move_redir' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'patrol/patrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'patrol/autopatrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'protect/modify' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/move_prot' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/protect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/unprotect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'renameuser/renameuser' => [ 'class' => 'MediaWiki\\Logging\\RenameuserLogFormatter', 'services' => [ 'TitleParser', ], ], 'rights/autopromote' => 'MediaWiki\\Logging\\RightsLogFormatter', 'rights/rights' => 'MediaWiki\\Logging\\RightsLogFormatter', 'suppress/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'tag/update' => 'MediaWiki\\Logging\\TagLogFormatter', 'upload/overwrite' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/revert' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/upload' => 'MediaWiki\\Logging\\UploadLogFormatter', ], 'ActionFilteredLogs' => [ 'block' => [ 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], 'unblock' => [ 'unblock', ], ], 'contentmodel' => [ 'change' => [ 'change', ], 'new' => [ 'new', ], ], 'delete' => [ 'delete' => [ 'delete', ], 'delete_redir' => [ 'delete_redir', 'delete_redir2', ], 'restore' => [ 'restore', ], 'event' => [ 'event', ], 'revision' => [ 'revision', ], ], 'import' => [ 'interwiki' => [ 'interwiki', ], 'upload' => [ 'upload', ], ], 'managetags' => [ 'create' => [ 'create', ], 'delete' => [ 'delete', ], 'activate' => [ 'activate', ], 'deactivate' => [ 'deactivate', ], ], 'move' => [ 'move' => [ 'move', ], 'move_redir' => [ 'move_redir', ], ], 'newusers' => [ 'create' => [ 'create', 'newusers', ], 'create2' => [ 'create2', ], 'autocreate' => [ 'autocreate', ], 'byemail' => [ 'byemail', ], ], 'protect' => [ 'protect' => [ 'protect', ], 'modify' => [ 'modify', ], 'unprotect' => [ 'unprotect', ], 'move_prot' => [ 'move_prot', ], ], 'rights' => [ 'rights' => [ 'rights', ], 'autopromote' => [ 'autopromote', ], ], 'suppress' => [ 'event' => [ 'event', ], 'revision' => [ 'revision', ], 'delete' => [ 'delete', ], 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], ], 'upload' => [ 'upload' => [ 'upload', ], 'overwrite' => [ 'overwrite', ], 'revert' => [ 'revert', ], ], ], 'NewUserLog' => true, 'PageCreationLog' => true, 'AllowSpecialInclusion' => true, 'DisableQueryPageUpdate' => false, 'CountCategorizedImagesAsUsed' => false, 'MaxRedirectLinksRetrieved' => 500, 'RangeContributionsCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 32, ], 'Actions' => [ ], 'DefaultRobotPolicy' => 'index,follow', 'NamespaceRobotPolicies' => [ ], 'ArticleRobotPolicies' => [ ], 'ExemptFromUserRobotsControl' => null, 'DebugAPI' => false, 'APIModules' => [ ], 'APIFormatModules' => [ ], 'APIMetaModules' => [ ], 'APIPropModules' => [ ], 'APIListModules' => [ ], 'APIMaxDBRows' => 5000, 'APIMaxResultSize' => 8388608, 'APIMaxUncachedDiffs' => 1, 'APIMaxLagThreshold' => 7, 'APICacheHelpTimeout' => 3600, 'APIUselessQueryPages' => [ 'MIMEsearch', 'LinkSearch', ], 'AjaxLicensePreview' => true, 'CrossSiteAJAXdomains' => [ ], 'CrossSiteAJAXdomainExceptions' => [ ], 'AllowedCorsHeaders' => [ 'Accept', 'Accept-Language', 'Content-Language', 'Content-Type', 'Accept-Encoding', 'DNT', 'Origin', 'User-Agent', 'Api-User-Agent', 'Promise-Non-Write-API-Action', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestSandboxSpecs' => [ ], 'RestModuleOverrides' => [ ], 'MaxShellMemory' => 307200, 'MaxShellFileSize' => 102400, 'MaxShellTime' => 180, 'MaxShellWallClockTime' => 180, 'ShellCgroup' => false, 'PhpCli' => '/usr/bin/php', 'ShellRestrictionMethod' => 'autodetect', 'ShellboxUrls' => [ 'default' => null, ], 'ShellboxSecretKey' => null, 'ShellboxShell' => '/bin/sh', 'HTTPTimeout' => 25, 'HTTPConnectTimeout' => 5.0, 'HTTPMaxTimeout' => 0, 'HTTPMaxConnectTimeout' => 0, 'HTTPImportTimeout' => 25, 'AsyncHTTPTimeout' => 25, 'HTTPProxy' => '', 'LocalVirtualHosts' => [ ], 'LocalHTTPProxy' => false, 'AllowExternalReqID' => false, 'GenerateReqIDFormat' => 'rand24', 'JobRunRate' => 1, 'RunJobsAsync' => false, 'UpdateRowsPerJob' => 300, 'UpdateRowsPerQuery' => 100, 'RedirectOnLogin' => null, 'VirtualRestConfig' => [ 'paths' => [ ], 'modules' => [ ], 'global' => [ 'timeout' => 360, 'forwardCookies' => false, 'HTTPProxy' => null, ], ], 'EventRelayerConfig' => [ 'default' => [ 'class' => 'Wikimedia\\EventRelayer\\EventRelayerNull', ], ], 'Pingback' => false, 'OriginTrials' => [ ], 'ReportToExpiry' => 86400, 'ReportToEndpoints' => [ ], 'FeaturePolicyReportOnly' => [ ], 'SkinsPreferred' => [ 'vector-2022', 'vector', ], 'SpecialContributeSkinsEnabled' => [ ], 'SpecialContributeNewPageTarget' => null, 'EnableEditRecovery' => false, 'EditRecoveryExpiry' => 2592000, 'UseCodexSpecialBlock' => false, 'ShowLogoutConfirmation' => false, 'EnableProtectionIndicators' => true, 'OutputPipelineStages' => [ ], 'FeatureShutdown' => [ ], 'CloneArticleParserOutput' => true, 'UseLeximorph' => false, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => true, 'ParserOptionsLogUnsafeSampleRate' => 0, ], 'type' => [ 'ConfigRegistry' => 'object', 'AssumeProxiesUseDefaultProtocolPorts' => 'boolean', 'ForceHTTPS' => 'boolean', 'ExtensionDirectory' => [ 'string', 'null', ], 'StyleDirectory' => [ 'string', 'null', ], 'UploadDirectory' => [ 'string', 'boolean', 'null', ], 'Logos' => [ 'object', 'boolean', ], 'ReferrerPolicy' => [ 'array', 'string', 'boolean', ], 'ActionPaths' => 'object', 'MainPageIsDomainRoot' => 'boolean', 'ImgAuthUrlPathMap' => 'object', 'LocalFileRepo' => 'object', 'ForeignFileRepos' => 'array', 'UseSharedUploads' => 'boolean', 'SharedUploadDirectory' => [ 'string', 'null', ], 'SharedUploadPath' => [ 'string', 'null', ], 'HashedSharedUploadDirectory' => 'boolean', 'FetchCommonsDescriptions' => 'boolean', 'SharedUploadDBname' => [ 'boolean', 'string', ], 'SharedUploadDBprefix' => 'string', 'CacheSharedUploads' => 'boolean', 'ForeignUploadTargets' => 'array', 'UploadDialog' => 'object', 'FileBackends' => 'object', 'LockManagers' => 'array', 'CopyUploadsDomains' => 'array', 'CopyUploadTimeout' => [ 'boolean', 'integer', ], 'SharedThumbnailScriptPath' => [ 'string', 'boolean', ], 'HashedUploadDirectory' => 'boolean', 'CSPUploadEntryPoint' => 'boolean', 'FileExtensions' => 'array', 'ProhibitedFileExtensions' => 'array', 'MimeTypeExclusions' => 'array', 'TrustedMediaFormats' => 'array', 'MediaHandlers' => 'object', 'NativeImageLazyLoading' => 'boolean', 'ParserTestMediaHandlers' => 'object', 'MaxInterlacingAreas' => 'object', 'SVGConverters' => 'object', 'SVGNativeRendering' => [ 'string', 'boolean', ], 'MaxImageArea' => [ 'string', 'integer', 'boolean', ], 'TiffThumbnailType' => 'array', 'GenerateThumbnailOnParse' => 'boolean', 'EnableAutoRotation' => [ 'boolean', 'null', ], 'Antivirus' => [ 'string', 'null', ], 'AntivirusSetup' => 'object', 'MimeDetectorCommand' => [ 'string', 'null', ], 'XMLMimeTypes' => 'object', 'ImageLimits' => 'array', 'ThumbLimits' => 'array', 'ThumbnailNamespaces' => 'array', 'ThumbnailSteps' => [ 'array', 'null', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EmailConfirmationBanner' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ExternalLinksDomainGaps' => 'object', 'ContentHandlers' => 'object', 'NamespaceContentModels' => 'object', 'TextModelsToParse' => 'array', 'ExternalStores' => 'array', 'ExternalServers' => 'object', 'DefaultExternalStore' => [ 'array', 'boolean', ], 'RevisionCacheExpiry' => 'integer', 'PageLanguageUseDB' => 'boolean', 'DiffEngine' => [ 'string', 'null', ], 'ExternalDiffEngine' => [ 'string', 'boolean', ], 'Wikidiff2Options' => 'object', 'RequestTimeLimit' => [ 'integer', 'null', ], 'CriticalSectionTimeLimit' => 'number', 'PoolCounterConf' => [ 'object', 'null', ], 'PoolCountClientConf' => 'object', 'MaxUserDBWriteDuration' => [ 'integer', 'boolean', ], 'MaxJobDBWriteDuration' => [ 'integer', 'boolean', ], 'MultiShardSiteStats' => 'boolean', 'ObjectCaches' => 'object', 'WANObjectCache' => 'object', 'MicroStashType' => [ 'string', 'integer', ], 'ParsoidCacheConfig' => 'object', 'ParsoidSelectiveUpdateSampleRate' => 'integer', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => 'string', 'PHPSessionHandling' => 'string', 'SuspiciousIpExpiry' => [ 'integer', 'boolean', ], 'MemCachedServers' => 'array', 'LocalisationCacheConf' => 'object', 'ExtensionInfoMTime' => [ 'integer', 'boolean', ], 'CdnServers' => 'object', 'CdnServersNoPurge' => 'object', 'HTCPRouting' => 'object', 'GrammarForms' => 'object', 'ExtraInterlanguageLinkPrefixes' => 'array', 'InterlanguageLinkCodeMap' => 'object', 'ExtraLanguageNames' => 'object', 'ExtraLanguageCodes' => 'object', 'DummyLanguageCodes' => 'object', 'DisabledVariants' => 'object', 'ForceUIMsgAsContentMsg' => 'object', 'RawHtmlMessages' => 'array', 'OverrideUcfirstCharacters' => 'object', 'XhtmlNamespaces' => 'object', 'BrowserFormatDetection' => 'string', 'SkinMetaTags' => 'object', 'SkipSkins' => 'object', 'FragmentMode' => 'array', 'FooterIcons' => 'object', 'InterwikiLogoOverride' => 'array', 'ResourceModules' => 'object', 'ResourceModuleSkinStyles' => 'object', 'ResourceLoaderSources' => 'object', 'ResourceLoaderMaxage' => 'object', 'ResourceLoaderMaxQueryLength' => [ 'integer', 'boolean', ], 'CanonicalNamespaceNames' => 'object', 'ExtraNamespaces' => 'object', 'ExtraGenderNamespaces' => 'object', 'NamespaceAliases' => 'object', 'CapitalLinkOverrides' => 'object', 'NamespacesWithSubpages' => 'object', 'NamespacesWithoutAutoSummaries' => 'array', 'ContentNamespaces' => 'array', 'ShortPagesNamespaceExclusions' => 'array', 'ExtraSignatureNamespaces' => 'array', 'InvalidRedirectTargets' => 'array', 'LocalInterwikis' => 'array', 'InterwikiCache' => [ 'boolean', 'object', ], 'SiteTypes' => 'object', 'UrlProtocols' => 'array', 'TidyConfig' => 'object', 'ParsoidSettings' => 'object', 'ParsoidExperimentalParserFunctionOutput' => 'boolean', 'NoFollowNsExceptions' => 'array', 'NoFollowDomainExceptions' => 'array', 'ExternalLinksIgnoreDomains' => 'array', 'EnableMagicLinks' => 'object', 'ManualRevertSearchRadius' => 'integer', 'RevertedTagMaxDepth' => 'integer', 'CentralIdLookupProviders' => 'object', 'CentralIdLookupProvider' => 'string', 'UserRegistrationProviders' => 'object', 'PasswordPolicy' => 'object', 'AuthManagerConfig' => [ 'object', 'null', ], 'AuthManagerAutoConfig' => 'object', 'RememberMe' => 'string', 'ReauthenticateTime' => 'object', 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => 'object', 'ChangeCredentialsBlacklist' => 'array', 'RemoveCredentialsBlacklist' => 'array', 'PasswordConfig' => 'object', 'PasswordResetRoutes' => 'object', 'SignatureAllowedLintErrors' => 'array', 'ReservedUsernames' => 'array', 'DefaultUserOptions' => 'object', 'ConditionalUserOptions' => 'object', 'HiddenPrefs' => 'array', 'UserJsPrefLimit' => 'integer', 'AuthenticationTokenVersion' => [ 'string', 'null', ], 'SessionProviders' => 'object', 'AutoCreateTempUser' => 'object', 'AutoblockExemptions' => 'array', 'BlockCIDRLimit' => 'object', 'EnableMultiBlocks' => 'boolean', 'GroupPermissions' => 'object', 'PrivilegedGroups' => 'array', 'RevokePermissions' => 'object', 'GroupInheritsPermissions' => 'object', 'ImplicitGroups' => 'array', 'GroupsAddToSelf' => 'object', 'GroupsRemoveFromSelf' => 'object', 'RestrictedGroups' => 'object', 'UserRequirementsPrivateConditions' => 'array', 'RestrictionTypes' => 'array', 'RestrictionLevels' => 'array', 'CascadingRestrictionLevels' => 'array', 'SemiprotectedRestrictionLevels' => 'array', 'NamespaceProtection' => 'object', 'NonincludableNamespaces' => 'object', 'Autopromote' => 'object', 'AutopromoteOnce' => 'object', 'AutopromoteOnceRCExcludedGroups' => 'array', 'AddGroups' => 'object', 'RemoveGroups' => 'object', 'AvailableRights' => 'array', 'ImplicitRights' => 'array', 'AccountCreationThrottle' => [ 'integer', 'array', ], 'TempAccountCreationThrottle' => 'array', 'TempAccountNameAcquisitionThrottle' => 'array', 'SpamRegex' => 'array', 'SummarySpamRegex' => 'array', 'DnsBlacklistUrls' => 'array', 'ProxyList' => [ 'string', 'array', ], 'ProxyWhitelist' => 'array', 'SoftBlockRanges' => 'array', 'RateLimits' => 'object', 'RateLimitsExcludedIPs' => 'array', 'ExternalQuerySources' => 'object', 'PasswordAttemptThrottle' => 'array', 'GrantPermissions' => 'object', 'GrantPermissionGroups' => 'object', 'GrantRiskGroups' => 'object', 'EnableBotPasswords' => 'boolean', 'BotPasswordsCluster' => [ 'string', 'boolean', ], 'BotPasswordsDatabase' => [ 'string', 'boolean', ], 'BotPasswordsLimit' => 'integer', 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ 'boolean', 'object', ], 'CSPUseReportURIDirective' => [ 'boolean', 'object', ], 'CSPFalsePositiveUrls' => 'object', 'AllowCrossOrigin' => 'boolean', 'RestAllowCrossOriginCookieAuth' => 'boolean', 'CookieSameSite' => [ 'string', 'null', ], 'CacheVaryCookies' => 'array', 'TrxProfilerLimits' => 'object', 'DebugLogGroups' => 'object', 'MWLoggerDefaultSpi' => 'object', 'Profiler' => 'object', 'StatsTarget' => [ 'string', 'null', ], 'StatsFormat' => [ 'string', 'null', ], 'StatsPrefix' => 'string', 'OpenTelemetryConfig' => [ 'object', 'null', ], 'OpenSearchTemplates' => 'object', 'NamespacesToBeSearchedDefault' => 'object', 'SitemapNamespaces' => [ 'boolean', 'array', ], 'SitemapNamespacesPriorities' => [ 'boolean', 'object', ], 'SitemapApiConfig' => 'object', 'SpecialSearchFormOptions' => 'object', 'SearchMatchRedirectPreference' => 'boolean', 'SearchRunSuggestedQuery' => 'boolean', 'PreviewOnOpenNamespaces' => 'object', 'ReadOnlyWatchedItemStore' => 'boolean', 'GitRepositoryViewers' => 'object', 'InstallerInitialPages' => 'array', 'RCLinkLimits' => 'array', 'RCLinkDays' => 'array', 'RCFeeds' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => 'boolean', 'EnableWatchstarPopover' => 'boolean', 'EnableWatchlistLabels' => 'boolean', 'WatchlistLabelsMaxPerUser' => 'integer', 'WatchlistPurgeRate' => 'number', 'WatchlistExpiryMaxDuration' => [ 'string', 'null', ], 'EnableChangesListQueryPartitioning' => 'boolean', 'ImportSources' => 'object', 'ExtensionFunctions' => 'array', 'ExtensionMessagesFiles' => 'object', 'MessagesDirs' => 'object', 'TranslationAliasesDirs' => 'object', 'ExtensionEntryPointListFiles' => 'object', 'ValidSkinNames' => 'object', 'SpecialPages' => 'object', 'ExtensionCredits' => 'object', 'Hooks' => 'object', 'ServiceWiringFiles' => 'array', 'JobClasses' => 'object', 'JobTypesExcludedFromDefaultQueue' => 'array', 'JobBackoffThrottling' => 'object', 'JobTypeConf' => 'object', 'SpecialPageCacheUpdates' => 'object', 'PagePropLinkInvalidations' => 'object', 'TempCategoryCollations' => 'array', 'SortedCategories' => 'boolean', 'TrackingCategories' => 'array', 'LogTypes' => 'array', 'LogRestrictions' => 'object', 'FilterLogTypes' => 'object', 'LogNames' => 'object', 'LogHeaders' => 'object', 'LogActions' => 'object', 'LogActionsHandlers' => 'object', 'ActionFilteredLogs' => 'object', 'RangeContributionsCIDRLimit' => 'object', 'Actions' => 'object', 'NamespaceRobotPolicies' => 'object', 'ArticleRobotPolicies' => 'object', 'ExemptFromUserRobotsControl' => [ 'array', 'null', ], 'APIModules' => 'object', 'APIFormatModules' => 'object', 'APIMetaModules' => 'object', 'APIPropModules' => 'object', 'APIListModules' => 'object', 'APIUselessQueryPages' => 'array', 'CrossSiteAJAXdomains' => 'object', 'CrossSiteAJAXdomainExceptions' => 'object', 'AllowedCorsHeaders' => 'array', 'RestAPIAdditionalRouteFiles' => 'array', 'RestSandboxSpecs' => 'object', 'RestModuleOverrides' => 'object', 'ShellRestrictionMethod' => [ 'string', 'boolean', ], 'ShellboxUrls' => 'object', 'ShellboxSecretKey' => [ 'string', 'null', ], 'ShellboxShell' => [ 'string', 'null', ], 'HTTPTimeout' => 'number', 'HTTPConnectTimeout' => 'number', 'HTTPMaxTimeout' => 'number', 'HTTPMaxConnectTimeout' => 'number', 'LocalVirtualHosts' => 'object', 'LocalHTTPProxy' => [ 'string', 'boolean', ], 'GenerateReqIDFormat' => 'string', 'VirtualRestConfig' => 'object', 'EventRelayerConfig' => 'object', 'Pingback' => 'boolean', 'OriginTrials' => 'array', 'ReportToExpiry' => 'integer', 'ReportToEndpoints' => 'array', 'FeaturePolicyReportOnly' => 'array', 'SkinsPreferred' => 'array', 'SpecialContributeSkinsEnabled' => 'array', 'SpecialContributeNewPageTarget' => [ 'string', 'null', ], 'EnableEditRecovery' => 'boolean', 'EditRecoveryExpiry' => 'integer', 'UseCodexSpecialBlock' => 'boolean', 'ShowLogoutConfirmation' => 'boolean', 'EnableProtectionIndicators' => 'boolean', 'OutputPipelineStages' => 'object', 'FeatureShutdown' => 'array', 'CloneArticleParserOutput' => 'boolean', 'UseLeximorph' => 'boolean', 'UsePostprocCacheLegacy' => 'boolean', 'UsePostprocCacheParsoid' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', ], 'mergeStrategy' => [ 'TiffThumbnailType' => 'replace', 'LBFactoryConf' => 'replace', 'InterwikiCache' => 'replace', 'PasswordPolicy' => 'array_replace_recursive', 'AuthManagerAutoConfig' => 'array_plus_2d', 'GroupPermissions' => 'array_plus_2d', 'RevokePermissions' => 'array_plus_2d', 'AddGroups' => 'array_merge_recursive', 'RemoveGroups' => 'array_merge_recursive', 'RateLimits' => 'array_plus_2d', 'GrantPermissions' => 'array_plus_2d', 'MWLoggerDefaultSpi' => 'replace', 'Profiler' => 'replace', 'Hooks' => 'array_merge_recursive', 'RestModuleOverrides' => 'array_replace_recursive', 'VirtualRestConfig' => 'array_plus_2d', ], 'dynamicDefault' => [ 'UsePathInfo' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUsePathInfo', ], ], 'Script' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultScript', ], ], 'LoadScript' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLoadScript', ], ], 'RestPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultRestPath', ], ], 'StylePath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultStylePath', ], ], 'LocalStylePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalStylePath', ], ], 'ExtensionAssetsPath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultExtensionAssetsPath', ], ], 'ArticlePath' => [ 'use' => [ 'Script', 'UsePathInfo', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultArticlePath', ], ], 'UploadPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUploadPath', ], ], 'FileCacheDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultFileCacheDirectory', ], ], 'Logo' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLogo', ], ], 'DeletedDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDeletedDirectory', ], ], 'ShowEXIF' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultShowEXIF', ], ], 'SharedPrefix' => [ 'use' => [ 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedPrefix', ], ], 'SharedSchema' => [ 'use' => [ 'DBmwschema', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedSchema', ], ], 'DBerrorLogTZ' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDBerrorLogTZ', ], ], 'Localtimezone' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocaltimezone', ], ], 'LocalTZoffset' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalTZoffset', ], ], 'ResourceBasePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultResourceBasePath', ], ], 'MetaNamespace' => [ 'use' => [ 'Sitename', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultMetaNamespace', ], ], 'CookieSecure' => [ 'use' => [ 'ForceHTTPS', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookieSecure', ], ], 'CookiePrefix' => [ 'use' => [ 'SharedDB', 'SharedPrefix', 'SharedTables', 'DBname', 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookiePrefix', ], ], 'ReadOnlyFile' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultReadOnlyFile', ], ], ], ], 'config-schema' => [ 'UploadStashScalerBaseUrl' => [ 'deprecated' => 'since 1.36 Use thumbProxyUrl in $wgLocalFileRepo', ], 'IllegalFileChars' => [ 'deprecated' => 'since 1.41; no longer customizable', ], 'ThumbnailNamespaces' => [ 'items' => [ 'type' => 'integer', ], ], 'LocalDatabases' => [ 'items' => [ 'type' => 'string', ], ], 'ParserCacheFilterConfig' => [ 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of namespace IDs to filter definitions.', 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of filter names to values.', 'properties' => [ 'minCpuTime' => [ 'type' => 'number', ], ], ], ], ], 'PHPSessionHandling' => [ 'deprecated' => 'since 1.45 Integration with PHP session handling will be removed in the future', ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], 'ChangeCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'RemoveCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'GroupPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GroupInheritsPermissions' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'AvailableRights' => [ 'items' => [ 'type' => 'string', ], ], 'ImplicitRights' => [ 'items' => [ 'type' => 'string', ], ], 'SoftBlockRanges' => [ 'items' => [ 'type' => 'string', ], ], 'ExternalQuerySources' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'enabled' => [ 'type' => 'boolean', 'default' => false, ], 'url' => [ 'type' => 'string', 'format' => 'uri', ], 'timeout' => [ 'type' => 'integer', 'default' => 10, ], ], 'required' => [ 'enabled', 'url', ], 'additionalProperties' => false, ], ], 'GrantPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GrantPermissionGroups' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'SitemapNamespacesPriorities' => [ 'deprecated' => 'since 1.45 and ignored', ], 'SitemapApiConfig' => [ 'additionalProperties' => [ 'enabled' => [ 'type' => 'bool', ], 'sitemapsPerIndex' => [ 'type' => 'int', ], 'pagesPerSitemap' => [ 'type' => 'int', ], 'expiry' => [ 'type' => 'int', ], ], ], 'SoftwareTags' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], 'JobBackoffThrottling' => [ 'additionalProperties' => [ 'type' => 'number', ], ], 'JobTypeConf' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'class' => [ 'type' => 'string', ], 'order' => [ 'type' => 'string', ], 'claimTTL' => [ 'type' => 'integer', ], ], ], ], 'TrackingCategories' => [ 'deprecated' => 'since 1.25 Extensions should now register tracking categories using the new extension registration system.', ], 'RangeContributionsCIDRLimit' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'RestSandboxSpecs' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'url' => [ 'type' => 'string', 'format' => 'url', ], 'name' => [ 'type' => 'string', ], 'file' => [ 'type' => 'string', ], 'msg' => [ 'type' => 'string', 'description' => 'a message key', ], ], ], ], 'RestModuleOverrides' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'mode' => [ 'type' => 'string', ], ], 'required' => [ 'mode', ], ], ], 'ShellboxUrls' => [ 'additionalProperties' => [ 'type' => [ 'string', 'boolean', 'null', ], ], ], ], 'obsolete-config' => [ 'MangleFlashPolicy' => 'Since 1.39; no longer has any effect.', 'EnableOpenSearchSuggest' => 'Since 1.35, no longer used', 'AutoloadAttemptLowercase' => 'Since 1.40; no longer has any effect.', ],]
Authentication providers are used by AuthManager when authenticating users.
getUniqueId()
Return a unique identifier for this instance.
Pre-authentication providers can prevent authentication early on.
Primary authentication providers associate submitted input data with a MediaWiki account.
Secondary providers act after input data is already associated with a MediaWiki account.
Interface for configuration instances.
Definition Config.php:18
Interface for objects (potentially) representing an editable wiki page.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
isNamed()
Is the user a normal non-temporary registered user?
getUser()
Returns the performer of the actions associated with this authority.
isTemp()
Is the user an autocreated temporary user?
authorizeWrite(string $action, PageIdentity $target, ?PermissionStatus $status=null)
Authorize write access.
probablyCan(string $action, PageIdentity $target, ?PermissionStatus $status=null)
Checks whether this authority can probably perform the given action on the given target page.
MediaWiki\Session entry point interface.
Service for looking up UserIdentity.
Interface for objects representing user identity.
Shared interface for rigor levels when dealing with User methods.
Interface for database access objects.
This class is a delegate to ILBFactory for a given database cluster.
$source