[ 'class' => SpecialBrokenRedirects::class, 'services' => [ 'ContentHandlerFactory', 'ConnectionProvider', 'LinkBatchFactory', ] ], 'Deadendpages' => [ 'class' => SpecialDeadendPages::class, 'services' => [ 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], 'DoubleRedirects' => [ 'class' => SpecialDoubleRedirects::class, 'services' => [ 'ContentHandlerFactory', 'LinkBatchFactory', 'ConnectionProvider', ] ], 'Longpages' => [ 'class' => SpecialLongPages::class, 'services' => [ // Same as for Shortpages 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', ] ], 'Ancientpages' => [ 'class' => SpecialAncientPages::class, 'services' => [ 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], 'Lonelypages' => [ 'class' => SpecialLonelyPages::class, 'services' => [ 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', 'LinksMigration', ] ], 'Fewestrevisions' => [ 'class' => SpecialFewestRevisions::class, 'services' => [ // Same as for Mostrevisions 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], 'Withoutinterwiki' => [ 'class' => SpecialWithoutInterwiki::class, 'services' => [ 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], 'Protectedpages' => [ 'class' => SpecialProtectedPages::class, 'services' => [ 'LinkBatchFactory', 'ConnectionProvider', 'CommentStore', 'RowCommentFormatter', 'RestrictionStore', ] ], 'Protectedtitles' => [ 'class' => SpecialProtectedTitles::class, 'services' => [ 'LinkBatchFactory', 'ConnectionProvider', ] ], 'Shortpages' => [ 'class' => SpecialShortPages::class, 'services' => [ // Same as for Longpages 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', ] ], 'Uncategorizedcategories' => [ 'class' => SpecialUncategorizedCategories::class, 'services' => [ // Same as for SpecialUncategorizedPages and SpecialUncategorizedTemplates 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], 'Uncategorizedimages' => [ 'class' => SpecialUncategorizedImages::class, 'services' => [ 'ConnectionProvider', ] ], 'Uncategorizedpages' => [ 'class' => SpecialUncategorizedPages::class, 'services' => [ // Same as for SpecialUncategorizedCategories and SpecialUncategorizedTemplates 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], 'Uncategorizedtemplates' => [ 'class' => SpecialUncategorizedTemplates::class, 'services' => [ // Same as for SpecialUncategorizedCategories and SpecialUncategorizedPages 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], 'Unusedcategories' => [ 'class' => SpecialUnusedCategories::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', ] ], 'Unusedimages' => [ 'class' => SpecialUnusedImages::class, 'services' => [ 'ConnectionProvider', ] ], 'Unusedtemplates' => [ 'class' => SpecialUnusedTemplates::class, 'services' => [ 'ConnectionProvider', 'LinksMigration', ] ], 'Unwatchedpages' => [ 'class' => SpecialUnwatchedPages::class, 'services' => [ 'LinkBatchFactory', 'ConnectionProvider', 'LanguageConverterFactory', ] ], 'Wantedcategories' => [ 'class' => SpecialWantedCategories::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], 'Wantedfiles' => [ 'class' => SpecialWantedFiles::class, 'services' => [ 'RepoGroup', 'ConnectionProvider', 'LinkBatchFactory', ] ], 'Wantedpages' => [ 'class' => SpecialWantedPages::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', 'LinksMigration', ] ], 'Wantedtemplates' => [ 'class' => SpecialWantedTemplates::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', 'LinksMigration', ] ], // List of pages 'Allpages' => [ 'class' => SpecialAllPages::class, 'services' => [ 'ConnectionProvider', 'SearchEngineFactory', 'PageStore', ] ], 'Prefixindex' => [ 'class' => SpecialPrefixIndex::class, 'services' => [ 'ConnectionProvider', 'LinkCache', ] ], 'Categories' => [ 'class' => SpecialCategories::class, 'services' => [ 'LinkBatchFactory', 'ConnectionProvider', ] ], 'Listredirects' => [ 'class' => SpecialListRedirects::class, 'services' => [ 'LinkBatchFactory', 'ConnectionProvider', 'WikiPageFactory', 'RedirectLookup' ] ], 'PagesWithProp' => [ 'class' => SpecialPagesWithProp::class, 'services' => [ 'ConnectionProvider', ] ], 'TrackingCategories' => [ 'class' => SpecialTrackingCategories::class, 'services' => [ 'LinkBatchFactory', 'TrackingCategories', ] ], // Authentication 'Userlogin' => [ 'class' => SpecialUserLogin::class, 'services' => [ 'AuthManager', 'UserIdentityUtils', ] ], 'Userlogout' => [ 'class' => SpecialUserLogout::class, 'services' => [ 'TempUserConfig', ], ], 'CreateAccount' => [ 'class' => SpecialCreateAccount::class, 'services' => [ 'AuthManager', 'FormatterFactory', 'UserIdentityUtils', ] ], 'LinkAccounts' => [ 'class' => SpecialLinkAccounts::class, 'services' => [ 'AuthManager', ] ], 'UnlinkAccounts' => [ 'class' => SpecialUnlinkAccounts::class, 'services' => [ 'AuthManager', ] ], 'ChangeCredentials' => [ 'class' => SpecialChangeCredentials::class, 'services' => [ 'AuthManager', ] ], 'RemoveCredentials' => [ 'class' => SpecialRemoveCredentials::class, 'services' => [ 'AuthManager', ] ], 'AuthenticationPopupSuccess' => [ 'class' => SpecialAuthenticationPopupSuccess::class, 'services' => [ 'SkinFactory', ] ], // Users and rights 'Activeusers' => [ 'class' => SpecialActiveUsers::class, 'services' => [ 'LinkBatchFactory', 'ConnectionProvider', 'UserGroupManager', 'UserIdentityLookup', 'HideUserUtils', 'TempUserConfig', ] ], 'Block' => [ 'class' => SpecialBlock::class, 'services' => [ 'BlockTargetFactory', 'BlockPermissionCheckerFactory', 'BlockUserFactory', 'DatabaseBlockStore', 'UserNameUtils', 'UserNamePrefixSearch', 'BlockActionInfo', 'TitleFormatter', 'NamespaceInfo' ] ], 'Unblock' => [ 'class' => SpecialUnblock::class, 'services' => [ 'UnblockUserFactory', 'BlockTargetFactory', 'DatabaseBlockStore', 'UserNameUtils', 'UserNamePrefixSearch', 'WatchlistManager', ] ], 'BlockList' => [ 'class' => SpecialBlockList::class, 'services' => [ 'LinkBatchFactory', 'DatabaseBlockStore', 'BlockRestrictionStore', 'ConnectionProvider', 'CommentStore', 'BlockTargetFactory', 'HideUserUtils', 'BlockActionInfo', 'RowCommentFormatter', 'TempUserConfig', ], ], 'AutoblockList' => [ 'class' => SpecialAutoblockList::class, 'services' => [ 'LinkBatchFactory', 'BlockRestrictionStore', 'ConnectionProvider', 'CommentStore', 'BlockTargetFactory', 'HideUserUtils', 'BlockActionInfo', 'RowCommentFormatter', ], ], 'ChangePassword' => [ 'class' => SpecialChangePassword::class, ], 'BotPasswords' => [ 'class' => SpecialBotPasswords::class, 'services' => [ 'PasswordFactory', 'AuthManager', 'CentralIdLookup', 'GrantsInfo', 'GrantsLocalization', ] ], 'PasswordReset' => [ 'class' => SpecialPasswordReset::class, 'services' => [ 'PasswordReset' ] ], 'DeletedContributions' => [ 'class' => SpecialDeletedContributions::class, 'services' => [ 'PermissionManager', 'ConnectionProvider', 'RevisionStore', 'NamespaceInfo', 'UserNameUtils', 'UserNamePrefixSearch', 'UserOptionsLookup', 'CommentFormatter', 'LinkBatchFactory', 'UserFactory', 'UserIdentityLookup', 'DatabaseBlockStore', 'TempUserConfig', ] ], 'Preferences' => [ 'class' => SpecialPreferences::class, 'services' => [ 'PreferencesFactory', 'UserOptionsManager', ] ], 'ResetTokens' => [ 'class' => SpecialResetTokens::class, ], 'Contributions' => [ 'class' => SpecialContributions::class, 'services' => [ 'LinkBatchFactory', 'PermissionManager', 'ConnectionProvider', 'RevisionStore', 'NamespaceInfo', 'UserNameUtils', 'UserNamePrefixSearch', 'UserOptionsLookup', 'CommentFormatter', 'UserFactory', 'UserIdentityLookup', 'DatabaseBlockStore', 'TempUserConfig', ] ], 'Listgrouprights' => [ 'class' => SpecialListGroupRights::class, 'services' => [ 'NamespaceInfo', 'UserGroupManager', 'LanguageConverterFactory', 'GroupPermissionsLookup', ] ], 'Listgrants' => [ 'class' => SpecialListGrants::class, 'services' => [ 'GrantsLocalization', ] ], 'Listusers' => [ 'class' => SpecialListUsers::class, 'services' => [ 'LinkBatchFactory', 'ConnectionProvider', 'UserGroupManager', 'UserIdentityLookup', 'HideUserUtils', 'TempUserConfig', ] ], 'Listadmins' => [ 'class' => SpecialListAdmins::class, ], 'Listbots' => [ 'class' => SpecialListBots::class, ], 'Userrights' => [ 'class' => SpecialUserRights::class, 'services' => [ 'UserGroupManagerFactory', 'UserNameUtils', 'UserNamePrefixSearch', 'UserFactory', 'ActorStoreFactory', 'WatchlistManager', 'TempUserConfig', ] ], 'EditWatchlist' => [ 'class' => SpecialEditWatchlist::class, 'services' => [ 'WatchedItemStore', 'TitleParser', 'GenderCache', 'LinkBatchFactory', 'NamespaceInfo', 'WikiPageFactory', 'WatchlistManager', ] ], 'PasswordPolicies' => [ 'class' => SpecialPasswordPolicies::class, 'services' => [ 'UserGroupManager', ] ], // Recent changes and logs 'Newimages' => [ 'class' => SpecialNewFiles::class, 'services' => [ 'MimeAnalyzer', 'GroupPermissionsLookup', 'ConnectionProvider', 'LinkBatchFactory', ] ], 'Log' => [ 'class' => SpecialLog::class, 'services' => [ 'LinkBatchFactory', 'ConnectionProvider', 'ActorNormalization', 'UserIdentityLookup', 'UserNameUtils', 'LogFormatterFactory', ] ], 'Watchlist' => [ 'class' => SpecialWatchlist::class, 'services' => [ 'WatchedItemStore', 'WatchlistManager', 'UserOptionsLookup', 'ChangeTagsStore', 'UserIdentityUtils', 'TempUserConfig', ] ], 'Newpages' => [ 'class' => SpecialNewPages::class, 'services' => [ 'LinkBatchFactory', 'ContentHandlerFactory', 'GroupPermissionsLookup', 'RevisionLookup', 'NamespaceInfo', 'UserOptionsLookup', 'RowCommentFormatter', 'ChangeTagsStore', 'TempUserConfig', ] ], 'Recentchanges' => [ 'class' => SpecialRecentChanges::class, 'services' => [ 'WatchedItemStore', 'MessageParser', 'UserOptionsLookup', 'ChangeTagsStore', 'UserIdentityUtils', 'TempUserConfig', ] ], 'Recentchangeslinked' => [ 'class' => SpecialRecentChangesLinked::class, 'services' => [ 'WatchedItemStore', 'MessageParser', 'UserOptionsLookup', 'SearchEngineFactory', 'ChangeTagsStore', 'UserIdentityUtils', 'TempUserConfig', ] ], 'Tags' => [ 'class' => SpecialTags::class, 'services' => [ 'ChangeTagsStore', ] ], // Media reports and uploads 'Listfiles' => [ 'class' => SpecialListFiles::class, 'services' => [ 'RepoGroup', 'ConnectionProvider', 'CommentStore', 'UserNameUtils', 'UserNamePrefixSearch', 'RowCommentFormatter', 'LinkBatchFactory', ] ], 'Filepath' => [ 'class' => SpecialFilepath::class, 'services' => [ 'SearchEngineFactory', ] ], 'MediaStatistics' => [ 'class' => SpecialMediaStatistics::class, 'services' => [ 'MimeAnalyzer', 'ConnectionProvider', 'LinkBatchFactory', ] ], 'MIMEsearch' => [ 'class' => SpecialMIMESearch::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], 'FileDuplicateSearch' => [ 'class' => SpecialFileDuplicateSearch::class, 'services' => [ 'LinkBatchFactory', 'RepoGroup', 'SearchEngineFactory', 'LanguageConverterFactory', ] ], 'Upload' => [ 'class' => SpecialUpload::class, 'services' => [ 'RepoGroup', 'UserOptionsLookup', 'NamespaceInfo', ] ], 'UploadStash' => [ 'class' => SpecialUploadStash::class, 'services' => [ 'RepoGroup', 'HttpRequestFactory', 'UrlUtils', 'ConnectionProvider', ] ], 'ListDuplicatedFiles' => [ 'class' => SpecialListDuplicatedFiles::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', ] ], // Data and tools 'ApiSandbox' => [ 'class' => SpecialApiSandbox::class, ], 'Interwiki' => [ 'class' => SpecialInterwiki::class, 'services' => [ 'ContentLanguage', 'InterwikiLookup', 'LanguageNameUtils', 'UrlUtils', 'ConnectionProvider', ] ], 'RestSandbox' => [ 'class' => SpecialRestSandbox::class, 'services' => [ 'UrlUtils', ] ], 'Statistics' => [ 'class' => SpecialStatistics::class, 'services' => [ 'UserGroupManager', ] ], 'Allmessages' => [ 'class' => SpecialAllMessages::class, 'services' => [ 'LanguageFactory', 'LanguageNameUtils', 'LocalisationCache', 'ConnectionProvider', ] ], 'Version' => [ 'class' => SpecialVersion::class, 'services' => [ 'ParserFactory', 'UrlUtils', 'ConnectionProvider', ] ], 'Lockdb' => [ 'class' => SpecialLockdb::class, ], 'Unlockdb' => [ 'class' => SpecialUnlockdb::class, ], 'NamespaceInfo' => [ 'class' => SpecialNamespaceInfo::class, 'services' => [ 'NamespaceInfo', ], ], // Redirecting special pages 'LinkSearch' => [ 'class' => SpecialLinkSearch::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', 'UrlUtils', ] ], 'Randompage' => [ 'class' => SpecialRandomPage::class, 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ] ], 'RandomInCategory' => [ 'class' => SpecialRandomInCategory::class, 'services' => [ 'ConnectionProvider', ] ], 'Randomredirect' => [ 'class' => SpecialRandomRedirect::class, 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ] ], 'Randomrootpage' => [ 'class' => SpecialRandomRootPage::class, 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ] ], 'GoToInterwiki' => [ 'class' => SpecialGoToInterwiki::class, ], // High use pages 'Mostlinkedcategories' => [ 'class' => SpecialMostLinkedCategories::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], 'Mostimages' => [ 'class' => SpecialMostImages::class, 'services' => [ 'ConnectionProvider', 'LanguageConverterFactory', ] ], 'Mostinterwikis' => [ 'class' => SpecialMostInterwikis::class, 'services' => [ 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', ] ], 'Mostlinked' => [ 'class' => SpecialMostLinked::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', 'LinksMigration', ] ], 'Mostlinkedtemplates' => [ 'class' => SpecialMostLinkedTemplates::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', 'LinksMigration', ] ], 'Mostcategories' => [ 'class' => SpecialMostCategories::class, 'services' => [ 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', ] ], 'Mostrevisions' => [ 'class' => SpecialMostRevisions::class, 'services' => [ // Same as for Fewestrevisions 'NamespaceInfo', 'ConnectionProvider', 'LinkBatchFactory', 'LanguageConverterFactory', ] ], // Page tools 'ComparePages' => [ 'class' => SpecialComparePages::class, 'services' => [ 'RevisionLookup', 'ContentHandlerFactory', ] ], 'Export' => [ 'class' => SpecialExport::class, 'services' => [ 'ConnectionProvider', 'WikiExporterFactory', 'TitleFormatter', 'LinksMigration', ] ], 'Import' => [ 'class' => SpecialImport::class, 'services' => [ 'WikiImporterFactory', ] ], 'Undelete' => [ 'class' => SpecialUndelete::class, 'services' => [ 'PermissionManager', 'RevisionStore', 'RevisionRenderer', 'ContentHandlerFactory', 'ChangeTagDefStore', 'LinkBatchFactory', 'RepoGroup', 'ConnectionProvider', 'UserOptionsLookup', 'WikiPageFactory', 'SearchEngineFactory', 'UndeletePageFactory', 'ArchivedRevisionLookup', 'CommentFormatter', 'WatchlistManager', ], ], 'Whatlinkshere' => [ 'class' => SpecialWhatLinksHere::class, 'services' => [ 'ConnectionProvider', 'LinkBatchFactory', 'ContentHandlerFactory', 'SearchEngineFactory', 'NamespaceInfo', 'TitleFactory', 'LinksMigration', ] ], 'MergeHistory' => [ 'class' => SpecialMergeHistory::class, 'services' => [ 'MergeHistoryFactory', 'LinkBatchFactory', 'ConnectionProvider', 'RevisionStore', 'CommentFormatter', 'ChangeTagsStore', ] ], 'ExpandTemplates' => [ 'class' => SpecialExpandTemplates::class, 'services' => [ 'ParserFactory', 'UserOptionsLookup', 'Tidy', ], ], 'ChangeContentModel' => [ 'class' => SpecialChangeContentModel::class, 'services' => [ 'ContentHandlerFactory', 'ContentModelChangeFactory', 'SpamChecker', 'RevisionLookup', 'WikiPageFactory', 'SearchEngineFactory', 'CollationFactory', ], ], // Other 'Booksources' => [ 'class' => SpecialBookSources::class, 'services' => [ 'RevisionLookup', 'TitleFactory', ] ], // Unlisted / redirects 'ApiHelp' => [ 'class' => SpecialApiHelp::class, 'services' => [ 'UrlUtils', ] ], 'Blankpage' => [ 'class' => SpecialBlankpage::class, ], 'DeletePage' => [ 'class' => SpecialDeletePage::class, 'services' => [ 'SearchEngineFactory', ] ], 'Diff' => [ 'class' => SpecialDiff::class, ], 'EditPage' => [ 'class' => SpecialEditPage::class, 'services' => [ 'SearchEngineFactory', ] ], 'EditTags' => [ 'class' => SpecialEditTags::class, 'services' => [ 'PermissionManager', 'ChangeTagsStore', ], ], 'Emailuser' => [ 'class' => SpecialEmailUser::class, 'services' => [ 'UserNameUtils', 'UserNamePrefixSearch', 'UserOptionsLookup', 'EmailUserFactory', 'UserFactory', ] ], 'Movepage' => [ 'class' => SpecialMovePage::class, 'services' => [ 'MovePageFactory', 'PermissionManager', 'UserOptionsLookup', 'ConnectionProvider', 'ContentHandlerFactory', 'NamespaceInfo', 'LinkBatchFactory', 'RepoGroup', 'WikiPageFactory', 'SearchEngineFactory', 'WatchlistManager', 'RestrictionStore', 'TitleFactory', 'DeletePageFactory', ] ], 'Mycontributions' => [ 'class' => SpecialMycontributions::class, 'services' => [ 'TempUserConfig', ], ], 'MyLanguage' => [ 'class' => SpecialMyLanguage::class, 'services' => [ 'LanguageNameUtils', 'RedirectLookup' ] ], 'Mylog' => [ 'class' => SpecialMylog::class, 'services' => [ 'TempUserConfig', ], ], 'Mypage' => [ 'class' => SpecialMypage::class, 'services' => [ 'TempUserConfig', ], ], 'Mytalk' => [ 'class' => SpecialMytalk::class, 'services' => [ 'TempUserConfig', ], ], 'PageHistory' => [ 'class' => SpecialPageHistory::class, 'services' => [ 'SearchEngineFactory', ] ], 'PageInfo' => [ 'class' => SpecialPageInfo::class, 'services' => [ 'SearchEngineFactory', ] ], 'ProtectPage' => [ 'class' => SpecialProtectPage::class, 'services' => [ 'SearchEngineFactory', ] ], 'Purge' => [ 'class' => SpecialPurge::class, 'services' => [ 'SearchEngineFactory', ] ], 'Myuploads' => [ 'class' => SpecialMyuploads::class, 'services' => [ 'TempUserConfig', ], ], 'AllMyUploads' => [ 'class' => SpecialAllMyUploads::class, ], 'NewSection' => [ 'class' => SpecialNewSection::class, 'services' => [ 'SearchEngineFactory', ] ], 'PermanentLink' => [ 'class' => SpecialPermanentLink::class, ], 'Redirect' => [ 'class' => SpecialRedirect::class, 'services' => [ 'RepoGroup', 'UserFactory', ] ], 'Renameuser' => [ 'class' => SpecialRenameUser::class, 'services' => [ 'ConnectionProvider', 'PermissionManager', 'TitleFactory', 'UserFactory', 'UserNamePrefixSearch', 'RenameUserFactory', ] ], 'Revisiondelete' => [ 'class' => SpecialRevisionDelete::class, 'services' => [ 'PermissionManager', 'RepoGroup', ], ], 'RunJobs' => [ 'class' => SpecialRunJobs::class, 'services' => [ 'JobRunner', 'ReadOnlyMode', ] ], 'Specialpages' => [ 'class' => SpecialSpecialPages::class, ], 'PageData' => [ 'class' => SpecialPageData::class, ], 'Contribute' => [ 'class' => SpecialContribute::class, ], 'TalkPage' => [ 'class' => SpecialTalkPage::class, 'services' => [ 'MainConfig', 'TitleParser', ], ], ]; /** @var array Special page name => class name */ private $list; /** @var array */ private $aliases; /** @var ServiceOptions */ private $options; /** @var Language */ private $contLang; /** * @var ObjectFactory * @noVarDump */ private $objectFactory; /** * @var HookContainer * @noVarDump */ private $hookContainer; /** * @var HookRunner * @noVarDump */ private $hookRunner; /** * @internal For use by ServiceWiring */ public const CONSTRUCTOR_OPTIONS = [ MainConfigNames::DisableInternalSearch, MainConfigNames::EmailAuthentication, MainConfigNames::EnableEmail, MainConfigNames::EnableJavaScriptTest, MainConfigNames::EnableSpecialMute, MainConfigNames::EnableEditRecovery, MainConfigNames::PageLanguageUseDB, MainConfigNames::SpecialPages, ]; /** * @var TitleFactory */ private $titleFactory; /** * @param ServiceOptions $options * @param Language $contLang * @param ObjectFactory $objectFactory * @param TitleFactory $titleFactory * @param HookContainer $hookContainer */ public function __construct( ServiceOptions $options, Language $contLang, ObjectFactory $objectFactory, TitleFactory $titleFactory, HookContainer $hookContainer ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; $this->contLang = $contLang; $this->objectFactory = $objectFactory; $this->titleFactory = $titleFactory; $this->hookContainer = $hookContainer; $this->hookRunner = new HookRunner( $hookContainer ); } /** * Returns a list of canonical special page names. * May be used to iterate over all registered special pages. * * @return string[] */ public function getNames(): array { return array_keys( $this->getPageList() ); } /** * Get the special page list as an array */ private function getPageList(): array { if ( !is_array( $this->list ) ) { $this->list = self::CORE_LIST; if ( !$this->options->get( MainConfigNames::DisableInternalSearch ) ) { $this->list['Search'] = [ 'class' => SpecialSearch::class, 'services' => [ 'SearchEngineConfig', 'SearchEngineFactory', 'NamespaceInfo', 'ContentHandlerFactory', 'InterwikiLookup', 'ReadOnlyMode', 'UserOptionsManager', 'LanguageConverterFactory', 'RepoGroup', 'SearchResultThumbnailProvider', 'TitleMatcher', ] ]; } if ( $this->options->get( MainConfigNames::EmailAuthentication ) ) { $this->list['Confirmemail'] = [ 'class' => SpecialConfirmEmail::class, 'services' => [ 'UserFactory', ] ]; $this->list['Invalidateemail'] = [ 'class' => SpecialEmailInvalidate::class, 'services' => [ 'UserFactory', ] ]; } if ( $this->options->get( MainConfigNames::EnableEmail ) ) { $this->list['ChangeEmail'] = [ 'class' => SpecialChangeEmail::class, 'services' => [ 'AuthManager', ], ]; } if ( $this->options->get( MainConfigNames::EnableJavaScriptTest ) ) { $this->list['JavaScriptTest'] = [ 'class' => SpecialJavaScriptTest::class ]; } if ( $this->options->get( MainConfigNames::EnableSpecialMute ) ) { $this->list['Mute'] = [ 'class' => SpecialMute::class, 'services' => [ 'CentralIdLookup', 'UserOptionsManager', 'UserIdentityLookup', 'UserIdentityUtils', ] ]; } if ( $this->options->get( MainConfigNames::PageLanguageUseDB ) ) { $this->list['PageLanguage'] = [ 'class' => SpecialPageLanguage::class, 'services' => [ 'ContentHandlerFactory', 'LanguageNameUtils', 'ConnectionProvider', 'SearchEngineFactory', ] ]; } if ( $this->options->get( MainConfigNames::EnableEditRecovery ) ) { $this->list['EditRecovery'] = [ 'class' => SpecialEditRecovery::class, 'services' => [ 'UserOptionsLookup', ], ]; } // Add extension special pages $this->list = array_merge( $this->list, $this->options->get( MainConfigNames::SpecialPages ) ); // This hook can be used to disable unwanted core special pages // or conditionally register special pages. $this->hookRunner->onSpecialPage_initList( $this->list ); } return $this->list; } /** * Initialise and return the list of special page aliases. Returns an array where * the key is an alias, and the value is the canonical name of the special page. * All registered special pages are guaranteed to map to themselves. */ private function getAliasList(): array { if ( $this->aliases === null ) { $aliases = $this->contLang->getSpecialPageAliases(); $pageList = $this->getPageList(); $this->aliases = []; $keepAlias = []; // Force every canonical name to be an alias for itself. foreach ( $pageList as $name => $stuff ) { $caseFoldedAlias = $this->contLang->caseFold( $name ); $this->aliases[$caseFoldedAlias] = $name; $keepAlias[$caseFoldedAlias] = 'canonical'; } // Check for $aliases being an array since Language::getSpecialPageAliases can return null if ( is_array( $aliases ) ) { foreach ( $aliases as $realName => $aliasList ) { $first = true; foreach ( $aliasList as $alias ) { $caseFoldedAlias = $this->contLang->caseFold( $alias ); if ( isset( $this->aliases[$caseFoldedAlias] ) && $realName === $this->aliases[$caseFoldedAlias] ) { $first = false; // Ignore same-realName conflicts continue; } if ( !isset( $keepAlias[$caseFoldedAlias] ) ) { $this->aliases[$caseFoldedAlias] = $realName; if ( $first ) { $keepAlias[$caseFoldedAlias] = 'first'; } } elseif ( $first ) { wfWarn( "First alias '$alias' for $realName conflicts with " . "{$keepAlias[$caseFoldedAlias]} alias for " . $this->aliases[$caseFoldedAlias] ); } $first = false; } } } } return $this->aliases; } /** * Given a special page name with a possible subpage, return an array * where the first element is the special page name and the second is the * subpage. * * @param string $alias * @return array [ String, String|null ], or [ null, null ] if the page is invalid */ public function resolveAlias( $alias ) { $bits = explode( '/', $alias, 2 ); $caseFoldedAlias = $this->contLang->caseFold( $bits[0] ); $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias ); $aliases = $this->getAliasList(); if ( !isset( $aliases[$caseFoldedAlias] ) ) { return [ null, null ]; } $name = $aliases[$caseFoldedAlias]; $par = $bits[1] ?? null; // T4087 return [ $name, $par ]; } /** * Check if a given name exist as a special page or as a special page alias * * @param string $name Name of a special page * @return bool True if a special page exists with this name */ public function exists( $name ) { [ $title, /*...*/ ] = $this->resolveAlias( $name ); $specialPageList = $this->getPageList(); return isset( $specialPageList[$title] ); } /** * Find the object with a given name and return it (or NULL) * * @param string $name Special page name, may be localised and/or an alias * @return SpecialPage|null SpecialPage object or null if the page doesn't exist */ public function getPage( $name ) { [ $realName, /*...*/ ] = $this->resolveAlias( $name ); $specialPageList = $this->getPageList(); if ( isset( $specialPageList[$realName] ) ) { $rec = $specialPageList[$realName]; if ( is_array( $rec ) || is_string( $rec ) || is_callable( $rec ) ) { $page = $this->objectFactory->createObject( $rec, [ 'allowClassName' => true, 'allowCallable' => true ] ); } else { $page = null; } if ( $page instanceof SpecialPage ) { $page->setHookContainer( $this->hookContainer ); $page->setContentLanguage( $this->contLang ); $page->setSpecialPageFactory( $this ); return $page; } // It's not a classname, nor a callback, nor a legacy constructor array, // nor a special page object. Give up. wfLogWarning( "Cannot instantiate special page $realName: bad spec!" ); } return null; } /** * Get listed special pages available to the current user. * * This includes both unrestricted pages, and restricted pages * that the current user has the required permissions for. * * @param User $user User object to check permissions provided * @return SpecialPage[] */ public function getUsablePages( User $user ): array { $pages = []; foreach ( $this->getPageList() as $name => $rec ) { $page = $this->getPage( $name ); if ( $page ) { // not null $page->setContext( RequestContext::getMain() ); if ( $page->isListed() && ( !$page->isRestricted() || $page->userCanExecute( $user ) ) ) { $pages[$name] = $page; } } } return $pages; } /** * Get listed special pages available to everyone by default. * * @return array */ public function getRegularPages(): array { $pages = []; foreach ( $this->getPageList() as $name => $rec ) { $page = $this->getPage( $name ); if ( $page && $page->isListed() && !$page->isRestricted() ) { $pages[$name] = $page; } } return $pages; } /** * Get listed special pages, including those that may require user rights. * * @since 1.42 * @return array */ public function getListedPages(): array { $pages = []; foreach ( $this->getPageList() as $name => $rec ) { $page = $this->getPage( $name ); if ( $page && $page->isListed() ) { $pages[$name] = $page; } } return $pages; } /** * Execute a special page path. * The path may contain parameters, e.g. Special:Name/Params * Extracts the special page name and call the execute method, passing the parameters * * Returns a title object if the page is redirected, false if there was no such special * page, and true if it was successful. * * @param PageReference|string $path * @param IContextSource $context * @param bool $including Bool output is being captured for use in {{special:whatever}} * @param LinkRenderer|null $linkRenderer (since 1.28) * * @return bool|Title */ public function executePath( $path, IContextSource $context, $including = false, ?LinkRenderer $linkRenderer = null ) { if ( $path instanceof PageReference ) { $path = $path->getDBkey(); } $bits = explode( '/', $path, 2 ); $name = $bits[0]; $par = $bits[1] ?? null; // T4087 $page = $this->getPage( $name ); if ( !$page ) { // Emulate SpecialPage::setHeaders() $context->getOutput()->setArticleRelated( false ); $context->getOutput()->setRobotPolicy( 'noindex,nofollow' ); if ( $context->getConfig()->get( MainConfigNames::Send404Code ) ) { $context->getOutput()->setStatusCode( 404 ); } $context->getOutput()->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' ); return false; } if ( !$including ) { ProfilingContext::singleton()->init( MW_ENTRY_POINT, $page->getName() ); // Narrow DB query expectations for this HTTP request $trxLimits = $context->getConfig()->get( MainConfigNames::TrxProfilerLimits ); $trxProfiler = Profiler::instance()->getTransactionProfiler(); if ( $context->getRequest()->wasPosted() && !$page->doesWrites() ) { $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ ); } } // Page exists, set the context $page->setContext( $context ); if ( !$including ) { // Redirect to canonical alias for GET commands // Not for POST, we'd lose the post data, so it's best to just distribute // the request. Such POST requests are possible for old extensions that // generate self-links without being aware that their default name has // changed. if ( $name != $page->getLocalName() && !$context->getRequest()->wasPosted() ) { $query = $context->getRequest()->getQueryValues(); unset( $query['title'] ); $title = $page->getPageTitle( $par ?? false ); $url = $title->getFullURL( $query ); $context->getOutput()->redirect( $url ); return $title; } // @phan-suppress-next-line PhanUndeclaredMethod $context->setTitle( $page->getPageTitle( $par ?? false ) ); } elseif ( !$page->isIncludable() ) { return false; } $page->including( $including ); if ( $linkRenderer ) { $page->setLinkRenderer( $linkRenderer ); } // Execute special page $page->run( $par ); return true; } /** * Just like executePath() but will override global variables and execute * the page in "inclusion" mode. Returns true if the execution was * successful or false if there was no such special page, or a title object * if it was a redirect. * * Also saves the current $wgTitle, $wgOut, $wgRequest, $wgUser and $wgLang * variables so that the special page will get the context it'd expect on a * normal request, and then restores them to their previous values after. * * @param PageReference $page * @param IContextSource $context * @param LinkRenderer|null $linkRenderer (since 1.28) * @return bool|Title */ public function capturePath( PageReference $page, IContextSource $context, ?LinkRenderer $linkRenderer = null ) { // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser,MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle global $wgTitle, $wgOut, $wgRequest, $wgUser, $wgLang; $main = RequestContext::getMain(); // Save current globals and main context $glob = [ 'title' => $wgTitle, 'output' => $wgOut, 'request' => $wgRequest, 'user' => $wgUser, 'language' => $wgLang, ]; $ctx = [ 'title' => $main->getTitle(), 'output' => $main->getOutput(), 'request' => $main->getRequest(), 'user' => $main->getUser(), 'language' => $main->getLanguage(), ]; if ( $main->canUseWikiPage() ) { $ctx['wikipage'] = $main->getWikiPage(); } // just needed for $wgTitle and RequestContext::setTitle $title = $this->titleFactory->castFromPageReference( $page ); // Override $wgTitle = $title; $wgOut = $context->getOutput(); $wgRequest = $context->getRequest(); $wgUser = $context->getUser(); $wgLang = $context->getLanguage(); // FIXME: Once reasonably certain that no SpecialPage subclasses // rely on direct RequestContext::getMain instead of their local // context getters, these can be removed (T323184) // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged @$main->setTitle( $title ); $main->setOutput( $context->getOutput() ); $main->setRequest( $context->getRequest() ); $main->setUser( $context->getUser() ); $main->setLanguage( $context->getLanguage() ); try { // The useful part return $this->executePath( $page, $context, true, $linkRenderer ); } finally { // Restore old globals and context $wgTitle = $glob['title']; $wgOut = $glob['output']; $wgRequest = $glob['request']; $wgUser = $glob['user']; $wgLang = $glob['language']; // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged @$main->setTitle( $ctx['title'] ); $main->setOutput( $ctx['output'] ); $main->setRequest( $ctx['request'] ); $main->setUser( $ctx['user'] ); $main->setLanguage( $ctx['language'] ); if ( isset( $ctx['wikipage'] ) ) { $main->setWikiPage( $ctx['wikipage'] ); } } } /** * Get the local name for a specified canonical name * * @param string $name * @param string|false|null $subpage * @return string */ public function getLocalNameFor( $name, $subpage = false ) { $aliases = $this->contLang->getSpecialPageAliases(); $aliasList = $this->getAliasList(); // Find the first alias that maps back to $name if ( isset( $aliases[$name] ) ) { $found = false; foreach ( $aliases[$name] as $alias ) { $caseFoldedAlias = $this->contLang->caseFold( $alias ); $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias ); if ( isset( $aliasList[$caseFoldedAlias] ) && $aliasList[$caseFoldedAlias] === $name ) { $name = $alias; $found = true; break; } } if ( !$found ) { wfWarn( "Did not find a usable alias for special page '$name'. " . "It seems all defined aliases conflict?" ); } } else { // Check if someone misspelled the correct casing if ( is_array( $aliases ) ) { foreach ( $aliases as $n => $values ) { if ( strcasecmp( $name, $n ) === 0 ) { wfWarn( "Found alias defined for $n when searching for " . "special page aliases for $name. Case mismatch?" ); return $this->getLocalNameFor( $n, $subpage ); } } } wfWarn( "Did not find alias for special page '$name'. " . "Perhaps no aliases are defined for it?" ); } if ( $subpage !== false && $subpage !== null ) { // Make sure it's in dbkey form $subpage = str_replace( ' ', '_', $subpage ); $name = "$name/$subpage"; } return $this->contLang->ucfirst( $name ); } /** * Get a title for a given alias * * @param string $alias * @return Title|null Title or null if there is no such alias */ public function getTitleForAlias( $alias ) { [ $name, $subpage ] = $this->resolveAlias( $alias ); if ( $name != null ) { return SpecialPage::getTitleFor( $name, $subpage ); } return null; } }