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 // If there was an ElevatedSecurityAuthenticationRequest in the original set of requests,
480 // there won't be one in the latest set of requests. To signal to providers that this is
481 // a reauthentication, pretend it's there anyway. (We can't straightforwardly include it
482 // in continueRequests, because its ->session member doesn't survive serialization, so
483 // loadFromSubmission will fail.)
484 if ( $elevatedSecurityReq &&
485 !AuthenticationRequest::getRequestByClass(
486 $reqs,
487 ElevatedSecurityAuthenticationRequest::class
488 )
489 ) {
490 $reqs[] = $elevatedSecurityReq;
491 }
492
493 $status = Status::newGood();
494 foreach ( $reqs as $req ) {
495 $req->returnToUrl = $state['returnToUrl'];
496 $status->merge( $req->validate() );
497 }
498 if ( !$status->isOK() ) {
499 $this->logger->debug( "Login failed at AuthRequest validation", [
500 'user' => $guessUserName,
501 'reason' => $status->getWikiText( false, false, 'en' ),
502 ] );
503 $res = AuthenticationResponse::newFail( $status->getMessage() );
504 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
505 [ $this->userFactory->newFromName( (string)$guessUserName ), $res ]
506 );
507 $session->remove( self::AUTHN_STATE );
508 $this->callLoginAuditHook( $state['reqs'], $res, $guessUserName );
509 return $res;
510 }
511
512 // Step 1: Choose a primary authentication provider, and call it until it succeeds.
513
514 if ( $state['primary'] === null ) {
515 // We haven't picked a PrimaryAuthenticationProvider yet
516 // @codeCoverageIgnoreStart
517 $guessUserName = null;
518 foreach ( $reqs as $req ) {
519 if ( $req->username !== null && $req->username !== '' ) {
520 if ( $guessUserName === null ) {
521 $guessUserName = $req->username;
522 } elseif ( $guessUserName !== $req->username ) {
523 $guessUserName = null;
524 break;
525 }
526 }
527 }
528 $state['guessUserName'] = $guessUserName;
529 // @codeCoverageIgnoreEnd
530 $state['reqs'] = $reqs;
531
532 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
533 $res = $provider->beginPrimaryAuthentication( $reqs );
534 switch ( $res->status ) {
535 case AuthenticationResponse::PASS:
536 $state['primary'] = $id;
537 $state['primaryResponse'] = $res;
538 $this->logger->debug( 'Primary login with {id} succeeded', [
539 'id' => $id,
540 ] );
541 break 2;
542 case AuthenticationResponse::FAIL:
543 $this->logger->debug( 'Login failed in primary authentication by {id}', [
544 'id' => $id,
545 ] );
546 if ( $res->createRequest || $state['maybeLink'] ) {
547 $res->createRequest = new CreateFromLoginAuthenticationRequest(
548 $res->createRequest, $state['maybeLink']
549 );
550 }
551 $this->callMethodOnProviders(
552 self::CALL_ALL,
553 'postAuthentication',
554 [
555 $this->userFactory->newFromName( (string)$guessUserName ),
556 $res
557 ]
558 );
559 $session->remove( self::AUTHN_STATE );
560 $this->callLoginAuditHook( $state['reqs'], $res, $guessUserName );
561 return $res;
562 case AuthenticationResponse::ABSTAIN:
563 // Continue loop
564 break;
565 case AuthenticationResponse::REDIRECT:
566 case AuthenticationResponse::UI:
567 $this->logger->debug( 'Primary login with {id} returned {status}', [
568 'id' => $id,
569 'status' => $res->status,
570 ] );
571 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
572 $state['primary'] = $id;
573 $state['continueRequests'] = $res->neededRequests;
574 $session->setSecret( self::AUTHN_STATE, $state );
575 return $res;
576
577 // @codeCoverageIgnoreStart
578 default:
579 throw new DomainException(
580 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
581 );
582 // @codeCoverageIgnoreEnd
583 }
584 }
585 if ( $state['primary'] === null ) {
586 $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
587 $response = AuthenticationResponse::newFail(
588 wfMessage( 'authmanager-authn-no-primary' )
589 );
590 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
591 [ $this->userFactory->newFromName( (string)$guessUserName ), $response ]
592 );
593 $session->remove( self::AUTHN_STATE );
594 return $response;
595 }
596 } elseif ( $state['primaryResponse'] === null ) {
597 $provider = $this->getAuthenticationProvider( $state['primary'] );
598 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
599 // Configuration changed? Force them to start over.
600 // @codeCoverageIgnoreStart
601 $response = AuthenticationResponse::newFail(
602 wfMessage( 'authmanager-authn-not-in-progress' )
603 );
604 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
605 [ $this->userFactory->newFromName( (string)$guessUserName ), $response ]
606 );
607 $session->remove( self::AUTHN_STATE );
608 return $response;
609 // @codeCoverageIgnoreEnd
610 }
611 $id = $provider->getUniqueId();
612 $res = $provider->continuePrimaryAuthentication( $reqs );
613 switch ( $res->status ) {
614 case AuthenticationResponse::PASS:
615 $state['primaryResponse'] = $res;
616 $this->logger->debug( 'Primary login with {id} succeeded', [
617 'id' => $id,
618 ] );
619 break;
620 case AuthenticationResponse::FAIL:
621 $this->logger->debug( 'Login failed in primary authentication by {id}', [
622 'id' => $id,
623 ] );
624 if ( $res->createRequest || $state['maybeLink'] ) {
625 $res->createRequest = new CreateFromLoginAuthenticationRequest(
626 $res->createRequest, $state['maybeLink']
627 );
628 }
629 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
630 [ $this->userFactory->newFromName( (string)$guessUserName ), $res ]
631 );
632 $session->remove( self::AUTHN_STATE );
633 $this->callLoginAuditHook( $state['reqs'], $res, $guessUserName );
634 return $res;
635 case AuthenticationResponse::REDIRECT:
636 case AuthenticationResponse::UI:
637 $this->logger->debug( 'Primary login with {id} returned {status}', [
638 'id' => $id,
639 'status' => $res->status,
640 ] );
641 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
642 $state['continueRequests'] = $res->neededRequests;
643 $session->setSecret( self::AUTHN_STATE, $state );
644 return $res;
645 default:
646 throw new DomainException(
647 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
648 );
649 }
650 }
651
652 $res = $state['primaryResponse'];
653 if ( $res->username === null ) {
654 // The user was authenticated successfully but had no wiki account (neither local
655 // nor central). This can happen when using a third-party identity provider. End
656 // this login attempt, but provide a way for the user to reuse this identity for
657 // signup or account linking.
658
659 $provider = $this->getAuthenticationProvider( $state['primary'] );
660 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
661 // Configuration changed? Force them to start over.
662 // @codeCoverageIgnoreStart
663 $response = AuthenticationResponse::newFail(
664 wfMessage( 'authmanager-authn-not-in-progress' )
665 );
666 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication',
667 [ $this->userFactory->newFromName( (string)$guessUserName ), $response ]
668 );
669 $session->remove( self::AUTHN_STATE );
670 $this->callLoginAuditHook( $state['reqs'], $res, $guessUserName );
671 return $response;
672 // @codeCoverageIgnoreEnd
673 }
674
675 if ( $elevatedSecurityReq ) {
676 // The user was asked to reauthenticate for elevated security but authenticated
677 // as a different user. Does not make sense, maybe some kind of phishing attempt?
678 $this->logger->info( 'Reauthentication failed because of user mismatch', [
679 'oldUserId' => $elevatedSecurityReq->userId,
680 'newUserName' => '<no user>',
681 ] );
682 $ret = AuthenticationResponse::newFail( wfMessage( 'authmanager-authn-reauth-switch' ) );
683 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ null, $ret ] );
684 $session->remove( self::AUTHN_STATE );
685 return $ret;
686 }
687
688 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
689 $res->linkRequest &&
690 // don't confuse the user with an incorrect message if linking is disabled
691 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
692 ) {
693 $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
694 $msg = 'authmanager-authn-no-local-user-link';
695 } else {
696 $msg = 'authmanager-authn-no-local-user';
697 }
698 $this->logger->debug(
699 'Primary login with {providerUniqueId} succeeded, but returned no user',
700 [ 'providerUniqueId' => $provider->getUniqueId() ]
701 );
702 $response = AuthenticationResponse::newRestart( wfMessage( $msg ) );
703 $response->neededRequests = $this->getAuthenticationRequestsInternal(
704 self::ACTION_LOGIN,
705 [],
706 $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
707 );
708 if ( $res->createRequest || $state['maybeLink'] ) {
709 $response->createRequest = new CreateFromLoginAuthenticationRequest(
710 $res->createRequest, $state['maybeLink']
711 );
712 $response->neededRequests[] = $response->createRequest;
713 }
714 $this->fillRequests( $response->neededRequests, self::ACTION_LOGIN, null, true );
715 $session->setSecret( self::AUTHN_STATE, [
716 'reqs' => [], // Will be filled in later
717 'primary' => null,
718 'primaryResponse' => null,
719 'secondary' => [],
720 'continueRequests' => $response->neededRequests,
721 ] + $state );
722
723 // Give the AuthManagerVerifyAuthentication hook a chance to interrupt - even though
724 // RESTART does not immediately result in a successful login, the response and session
725 // state can hold information identifying a (remote) user, and that could be turned
726 // into access to that user's account in a follow-up request.
727 if ( !$this->runVerifyHook( self::ACTION_LOGIN, null, $response, $state['primary'] ) ) {
728 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ null, $response ] );
729 $session->remove( self::AUTHN_STATE );
730 $this->callLoginAuditHook( $state['reqs'], $response, null );
731 return $response;
732 }
733
734 return $response;
735 }
736
737 // Step 2: Primary authentication succeeded, create the User object
738 // (and add the user locally if necessary)
739
740 $user = $this->userFactory->newFromName(
741 (string)$res->username,
742 UserRigorOptions::RIGOR_USABLE
743 );
744 if ( !$user ) {
745 $provider = $this->getAuthenticationProvider( $state['primary'] );
746 throw new DomainException(
747 get_class( $provider ) . " returned an invalid username: {$res->username}"
748 );
749 }
750
751 if ( $elevatedSecurityReq && $elevatedSecurityReq->userId !== $user->getId() ) {
752 // The user was asked to reauthenticate for elevated security but authenticated
753 // as a different user. Does not make sense, maybe some kind of phishing attempt?
754 $this->logger->info( 'Reauthentication failed because of user mismatch', [
755 'oldUserId' => $elevatedSecurityReq->userId,
756 'newUserName' => $res->username,
757 ] );
758 $ret = AuthenticationResponse::newFail( wfMessage( 'authmanager-authn-reauth-switch' ) );
759 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ null, $ret ] );
760 $session->remove( self::AUTHN_STATE );
761 $this->callLoginAuditHook( $state['reqs'], $ret, null );
762 return $ret;
763 }
764
765 if ( !$user->isRegistered() ) {
766 // User doesn't exist locally. Create it.
767 $this->logger->info( 'Auto-creating {user} on login', [
768 'user' => $user->getName(),
769 ] );
770 // Also use $user as performer, because the performer will be used for permission
771 // checks and global rights extensions might add rights based on the username,
772 // even if the user doesn't exist at this point.
773 $status = $this->autoCreateUser( $user, $state['primary'], false, true, $user );
774 if ( !$status->isGood() ) {
775 $response = AuthenticationResponse::newFail(
776 Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
777 );
778 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $response ] );
779 $session->remove( self::AUTHN_STATE );
780
781 // T390051: Don't use the $user provided to ::autoCreateUser for the "user being authenticated
782 // against" for the user provided in the AuthManagerLoginAuthenticateAudit hook run, as
783 // ::autoCreateUser may reset $user to an anon user.
784 $userForHook = $this->userFactory->newFromName(
785 (string)$res->username, UserRigorOptions::RIGOR_USABLE
786 );
787 $this->callLoginAuditHook( $state['reqs'], $response, $userForHook );
788 return $response;
789 }
790 }
791
792 // Step 3: Iterate over all the secondary authentication providers.
793
794 $beginReqs = $state['reqs'];
795
796 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
797 if ( !isset( $state['secondary'][$id] ) ) {
798 // This provider isn't started yet, so we pass it the set
799 // of reqs from beginAuthentication instead of whatever
800 // might have been used by a previous provider in line.
801 $func = 'beginSecondaryAuthentication';
802 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
803 } elseif ( !$state['secondary'][$id] ) {
804 $func = 'continueSecondaryAuthentication';
805 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
806 } else {
807 continue;
808 }
809 switch ( $res->status ) {
810 case AuthenticationResponse::PASS:
811 $this->logger->debug( 'Secondary login with {id} succeeded', [
812 'id' => $id,
813 ] );
814 // fall through
815 case AuthenticationResponse::ABSTAIN:
816 $state['secondary'][$id] = true;
817 break;
818 case AuthenticationResponse::FAIL:
819 $this->logger->debug( 'Login failed in secondary authentication by {id}', [
820 'id' => $id,
821 ] );
822 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $res ] );
823 $session->remove( self::AUTHN_STATE );
824 $this->callLoginAuditHook( $state['reqs'], $res, $user );
825 return $res;
826 case AuthenticationResponse::REDIRECT:
827 case AuthenticationResponse::UI:
828 $this->logger->debug( 'Secondary login with {id} returned {status}', [
829 'id' => $id,
830 'status' => $res->status,
831 ] );
832 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
833 $state['secondary'][$id] = false;
834 $state['continueRequests'] = $res->neededRequests;
835 $session->setSecret( self::AUTHN_STATE, $state );
836 return $res;
837
838 // @codeCoverageIgnoreStart
839 default:
840 throw new DomainException(
841 get_class( $provider ) . "::{$func}() returned $res->status"
842 );
843 // @codeCoverageIgnoreEnd
844 }
845 }
846
847 // Step 4: Authentication complete! Give hook handlers a chance to interrupt, then
848 // set the user in the session and clean up.
849
850 $response = AuthenticationResponse::newPass( $user->getName() );
851 if ( !$this->runVerifyHook( self::ACTION_LOGIN, $user, $response, $state['primary'] ) ) {
852 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $response ] );
853 $session->remove( self::AUTHN_STATE );
854 $this->callLoginAuditHook( $state['reqs'], $response, $user );
855 return $response;
856 }
857 $this->logger->info( 'Login for {user} succeeded from {clientIp}',
858 $this->request->getSecurityLogContext( $user ) );
859
860 // Determine whether to remember the user's login
861 // For reauths, the "remember me" checkbox isn't shown, so don't modify the rememberMe state
862 if ( $elevatedSecurityReq ) {
863 $rememberMe = null;
864 } else {
865 $rememberMeConfig = $this->config->get( MainConfigNames::RememberMe );
866 if ( $rememberMeConfig === RememberMeAuthenticationRequest::ALWAYS_REMEMBER ) {
867 $rememberMe = true;
868 } elseif ( $rememberMeConfig === RememberMeAuthenticationRequest::NEVER_REMEMBER ) {
869 $rememberMe = false;
870 } else {
872 $req = AuthenticationRequest::getRequestByClass(
873 $beginReqs, RememberMeAuthenticationRequest::class
874 );
875
876 // T369668: Before we conclude, let's make sure the user hasn't specified
877 // that they want their login remembered elsewhere like in the central domain.
878 // If the user clicked "remember me" in the central domain, then we should
879 // prioritise that when we call continuePrimaryAuthentication() in the provider
880 // that makes calls continuePrimaryAuthentication(). NOTE: It is the responsibility
881 // of the provider to refresh the "remember me" state that will be applied to
882 // the local wiki.
883 $rememberMe = ( $req && $req->rememberMe ) ||
884 $this->getAuthenticationSessionData( self::REMEMBER_ME );
885 }
886 }
887
888 $loginWasInteractive = $this->getAuthenticationSessionData( self::LOGIN_WAS_INTERACTIVE, true );
889 // If the login was not interactive, don't set the securityLevel in the session
890 // This ensures that only interactive reauthentications count
891 $securityLevel = $loginWasInteractive ? $elevatedSecurityReq?->securityLevel : null;
892
893 $performer = $session->getUser();
894 // If the session is associated with a temporary account user, invalidate its
895 // session and remove the TempUser:name property from the session
896 // This is necessary in order to ensure that the temporary account session is exited
897 // when the user transitions to a logged-in named account
898 if ( $session->getUser()->isTemp() ) {
899 $this->sessionManager->invalidateSessionsForUser( $session->getUser() );
900 $session->remove( 'TempUser:name' );
901 $performer = new User();
902 }
903
904 $this->setSessionDataForUser( $user, $rememberMe, $securityLevel );
905 $this->callMethodOnProviders( self::CALL_ALL, 'postAuthentication', [ $user, $response ] );
906 $session->remove( self::AUTHN_STATE );
907 $this->removeAuthenticationSessionData( null );
908 $this->callLoginAuditHook( $state['reqs'], $response, $user, [
909 'performer' => $performer,
910 'securityLevel' => $securityLevel
911 ] );
912 return $response;
913 } catch ( Exception $ex ) {
914 $session->remove( self::AUTHN_STATE );
915 throw $ex;
916 }
917 }
918
938 public function securitySensitiveOperationStatus( $operation ) {
939 $status = self::SEC_OK;
940
941 $this->logger->debug( __METHOD__ . ': Checking {operation}', [
942 'operation' => $operation,
943 ] );
944
945 $session = $this->request->getSession();
946 $aId = $session->getUser()->getId();
947 if ( $aId === 0 ) {
948 // User isn't authenticated. DWIM?
949 $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
950 $this->logger->info( __METHOD__ . ': Not logged in! {operation} is {status}', [
951 'operation' => $operation,
952 'status' => $status,
953 ] );
954
955 return $status;
956 }
957
958 if ( $session->canSetUser() ) {
959 $id = $session->get( 'AuthManager:lastAuthId' );
960 $lastAuthTimestamps = $session->get( 'AuthManager:lastAuthTimestamps', [] );
961 $last = $lastAuthTimestamps[$operation] ?? null;
962 if ( $id !== $aId || $last === null ) {
963 // Forever ago
964 $timeSinceAuth = PHP_INT_MAX;
965 } else {
966 $timeSinceAuth = max( 0, time() - $last );
967 }
968
969 $thresholds = $this->config->get( MainConfigNames::ReauthenticateTime );
970 if ( isset( $thresholds[$operation] ) ) {
971 $threshold = $thresholds[$operation];
972 } elseif ( isset( $thresholds['default'] ) ) {
973 $threshold = $thresholds['default'];
974 } else {
975 throw new UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
976 }
977
978 if ( $threshold >= 0 && $timeSinceAuth > $threshold ) {
979 $status = self::SEC_REAUTH;
980 }
981 } else {
982 $timeSinceAuth = -1;
983
984 $status = $session->allowSecuritySensitiveOperationIfCannotReauthenticate() ?
985 self::SEC_OK : self::SEC_FAIL;
986 }
987
988 $oldStatus = $status;
989
990 $this->getHookRunner()->onSecuritySensitiveOperationStatus(
991 $status,
992 $operation,
993 $session,
994 $timeSinceAuth
995 );
996
997 if ( $oldStatus === self::SEC_OK && $status !== self::SEC_OK ) {
998 $this->logger->info(
999 __METHOD__ .
1000 ': {operation} escalated from {oldstatus} to {status} for {user} in ' .
1001 'SecuritySensitiveOperationStatusHook hook',
1002 [
1003 'operation' => $operation,
1004 'oldstatus' => $oldStatus,
1005 'status' => $status,
1006 ] + $this->getRequest()->getSecurityLogContext( $session->getUser() )
1007 );
1008 } elseif ( $oldStatus !== self::SEC_OK && $status === self::SEC_OK ) {
1009 $this->logger->info(
1010 __METHOD__ .
1011 ': {operation} downgraded from {oldstatus} to {status} for {user} in ' .
1012 'SecuritySensitiveOperationStatusHook hook',
1013 [
1014 'operation' => $operation,
1015 'oldstatus' => $oldStatus,
1016 'status' => $status,
1017 ] + $this->getRequest()->getSecurityLogContext( $session->getUser() )
1018 );
1019 }
1020
1021 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
1022 if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
1023 $status = self::SEC_FAIL;
1024 }
1025
1026 $this->logger->info( __METHOD__ . ': {operation} is {status} for {user}',
1027 [
1028 'operation' => $operation,
1029 'status' => $status,
1030 ] + $this->getRequest()->getSecurityLogContext( $session->getUser() )
1031 );
1032
1033 return $status;
1034 }
1035
1045 public function userCanAuthenticate( $username ) {
1046 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1047 if ( $provider->testUserCanAuthenticate( $username ) ) {
1048 return true;
1049 }
1050 }
1051 return false;
1052 }
1053
1068 public function normalizeUsername( $username ) {
1069 $ret = [];
1070 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1071 $normalized = $provider->providerNormalizeUsername( $username );
1072 if ( $normalized !== null ) {
1073 $ret[$normalized] = true;
1074 }
1075 }
1076 return array_keys( $ret );
1077 }
1078
1096 $context = RequestContext::getMain();
1097 $user = $context->getRequest()->getSession()->getUser();
1098
1099 $context->setUser( $user );
1100
1101 // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage, MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgLang
1102 global $wgLang;
1103 // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
1104 $wgLang = $context->getLanguage();
1105 }
1106
1107 // endregion -- end of Authentication
1108
1109 /***************************************************************************/
1110 // region Authentication data changing
1120 public function revokeAccessForUser( $username ) {
1121 $this->logger->info( 'Revoking access for {user}', [
1122 'user' => $username,
1123 ] );
1124 $this->callMethodOnProviders( self::CALL_PRIMARY | self::CALL_SECONDARY, 'providerRevokeAccessForUser',
1125 [ $username ]
1126 );
1127 }
1128
1138 public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
1139 if ( $checkData ) {
1140 $status = Status::wrap( $req->validate() );
1141 if ( !$status->isOK() ) {
1142 $this->logger->debug( "Auth data change failed at AuthRequest validation", [
1143 'user' => $req->username,
1144 'reason' => $status->getWikiText( false, false, 'en' ),
1145 ] );
1146 return $status;
1147 }
1148 }
1149
1150 $any = false;
1151 $providers = $this->getPrimaryAuthenticationProviders() +
1152 $this->getSecondaryAuthenticationProviders();
1153
1154 foreach ( $providers as $provider ) {
1155 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
1156 if ( !$status->isGood() ) {
1157 // If status is not good because reset email password last attempt was within
1158 // $wgPasswordReminderResendTime then return good status with throttled-mailpassword value;
1159 // otherwise, return the $status wrapped.
1160 return $status->hasMessage( 'throttled-mailpassword' )
1161 ? Status::newGood( 'throttled-mailpassword' )
1162 : Status::wrap( $status );
1163 }
1164 $any = $any || $status->value !== 'ignored';
1165 }
1166 if ( !$any ) {
1167 return Status::newGood( 'ignored' )
1168 ->warning( 'authmanager-change-not-supported' );
1169 }
1170 return Status::newGood();
1171 }
1172
1190 public function changeAuthenticationData( AuthenticationRequest $req, $isAddition = false ) {
1191 $status = Status::wrap( $req->validate() );
1192 if ( !$status->isOK() ) {
1193 // Caller should have tried with allowsAuthenticationDataChange() first.
1194 throw new LogicException( "Invalid auth data submitted for change for '{$req->username}': "
1195 . $status->getWikiText( false, false, 'en' ) );
1196 }
1197
1198 $this->logger->info( 'Changing authentication data for {user} class {what}', [
1199 'user' => is_string( $req->username ) ? $req->username : '<no name>',
1200 'what' => get_class( $req ),
1201 ] );
1202
1203 $this->callMethodOnProviders( self::CALL_PRIMARY | self::CALL_SECONDARY, 'providerChangeAuthenticationData',
1204 [ $req ]
1205 );
1206
1207 // When the main account's authentication data is changed, invalidate
1208 // all BotPasswords too.
1209 if ( !$isAddition ) {
1210 $this->botPasswordStore->invalidateUserPasswords( (string)$req->username );
1211 }
1212 }
1213
1214 // endregion -- end of Authentication data changing
1215
1216 /***************************************************************************/
1217 // region Account creation
1224 public function canCreateAccounts() {
1225 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1226 switch ( $provider->accountCreationType() ) {
1227 case PrimaryAuthenticationProvider::TYPE_CREATE:
1228 case PrimaryAuthenticationProvider::TYPE_LINK:
1229 return true;
1230 }
1231 }
1232 return false;
1233 }
1234
1243 public function canCreateAccount( $username, $options = [] ) {
1244 // Back compat
1245 if ( is_int( $options ) ) {
1246 $options = [ 'flags' => $options ];
1247 }
1248 $options += [
1249 'flags' => IDBAccessObject::READ_NORMAL,
1250 'creating' => false,
1251 ];
1252 $flags = $options['flags'];
1253
1254 if ( !$this->canCreateAccounts() ) {
1255 return Status::newFatal( 'authmanager-create-disabled' );
1256 }
1257
1258 if ( $this->userExists( $username, $flags ) ) {
1259 return Status::newFatal( 'userexists' );
1260 }
1261
1262 $user = $this->userFactory->newFromName( (string)$username, UserRigorOptions::RIGOR_CREATABLE );
1263 if ( !is_object( $user ) ) {
1264 return Status::newFatal( 'noname' );
1265 } else {
1266 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
1267 if ( $user->isRegistered() ) {
1268 return Status::newFatal( 'userexists' );
1269 }
1270 }
1271
1272 // Denied by providers?
1273 $providers = $this->getPreAuthenticationProviders() +
1274 $this->getPrimaryAuthenticationProviders() +
1275 $this->getSecondaryAuthenticationProviders();
1276 foreach ( $providers as $provider ) {
1277 $status = $provider->testUserForCreation( $user, false, $options );
1278 if ( !$status->isGood() ) {
1279 return Status::wrap( $status );
1280 }
1281 }
1282
1283 return Status::newGood();
1284 }
1285
1291 private function authorizeInternal(
1292 callable $authorizer,
1293 string $action
1294 ): StatusValue {
1295 // Wiki is read-only?
1296 if ( $this->readOnlyMode->isReadOnly() ) {
1297 return StatusValue::newFatal( 'readonlytext', $this->readOnlyMode->getReason() );
1298 }
1299
1300 $permStatus = new PermissionStatus();
1301 if ( !$authorizer(
1302 $action,
1303 SpecialPage::getTitleFor( 'CreateAccount' ),
1304 $permStatus
1305 ) ) {
1306 return $permStatus;
1307 }
1308
1309 $ip = $this->getRequest()->getIP();
1310 if ( $this->blockManager->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1311 return StatusValue::newFatal( 'sorbs_create_account_reason' );
1312 }
1313
1314 return StatusValue::newGood();
1315 }
1316
1328 public function probablyCanCreateAccount( Authority $creator ): StatusValue {
1329 return $this->authorizeInternal(
1330 static function (
1331 string $action,
1332 PageIdentity $target,
1333 PermissionStatus $status
1334 ) use ( $creator ) {
1335 return $creator->probablyCan( $action, $target, $status );
1336 },
1337 'createaccount'
1338 );
1339 }
1340
1352 public function authorizeCreateAccount( Authority $creator ): StatusValue {
1353 return $this->authorizeInternal(
1354 static function (
1355 string $action,
1356 PageIdentity $target,
1357 PermissionStatus $status
1358 ) use ( $creator ) {
1359 return $creator->authorizeWrite( $action, $target, $status );
1360 },
1361 'createaccount'
1362 );
1363 }
1364
1384 public function beginAccountCreation( Authority $creator, array $reqs, $returnToUrl ) {
1385 $session = $this->request->getSession();
1386 if ( $creator->isTemp() ) {
1387 // For a temp account creating a permanent account, we do not want the temporary
1388 // account to be associated with the created permanent account. To avoid this,
1389 // invalidate their sessions, set the session user to a new anonymous user, save it,
1390 // set the request context from the new session user account. (T393628)
1391 $creatorUser = $this->userFactory->newFromUserIdentity( $creator->getUser() );
1392 $this->sessionManager->invalidateSessionsForUser( $creatorUser );
1393 $creator = $this->userFactory->newAnonymous();
1394 $session->setUser( $creator );
1395 // Ensure the temporary account username is also cleared from the session, this is set
1396 // in TempUserCreator::acquireAndStashName
1397 $session->remove( 'TempUser:name' );
1398 $session->save();
1399 $this->setRequestContextUserFromSessionUser();
1400 }
1401 if ( !$this->canCreateAccounts() ) {
1402 // Caller should have called canCreateAccounts()
1403 $session->remove( self::ACCOUNT_CREATION_STATE );
1404 throw new LogicException( 'Account creation is not possible' );
1405 }
1406
1407 try {
1408 $username = AuthenticationRequest::getUsernameFromRequests( $reqs );
1409 } catch ( UnexpectedValueException ) {
1410 $username = null;
1411 }
1412 if ( $username === null ) {
1413 $this->logger->debug( __METHOD__ . ': No username provided' );
1414 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1415 }
1416
1417 // Permissions check
1418 $status = Status::wrap( $this->authorizeCreateAccount( $creator ) );
1419 if ( !$status->isGood() ) {
1420 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1421 'user' => $username,
1422 'creator' => $creator->getUser()->getName(),
1423 'reason' => $status->getWikiText( false, false, 'en' )
1424 ] );
1425 return AuthenticationResponse::newFail( $status->getMessage() );
1426 }
1427
1428 // Avoid deadlocks by placing no shared or exclusive gap locks (T199393)
1429 // As defense in-depth, PrimaryAuthenticationProvider::testUserExists only
1430 // supports READ_NORMAL/READ_LATEST (no support for recency query flags).
1431 $status = $this->canCreateAccount(
1432 $username, [ 'flags' => IDBAccessObject::READ_LATEST, 'creating' => true ]
1433 );
1434 if ( !$status->isGood() ) {
1435 $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1436 'user' => $username,
1437 'creator' => $creator->getUser()->getName(),
1438 'reason' => $status->getWikiText( false, false, 'en' )
1439 ] );
1440 return AuthenticationResponse::newFail( $status->getMessage() );
1441 }
1442
1443 $user = $this->userFactory->newFromName( (string)$username, UserRigorOptions::RIGOR_CREATABLE );
1444 foreach ( $reqs as $req ) {
1445 $req->username = $username;
1446 $req->returnToUrl = $returnToUrl;
1447 if ( $req instanceof UserDataAuthenticationRequest ) {
1448 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable user should be checked and valid here
1449 $status = $req->populateUser( $user );
1450 if ( !$status->isGood() ) {
1451 $status = Status::wrap( $status );
1452 $session->remove( self::ACCOUNT_CREATION_STATE );
1453 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1454 'user' => $user->getName(),
1455 'creator' => $creator->getUser()->getName(),
1456 'reason' => $status->getWikiText( false, false, 'en' ),
1457 ] );
1458 return AuthenticationResponse::newFail( $status->getMessage() );
1459 }
1460 }
1461 }
1462
1463 $this->removeAuthenticationSessionData( null );
1464
1465 $state = [
1466 'username' => $username,
1467 'userid' => 0,
1468 'creatorid' => $creator->getUser()->getId(),
1469 'creatorname' => $creator->getUser()->getName(),
1470 'reqs' => $reqs,
1471 'returnToUrl' => $returnToUrl,
1472 'providerIds' => $this->getProviderIds(),
1473 'primary' => null,
1474 'primaryResponse' => null,
1475 'secondary' => [],
1476 'continueRequests' => [],
1477 'maybeLink' => [],
1478 'ranPreTests' => false,
1479 ];
1480
1481 // Special case: converting a login to an account creation
1482 $req = AuthenticationRequest::getRequestByClass(
1483 $reqs, CreateFromLoginAuthenticationRequest::class
1484 );
1485 if ( $req ) {
1486 $state['maybeLink'] = $req->maybeLink;
1487
1488 if ( $req->createRequest ) {
1489 $reqs[] = $req->createRequest;
1490 $state['reqs'][] = $req->createRequest;
1491 }
1492 }
1493
1494 $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1495 $session->persist();
1496 $this->logger->debug( __METHOD__ . ': Proceeding with account creation for {username} by {creator}', [
1497 'username' => $user->getName(),
1498 'creator' => $creator->getUser()->getName(),
1499 ] );
1500
1501 return $this->continueAccountCreation( $reqs );
1502 }
1503
1509 public function continueAccountCreation( array $reqs ) {
1510 $session = $this->request->getSession();
1511 try {
1512 if ( !$this->canCreateAccounts() ) {
1513 // Caller should have called canCreateAccounts()
1514 $session->remove( self::ACCOUNT_CREATION_STATE );
1515 throw new LogicException( 'Account creation is not possible' );
1516 }
1517
1518 $state = $session->getSecret( self::ACCOUNT_CREATION_STATE );
1519 if ( !is_array( $state ) ) {
1520 return AuthenticationResponse::newFail(
1521 wfMessage( 'authmanager-create-not-in-progress' )
1522 );
1523 }
1524 $state['continueRequests'] = [];
1525
1526 // Step 0: Prepare and validate the input
1527
1528 $user = $this->userFactory->newFromName(
1529 (string)$state['username'],
1530 UserRigorOptions::RIGOR_CREATABLE
1531 );
1532 if ( !is_object( $user ) ) {
1533 $session->remove( self::ACCOUNT_CREATION_STATE );
1534 $this->logger->debug( __METHOD__ . ': Invalid username', [
1535 'user' => $state['username'],
1536 ] );
1537 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1538 }
1539
1540 if ( $state['creatorid'] ) {
1541 $creator = $this->userFactory->newFromId( (int)$state['creatorid'] );
1542 } else {
1543 $creator = $this->userFactory->newAnonymous();
1544 $creator->setName( $state['creatorname'] );
1545 }
1546
1547 if ( $state['providerIds'] !== $this->getProviderIds() ) {
1548 // An inconsistent AuthManagerFilterProviders hook, or site configuration changed
1549 // while the user was in the middle of authentication. The first is a bug, the
1550 // second is rare but expected when deploying a config change. Try handle in a way
1551 // that's useful for both cases.
1552 // @codeCoverageIgnoreStart
1553 MWExceptionHandler::logException( new NormalizedException(
1554 'Authentication failed because of inconsistent provider array',
1555 [ 'old' => json_encode( $state['providerIds'] ), 'new' => json_encode( $this->getProviderIds() ) ]
1556 ) );
1557 $ret = AuthenticationResponse::newFail(
1558 wfMessage( 'authmanager-create-not-in-progress' )
1559 );
1560 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1561 $session->remove( self::ACCOUNT_CREATION_STATE );
1562 return $ret;
1563 // @codeCoverageIgnoreEnd
1564 }
1565
1566 // Avoid account creation races on double submissions
1567 $cache = $this->objectCacheFactory->getLocalClusterInstance();
1568 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1569 if ( !$lock ) {
1570 // Don't clear account creation state for this code path because the process that won the race owns it.
1571 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1572 'user' => $user->getName(),
1573 'creator' => $creator->getName(),
1574 ] );
1575 return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1576 }
1577
1578 // Permissions check
1579 $status = Status::wrap( $this->authorizeCreateAccount( $creator ) );
1580 if ( !$status->isGood() ) {
1581 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1582 'user' => $user->getName(),
1583 'creator' => $creator->getName(),
1584 'reason' => $status->getWikiText( false, false, 'en' )
1585 ] );
1586 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1587 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1588 $session->remove( self::ACCOUNT_CREATION_STATE );
1589 return $ret;
1590 }
1591
1592 // Load from primary DB for existence check
1593 $user->load( IDBAccessObject::READ_LATEST );
1594
1595 if ( $state['userid'] === 0 ) {
1596 if ( $user->isRegistered() ) {
1597 $this->logger->debug( __METHOD__ . ': User exists locally', [
1598 'user' => $user->getName(),
1599 'creator' => $creator->getName(),
1600 ] );
1601 $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1602 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1603 $session->remove( self::ACCOUNT_CREATION_STATE );
1604 return $ret;
1605 }
1606 } else {
1607 if ( !$user->isRegistered() ) {
1608 $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1609 'user' => $user->getName(),
1610 'creator' => $creator->getName(),
1611 'expected_id' => $state['userid'],
1612 ] );
1613 throw new UnexpectedValueException(
1614 "User \"{$state['username']}\" should exist now, but doesn't!"
1615 );
1616 }
1617 if ( $user->getId() !== $state['userid'] ) {
1618 $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1619 'user' => $user->getName(),
1620 'creator' => $creator->getName(),
1621 'expected_id' => $state['userid'],
1622 'actual_id' => $user->getId(),
1623 ] );
1624 throw new UnexpectedValueException(
1625 "User \"{$state['username']}\" exists, but " .
1626 "ID {$user->getId()} !== {$state['userid']}!"
1627 );
1628 }
1629 }
1630 foreach ( $state['reqs'] as $req ) {
1631 if ( $req instanceof UserDataAuthenticationRequest ) {
1632 $status = $req->populateUser( $user );
1633 if ( !$status->isGood() ) {
1634 // This should never happen...
1635 $status = Status::wrap( $status );
1636 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1637 'user' => $user->getName(),
1638 'creator' => $creator->getName(),
1639 'reason' => $status->getWikiText( false, false, 'en' ),
1640 ] );
1641 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1642 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1643 [ $user, $creator, $ret ]
1644 );
1645 $session->remove( self::ACCOUNT_CREATION_STATE );
1646 return $ret;
1647 }
1648 }
1649 }
1650
1651 $status = Status::newGood();
1652 foreach ( $reqs as $req ) {
1653 $req->returnToUrl = $state['returnToUrl'];
1654 $req->username = $state['username'];
1655 $status->merge( $req->validate() );
1656 }
1657 if ( !$status->isOK() ) {
1658 $this->logger->debug( "Account creation failed at AuthRequest validation", [
1659 'user' => $user->getName(),
1660 'creator' => $creator->getName(),
1661 'reason' => $status->getWikiText( false, false, 'en' ),
1662 ] );
1663 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1664 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1665 $session->remove( self::ACCOUNT_CREATION_STATE );
1666 return $ret;
1667 }
1668
1669 // Run pre-creation tests, if we haven't already
1670 if ( !$state['ranPreTests'] ) {
1671 $providers = $this->getPreAuthenticationProviders() +
1672 $this->getPrimaryAuthenticationProviders() +
1673 $this->getSecondaryAuthenticationProviders();
1674 foreach ( $providers as $id => $provider ) {
1675 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1676 if ( !$status->isGood() ) {
1677 $this->logger->debug( __METHOD__ . ': Fail in pre-authentication by {id}', [
1678 'id' => $id,
1679 'user' => $user->getName(),
1680 'creator' => $creator->getName(),
1681 ] );
1682 $ret = AuthenticationResponse::newFail(
1683 Status::wrap( $status )->getMessage()
1684 );
1685 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1686 [ $user, $creator, $ret ]
1687 );
1688 $session->remove( self::ACCOUNT_CREATION_STATE );
1689 return $ret;
1690 }
1691 }
1692
1693 $state['ranPreTests'] = true;
1694 }
1695
1696 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1697
1698 if ( $state['primary'] === null ) {
1699 // We haven't picked a PrimaryAuthenticationProvider yet
1700 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1701 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1702 continue;
1703 }
1704 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1705 switch ( $res->status ) {
1706 case AuthenticationResponse::PASS:
1707 $this->logger->debug( __METHOD__ . ': Primary creation passed by {id}', [
1708 'id' => $id,
1709 'user' => $user->getName(),
1710 'creator' => $creator->getName(),
1711 ] );
1712 $state['primary'] = $id;
1713 $state['primaryResponse'] = $res;
1714 break 2;
1715 case AuthenticationResponse::FAIL:
1716 $this->logger->debug( __METHOD__ . ': Primary creation failed by {id}', [
1717 'id' => $id,
1718 'user' => $user->getName(),
1719 'creator' => $creator->getName(),
1720 ] );
1721 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1722 [ $user, $creator, $res ]
1723 );
1724 $session->remove( self::ACCOUNT_CREATION_STATE );
1725 return $res;
1726 case AuthenticationResponse::ABSTAIN:
1727 // Continue loop
1728 break;
1729 case AuthenticationResponse::REDIRECT:
1730 case AuthenticationResponse::UI:
1731 $this->logger->debug( __METHOD__ . ': Primary creation {status} by {id}', [
1732 'status' => $res->status,
1733 'id' => $id,
1734 'user' => $user->getName(),
1735 'creator' => $creator->getName(),
1736 ] );
1737 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1738 $state['primary'] = $id;
1739 $state['continueRequests'] = $res->neededRequests;
1740 $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1741 return $res;
1742
1743 // @codeCoverageIgnoreStart
1744 default:
1745 throw new DomainException(
1746 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1747 );
1748 // @codeCoverageIgnoreEnd
1749 }
1750 }
1751 if ( $state['primary'] === null ) {
1752 $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1753 'user' => $user->getName(),
1754 'creator' => $creator->getName(),
1755 ] );
1756 $ret = AuthenticationResponse::newFail(
1757 wfMessage( 'authmanager-create-no-primary' )
1758 );
1759 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1760 $session->remove( self::ACCOUNT_CREATION_STATE );
1761 return $ret;
1762 }
1763 } elseif ( $state['primaryResponse'] === null ) {
1764 $provider = $this->getAuthenticationProvider( $state['primary'] );
1765 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1766 // Configuration changed? Force them to start over.
1767 // @codeCoverageIgnoreStart
1768 $ret = AuthenticationResponse::newFail(
1769 wfMessage( 'authmanager-create-not-in-progress' )
1770 );
1771 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1772 $session->remove( self::ACCOUNT_CREATION_STATE );
1773 return $ret;
1774 // @codeCoverageIgnoreEnd
1775 }
1776 $id = $provider->getUniqueId();
1777 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1778 switch ( $res->status ) {
1779 case AuthenticationResponse::PASS:
1780 $this->logger->debug( __METHOD__ . ': Primary creation passed by {id}', [
1781 'id' => $id,
1782 'user' => $user->getName(),
1783 'creator' => $creator->getName(),
1784 ] );
1785 $state['primaryResponse'] = $res;
1786 break;
1787 case AuthenticationResponse::FAIL:
1788 $this->logger->debug( __METHOD__ . ': Primary creation failed by {id}', [
1789 'id' => $id,
1790 'user' => $user->getName(),
1791 'creator' => $creator->getName(),
1792 ] );
1793 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1794 [ $user, $creator, $res ]
1795 );
1796 $session->remove( self::ACCOUNT_CREATION_STATE );
1797 return $res;
1798 case AuthenticationResponse::REDIRECT:
1799 case AuthenticationResponse::UI:
1800 $this->logger->debug( __METHOD__ . ': Primary creation {status} by {id}', [
1801 'status' => $res->status,
1802 'id' => $id,
1803 'user' => $user->getName(),
1804 'creator' => $creator->getName(),
1805 ] );
1806 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1807 $state['continueRequests'] = $res->neededRequests;
1808 $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1809 return $res;
1810 default:
1811 throw new DomainException(
1812 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1813 );
1814 }
1815 }
1816
1817 // Step 2: Primary authentication succeeded. Give hook handlers a chance to interrupt,
1818 // then create the User object and add the user locally.
1819
1820 if ( $state['userid'] === 0 ) {
1821 $response = $state['primaryResponse'];
1822 if ( !$this->runVerifyHook( self::ACTION_CREATE, $user, $response, $state['primary'] ) ) {
1823 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation',
1824 [ $user, $creator, $response ]
1825 );
1826 $session->remove( self::ACCOUNT_CREATION_STATE );
1827 return $response;
1828 }
1829 $this->logger->info( 'Creating user {user} during account creation', [
1830 'user' => $user->getName(),
1831 'creator' => $creator->getName(),
1832 ] );
1833 $status = $user->addToDatabase();
1834 if ( !$status->isOK() ) {
1835 // @codeCoverageIgnoreStart
1836 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1837 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1838 $session->remove( self::ACCOUNT_CREATION_STATE );
1839 return $ret;
1840 // @codeCoverageIgnoreEnd
1841 }
1842 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1843 $this->getHookRunner()->onLocalUserCreated( $user, false );
1844 $this->notificationService->notify(
1845 new WelcomeNotification( $user ),
1846 new RecipientSet( [ $user ] )
1847 );
1848 $user->saveSettings();
1849 $state['userid'] = $user->getId();
1850
1851 // Update user count
1852 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1853
1854 // Watch user's userpage and talk page
1855 $this->watchlistManager->addWatchIgnoringRights( $user, $user->getUserPage() );
1856
1857 // Inform the provider
1858 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable
1859 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1860
1861 // Log the creation
1862 if ( $this->config->get( MainConfigNames::NewUserLog ) ) {
1863 $isNamed = $creator->isNamed();
1864 $logEntry = new ManualLogEntry(
1865 'newusers',
1866 $logSubtype ?: ( $isNamed ? 'create2' : 'create' )
1867 );
1868 $logEntry->setPerformer( $isNamed ? $creator : $user );
1869 $logEntry->setTarget( $user->getUserPage() );
1871 $req = AuthenticationRequest::getRequestByClass(
1872 $state['reqs'], CreationReasonAuthenticationRequest::class
1873 );
1874 $logEntry->setComment( $req ? $req->reason : '' );
1875 $logEntry->setParameters( [
1876 '4::userid' => $user->getId(),
1877 ] );
1878 $logid = $logEntry->insert();
1879 $logEntry->publish( $logid );
1880 }
1881 }
1882
1883 // Step 3: Iterate over all the secondary authentication providers.
1884
1885 $beginReqs = $state['reqs'];
1886
1887 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1888 if ( !isset( $state['secondary'][$id] ) ) {
1889 // This provider isn't started yet, so we pass it the set
1890 // of reqs from beginAuthentication instead of whatever
1891 // might have been used by a previous provider in line.
1892 $func = 'beginSecondaryAccountCreation';
1893 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1894 } elseif ( !$state['secondary'][$id] ) {
1895 $func = 'continueSecondaryAccountCreation';
1896 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1897 } else {
1898 continue;
1899 }
1900 switch ( $res->status ) {
1901 case AuthenticationResponse::PASS:
1902 $this->logger->debug( __METHOD__ . ': Secondary creation passed by {id}', [
1903 'id' => $id,
1904 'user' => $user->getName(),
1905 'creator' => $creator->getName(),
1906 ] );
1907 // fall through
1908 case AuthenticationResponse::ABSTAIN:
1909 $state['secondary'][$id] = true;
1910 break;
1911 case AuthenticationResponse::REDIRECT:
1912 case AuthenticationResponse::UI:
1913 $this->logger->debug( __METHOD__ . ': Secondary creation {status} by {id}', [
1914 'status' => $res->status,
1915 'id' => $id,
1916 'user' => $user->getName(),
1917 'creator' => $creator->getName(),
1918 ] );
1919 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1920 $state['secondary'][$id] = false;
1921 $state['continueRequests'] = $res->neededRequests;
1922 $session->setSecret( self::ACCOUNT_CREATION_STATE, $state );
1923 return $res;
1924 case AuthenticationResponse::FAIL:
1925 throw new DomainException(
1926 get_class( $provider ) . "::{$func}() returned $res->status." .
1927 ' Secondary providers are not allowed to fail account creation, that' .
1928 ' should have been done via testForAccountCreation().'
1929 );
1930 // @codeCoverageIgnoreStart
1931 default:
1932 throw new DomainException(
1933 get_class( $provider ) . "::{$func}() returned $res->status"
1934 );
1935 // @codeCoverageIgnoreEnd
1936 }
1937 }
1938
1939 $id = $user->getId();
1940 $name = $user->getName();
1941 $req = new CreatedAccountAuthenticationRequest( $id, $name );
1942 $ret = AuthenticationResponse::newPass( $name );
1943 $ret->loginRequest = $req;
1944 $this->createdAccountAuthenticationRequests[] = $req;
1945
1946 $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1947 'user' => $user->getName(),
1948 'creator' => $creator->getName(),
1949 ] );
1950
1951 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountCreation', [ $user, $creator, $ret ] );
1952 $session->remove( self::ACCOUNT_CREATION_STATE );
1953 $this->removeAuthenticationSessionData( null );
1954 return $ret;
1955 } catch ( Exception $ex ) {
1956 $session->remove( self::ACCOUNT_CREATION_STATE );
1957 throw $ex;
1958 }
1959 }
1960
1968 private function logAutocreationAttempt( Status $status, User $targetUser, $source, $login ) {
1969 if ( $status->isOK() && !$status->isGood() ) {
1970 return; // user already existed, no need to log
1971 }
1972
1973 $firstMessage = $status->getMessages( 'error' )[0] ?? $status->getMessages( 'warning' )[0] ?? null;
1974
1975 $this->authEventsLogger->info( 'Autocreation attempt', [
1976 'event' => 'autocreate',
1977 'successful' => $status->isGood(),
1978 'status' => $firstMessage ? $firstMessage->getKey() : '-',
1979 'accountType' => $this->identityUtils->getShortUserTypeInternal( $targetUser ),
1980 'source' => $source,
1981 'login' => $login,
1982 ] );
1983 }
1984
1995 private function autocreatingTempUserToAppealBlock(
1996 StatusValue $status,
1997 string $source,
1998 User $performer
1999 ): bool {
2000 $block = $status instanceof PermissionStatus ? $status->getBlock() : null;
2001 if ( !( $block instanceof AbstractBlock ) ) {
2002 return false;
2003 }
2004 $title = RequestContext::getMain()->getTitle();
2005 return $title && $title->isSpecial( 'Mytalk' ) &&
2006 $source === self::AUTOCREATE_SOURCE_TEMP &&
2007 $performer->isAnon() &&
2008 count( $status->getErrors() ) === 1 &&
2009 !$block->appliesToUsertalk( $performer->getTalkPage() );
2010 }
2011
2037 public function autoCreateUser(
2038 User $user,
2039 $source,
2040 $login = true,
2041 $log = true,
2042 ?Authority $performer = null,
2043 array $tags = []
2044 ) {
2045 $validSources = [
2046 self::AUTOCREATE_SOURCE_SESSION,
2047 self::AUTOCREATE_SOURCE_MAINT,
2048 self::AUTOCREATE_SOURCE_TEMP
2049 ];
2050 if ( !in_array( $source, $validSources, true )
2051 && !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
2052 ) {
2053 throw new InvalidArgumentException( "Unknown auto-creation source: $source" );
2054 }
2055
2056 $username = $user->getName();
2057
2058 // Try the local user from the replica DB, then fall back to the primary.
2059 $localUserIdentity = $this->userIdentityLookup->getUserIdentityByName( $username );
2060 // @codeCoverageIgnoreStart
2061 if ( ( !$localUserIdentity || !$localUserIdentity->isRegistered() )
2062 && $this->loadBalancer->getReaderIndex() !== 0
2063 ) {
2064 $localUserIdentity = $this->userIdentityLookup->getUserIdentityByName(
2065 $username, IDBAccessObject::READ_LATEST
2066 );
2067 }
2068 // @codeCoverageIgnoreEnd
2069 $localId = ( $localUserIdentity && $localUserIdentity->isRegistered() )
2070 ? $localUserIdentity->getId()
2071 : null;
2072
2073 if ( $localId ) {
2074 $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
2075 'username' => $username,
2076 ] );
2077 $user->setId( $localId );
2078
2079 // Can't rely on a replica read, not even when getUserIdentityByName() used
2080 // READ_NORMAL, because that method has an in-process cache not shared
2081 // with loadFromId.
2082 $user->loadFromId( IDBAccessObject::READ_LATEST );
2083 if ( $login ) {
2084 $remember = $source === self::AUTOCREATE_SOURCE_TEMP;
2085 $this->setSessionDataForUser( $user, $remember, null );
2086 }
2087 return Status::newGood()->warning( 'userexists' );
2088 }
2089
2090 // Wiki is read-only?
2091 if ( $this->readOnlyMode->isReadOnly() ) {
2092 $reason = $this->readOnlyMode->getReason();
2093 $this->logger->debug( __METHOD__ . ': denied because of read only mode: {reason}', [
2094 'username' => $username,
2095 'reason' => $reason,
2096 ] );
2097 $user->setId( 0 );
2098 $user->loadFromId();
2099 $fatalStatus = Status::newFatal( 'readonlytext', $reason );
2100 $this->logAutocreationAttempt( $fatalStatus, $user, $source, $login );
2101 return $fatalStatus;
2102 }
2103
2104 // If there is a non-anonymous performer, don't use their session
2105 $session = null;
2106 $performer ??= $user;
2107 if ( !$performer->isRegistered() || $performer->getUser()->equals( $user ) ) {
2108 // $performer is anonymous, or refers to the same user as $user (i.e., this isn't
2109 // an autocreation attempt via Special:CreateLocalAccount or by the maintenance script)
2110 $session = $this->request->getSession();
2111 }
2112
2113 // Is the username usable? (Previously isCreatable() was checked here but
2114 // that doesn't work with auto-creation of TempUser accounts by CentralAuth)
2115 if ( !$this->userNameUtils->isUsable( $username ) ) {
2116 $this->logger->debug( __METHOD__ . ': name "{username}" is not usable', [
2117 'username' => $username,
2118 ] );
2119 $user->setId( 0 );
2120 $user->loadFromId();
2121 $fatalStatus = Status::newFatal( 'noname' );
2122 $this->logAutocreationAttempt( $fatalStatus, $user, $source, $login );
2123 return $fatalStatus;
2124 }
2125
2126 // Is the IP user able to create accounts?
2127 $bypassAuthorization = $session && $session->getProvider()->canAlwaysAutocreate();
2128 if ( $source !== self::AUTOCREATE_SOURCE_MAINT && !$bypassAuthorization ) {
2129 $status = $this->authorizeAutoCreateAccount( $performer );
2130 if ( !$status->isOk() ) {
2131 if ( $this->autocreatingTempUserToAppealBlock( $status, $source, $performer ) ) {
2132 $this->logger->info( __METHOD__ . ': autocreating temporary user to appeal a block', [
2133 'username' => $username,
2134 'creator' => $performer->getUser()->getName(),
2135 ] );
2136 } else {
2137 $this->logger->debug( __METHOD__ . ': cannot create or autocreate accounts', [
2138 'username' => $username,
2139 'creator' => $performer->getUser()->getName(),
2140 ] );
2141 $user->setId( 0 );
2142 $user->loadFromId();
2143 $statusWrapped = Status::wrap( $status );
2144 $this->logAutocreationAttempt( $statusWrapped, $user, $source, $login );
2145 return $statusWrapped;
2146 }
2147 }
2148 }
2149
2150 // Avoid account creation races on double submissions
2151 $cache = $this->objectCacheFactory->getLocalClusterInstance();
2152 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2153 if ( !$lock ) {
2154 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
2155 'user' => $username,
2156 ] );
2157 $user->setId( 0 );
2158 $user->loadFromId();
2159 $status = Status::newFatal( 'usernameinprogress' );
2160 $this->logAutocreationAttempt( $status, $user, $source, $login );
2161 return $status;
2162 }
2163
2164 // Denied by providers?
2165 $options = [
2166 'flags' => IDBAccessObject::READ_LATEST,
2167 'creating' => true,
2168 'canAlwaysAutocreate' => $session && $session->getProvider()->canAlwaysAutocreate(),
2169 'performer' => $performer,
2170 ];
2171 $providers = $this->getPreAuthenticationProviders() +
2172 $this->getPrimaryAuthenticationProviders() +
2173 $this->getSecondaryAuthenticationProviders();
2174 foreach ( $providers as $provider ) {
2175 $status = $provider->testUserForCreation( $user, $source, $options );
2176 if ( !$status->isGood() ) {
2177 $ret = Status::wrap( $status );
2178 $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
2179 'username' => $username,
2180 'reason' => $ret->getWikiText( false, false, 'en' ),
2181 ] );
2182 $user->setId( 0 );
2183 $user->loadFromId();
2184 $this->logAutocreationAttempt( $ret, $user, $source, $login );
2185 return $ret;
2186 }
2187 }
2188
2189 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2190 if ( $cache->get( $backoffKey ) ) {
2191 $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
2192 'username' => $username,
2193 ] );
2194 $user->setId( 0 );
2195 $user->loadFromId();
2196 $status = Status::newFatal( 'authmanager-autocreate-exception' );
2197 $this->logAutocreationAttempt( $status, $user, $source, $login );
2198 return $status;
2199
2200 }
2201
2202 // Checks passed, create the user...
2203 $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
2204 $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
2205 'username' => $username,
2206 'from' => $from
2207 ] + $this->request->getSecurityLogContext( $performer->getUser() )
2208 );
2209
2210 // Ignore warnings about primary connections/writes...hard to avoid here
2211 $fname = __METHOD__;
2212 $trxLimits = $this->config->get( MainConfigNames::TrxProfilerLimits );
2213 $trxProfiler = Profiler::instance()->getTransactionProfiler();
2214 $trxProfiler->redefineExpectations( $trxLimits['POST'], $fname );
2215 DeferredUpdates::addCallableUpdate( static function () use ( $trxProfiler, $trxLimits, $fname ) {
2216 $trxProfiler->redefineExpectations( $trxLimits['PostSend-POST'], $fname );
2217 } );
2218
2219 try {
2220 $status = $user->addToDatabase();
2221 if ( !$status->isOK() ) {
2222 // Double-check for a race condition (T70012). We make use of the fact that when
2223 // addToDatabase fails due to the user already existing, the user object gets loaded.
2224 if ( $user->getId() ) {
2225 $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
2226 'username' => $username,
2227 ] );
2228 if ( $login ) {
2229 $remember = $source === self::AUTOCREATE_SOURCE_TEMP;
2230 $this->setSessionDataForUser( $user, $remember, null );
2231 }
2232 $status = Status::newGood()->warning( 'userexists' );
2233 } else {
2234 $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
2235 'username' => $username,
2236 'msg' => $status->getWikiText( false, false, 'en' )
2237 ] );
2238 $user->setId( 0 );
2239 $user->loadFromId();
2240 }
2241 $this->logAutocreationAttempt( $status, $user, $source, $login );
2242 return $status;
2243 }
2244 } catch ( Exception $ex ) {
2245 $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
2246 'username' => $username,
2247 'exception' => $ex,
2248 ] );
2249 // Do not keep throwing errors for a while
2250 $cache->set( $backoffKey, 1, 600 );
2251 // Bubble up error; which should normally trigger DB rollbacks
2252 throw $ex;
2253 }
2254
2255 $this->setDefaultUserOptions( $user, false );
2256
2257 // Inform the providers
2258 $this->callMethodOnProviders( self::CALL_PRIMARY | self::CALL_SECONDARY, 'autoCreatedAccount',
2259 [ $user, $source ]
2260 );
2261
2262 $this->getHookRunner()->onLocalUserCreated( $user, true );
2263 $user->saveSettings();
2264
2265 // Update user count
2266 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
2267 // Watch user's userpage and talk page (except temp users)
2268 if ( $source !== self::AUTOCREATE_SOURCE_TEMP ) {
2269 DeferredUpdates::addCallableUpdate( function () use ( $user ) {
2270 $this->watchlistManager->addWatchIgnoringRights( $user, $user->getUserPage() );
2271 } );
2272 }
2273
2274 // Log the creation
2275 if ( $this->config->get( MainConfigNames::NewUserLog ) && $log ) {
2276 $logEntry = new ManualLogEntry( 'newusers', 'autocreate' );
2277 $logEntry->setPerformer( $user );
2278 $logEntry->setTarget( $user->getUserPage() );
2279 $logEntry->setComment( '' );
2280 $logEntry->setParameters( [
2281 '4::userid' => $user->getId(),
2282 ] );
2283 $logid = $logEntry->insert();
2284
2285 if ( $tags !== [] ) {
2286 // ManualLogEntry::insert doesn't insert tags
2287 $this->changeTagsStore->addTags( $tags, null, null, $logid );
2288 }
2289 }
2290
2291 if ( $login ) {
2292 $remember = $source === self::AUTOCREATE_SOURCE_TEMP;
2293 $this->setSessionDataForUser( $user, $remember, null );
2294 }
2295 $retStatus = Status::newGood();
2296 $this->logAutocreationAttempt( $retStatus, $user, $source, $login );
2297 return $retStatus;
2298 }
2299
2307 private function authorizeAutoCreateAccount( Authority $creator ) {
2308 return $this->authorizeInternal(
2309 static function (
2310 string $action,
2311 PageIdentity $target,
2312 PermissionStatus $status
2313 ) use ( $creator ) {
2314 return $creator->authorizeWrite( $action, $target, $status );
2315 },
2316 'autocreateaccount'
2317 );
2318 }
2319
2320 // endregion -- end of Account creation
2321
2322 /***************************************************************************/
2323 // region Account linking
2330 public function canLinkAccounts() {
2331 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2332 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2333 return true;
2334 }
2335 }
2336 return false;
2337 }
2338
2348 public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
2349 $session = $this->request->getSession();
2350 $session->remove( self::ACCOUNT_LINK_STATE );
2351
2352 if ( !$this->canLinkAccounts() ) {
2353 // Caller should have called canLinkAccounts()
2354 throw new LogicException( 'Account linking is not possible' );
2355 }
2356
2357 if ( !$user->isRegistered() ) {
2358 if ( !$this->userNameUtils->isUsable( $user->getName() ) ) {
2359 $msg = wfMessage( 'noname' );
2360 } else {
2361 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
2362 }
2363 return AuthenticationResponse::newFail( $msg );
2364 }
2365
2366 $status = Status::newGood();
2367 foreach ( $reqs as $req ) {
2368 $req->username = $user->getName();
2369 $req->returnToUrl = $returnToUrl;
2370 $status->merge( $req->validate() );
2371 }
2372 if ( !$status->isOK() ) {
2373 $this->logger->debug( "Account linking failed at AuthRequest validation", [
2374 'user' => $user->getName(),
2375 'reason' => $status->getWikiText( false, false, 'en' ),
2376 ] );
2377 $ret = AuthenticationResponse::newFail( $status->getMessage() );
2378 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2379 return $ret;
2380 }
2381
2382 $this->removeAuthenticationSessionData( null );
2383
2384 $providers = $this->getPreAuthenticationProviders();
2385 foreach ( $providers as $id => $provider ) {
2386 $status = $provider->testForAccountLink( $user );
2387 if ( !$status->isGood() ) {
2388 $this->logger->debug( __METHOD__ . ': Account linking pre-check failed by {id}', [
2389 'id' => $id,
2390 'user' => $user->getName(),
2391 ] );
2392 $ret = AuthenticationResponse::newFail(
2393 Status::wrap( $status )->getMessage()
2394 );
2395 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2396 return $ret;
2397 }
2398 }
2399
2400 $state = [
2401 'username' => $user->getName(),
2402 'userid' => $user->getId(),
2403 'returnToUrl' => $returnToUrl,
2404 'providerIds' => $this->getProviderIds(),
2405 'primary' => null,
2406 'continueRequests' => [],
2407 ];
2408
2409 $providers = $this->getPrimaryAuthenticationProviders();
2410 foreach ( $providers as $id => $provider ) {
2411 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
2412 continue;
2413 }
2414
2415 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
2416 switch ( $res->status ) {
2417 case AuthenticationResponse::PASS:
2418 $this->logger->info( 'Account linked to {user} by {id}', [
2419 'id' => $id,
2420 'user' => $user->getName(),
2421 ] );
2422 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2423 [ $user, $res ]
2424 );
2425 return $res;
2426
2427 case AuthenticationResponse::FAIL:
2428 $this->logger->debug( __METHOD__ . ': Account linking failed by {id}', [
2429 'id' => $id,
2430 'user' => $user->getName(),
2431 ] );
2432 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2433 [ $user, $res ]
2434 );
2435 return $res;
2436
2437 case AuthenticationResponse::ABSTAIN:
2438 // Continue loop
2439 break;
2440
2441 case AuthenticationResponse::REDIRECT:
2442 case AuthenticationResponse::UI:
2443 $this->logger->debug( __METHOD__ . ': Account linking {status} by {id}', [
2444 'status' => $res->status,
2445 'id' => $id,
2446 'user' => $user->getName(),
2447 ] );
2448 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
2449 $state['primary'] = $id;
2450 $state['continueRequests'] = $res->neededRequests;
2451 $session->setSecret( self::ACCOUNT_LINK_STATE, $state );
2452 $session->persist();
2453 return $res;
2454
2455 // @codeCoverageIgnoreStart
2456 default:
2457 throw new DomainException(
2458 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
2459 );
2460 // @codeCoverageIgnoreEnd
2461 }
2462 }
2463
2464 $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
2465 'user' => $user->getName(),
2466 ] );
2467 $ret = AuthenticationResponse::newFail(
2468 wfMessage( 'authmanager-link-no-primary' )
2469 );
2470 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2471 return $ret;
2472 }
2473
2479 public function continueAccountLink( array $reqs ) {
2480 $session = $this->request->getSession();
2481 try {
2482 if ( !$this->canLinkAccounts() ) {
2483 // Caller should have called canLinkAccounts()
2484 $session->remove( self::ACCOUNT_LINK_STATE );
2485 throw new LogicException( 'Account linking is not possible' );
2486 }
2487
2488 $state = $session->getSecret( self::ACCOUNT_LINK_STATE );
2489 if ( !is_array( $state ) ) {
2490 return AuthenticationResponse::newFail(
2491 wfMessage( 'authmanager-link-not-in-progress' )
2492 );
2493 }
2494 $state['continueRequests'] = [];
2495
2496 // Step 0: Prepare and validate the input
2497
2498 $user = $this->userFactory->newFromName(
2499 (string)$state['username'],
2500 UserRigorOptions::RIGOR_USABLE
2501 );
2502 if ( !is_object( $user ) ) {
2503 $session->remove( self::ACCOUNT_LINK_STATE );
2504 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
2505 }
2506 if ( $user->getId() !== $state['userid'] ) {
2507 throw new UnexpectedValueException(
2508 "User \"{$state['username']}\" is valid, but " .
2509 "ID {$user->getId()} !== {$state['userid']}!"
2510 );
2511 }
2512
2513 if ( $state['providerIds'] !== $this->getProviderIds() ) {
2514 // An inconsistent AuthManagerFilterProviders hook, or site configuration changed
2515 // while the user was in the middle of authentication. The first is a bug, the
2516 // second is rare but expected when deploying a config change. Try handle in a way
2517 // that's useful for both cases.
2518 // @codeCoverageIgnoreStart
2519 MWExceptionHandler::logException( new NormalizedException(
2520 'Authentication failed because of inconsistent provider array',
2521 [ 'old' => json_encode( $state['providerIds'] ), 'new' => json_encode( $this->getProviderIds() ) ]
2522 ) );
2523 $ret = AuthenticationResponse::newFail(
2524 wfMessage( 'authmanager-link-not-in-progress' )
2525 );
2526 $this->callMethodOnProviders( self::CALL_ALL, 'postAccountLink', [ $user, $ret ] );
2527 $session->remove( self::ACCOUNT_LINK_STATE );
2528 return $ret;
2529 // @codeCoverageIgnoreEnd
2530 }
2531
2532 $status = Status::newGood();
2533 foreach ( $reqs as $req ) {
2534 $req->username = $state['username'];
2535 $req->returnToUrl = $state['returnToUrl'];
2536 $status->merge( $req->validate() );
2537 }
2538 if ( !$status->isOK() ) {
2539 $this->logger->debug( "Account linking failed at AuthRequest validation", [
2540 'user' => $user->getName(),
2541 'reason' => $status->getWikiText( false, false, 'en' ),
2542 ] );
2543 $ret = AuthenticationResponse::newFail( $status->getMessage() );
2544 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2545 $session->remove( self::ACCOUNT_LINK_STATE );
2546 return $ret;
2547 }
2548
2549 // Step 1: Call the primary again until it succeeds
2550
2551 $provider = $this->getAuthenticationProvider( $state['primary'] );
2552 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
2553 // Configuration changed? Force them to start over.
2554 // @codeCoverageIgnoreStart
2555 $ret = AuthenticationResponse::newFail(
2556 wfMessage( 'authmanager-link-not-in-progress' )
2557 );
2558 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink', [ $user, $ret ] );
2559 $session->remove( self::ACCOUNT_LINK_STATE );
2560 return $ret;
2561 // @codeCoverageIgnoreEnd
2562 }
2563 $id = $provider->getUniqueId();
2564 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
2565 switch ( $res->status ) {
2566 case AuthenticationResponse::PASS:
2567 $this->logger->info( 'Account linked to {user} by {id}', [
2568 'id' => $id,
2569 'user' => $user->getName(),
2570 ] );
2571 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2572 [ $user, $res ]
2573 );
2574 $session->remove( self::ACCOUNT_LINK_STATE );
2575 return $res;
2576 case AuthenticationResponse::FAIL:
2577 $this->logger->debug( __METHOD__ . ': Account linking failed by {id}', [
2578 'id' => $id,
2579 'user' => $user->getName(),
2580 ] );
2581 $this->callMethodOnProviders( self::CALL_PRE | self::CALL_PRIMARY, 'postAccountLink',
2582 [ $user, $res ]
2583 );
2584 $session->remove( self::ACCOUNT_LINK_STATE );
2585 return $res;
2586 case AuthenticationResponse::REDIRECT:
2587 case AuthenticationResponse::UI:
2588 $this->logger->debug( __METHOD__ . ': Account linking {status} by {id}', [
2589 'status' => $res->status,
2590 'id' => $id,
2591 'user' => $user->getName(),
2592 ] );
2593 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
2594 $state['continueRequests'] = $res->neededRequests;
2595 $session->setSecret( self::ACCOUNT_LINK_STATE, $state );
2596 return $res;
2597 default:
2598 throw new DomainException(
2599 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
2600 );
2601 }
2602 } catch ( Exception $ex ) {
2603 $session->remove( self::ACCOUNT_LINK_STATE );
2604 throw $ex;
2605 }
2606 }
2607
2608 // endregion -- end of Account linking
2609
2610 /***************************************************************************/
2611 // region Information methods
2636 public function getAuthenticationRequests( $action, ?UserIdentity $user = null, array $options = [] ) {
2637 $options = [ 'securityLevel' => $options['securityLevel'] ?? null ];
2638 $providerAction = $action;
2639
2640 if ( $options['securityLevel'] !== null && $action !== self::ACTION_LOGIN ) {
2641 throw new InvalidArgumentException( "The 'securityLevel' option can only be used for the "
2642 . "'login' action, not '$action'" );
2643 } elseif ( $options['securityLevel'] !== null
2644 && $this->getRequest()->getSession()->getUser()->isAnon()
2645 ) {
2646 throw new InvalidArgumentException( "The 'securityLevel' option can only be used when the "
2647 . 'current user is logged in' );
2648 }
2649
2650 // Figure out which providers to query
2651 switch ( $action ) {
2652 case self::ACTION_LOGIN:
2653 case self::ACTION_CREATE:
2654 $providers = $this->getPreAuthenticationProviders() +
2655 $this->getPrimaryAuthenticationProviders() +
2656 $this->getSecondaryAuthenticationProviders();
2657 break;
2658
2659 case self::ACTION_LOGIN_CONTINUE:
2660 $state = $this->request->getSession()->getSecret( self::AUTHN_STATE );
2661 return is_array( $state ) ? $state['continueRequests'] : [];
2662
2663 case self::ACTION_CREATE_CONTINUE:
2664 $state = $this->request->getSession()->getSecret( self::ACCOUNT_CREATION_STATE );
2665 return is_array( $state ) ? $state['continueRequests'] : [];
2666
2667 case self::ACTION_LINK:
2668 $providers = [];
2669 foreach ( $this->getPrimaryAuthenticationProviders() as $p ) {
2670 if ( $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2671 $providers[] = $p;
2672 }
2673 }
2674 break;
2675
2676 case self::ACTION_UNLINK:
2677 $providers = [];
2678 foreach ( $this->getPrimaryAuthenticationProviders() as $p ) {
2679 if ( $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2680 $providers[] = $p;
2681 }
2682 }
2683
2684 // To providers, unlink and remove are identical.
2685 $providerAction = self::ACTION_REMOVE;
2686 break;
2687
2688 case self::ACTION_LINK_CONTINUE:
2689 $state = $this->request->getSession()->getSecret( self::ACCOUNT_LINK_STATE );
2690 return is_array( $state ) ? $state['continueRequests'] : [];
2691
2692 case self::ACTION_CHANGE:
2693 case self::ACTION_REMOVE:
2694 $providers = $this->getPrimaryAuthenticationProviders() +
2695 $this->getSecondaryAuthenticationProviders();
2696 break;
2697
2698 // @codeCoverageIgnoreStart
2699 default:
2700 throw new DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2701 }
2702 // @codeCoverageIgnoreEnd
2703
2704 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2705 }
2706
2716 private function getAuthenticationRequestsInternal(
2717 $providerAction, array $options, array $providers, ?UserIdentity $user = null
2718 ) {
2719 $user = $user ?: RequestContext::getMain()->getUser();
2720 $options['username'] = $user->isRegistered() ? $user->getName() : null;
2721 $options += [ 'securityLevel' => null ];
2722
2723 // Query them and merge results
2724 $reqs = [];
2725 foreach ( $providers as $provider ) {
2726 $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2727 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2728 $id = $req->getUniqueId();
2729
2730 // If a required request if from a Primary, mark it as "primary-required" instead
2731 if ( $isPrimary && $req->required ) {
2732 $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2733 }
2734
2735 if (
2736 !isset( $reqs[$id] )
2737 || $req->required === AuthenticationRequest::REQUIRED
2738 || $reqs[$id]->required === AuthenticationRequest::OPTIONAL
2739 ) {
2740 $reqs[$id] = $req;
2741 }
2742 }
2743 }
2744
2745 // AuthManager has its own req for some actions
2746 switch ( $providerAction ) {
2747 case self::ACTION_LOGIN:
2748 $options['username'] = null; // Don't fill in the username below
2749 if ( $options['securityLevel'] !== null ) {
2750 $reqs[] = ElevatedSecurityAuthenticationRequest::create(
2751 $this->getRequest()->getSession(), $options['securityLevel'] );
2752 } else {
2753 $reqs[] = new RememberMeAuthenticationRequest(
2754 $this->config->get( MainConfigNames::RememberMe )
2755 );
2756 }
2757 break;
2758
2759 case self::ACTION_CREATE:
2760 $reqs[] = new UsernameAuthenticationRequest;
2761 $reqs[] = new UserDataAuthenticationRequest;
2762
2763 // Registered users should be prompted to provide a rationale for account creations,
2764 // except for the case of a temporary user registering a full account (T328718).
2765 if (
2766 $options['username'] !== null &&
2767 !$this->userNameUtils->isTemp( $options['username'] )
2768 ) {
2769 $reqs[] = new CreationReasonAuthenticationRequest;
2770 $options['username'] = null; // Don't fill in the username below
2771 }
2772 break;
2773 }
2774
2775 // Fill in reqs data
2776 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2777
2778 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2779 if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2780 $reqs = array_filter( $reqs, function ( $req ) {
2781 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2782 } );
2783 }
2784
2785 return array_values( $reqs );
2786 }
2787
2795 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2796 foreach ( $reqs as $req ) {
2797 if ( !$req->action || $forceAction ) {
2798 $req->action = $action;
2799 }
2800 $req->username ??= $username;
2801 }
2802 }
2803
2810 public function userExists( $username, $flags = IDBAccessObject::READ_NORMAL ) {
2811 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2812 if ( $provider->testUserExists( $username, $flags ) ) {
2813 return true;
2814 }
2815 }
2816
2817 return false;
2818 }
2819
2831 public function allowsPropertyChange( $property ) {
2832 $providers = $this->getPrimaryAuthenticationProviders() +
2833 $this->getSecondaryAuthenticationProviders();
2834 foreach ( $providers as $provider ) {
2835 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2836 return false;
2837 }
2838 }
2839 return true;
2840 }
2841
2850 public function getAuthenticationProvider( $id ) {
2851 // Fast version
2852 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2853 return $this->allAuthenticationProviders[$id];
2854 }
2855
2856 // Slow version: instantiate each kind and check
2857 $providers = $this->getPrimaryAuthenticationProviders();
2858 if ( isset( $providers[$id] ) ) {
2859 return $providers[$id];
2860 }
2861 $providers = $this->getSecondaryAuthenticationProviders();
2862 if ( isset( $providers[$id] ) ) {
2863 return $providers[$id];
2864 }
2865 $providers = $this->getPreAuthenticationProviders();
2866 if ( isset( $providers[$id] ) ) {
2867 return $providers[$id];
2868 }
2869
2870 return null;
2871 }
2872
2873 // endregion -- end of Information methods
2874
2875 /***************************************************************************/
2876 // region Internal methods
2885 public function setAuthenticationSessionData( $key, $data ) {
2886 $session = $this->request->getSession();
2887 $arr = $session->getSecret( 'authData' );
2888 if ( !is_array( $arr ) ) {
2889 $arr = [];
2890 }
2891 $arr[$key] = $data;
2892 $session->setSecret( 'authData', $arr );
2893 }
2894
2902 public function getAuthenticationSessionData( $key, $default = null ) {
2903 $arr = $this->request->getSession()->getSecret( 'authData' );
2904 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2905 return $arr[$key];
2906 } else {
2907 return $default;
2908 }
2909 }
2910
2916 public function removeAuthenticationSessionData( $key ) {
2917 $session = $this->request->getSession();
2918 if ( $key === null ) {
2919 $session->remove( 'authData' );
2920 } else {
2921 $arr = $session->getSecret( 'authData' );
2922 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2923 unset( $arr[$key] );
2924 $session->setSecret( 'authData', $arr );
2925 }
2926 }
2927 }
2928
2936 protected function providerArrayFromSpecs( $class, array $specs ) {
2937 $i = 0;
2938 foreach ( $specs as &$spec ) {
2939 $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2940 }
2941 unset( $spec );
2942 // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2943 usort( $specs, static function ( $a, $b ) {
2944 return $a['sort'] <=> $b['sort']
2945 ?: $a['sort2'] <=> $b['sort2'];
2946 } );
2947
2948 $ret = [];
2949 foreach ( $specs as $spec ) {
2951 $provider = $this->objectFactory->createObject( $spec, [ 'assertClass' => $class ] );
2952 $provider->init( $this->logger, $this, $this->getHookContainer(), $this->config, $this->userNameUtils );
2953 $id = $provider->getUniqueId();
2954 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2955 throw new RuntimeException(
2956 "Duplicate specifications for id $id (classes " .
2957 get_class( $provider ) . ' and ' .
2958 get_class( $this->allAuthenticationProviders[$id] ) . ')'
2959 );
2960 }
2961 // @phan-suppress-next-line PhanTypeMismatchProperty
2962 $this->allAuthenticationProviders[$id] = $provider;
2963 $ret[$id] = $provider;
2964 }
2965 return $ret;
2966 }
2967
2972 protected function getPreAuthenticationProviders() {
2973 if ( $this->preAuthenticationProviders === null ) {
2974 $this->initializeAuthenticationProviders();
2975 }
2976 return $this->preAuthenticationProviders;
2977 }
2978
2984 if ( $this->primaryAuthenticationProviders === null ) {
2985 $this->initializeAuthenticationProviders();
2986 }
2987 return $this->primaryAuthenticationProviders;
2988 }
2989
2995 if ( $this->secondaryAuthenticationProviders === null ) {
2996 $this->initializeAuthenticationProviders();
2997 }
2998 return $this->secondaryAuthenticationProviders;
2999 }
3000
3001 private function getProviderIds(): array {
3002 return [
3003 'preauth' => array_keys( $this->getPreAuthenticationProviders() ),
3004 'primaryauth' => array_keys( $this->getPrimaryAuthenticationProviders() ),
3005 'secondaryauth' => array_keys( $this->getSecondaryAuthenticationProviders() ),
3006 ];
3007 }
3008
3009 private function initializeAuthenticationProviders() {
3010 $conf = $this->config->get( MainConfigNames::AuthManagerConfig )
3011 ?: $this->config->get( MainConfigNames::AuthManagerAutoConfig );
3012
3013 $providers = array_map( static fn ( $stepConf ) => array_fill_keys( array_keys( $stepConf ), true ), $conf );
3014 $this->getHookRunner()->onAuthManagerFilterProviders( $providers );
3015 foreach ( $conf as $step => $stepConf ) {
3016 $conf[$step] = array_intersect_key( $stepConf, array_filter( $providers[$step] ) );
3017 }
3018
3019 $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
3020 PreAuthenticationProvider::class, $conf['preauth']
3021 );
3022 $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
3023 PrimaryAuthenticationProvider::class, $conf['primaryauth']
3024 );
3025 $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
3026 SecondaryAuthenticationProvider::class, $conf['secondaryauth']
3027 );
3028 }
3029
3037 private function setSessionDataForUser( $user, $remember = null, $securityLevel = null ) {
3038 $session = $this->request->getSession();
3039 $delay = $session->delaySave();
3040
3041 // If the user just logged into this account, they should not have elevated security.
3042 if ( !$user->equals( $session->getUser() ) ) {
3043 $session->set( 'AuthManager:lastAuthTimestamps', [] );
3044 $securityLevel = null;
3045 }
3046
3047 $session->resetId();
3048 $session->resetAllTokens();
3049 if ( $session->canSetUser() ) {
3050 $session->setUser( $user );
3051 }
3052 if ( $remember !== null ) {
3053 $session->setRememberUser( $remember );
3054 }
3055
3056 $session->set( 'AuthManager:lastAuthId', $user->getId() );
3057 if ( $securityLevel !== null ) {
3058 $lastAuthTimestamps = $session->get( 'AuthManager:lastAuthTimestamps', [] );
3059 $lastAuthTimestamps[$securityLevel] = time();
3060 $session->set( 'AuthManager:lastAuthTimestamps', $lastAuthTimestamps );
3061 }
3062
3063 $session->persist();
3064 \Wikimedia\ScopedCallback::consume( $delay );
3065
3066 $this->getHookRunner()->onUserLoggedIn( $user );
3067 }
3068
3073 private function setDefaultUserOptions( User $user, $useContextLang ) {
3074 $user->setToken();
3075
3076 $lang = $useContextLang ? RequestContext::getMain()->getLanguage() : $this->contentLanguage;
3077 $this->userOptionsManager->setOption(
3078 $user,
3079 'language',
3080 $this->languageConverterFactory->getLanguageConverter( $lang )->getPreferredVariant()
3081 );
3082
3083 $contLangConverter = $this->languageConverterFactory->getLanguageConverter( $this->contentLanguage );
3084 if ( $contLangConverter->hasVariants() ) {
3085 $this->userOptionsManager->setOption(
3086 $user,
3087 'variant',
3088 $contLangConverter->getPreferredVariant()
3089 );
3090 }
3091 }
3092
3096 private function runVerifyHook(
3097 string $action,
3098 ?UserIdentity $user,
3099 AuthenticationResponse &$response,
3100 string $primaryId
3101 ): bool {
3102 $oldResponse = $response;
3103 $info = [
3104 'action' => $action,
3105 'primaryId' => $primaryId,
3106 ];
3107 $proceed = $this->getHookRunner()->onAuthManagerVerifyAuthentication( $user, $response, $this, $info );
3108 if ( !( $response instanceof AuthenticationResponse ) ) {
3109 throw new LogicException( '$response must be an AuthenticationResponse' );
3110 } elseif ( $proceed && $response !== $oldResponse ) {
3111 throw new LogicException(
3112 'AuthManagerVerifyAuthenticationHook must not modify the response unless it returns false' );
3113 } elseif ( !$proceed && $response->status !== AuthenticationResponse::FAIL ) {
3114 throw new LogicException(
3115 'AuthManagerVerifyAuthenticationHook must set the response to FAIL if it returns false' );
3116 }
3117 if ( !$proceed ) {
3118 $this->logger->info(
3119 $action . ' action for {user} from {clientIp} prevented by '
3120 . 'AuthManagerVerifyAuthentication hook: {reason}',
3121 [
3122 'user' => $user ? $user->getName() : '<null>',
3123 'reason' => $response->message->getKey(),
3124 'primaryId' => $primaryId,
3125 ] + $this->request->getSecurityLogContext( $user )
3126 );
3127 }
3128 return $proceed;
3129 }
3130
3136 private function callMethodOnProviders( $which, $method, array $args ) {
3137 $providers = [];
3138 if ( $which & self::CALL_PRE ) {
3139 $providers += $this->getPreAuthenticationProviders();
3140 }
3141 if ( $which & self::CALL_PRIMARY ) {
3142 $providers += $this->getPrimaryAuthenticationProviders();
3143 }
3144 if ( $which & self::CALL_SECONDARY ) {
3145 $providers += $this->getSecondaryAuthenticationProviders();
3146 }
3147 foreach ( $providers as $provider ) {
3148 $provider->$method( ...$args );
3149 }
3150 }
3151
3164 private function callLoginAuditHook(
3165 array $reqs,
3166 AuthenticationResponse $res,
3167 $user,
3168 array $options = []
3169 ) {
3170 if ( $user instanceof User ) {
3171 $guessUserName = $user->getName();
3172 } else {
3173 $guessUserName = $user;
3174 $user = null;
3175 }
3176
3177 $derivedOptions = [
3178 'performer' => $this->request->getSession()->getUser()
3179 ];
3180 $elevatedSecurityReq = AuthenticationRequest::getRequestByClass(
3181 $reqs, ElevatedSecurityAuthenticationRequest::class
3182 );
3183 // Check that the ElevatedSecurityAuthenticationRequest is valid before trusting it
3184 if ( $elevatedSecurityReq && $elevatedSecurityReq->validate()->isOK() ) {
3185 $derivedOptions['securityLevel'] = $elevatedSecurityReq->securityLevel;
3186 }
3187
3188 $hookOptions = $options + $derivedOptions;
3189 if ( array_key_exists( 'securityLevel', $options ) && $options['securityLevel'] === null ) {
3190 // Special case: if the caller explicitly sets 'securityLevel' to null, don't pass it to the hook
3191 unset( $hookOptions[ 'securityLevel' ] );
3192 }
3193 $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit( $res, $user, $guessUserName, $hookOptions );
3194 }
3195
3199 private function getHookContainer() {
3200 return $this->hookContainer;
3201 }
3202
3206 private function getHookRunner() {
3207 return $this->hookRunner;
3208 }
3209
3210 // endregion -- end of Internal methods
3211
3212}
3213
3214/*
3215 * This file uses VisualStudio style region/endregion fold markers which are
3216 * recognised by PHPStorm. If modelines are enabled, the following editor
3217 * configuration will also enable folding in vim, if it is in the last 5 lines
3218 * of the file. We also use "@name" which creates sections in Doxygen.
3219 *
3220 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
3221 */
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli') global $wgLang
Definition Setup.php:498
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.
merge( $other, $overwriteValue=false)
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:129
getId( $wikiId=self::LOCAL)
Get the user's ID.
Definition User.php:1482
getUserPage()
Get this user's personal page title.
Definition User.php:2700
addToDatabase()
Add this existing user object to the database.
Definition User.php:2531
loadFromId( $flags=IDBAccessObject::READ_NORMAL)
Load user table data, given mId has already been set.
Definition User.php:485
setId( $v)
Set the user and reload all fields according to a given ID.
Definition User.php:1506
saveSettings()
Save this user's settings into the database.
Definition User.php:2325
isRegistered()
Get whether the user is registered.
Definition User.php:2081
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1515
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.
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, 'EnableChunkedUploads'=> 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'=>[], 'DefaultLockManager'=> null, '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'=> true, '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 -l $lang -o $output $input', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> true, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, 'MaxAnimatedWebPArea'=> 12500000, 'WebPThumbnailType'=>['webp', 'image/webp',], '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, 'RestTermsOfServiceUrl'=> null, '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'=>[], 'RemoteVirtualDomainsMapping'=>[], '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, 'SplitParsoidParserCache'=> true, '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, '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',],],], 'EnableSectionShare'=> false, '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, ], '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' => [ ], 'RestrictUserPageEditing' => false, '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', 'editmywatchlist' => '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', 'editmywatchlist' => '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', 'managesessions' => 'low', ], 'EnableBotPasswords' => true, 'BotPasswordsCluster' => false, 'BotPasswordsDatabase' => false, 'BotPasswordsLimit' => 100, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, 'ReauthenticateForActions' => [ 'edituserjs' => 'edituserjscss', 'editusercss' => 'edituserjscss', 'editsitejs' => 'editsitejscss', 'editsitecss' => 'editsitejscss', ], '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, '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, ], 'RestrictedTagViewRights' => [ ], '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' => [ ], 'RestLocalModuleTestBaseUrl' => null, 'RestModuleOverrides' => [ ], 'RestExternalModules' => [ ], '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, 'ReturnExperimentalPFragmentTypes' => [ ], 'UseParsoidLinksUpdate' => null, 'UseParsoidMessages' => null, ], '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', 'DefaultLockManager' => [ 'string', 'null', ], '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', ], 'WebPThumbnailType' => 'array', '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', ], 'RestTermsOfServiceUrl' => [ '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', 'RemoteVirtualDomainsMapping' => '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', 'SplitParsoidParserCache' => 'boolean', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => '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', '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', 'RestrictUserPageEditing' => 'boolean', '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', 'ReauthenticateForActions' => 'object', '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', 'RestrictedTagViewRights' => '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', 'RestLocalModuleTestBaseUrl' => [ 'string', 'null', ], 'RestModuleOverrides' => 'object', 'RestExternalModules' => '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', 'ReturnExperimentalPFragmentTypes' => 'array', 'UseParsoidLinksUpdate' => [ 'boolean', 'null', ], 'UseParsoidMessages' => [ 'boolean', 'null', ], ], 'mergeStrategy' => [ 'WebPThumbnailType' => 'replace', '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', 'RestExternalModules' => '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', ], ], ], ], ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], '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', ], 'skipRedirects' => [ 'type' => 'bool', ], ], ], 'SoftwareTags' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], 'UseCopyrightUpload' => [ 'deprecated' => 'since 1.47 This feature is being removed.', ], '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', ], ], 'RestModuleOverrides' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'availability' => [ 'type' => 'string', ], ], 'required' => [ 'availability', ], ], ], 'RestExternalModules' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'info' => [ 'type' => 'object', 'properties' => [ 'version' => [ 'type' => 'string', ], 'title' => [ 'type' => 'string', ], 'x-i18n-title' => [ 'type' => 'string', ], 'description' => [ 'type' => 'string', ], 'x-i18n-description' => [ 'type' => 'string', ], ], 'required' => [ 'version', ], ], 'base' => [ 'type' => 'string', 'format' => 'uri', ], 'spec' => [ 'type' => 'string', 'format' => 'uri', ], ], 'required' => [ 'info', 'base', 'spec', ], ], ], '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