Файловый менеджер - Редактировать - /var/www/html/specialpage.zip
Ðазад
PK ! �}q�* * IncludableSpecialPage.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\SpecialPage; /** * Shortcut to construct an includable special page. * * @ingroup SpecialPage */ class IncludableSpecialPage extends SpecialPage { public function __construct( $name, $restriction = '', $listed = true, $function = false, $file = 'default' ) { parent::__construct( $name, $restriction, $listed, $function, $file, true ); } public function isIncludable() { return true; } } /** @deprecated class alias since 1.41 */ class_alias( IncludableSpecialPage::class, 'IncludableSpecialPage' ); PK ! ,�I�� � SpecialPageFactory.phpnu �Iw�� <?php /** * Factory for handling the special page list and generating SpecialPage objects. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage * @defgroup SpecialPage SpecialPage */ namespace MediaWiki\SpecialPage; use MediaWiki\Config\ServiceOptions; use MediaWiki\Context\IContextSource; use MediaWiki\Context\RequestContext; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Language\Language; use MediaWiki\Linker\LinkRenderer; use MediaWiki\MainConfigNames; use MediaWiki\Page\PageReference; use MediaWiki\Profiler\ProfilingContext; use MediaWiki\Specials\Redirects\SpecialAllMyUploads; use MediaWiki\Specials\Redirects\SpecialListAdmins; use MediaWiki\Specials\Redirects\SpecialListBots; use MediaWiki\Specials\Redirects\SpecialMycontributions; use MediaWiki\Specials\Redirects\SpecialMylog; use MediaWiki\Specials\Redirects\SpecialMypage; use MediaWiki\Specials\Redirects\SpecialMytalk; use MediaWiki\Specials\Redirects\SpecialMyuploads; use MediaWiki\Specials\Redirects\SpecialTalkPage; use MediaWiki\Specials\SpecialActiveUsers; use MediaWiki\Specials\SpecialAllMessages; use MediaWiki\Specials\SpecialAllPages; use MediaWiki\Specials\SpecialAncientPages; use MediaWiki\Specials\SpecialApiHelp; use MediaWiki\Specials\SpecialApiSandbox; use MediaWiki\Specials\SpecialAuthenticationPopupSuccess; use MediaWiki\Specials\SpecialAutoblockList; use MediaWiki\Specials\SpecialBlankpage; use MediaWiki\Specials\SpecialBlock; use MediaWiki\Specials\SpecialBlockList; use MediaWiki\Specials\SpecialBookSources; use MediaWiki\Specials\SpecialBotPasswords; use MediaWiki\Specials\SpecialBrokenRedirects; use MediaWiki\Specials\SpecialCategories; use MediaWiki\Specials\SpecialChangeContentModel; use MediaWiki\Specials\SpecialChangeCredentials; use MediaWiki\Specials\SpecialChangeEmail; use MediaWiki\Specials\SpecialChangePassword; use MediaWiki\Specials\SpecialComparePages; use MediaWiki\Specials\SpecialConfirmEmail; use MediaWiki\Specials\SpecialContribute; use MediaWiki\Specials\SpecialContributions; use MediaWiki\Specials\SpecialCreateAccount; use MediaWiki\Specials\SpecialDeadendPages; use MediaWiki\Specials\SpecialDeletedContributions; use MediaWiki\Specials\SpecialDeletePage; use MediaWiki\Specials\SpecialDiff; use MediaWiki\Specials\SpecialDoubleRedirects; use MediaWiki\Specials\SpecialEditPage; use MediaWiki\Specials\SpecialEditRecovery; use MediaWiki\Specials\SpecialEditTags; use MediaWiki\Specials\SpecialEditWatchlist; use MediaWiki\Specials\SpecialEmailInvalidate; use MediaWiki\Specials\SpecialEmailUser; use MediaWiki\Specials\SpecialExpandTemplates; use MediaWiki\Specials\SpecialExport; use MediaWiki\Specials\SpecialFewestRevisions; use MediaWiki\Specials\SpecialFileDuplicateSearch; use MediaWiki\Specials\SpecialFilepath; use MediaWiki\Specials\SpecialGoToInterwiki; use MediaWiki\Specials\SpecialImport; use MediaWiki\Specials\SpecialJavaScriptTest; use MediaWiki\Specials\SpecialLinkAccounts; use MediaWiki\Specials\SpecialLinkSearch; use MediaWiki\Specials\SpecialListDuplicatedFiles; use MediaWiki\Specials\SpecialListFiles; use MediaWiki\Specials\SpecialListGrants; use MediaWiki\Specials\SpecialListGroupRights; use MediaWiki\Specials\SpecialListRedirects; use MediaWiki\Specials\SpecialListUsers; use MediaWiki\Specials\SpecialLockdb; use MediaWiki\Specials\SpecialLog; use MediaWiki\Specials\SpecialLonelyPages; use MediaWiki\Specials\SpecialLongPages; use MediaWiki\Specials\SpecialMediaStatistics; use MediaWiki\Specials\SpecialMergeHistory; use MediaWiki\Specials\SpecialMIMESearch; use MediaWiki\Specials\SpecialMostCategories; use MediaWiki\Specials\SpecialMostImages; use MediaWiki\Specials\SpecialMostInterwikis; use MediaWiki\Specials\SpecialMostLinked; use MediaWiki\Specials\SpecialMostLinkedCategories; use MediaWiki\Specials\SpecialMostLinkedTemplates; use MediaWiki\Specials\SpecialMostRevisions; use MediaWiki\Specials\SpecialMovePage; use MediaWiki\Specials\SpecialMute; use MediaWiki\Specials\SpecialMyLanguage; use MediaWiki\Specials\SpecialNamespaceInfo; use MediaWiki\Specials\SpecialNewFiles; use MediaWiki\Specials\SpecialNewPages; use MediaWiki\Specials\SpecialNewSection; use MediaWiki\Specials\SpecialPageData; use MediaWiki\Specials\SpecialPageHistory; use MediaWiki\Specials\SpecialPageInfo; use MediaWiki\Specials\SpecialPageLanguage; use MediaWiki\Specials\SpecialPagesWithProp; use MediaWiki\Specials\SpecialPasswordPolicies; use MediaWiki\Specials\SpecialPasswordReset; use MediaWiki\Specials\SpecialPermanentLink; use MediaWiki\Specials\SpecialPreferences; use MediaWiki\Specials\SpecialPrefixIndex; use MediaWiki\Specials\SpecialProtectedPages; use MediaWiki\Specials\SpecialProtectedTitles; use MediaWiki\Specials\SpecialProtectPage; use MediaWiki\Specials\SpecialPurge; use MediaWiki\Specials\SpecialRandomInCategory; use MediaWiki\Specials\SpecialRandomPage; use MediaWiki\Specials\SpecialRandomRedirect; use MediaWiki\Specials\SpecialRandomRootPage; use MediaWiki\Specials\SpecialRecentChanges; use MediaWiki\Specials\SpecialRecentChangesLinked; use MediaWiki\Specials\SpecialRedirect; use MediaWiki\Specials\SpecialRemoveCredentials; use MediaWiki\Specials\SpecialRenameUser; use MediaWiki\Specials\SpecialResetTokens; use MediaWiki\Specials\SpecialRestSandbox; use MediaWiki\Specials\SpecialRevisionDelete; use MediaWiki\Specials\SpecialRunJobs; use MediaWiki\Specials\SpecialSearch; use MediaWiki\Specials\SpecialShortPages; use MediaWiki\Specials\SpecialSpecialPages; use MediaWiki\Specials\SpecialStatistics; use MediaWiki\Specials\SpecialTags; use MediaWiki\Specials\SpecialTrackingCategories; use MediaWiki\Specials\SpecialUnblock; use MediaWiki\Specials\SpecialUncategorizedCategories; use MediaWiki\Specials\SpecialUncategorizedImages; use MediaWiki\Specials\SpecialUncategorizedPages; use MediaWiki\Specials\SpecialUncategorizedTemplates; use MediaWiki\Specials\SpecialUndelete; use MediaWiki\Specials\SpecialUnlinkAccounts; use MediaWiki\Specials\SpecialUnlockdb; use MediaWiki\Specials\SpecialUnusedCategories; use MediaWiki\Specials\SpecialUnusedImages; use MediaWiki\Specials\SpecialUnusedTemplates; use MediaWiki\Specials\SpecialUnwatchedPages; use MediaWiki\Specials\SpecialUpload; use MediaWiki\Specials\SpecialUploadStash; use MediaWiki\Specials\SpecialUserLogin; use MediaWiki\Specials\SpecialUserLogout; use MediaWiki\Specials\SpecialUserRights; use MediaWiki\Specials\SpecialVersion; use MediaWiki\Specials\SpecialWantedCategories; use MediaWiki\Specials\SpecialWantedFiles; use MediaWiki\Specials\SpecialWantedPages; use MediaWiki\Specials\SpecialWantedTemplates; use MediaWiki\Specials\SpecialWatchlist; use MediaWiki\Specials\SpecialWhatLinksHere; use MediaWiki\Specials\SpecialWithoutInterwiki; use MediaWiki\Title\Title; use MediaWiki\Title\TitleFactory; use MediaWiki\User\User; use Profiler; use Wikimedia\DebugInfo\DebugInfoTrait; use Wikimedia\ObjectFactory\ObjectFactory; /** * Factory for handling the special page list and generating SpecialPage objects. * * To add a special page in an extension, add to $wgSpecialPages either * an object instance or an array containing the name and constructor * parameters. The latter is preferred for performance reasons. * * The object instantiated must be either an instance of SpecialPage or a * sub-class thereof. It must have an execute() method, which sends the HTML * for the special page to $wgOut. The parent class has an execute() method * which distributes the call to the historical global functions. Additionally, * execute() also checks if the user has the necessary access privileges * and bails out if not. * * To add a core special page, use the similar static list in * SpecialPageFactory::$list. To remove a core static special page at runtime, use * a SpecialPage_initList hook. * * @ingroup SpecialPage * @since 1.17 */ class SpecialPageFactory { use DebugInfoTrait; /** * List of special page names to the subclass of SpecialPage which handles them. */ private const CORE_LIST = [ // Maintenance Reports 'BrokenRedirects' => [ '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', ] ], 'Userlogout' => [ 'class' => SpecialUserLogout::class, 'services' => [ 'TempUserConfig', ], ], 'CreateAccount' => [ 'class' => SpecialCreateAccount::class, 'services' => [ 'AuthManager', 'FormatterFactory', ] ], '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', ] ], 'Block' => [ 'class' => SpecialBlock::class, 'services' => [ 'BlockUtils', 'BlockPermissionCheckerFactory', 'BlockUserFactory', 'DatabaseBlockStore', 'UserNameUtils', 'UserNamePrefixSearch', 'BlockActionInfo', 'TitleFormatter', 'NamespaceInfo' ] ], 'Unblock' => [ 'class' => SpecialUnblock::class, 'services' => [ 'UnblockUserFactory', 'BlockUtils', 'DatabaseBlockStore', 'UserNameUtils', 'UserNamePrefixSearch', 'WatchlistManager', ] ], 'BlockList' => [ 'class' => SpecialBlockList::class, 'services' => [ 'LinkBatchFactory', 'DatabaseBlockStore', 'BlockRestrictionStore', 'ConnectionProvider', 'CommentStore', 'BlockUtils', 'HideUserUtils', 'BlockActionInfo', 'RowCommentFormatter', ], ], 'AutoblockList' => [ 'class' => SpecialAutoblockList::class, 'services' => [ 'LinkBatchFactory', 'BlockRestrictionStore', 'ConnectionProvider', 'CommentStore', 'BlockUtils', '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', ] ], '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', 'MessageCache', 'UserOptionsLookup', 'ChangeTagsStore', 'UserIdentityUtils', 'TempUserConfig', ] ], 'Recentchangeslinked' => [ 'class' => SpecialRecentChangesLinked::class, 'services' => [ 'WatchedItemStore', 'MessageCache', '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', 'CommentFormatter', '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, ], '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', ] ], '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', 'MovePageFactory', 'PermissionManager', 'TitleFactory', 'UserFactory', 'UserNamePrefixSearch', 'UserNameUtils', ] ], '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 * * @return 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. * @return array */ 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<string,SpecialPage> */ 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<string,SpecialPage> */ 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; } } PK ! �,=]�u �u QueryPage.phpnu �Iw�� <?php /** * Base code for "query" special pages. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage */ namespace MediaWiki\SpecialPage; use Exception; use MediaWiki\Cache\LinkBatchFactory; use MediaWiki\Config\Config; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Linker\LinkTarget; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Output\OutputPage; use MediaWiki\Specials\SpecialAncientPages; use MediaWiki\Specials\SpecialBrokenRedirects; use MediaWiki\Specials\SpecialDeadendPages; use MediaWiki\Specials\SpecialDoubleRedirects; use MediaWiki\Specials\SpecialFewestRevisions; use MediaWiki\Specials\SpecialLinkSearch; use MediaWiki\Specials\SpecialListDuplicatedFiles; use MediaWiki\Specials\SpecialListRedirects; use MediaWiki\Specials\SpecialLonelyPages; use MediaWiki\Specials\SpecialLongPages; use MediaWiki\Specials\SpecialMediaStatistics; use MediaWiki\Specials\SpecialMIMESearch; use MediaWiki\Specials\SpecialMostCategories; use MediaWiki\Specials\SpecialMostImages; use MediaWiki\Specials\SpecialMostInterwikis; use MediaWiki\Specials\SpecialMostLinked; use MediaWiki\Specials\SpecialMostLinkedCategories; use MediaWiki\Specials\SpecialMostLinkedTemplates; use MediaWiki\Specials\SpecialMostRevisions; use MediaWiki\Specials\SpecialShortPages; use MediaWiki\Specials\SpecialUncategorizedCategories; use MediaWiki\Specials\SpecialUncategorizedImages; use MediaWiki\Specials\SpecialUncategorizedPages; use MediaWiki\Specials\SpecialUncategorizedTemplates; use MediaWiki\Specials\SpecialUnusedCategories; use MediaWiki\Specials\SpecialUnusedImages; use MediaWiki\Specials\SpecialUnusedTemplates; use MediaWiki\Specials\SpecialUnwatchedPages; use MediaWiki\Specials\SpecialWantedCategories; use MediaWiki\Specials\SpecialWantedFiles; use MediaWiki\Specials\SpecialWantedPages; use MediaWiki\Specials\SpecialWantedTemplates; use MediaWiki\Specials\SpecialWithoutInterwiki; use MediaWiki\Xml\Xml; use Skin; use stdClass; use Wikimedia\Rdbms\DBError; use Wikimedia\Rdbms\IConnectionProvider; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\ILoadBalancer; use Wikimedia\Rdbms\IReadableDatabase; use Wikimedia\Rdbms\IResultWrapper; use Wikimedia\Rdbms\SelectQueryBuilder; /** * This is a class for doing query pages; since they're almost all the same, * we factor out some of the functionality into a superclass, and let * subclasses derive from it. * * @stable to extend * * @ingroup SpecialPage */ abstract class QueryPage extends SpecialPage { /** @var bool Whether or not we want plain listoutput rather than an ordered list */ protected $listoutput = false; /** @var int The offset and limit in use, as passed to the query() function */ protected $offset = 0; /** @var int */ protected $limit = 0; /** * The number of rows returned by the query. Reading this variable * only makes sense in functions that are run after the query has been * done, such as preprocessResults() and formatRow(). * * @var int */ protected $numRows; /** * @var string|null|false */ protected $cachedTimestamp = null; /** * @var bool Whether to show prev/next links */ protected $shownavigation = true; /** @var ILoadBalancer|null */ private $loadBalancer = null; /** @var IConnectionProvider|null */ private $databaseProvider = null; /** @var LinkBatchFactory|null */ private $linkBatchFactory = null; /** * Get a list of query page classes and their associated special pages, * for periodic updates. * * DO NOT CHANGE THIS LIST without testing that * maintenance/updateSpecialPages.php still works. * * @return array[] List of [ string $class, string $specialPageName, ?int $limit (optional) ]. * Limit defaults to $wgQueryCacheLimit if not given. */ public static function getPages() { static $qp = null; if ( $qp === null ) { $qp = [ [ SpecialAncientPages::class, 'Ancientpages' ], [ SpecialBrokenRedirects::class, 'BrokenRedirects' ], [ SpecialDeadendPages::class, 'Deadendpages' ], [ SpecialDoubleRedirects::class, 'DoubleRedirects' ], [ SpecialListDuplicatedFiles::class, 'ListDuplicatedFiles' ], [ SpecialLinkSearch::class, 'LinkSearch' ], [ SpecialListRedirects::class, 'Listredirects' ], [ SpecialLonelyPages::class, 'Lonelypages' ], [ SpecialLongPages::class, 'Longpages' ], [ SpecialMediaStatistics::class, 'MediaStatistics', SpecialMediaStatistics::MAX_LIMIT ], [ SpecialMIMESearch::class, 'MIMEsearch' ], [ SpecialMostCategories::class, 'Mostcategories' ], [ SpecialMostImages::class, 'Mostimages' ], [ SpecialMostInterwikis::class, 'Mostinterwikis' ], [ SpecialMostLinkedCategories::class, 'Mostlinkedcategories' ], [ SpecialMostLinkedTemplates::class, 'Mostlinkedtemplates' ], [ SpecialMostLinked::class, 'Mostlinked' ], [ SpecialMostRevisions::class, 'Mostrevisions' ], [ SpecialFewestRevisions::class, 'Fewestrevisions' ], [ SpecialShortPages::class, 'Shortpages' ], [ SpecialUncategorizedCategories::class, 'Uncategorizedcategories' ], [ SpecialUncategorizedPages::class, 'Uncategorizedpages' ], [ SpecialUncategorizedImages::class, 'Uncategorizedimages' ], [ SpecialUncategorizedTemplates::class, 'Uncategorizedtemplates' ], [ SpecialUnusedCategories::class, 'Unusedcategories' ], [ SpecialUnusedImages::class, 'Unusedimages' ], [ SpecialWantedCategories::class, 'Wantedcategories' ], [ SpecialWantedFiles::class, 'Wantedfiles' ], [ SpecialWantedPages::class, 'Wantedpages' ], [ SpecialWantedTemplates::class, 'Wantedtemplates' ], [ SpecialUnwatchedPages::class, 'Unwatchedpages' ], [ SpecialUnusedTemplates::class, 'Unusedtemplates' ], [ SpecialWithoutInterwiki::class, 'Withoutinterwiki' ], ]; ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onWgQueryPages( $qp ); } return $qp; } /** * @since 1.36 * @param LinkBatchFactory $linkBatchFactory */ final protected function setLinkBatchFactory( LinkBatchFactory $linkBatchFactory ) { $this->linkBatchFactory = $linkBatchFactory; } /** * @since 1.36 * @return LinkBatchFactory */ final protected function getLinkBatchFactory(): LinkBatchFactory { if ( $this->linkBatchFactory === null ) { // Fallback if not provided // TODO Change to wfWarn in a future release $this->linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory(); } return $this->linkBatchFactory; } /** * Get a list of disabled query pages and their run mode * @param Config $config * @return string[] */ public static function getDisabledQueryPages( Config $config ) { $disableQueryPageUpdate = $config->get( MainConfigNames::DisableQueryPageUpdate ); if ( !is_array( $disableQueryPageUpdate ) ) { return []; } $pages = []; foreach ( $disableQueryPageUpdate as $name => $runMode ) { if ( is_int( $name ) ) { // The run mode may be omitted $pages[$runMode] = 'disabled'; } else { $pages[$name] = $runMode; } } return $pages; } /** * A mutator for $this->listoutput; * * @param bool $bool */ protected function setListoutput( $bool ) { $this->listoutput = $bool; } /** * Subclasses return an SQL query here, formatted as an array with the * following keys: * tables => Table(s) for passing to Database::select() * fields => Field(s) for passing to Database::select(), may be * * conds => WHERE conditions * options => options * join_conds => JOIN conditions * * Note that the query itself should return the following three columns: * 'namespace', 'title', and 'value'. 'value' is used for sorting. * * These may be stored in the querycache table for expensive queries, * and that cached data will be returned sometimes, so the presence of * extra fields can't be relied upon. The cached 'value' column will be * an integer; non-numeric values are useful only for sorting the * initial query (except if they're timestamps, see usesTimestamps()). * * Don't include an ORDER or LIMIT clause, they will be added. * * @return array * @since 1.18, abstract since 1.43 */ abstract public function getQueryInfo(); /** * Subclasses return an array of fields to order by here. Don't append * DESC to the field names, that'll be done automatically if * sortDescending() returns true. * @stable to override * @return string[] * @since 1.18 */ protected function getOrderFields() { return [ 'value' ]; } /** * Does this query return timestamps rather than integers in its * 'value' field? If true, this class will convert 'value' to a * UNIX timestamp for caching. * NOTE: formatRow() may get timestamps in TS_MW (mysql), TS_DB (pgsql) * or TS_UNIX (querycache) format, so be sure to always run them * through wfTimestamp() * @stable to override * @return bool * @since 1.18 */ public function usesTimestamps() { return false; } /** * Override to sort by increasing values * * @stable to override * @return bool */ protected function sortDescending() { return true; } /** * Should this query page only be updated offline on large wikis? * * If the query for this page is considered too expensive to run on-demand * for large wikis (e.g. every time the special page is viewed), then * implement this as returning true. * * "Large wikis" are those that enable $wgMiserMode. The value of * ::isExpensive() has no effect by default when $wgMiserMode is off. * * It is expected that such large wikis, periodically run the * UpdateSpecialPages maintenance script to update these query pages. * * By enabling $wgDisableQueryPages, all query pages will be considered * as expensive and updated offline only, even query pages that do not * mark themselves as expensive. * * @stable to override * @return bool */ public function isExpensive() { return $this->getConfig()->get( MainConfigNames::DisableQueryPages ); } /** * Is the output of this query cacheable? Non-cacheable expensive pages * will be disabled in miser mode and will not have their results written * to the querycache table. * @stable to override * @return bool * @since 1.18 */ public function isCacheable() { return true; } /** * Whether or not the output of the page in question is retrieved from * the database cache. * * @stable to override * @return bool */ public function isCached() { return $this->isExpensive() && $this->getConfig()->get( MainConfigNames::MiserMode ); } /** * Sometimes we don't want to build rss / atom feeds. * * @stable to override * @return bool */ public function isSyndicated() { return true; } /** * Formats the results of the query for display. The skin is the current * skin; you can use it for making links. The result is a single row of * result data. You should be able to grab SQL results from it. * If the function returns false, the line output will be skipped. * @param Skin $skin * @param stdClass $result Result row * @return string|bool String or false to skip */ abstract protected function formatResult( $skin, $result ); /** * The content returned by this function will be output before any result * * @stable to override * @return string */ protected function getPageHeader() { return ''; } /** * Outputs some kind of an informative message (via OutputPage) to let the * user know that the query returned nothing and thus there's nothing to * show. * * @since 1.26 */ protected function showEmptyText() { $this->getOutput()->addWikiMsg( 'specialpage-empty' ); } /** * If using extra form wheely-dealies, return a set of parameters here * as an associative array. They will be encoded and added to the paging * links (prev/next/lengths). * * @stable to override * @return array */ protected function linkParameters() { return []; } /** * Clear the cache and save new results * * @stable to override * * @param int|false $limit Limit for SQL statement or false for no limit * @param bool $unused Unused since 1.43, kept for backwards-compatibility * @throws DBError|Exception * @return bool|int */ public function recache( $limit, $unused = true ) { if ( !$this->isCacheable() ) { return 0; } $fname = static::class . '::recache'; $dbw = $this->getDatabaseProvider()->getPrimaryDatabase(); // Do query $res = $this->reallyDoQuery( $limit, false ); $num = false; if ( $res ) { $num = $res->numRows(); // Fetch results $vals = []; foreach ( $res as $i => $row ) { if ( isset( $row->value ) ) { if ( $this->usesTimestamps() ) { $value = (int)wfTimestamp( TS_UNIX, $row->value ); } else { $value = intval( $row->value ); // T16414 } } else { $value = $i; } $vals[] = [ 'qc_type' => $this->getName(), 'qc_namespace' => $row->namespace, 'qc_title' => $row->title, 'qc_value' => $value ]; } $dbw->doAtomicSection( $fname, function ( IDatabase $dbw, $fname ) use ( $vals ) { // Clear out any old cached data $dbw->newDeleteQueryBuilder() ->deleteFrom( 'querycache' ) ->where( [ 'qc_type' => $this->getName() ] ) ->caller( $fname )->execute(); // Update the querycache_info record for the page $dbw->newInsertQueryBuilder() ->insertInto( 'querycache_info' ) ->row( [ 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ] ) ->onDuplicateKeyUpdate() ->uniqueIndexFields( [ 'qci_type' ] ) ->set( [ 'qci_timestamp' => $dbw->timestamp() ] ) ->caller( $fname )->execute(); } ); // Save results into the querycache table on the primary DB if ( count( $vals ) ) { foreach ( array_chunk( $vals, 500 ) as $chunk ) { $dbw->newInsertQueryBuilder() ->insertInto( 'querycache' ) ->rows( $chunk ) ->caller( $fname )->execute(); } } } return $num; } /** * Get a DB connection to be used for slow recache queries * @stable to override * @return IDatabase */ protected function getRecacheDB() { return $this->getDBLoadBalancer() ->getConnection( ILoadBalancer::DB_REPLICA, 'vslow' ); } /** * Remove a cached result. * Useful for interactive backlogs where the user can fix problems in-place. * @param LinkTarget $title The page to remove. * @since 1.34 */ public function delete( LinkTarget $title ) { if ( $this->isCached() ) { $dbw = $this->getDatabaseProvider()->getPrimaryDatabase(); $dbw->newDeleteQueryBuilder() ->deleteFrom( 'querycache' ) ->where( [ 'qc_type' => $this->getName(), 'qc_namespace' => $title->getNamespace(), 'qc_title' => $title->getDBkey(), ] ) ->caller( __METHOD__ )->execute(); } } /** * Remove all cached value * This is needed when the page is no longer using the cache * @since 1.36 */ public function deleteAllCachedData() { $fname = static::class . '::' . __FUNCTION__; $dbw = $this->getDatabaseProvider()->getPrimaryDatabase(); $dbw->newDeleteQueryBuilder() ->deleteFrom( 'querycache' ) ->where( [ 'qc_type' => $this->getName() ] ) ->caller( $fname )->execute(); $dbw->newDeleteQueryBuilder() ->deleteFrom( 'querycachetwo' ) ->where( [ 'qcc_type' => $this->getName() ] ) ->caller( $fname )->execute(); $dbw->newDeleteQueryBuilder() ->deleteFrom( 'querycache_info' ) ->where( [ 'qci_type' => $this->getName() ] ) ->caller( $fname )->execute(); } /** * Run the query and return the result * @stable to override * @param int|false $limit Numerical limit or false for no limit * @param int|false $offset Numerical offset or false for no offset * @return IResultWrapper * @since 1.18 */ public function reallyDoQuery( $limit, $offset = false ) { $fname = static::class . '::reallyDoQuery'; $dbr = $this->getRecacheDB(); $query = $this->getQueryInfo(); $order = $this->getOrderFields(); if ( $this->sortDescending() ) { foreach ( $order as &$field ) { $field .= ' DESC'; } } $tables = isset( $query['tables'] ) ? (array)$query['tables'] : []; $fields = isset( $query['fields'] ) ? (array)$query['fields'] : []; $conds = isset( $query['conds'] ) ? (array)$query['conds'] : []; $options = isset( $query['options'] ) ? (array)$query['options'] : []; $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : []; $queryBuilder = $dbr->newSelectQueryBuilder() ->tables( $tables ) ->fields( $fields ) ->conds( $conds ) ->caller( $fname ) ->options( $options ) ->joinConds( $join_conds ); if ( $order ) { $queryBuilder->orderBy( $order ); } if ( $limit !== false ) { $queryBuilder->limit( intval( $limit ) ); } if ( $offset !== false ) { $queryBuilder->offset( intval( $offset ) ); } return $queryBuilder->fetchResultSet(); } /** * Somewhat deprecated, you probably want to be using execute() * @param int|false $offset * @param int|false $limit * @return IResultWrapper */ public function doQuery( $offset = false, $limit = false ) { if ( $this->isCached() && $this->isCacheable() ) { return $this->fetchFromCache( $limit, $offset ); } else { return $this->reallyDoQuery( $limit, $offset ); } } /** * Fetch the query results from the query cache * @stable to override * * @param int|false $limit Numerical limit or false for no limit * @param int|false $offset Numerical offset or false for no offset * @return IResultWrapper * @since 1.18 */ public function fetchFromCache( $limit, $offset = false ) { $dbr = $this->getDatabaseProvider()->getReplicaDatabase(); $queryBuilder = $dbr->newSelectQueryBuilder() ->select( [ 'qc_type', 'namespace' => 'qc_namespace', 'title' => 'qc_title', 'value' => 'qc_value' ] ) ->from( 'querycache' ) ->where( [ 'qc_type' => $this->getName() ] ); if ( $limit !== false ) { $queryBuilder->limit( intval( $limit ) ); } if ( $offset !== false ) { $queryBuilder->offset( intval( $offset ) ); } $order = $this->getCacheOrderFields(); if ( $this->sortDescending() ) { $queryBuilder->orderBy( $order, SelectQueryBuilder::SORT_DESC ); } else { $queryBuilder->orderBy( $order ); } return $queryBuilder->caller( __METHOD__ )->fetchResultSet(); } /** * Return the order fields for fetchFromCache. Default is to always use * "ORDER BY value" which was the default prior to this function. * @stable to override * @return array * @since 1.29 */ protected function getCacheOrderFields() { return [ 'value' ]; } /** * @return string|false */ public function getCachedTimestamp() { if ( $this->cachedTimestamp === null ) { $dbr = $this->getDatabaseProvider()->getReplicaDatabase(); $fname = static::class . '::getCachedTimestamp'; $this->cachedTimestamp = $dbr->newSelectQueryBuilder() ->select( 'qci_timestamp' ) ->from( 'querycache_info' ) ->where( [ 'qci_type' => $this->getName() ] ) ->caller( $fname )->fetchField(); } return $this->cachedTimestamp; } /** * Returns limit and offset, as returned by $this->getRequest()->getLimitOffsetForUser(). * Subclasses may override this to further restrict or modify limit and offset. * * @note Restricts the offset parameter, as most query pages have inefficient paging * * Its generally expected that the returned limit will not be 0, and the returned * offset will be less than the max results. * * @since 1.26 * @return int[] list( $limit, $offset ) */ protected function getLimitOffset() { [ $limit, $offset ] = $this->getRequest() ->getLimitOffsetForUser( $this->getUser() ); if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) { $maxResults = $this->getMaxResults(); // Can't display more than max results on a page $limit = min( $limit, $maxResults ); // Can't skip over more than the end of $maxResults $offset = min( $offset, $maxResults + 1 ); } return [ $limit, $offset ]; } /** * What is limit to fetch from DB * * Used to make it appear the DB stores less results then it actually does * @param int $uiLimit Limit from UI * @param int $uiOffset Offset from UI * @return int Limit to use for DB (not including extra row to see if at end) */ protected function getDBLimit( $uiLimit, $uiOffset ) { $maxResults = $this->getMaxResults(); if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) { $limit = min( $uiLimit + 1, $maxResults - $uiOffset ); return max( $limit, 0 ); } else { return $uiLimit + 1; } } /** * Get max number of results we can return in miser mode. * * Most QueryPage subclasses use inefficient paging, so limit the max amount we return * This matters for uncached query pages that might otherwise accept an offset of 3 million * * @stable to override * @since 1.27 * @return int */ protected function getMaxResults() { // Max of 10000, unless we store more than 10000 in query cache. return max( $this->getConfig()->get( MainConfigNames::QueryCacheLimit ), 10000 ); } /** * This is the actual workhorse. It does everything needed to make a * real, honest-to-gosh query page. * @stable to override * @param string|null $par */ public function execute( $par ) { $this->checkPermissions(); $this->setHeaders(); $this->outputHeader(); $out = $this->getOutput(); if ( $this->isCached() && !$this->isCacheable() ) { $out->addWikiMsg( 'querypage-disabled' ); return; } $out->setSyndicated( $this->isSyndicated() ); if ( $this->limit == 0 && $this->offset == 0 ) { [ $this->limit, $this->offset ] = $this->getLimitOffset(); } $dbLimit = $this->getDBLimit( $this->limit, $this->offset ); // @todo Use doQuery() if ( !$this->isCached() ) { // select one extra row for navigation $res = $this->reallyDoQuery( $dbLimit, $this->offset ); } else { // Get the cached result, select one extra row for navigation $res = $this->fetchFromCache( $dbLimit, $this->offset ); if ( !$this->listoutput ) { // Fetch the timestamp of this update $ts = $this->getCachedTimestamp(); $lang = $this->getLanguage(); $maxResults = $lang->formatNum( $this->getConfig()->get( MainConfigNames::QueryCacheLimit ) ); if ( $ts ) { $user = $this->getUser(); $updated = $lang->userTimeAndDate( $ts, $user ); $updateddate = $lang->userDate( $ts, $user ); $updatedtime = $lang->userTime( $ts, $user ); $out->addMeta( 'Data-Cache-Time', $ts ); $out->addJsConfigVars( 'dataCacheTime', $ts ); $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults ); } else { $out->addWikiMsg( 'perfcached', $maxResults ); } // If updates on this page have been disabled, let the user know // that the data set won't be refreshed for now $disabledQueryPages = self::getDisabledQueryPages( $this->getConfig() ); if ( isset( $disabledQueryPages[$this->getName()] ) ) { $runMode = $disabledQueryPages[$this->getName()]; if ( $runMode === 'disabled' ) { $out->wrapWikiMsg( "<div class=\"mw-querypage-no-updates\">\n$1\n</div>", 'querypage-no-updates' ); } else { // Messages used here: querypage-updates-periodical $out->wrapWikiMsg( "<div class=\"mw-querypage-updates-" . $runMode . "\">\n$1\n</div>", 'querypage-updates-' . $runMode ); } } } } $this->numRows = $res->numRows(); $dbr = $this->getRecacheDB(); $this->preprocessResults( $dbr, $res ); $out->addHTML( Xml::openElement( 'div', [ 'class' => 'mw-spcontent' ] ) ); // Top header and navigation if ( $this->shownavigation ) { $out->addHTML( $this->getPageHeader() ); if ( $this->numRows > 0 ) { $out->addHTML( $this->msg( 'showingresultsinrange' )->numParams( min( $this->numRows, $this->limit ), // do not show the one extra row, if exist $this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() ); // Disable the "next" link when we reach the end $miserMaxResults = $this->getConfig()->get( MainConfigNames::MiserMode ) && ( $this->offset + $this->limit >= $this->getMaxResults() ); $atEnd = ( $this->numRows <= $this->limit ) || $miserMaxResults; $paging = $this->buildPrevNextNavigation( $this->offset, $this->limit, $this->linkParameters(), $atEnd, $par ); $out->addHTML( '<p>' . $paging . '</p>' ); } else { // No results to show, so don't bother with "showing X of Y" etc. // -- just let the user know and give up now $this->showEmptyText(); $out->addHTML( Xml::closeElement( 'div' ) ); return; } } // The actual results; specialist subclasses will want to handle this // with more than a straight list, so we hand them the info, plus // an OutputPage, and let them get on with it $this->outputResults( $out, $this->getSkin(), $dbr, // Should use IResultWrapper for this $res, min( $this->numRows, $this->limit ), // do not format the one extra row, if exist $this->offset ); // Repeat the paging links at the bottom if ( $this->shownavigation ) { // @phan-suppress-next-line PhanPossiblyUndeclaredVariable paging is set when used here $out->addHTML( '<p>' . $paging . '</p>' ); } $out->addHTML( Xml::closeElement( 'div' ) ); } /** * Format and output report results using the given information plus * OutputPage * * @stable to override * * @param OutputPage $out OutputPage to print to * @param Skin $skin User skin to use * @param IReadableDatabase $dbr Database (read) connection to use * @param IResultWrapper $res Result pointer * @param int $num Number of available result rows * @param int $offset Paging offset */ protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) { if ( $num > 0 ) { $html = []; if ( !$this->listoutput ) { $html[] = $this->openList( $offset ); } // $res might contain the whole 1,000 rows, so we read up to // $num [should update this to use a Pager] // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.Found for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) { $line = $this->formatResult( $skin, $row ); if ( $line ) { $html[] = $this->listoutput ? $line : "<li>{$line}</li>\n"; } } if ( !$this->listoutput ) { $html[] = $this->closeList(); } $html = $this->listoutput ? $this->getContentLanguage()->listToText( $html ) : implode( '', $html ); $out->addHTML( $html ); } } /** * @param int $offset * @return string */ protected function openList( $offset ) { return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n"; } /** * @return string */ protected function closeList() { return "</ol>\n"; } /** * Do any necessary preprocessing of the result object. * @stable to override * @param IDatabase $db * @param IResultWrapper $res */ protected function preprocessResults( $db, $res ) { } /** * Creates a new LinkBatch object, adds all pages from the passed result wrapper (MUST include * title and optional the namespace field) and executes the batch. This operation will pre-cache * LinkCache information like page existence and information for stub color and redirect hints. * * @note Call self::setLinkBatchFactory from special page constructor when use * * @param IResultWrapper $res The result wrapper to process. Needs to include the title * field and namespace field, if the $ns parameter isn't set. * @param int|null $ns Use this namespace for the given titles in the result wrapper, * instead of the namespace value of $res. */ protected function executeLBFromResultWrapper( IResultWrapper $res, $ns = null ) { if ( !$res->numRows() ) { return; } $batch = $this->getLinkBatchFactory()->newLinkBatch(); foreach ( $res as $row ) { $batch->add( $ns ?? (int)$row->namespace, $row->title ); } $batch->execute(); $res->seek( 0 ); } /** * @since 1.36 * @deprecated since 1.43, use self::setDatabaseProvider * @param ILoadBalancer $loadBalancer */ final protected function setDBLoadBalancer( ILoadBalancer $loadBalancer ) { $this->loadBalancer = $loadBalancer; } /** * @since 1.36 * @deprecated since 1.43, use self::getDatabaseProvider * @return ILoadBalancer */ final protected function getDBLoadBalancer(): ILoadBalancer { if ( $this->loadBalancer === null ) { // Fallback if not provided // TODO Change to wfWarn in a future release $this->loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer(); } return $this->loadBalancer; } /** * @since 1.41 * @param IConnectionProvider $databaseProvider */ final protected function setDatabaseProvider( IConnectionProvider $databaseProvider ) { $this->databaseProvider = $databaseProvider; } /** * @since 1.41 * @return IConnectionProvider */ final protected function getDatabaseProvider(): IConnectionProvider { if ( $this->databaseProvider === null ) { $this->databaseProvider = MediaWikiServices::getInstance()->getConnectionProvider(); } return $this->databaseProvider; } } /** @deprecated class alias since 1.41 */ class_alias( QueryPage::class, 'QueryPage' ); PK ! �)- PageQueryPage.phpnu �Iw�� <?php /** * Variant of QueryPage which formats the result as a simple link to the page. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage */ namespace MediaWiki\SpecialPage; use HtmlArmor; use MediaWiki\Html\Html; use MediaWiki\Language\ILanguageConverter; use MediaWiki\Linker\Linker; use MediaWiki\MediaWikiServices; use MediaWiki\Title\Title; use Skin; use stdClass; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\IResultWrapper; /** * Variant of QueryPage which formats the result as a simple link to the page * * @stable to extend * @ingroup SpecialPage */ abstract class PageQueryPage extends QueryPage { /** @var ILanguageConverter|null */ private $languageConverter = null; /** * Run a LinkBatch to pre-cache LinkCache information, * like page existence and information for stub color and redirect hints. * This should be done for live data and cached data. * * @stable to override * * @param IDatabase $db * @param IResultWrapper $res */ public function preprocessResults( $db, $res ) { $this->executeLBFromResultWrapper( $res ); } /** * @since 1.36 * @param ILanguageConverter $languageConverter */ final protected function setLanguageConverter( ILanguageConverter $languageConverter ) { $this->languageConverter = $languageConverter; } /** * @note Call self::setLanguageConverter in your constructor when overriding * * @since 1.36 * @return ILanguageConverter */ final protected function getLanguageConverter(): ILanguageConverter { if ( $this->languageConverter === null ) { // Fallback if not provided // TODO Change to wfWarn in a future release $this->languageConverter = MediaWikiServices::getInstance()->getLanguageConverterFactory() ->getLanguageConverter( $this->getContentLanguage() ); } return $this->languageConverter; } /** * Format the result as a simple link to the page * * @stable to override * * @param Skin $skin * @param stdClass $row Result row * @return string */ public function formatResult( $skin, $row ) { $title = Title::makeTitleSafe( $row->namespace, $row->title ); if ( $title instanceof Title ) { $text = $this->getLanguageConverter()->convertHtml( $title->getPrefixedText() ); return $this->getLinkRenderer()->makeLink( $title, new HtmlArmor( $text ) ); } else { return Html::element( 'span', [ 'class' => 'mw-invalidtitle' ], Linker::getInvalidTitleDescription( $this->getContext(), $row->namespace, $row->title ) ); } } } /** @deprecated class alias since 1.41 */ class_alias( PageQueryPage::class, 'PageQueryPage' ); PK ! "� r$� $� ContributionsSpecialPage.phpnu �Iw�� <?php /** * Implements Special:Contributions * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage */ namespace MediaWiki\SpecialPage; use LogEventsList; use MediaWiki\Block\Block; use MediaWiki\Block\DatabaseBlockStore; use MediaWiki\Html\Html; use MediaWiki\HTMLForm\Field\HTMLMultiSelectField; use MediaWiki\HTMLForm\HTMLForm; use MediaWiki\MainConfigNames; use MediaWiki\Pager\ContribsPager; use MediaWiki\Pager\ContributionsPager; use MediaWiki\Permissions\PermissionManager; use MediaWiki\PoolCounter\PoolCounterWorkViaCallback; use MediaWiki\Specials\SpecialUserRights; use MediaWiki\Status\Status; use MediaWiki\Title\NamespaceInfo; use MediaWiki\Title\Title; use MediaWiki\User\ExternalUserNames; use MediaWiki\User\Options\UserOptionsLookup; use MediaWiki\User\User; use MediaWiki\User\UserFactory; use MediaWiki\User\UserIdentity; use MediaWiki\User\UserIdentityLookup; use MediaWiki\User\UserNamePrefixSearch; use MediaWiki\User\UserNameUtils; use MediaWiki\User\UserRigorOptions; use Wikimedia\IPUtils; use Wikimedia\Rdbms\IConnectionProvider; /** * Show user contributions in a paged list. * * This was refactored out from SpecialContributions to make it easier to * add new special pages with similar functionality and similar output. * Hooks formerly for SpecialContributions are run here to avoid needing * to duplicate hooks for each subclass. * * The subclass must provide an implementation of ::getPager, and may * disable syndication feed functionality by overriding ::providesFeeds. * * @stable to extend * @ingroup SpecialPage * @since 1.43 Refactored from SpecialContributions */ class ContributionsSpecialPage extends IncludableSpecialPage { /** @var array */ protected $opts = []; /** @var bool */ protected $formErrors = false; protected IConnectionProvider $dbProvider; protected NamespaceInfo $namespaceInfo; protected PermissionManager $permissionManager; protected UserNameUtils $userNameUtils; protected UserNamePrefixSearch $userNamePrefixSearch; protected UserOptionsLookup $userOptionsLookup; protected UserFactory $userFactory; protected UserIdentityLookup $userIdentityLookup; protected DatabaseBlockStore $blockStore; /** * @param PermissionManager $permissionManager * @param IConnectionProvider $dbProvider * @param NamespaceInfo $namespaceInfo * @param UserNameUtils $userNameUtils * @param UserNamePrefixSearch $userNamePrefixSearch * @param UserOptionsLookup $userOptionsLookup * @param UserFactory $userFactory * @param UserIdentityLookup $userIdentityLookup * @param DatabaseBlockStore $blockStore * @param string $name * @param string $restriction */ public function __construct( PermissionManager $permissionManager, IConnectionProvider $dbProvider, NamespaceInfo $namespaceInfo, UserNameUtils $userNameUtils, UserNamePrefixSearch $userNamePrefixSearch, UserOptionsLookup $userOptionsLookup, UserFactory $userFactory, UserIdentityLookup $userIdentityLookup, DatabaseBlockStore $blockStore, $name, $restriction = '' ) { parent::__construct( $name, $restriction ); $this->permissionManager = $permissionManager; $this->dbProvider = $dbProvider; $this->namespaceInfo = $namespaceInfo; $this->userNameUtils = $userNameUtils; $this->userNamePrefixSearch = $userNamePrefixSearch; $this->userOptionsLookup = $userOptionsLookup; $this->userFactory = $userFactory; $this->userIdentityLookup = $userIdentityLookup; $this->blockStore = $blockStore; } /** * @inheritDoc */ public function execute( $par ) { $this->setHeaders(); $this->outputHeader(); $this->checkPermissions(); $out = $this->getOutput(); // Modules required for viewing the list of contributions (also when included on other pages) $out->addModuleStyles( [ 'jquery.makeCollapsible.styles', 'mediawiki.interface.helpers.styles', 'mediawiki.special', 'mediawiki.special.changeslist', ] ); $out->addBodyClasses( 'mw-special-ContributionsSpecialPage' ); $out->addModules( [ // Certain skins e.g. Minerva might have disabled this module. 'mediawiki.page.ready' ] ); $this->addHelpLink( 'Help:User contributions' ); $request = $this->getRequest(); $target = $par ?? $request->getVal( 'target', '' ); '@phan-var string $target'; // getVal does not return null here // Normalize underscores that may be present in the target parameter // if it was passed in as a path param, rather than a query param // where HTMLForm may have already performed preprocessing (T372444). $target = $this->userNameUtils->getCanonical( $target, UserNameUtils::RIGOR_NONE ); $this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' ); // Explicitly check for false or empty string as this needs to account // for the rare case where the target parameter is '0' which is a valid // target but resolves to false in boolean context (T379515). if ( $target === false || $target === '' ) { $out->addHTML( $this->getForm( $this->opts ) ); return; } $user = $this->getUser(); $this->opts['limit'] = $request->getInt( 'limit', $this->userOptionsLookup->getIntOption( $user, 'rclimit' ) ); $this->opts['target'] = $target; $this->opts['topOnly'] = $request->getBool( 'topOnly' ); $this->opts['newOnly'] = $request->getBool( 'newOnly' ); $this->opts['hideMinor'] = $request->getBool( 'hideMinor' ); $ns = $request->getVal( 'namespace', null ); if ( $ns !== null && $ns !== '' && $ns !== 'all' ) { $this->opts['namespace'] = intval( $ns ); } else { $this->opts['namespace'] = ''; } // Backwards compatibility: Before using OOUI form the old HTML form had // fields for nsInvert and associated. These have now been replaced with the // wpFilters query string parameters. These are retained to keep old URIs working. $this->opts['associated'] = $request->getBool( 'associated' ); $this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' ); $nsFilters = $request->getArray( 'wpfilters', null ); if ( $nsFilters !== null ) { $this->opts['associated'] = in_array( 'associated', $nsFilters ); $this->opts['nsInvert'] = in_array( 'nsInvert', $nsFilters ); } $this->opts['tagfilter'] = array_filter( explode( '|', (string)$request->getVal( 'tagfilter' ) ), static function ( $el ) { return $el !== ''; } ); $this->opts['tagInvert'] = $request->getBool( 'tagInvert' ); // Allows reverts to have the bot flag in recent changes. It is just here to // be passed in the form at the top of the page if ( $this->permissionManager->userHasRight( $user, 'markbotedits' ) && $request->getBool( 'bot' ) ) { $this->opts['bot'] = '1'; } $this->opts['year'] = $request->getIntOrNull( 'year' ); $this->opts['month'] = $request->getIntOrNull( 'month' ); $this->opts['start'] = $request->getVal( 'start' ); $this->opts['end'] = $request->getVal( 'end' ); $notExternal = !ExternalUserNames::isExternal( $target ); if ( $notExternal ) { $nt = Title::makeTitleSafe( NS_USER, $target ); if ( !$nt ) { $out->addHTML( $this->getForm( $this->opts ) ); return; } $target = $nt->getText(); if ( IPUtils::isValidRange( $target ) ) { $target = IPUtils::sanitizeRange( $target ); } } $userObj = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE ); if ( !$userObj ) { $out->addHTML( $this->getForm( $this->opts ) ); return; } $out->addSubtitle( $this->contributionsSub( $userObj, $target ) ); $out->setPageTitleMsg( $this->msg( $this->getResultsPageTitleMessageKey( $userObj ), $target ) ); # For IP ranges, we want the contributionsSub, but not the skin-dependent # links under 'Tools', which may include irrelevant links like 'Logs'. if ( $notExternal && !IPUtils::isValidRange( $target ) && ( $this->userNameUtils->isIP( $target ) || $userObj->isRegistered() ) ) { // Don't add non-existent users, because hidden users // that we add here will be removed later to pretend // that they don't exist, and if users that actually don't // exist are added here and then not removed, it exposes // which users exist and are hidden vs. which actually don't // exist. But, do set the relevant user for single IPs. $this->getSkin()->setRelevantUser( $userObj ); } $this->opts = ContribsPager::processDateFilter( $this->opts ); if ( $this->opts['namespace'] !== '' && $this->opts['namespace'] < NS_MAIN ) { $this->getOutput()->wrapWikiMsg( "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>", [ 'negative-namespace-not-supported' ] ); $out->addHTML( $this->getForm( $this->opts ) ); return; } if ( $this->providesFeeds() ) { $feedType = $request->getVal( 'feed' ); $feedParams = [ 'action' => 'feedcontributions', 'user' => $target, ]; if ( $this->opts['topOnly'] ) { $feedParams['toponly'] = true; } if ( $this->opts['newOnly'] ) { $feedParams['newonly'] = true; } if ( $this->opts['hideMinor'] ) { $feedParams['hideminor'] = true; } if ( $this->opts['deletedOnly'] ) { $feedParams['deletedonly'] = true; } if ( $this->opts['tagfilter'] !== [] ) { $feedParams['tagfilter'] = $this->opts['tagfilter']; } if ( $this->opts['namespace'] !== '' ) { $feedParams['namespace'] = $this->opts['namespace']; } // Don't use year and month for the feed URL, but pass them on if // we redirect to API (if $feedType is specified) if ( $feedType && isset( $this->opts['year'] ) ) { $feedParams['year'] = $this->opts['year']; } if ( $feedType && isset( $this->opts['month'] ) ) { $feedParams['month'] = $this->opts['month']; } if ( $feedType ) { // Maintain some level of backwards compatibility // If people request feeds using the old parameters, redirect to API $feedParams['feedformat'] = $feedType; $url = wfAppendQuery( wfScript( 'api' ), $feedParams ); $out->redirect( $url, '301' ); return; } // Add RSS/atom links $this->addFeedLinks( $feedParams ); } if ( $this->getHookRunner()->onSpecialContributionsBeforeMainOutput( $notExternal ? $userObj->getId() : 0, $userObj, $this ) ) { $out->addHTML( $this->getForm( $this->opts ) ); if ( $this->formErrors ) { return; } // We want a pure UserIdentity for imported actors, so the first letter // of them is in lowercase and queryable. $userIdentity = $notExternal ? $userObj : $this->userIdentityLookup->getUserIdentityByName( $target ) ?? $userObj; $pager = $this->getPager( $userIdentity ); if ( IPUtils::isValidRange( $target ) && !ContribsPager::isQueryableRange( $target, $this->getConfig() ) ) { // Valid range, but outside CIDR limit. $limits = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit ); $limit = $limits[ IPUtils::isIPv4( $target ) ? 'IPv4' : 'IPv6' ]; $out->addWikiMsg( 'sp-contributions-outofrange', $limit ); } else { // @todo We just want a wiki ID here, not a "DB domain", but // current status of MediaWiki conflates the two. See T235955. $poolKey = $this->dbProvider->getReplicaDatabase()->getDomainID() . ':Special' . $this->mName . ':'; if ( $this->getUser()->isAnon() ) { $poolKey .= 'a:' . $this->getUser()->getName(); } else { $poolKey .= 'u:' . $this->getUser()->getId(); } $work = new PoolCounterWorkViaCallback( 'Special' . $this->mName, $poolKey, [ 'doWork' => function () use ( $pager, $out, $target ) { if ( !$pager->getNumRows() ) { $out->addWikiMsg( 'nocontribs', $target ); } else { # Show a message about replica DB lag, if applicable $lag = $pager->getDatabase()->getSessionLagStatus()['lag']; if ( $lag > 0 ) { $out->showLagWarning( $lag ); } $output = $pager->getBody(); if ( !$this->including() ) { $output = $pager->getNavigationBar() . $output . $pager->getNavigationBar(); } $out->addHTML( $output ); } }, 'error' => function () use ( $out ) { $msg = $this->getUser()->isAnon() ? 'sp-contributions-concurrency-ip' : 'sp-contributions-concurrency-user'; $out->addHTML( Html::errorBox( $out->msg( $msg )->parse() ) ); } ] ); $work->execute(); } $out->setPreventClickjacking( $pager->getPreventClickjacking() ); # Show the appropriate "footer" message - WHOIS tools, etc. if ( IPUtils::isValidRange( $target ) && ContribsPager::isQueryableRange( $target, $this->getConfig() ) ) { $message = 'sp-contributions-footer-anon-range'; } elseif ( IPUtils::isIPAddress( $target ) ) { $message = 'sp-contributions-footer-anon'; } elseif ( $userObj->isAnon() ) { // No message for non-existing users $message = ''; } elseif ( $userObj->isHidden() && !$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' ) ) { // User is registered, but make sure that the viewer can't see them, to avoid // having different behavior for missing and hidden users; see T120883 $message = ''; } else { // Not hidden, or hidden but the viewer can still see it $message = 'sp-contributions-footer'; } if ( $message && !$this->including() && !$this->msg( $message, $target )->isDisabled() ) { $out->wrapWikiMsg( "<div class='mw-contributions-footer'>\n$1\n</div>", [ $message, $target ] ); } } } /** * Generates the subheading with links * @param User $userObj User object for the target * @param string $targetName This mostly the same as $userObj->getName() but * normalization may make it differ. // T272225 * @return string Appropriately-escaped HTML to be output literally */ protected function contributionsSub( $userObj, $targetName ) { $isAnon = $userObj->isAnon(); if ( !$isAnon && $userObj->isHidden() && !$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' ) ) { // T120883 if the user is hidden and the viewer cannot see hidden // users, pretend like it does not exist at all. $isAnon = true; } if ( $isAnon ) { // Show a warning message that the user being searched for doesn't exist. // UserNameUtils::isIP returns true for IP address and usemod IPs like '123.123.123.xxx', // but returns false for IP ranges. We don't want to suggest either of these are // valid usernames which we would with the 'contributions-userdoesnotexist' message. if ( !$this->userNameUtils->isIP( $userObj->getName() ) && !IPUtils::isValidRange( $userObj->getName() ) ) { $this->getOutput()->addHTML( Html::warningBox( $this->getOutput()->msg( 'contributions-userdoesnotexist', wfEscapeWikiText( $userObj->getName() ) )->parse(), 'mw-userpage-userdoesnotexist' ) ); if ( !$this->including() ) { $this->getOutput()->setStatusCode( 404 ); } } $user = htmlspecialchars( $userObj->getName() ); } else { $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() ); } $nt = $userObj->getUserPage(); $talk = $userObj->getTalkPage(); $links = ''; // T211910. Don't show action links if a range is outside block limit $showForIp = IPUtils::isValid( $userObj ) || ( IPUtils::isValidRange( $userObj ) && ContribsPager::isQueryableRange( $userObj, $this->getConfig() ) ); // T276306. if the user is hidden and the viewer cannot see hidden, pretend that it does not exist $registeredAndVisible = $userObj->isRegistered() && ( !$userObj->isHidden() || $this->permissionManager->userHasRight( $this->getUser(), 'hideuser' ) ); $shouldShowLinks = $talk && ( $registeredAndVisible || $showForIp ); if ( $shouldShowLinks ) { $tools = $this->getUserLinks( $this, $userObj ); $links = Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] ); foreach ( $tools as $tool ) { $links .= Html::rawElement( 'span', [], $tool ) . ' '; } $links = trim( $links ) . Html::closeElement( 'span' ); // Show a note if the user is blocked and display the last block log entry. // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs, // and also this will display a totally irrelevant log entry as a current block. $shouldShowBlocks = $this->shouldShowBlockLogExtract( $userObj ); if ( $shouldShowBlocks ) { // For IP ranges you must give DatabaseBlock::newFromTarget the CIDR string // and not a user object. if ( IPUtils::isValidRange( $userObj->getName() ) ) { $block = $this->blockStore ->newFromTarget( $userObj->getName(), $userObj->getName() ); } else { $block = $this->blockStore->newFromTarget( $userObj, $userObj ); } if ( $block !== null && $block->getType() != Block::TYPE_AUTO ) { if ( $block->getType() == Block::TYPE_RANGE ) { $nt = $this->namespaceInfo->getCanonicalName( NS_USER ) . ':' . $block->getTargetName(); } $out = $this->getOutput(); // showLogExtract() wants first parameter by reference if ( $userObj->isAnon() ) { $msgKey = $block->isSitewide() ? 'sp-contributions-blocked-notice-anon' : 'sp-contributions-blocked-notice-anon-partial'; } else { $msgKey = $block->isSitewide() ? 'sp-contributions-blocked-notice' : 'sp-contributions-blocked-notice-partial'; } // Allow local styling overrides for different types of block $class = $block->isSitewide() ? 'mw-contributions-blocked-notice' : 'mw-contributions-blocked-notice-partial'; LogEventsList::showLogExtract( $out, 'block', $nt, '', [ 'lim' => 1, 'showIfEmpty' => false, 'msgKey' => [ $msgKey, $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice' ], 'offset' => '', # don't use WebRequest parameter offset 'wrap' => Html::rawElement( 'div', [ 'class' => $class ], '$1' ), ] ); } } } // First subheading. "For Username (talk | block log | logs | etc.)" $userName = $userObj->getName(); $subHeadingsHtml = Html::rawElement( 'div', [ 'class' => 'mw-contributions-user-tools' ], $this->msg( 'contributions-subtitle' )->rawParams( $user )->params( $userName ) . ' ' . $links ); // Second subheading. "A user with 37,208 edits. Account created on 2008-09-17." if ( $talk && $registeredAndVisible ) { $editCount = $userObj->getEditCount(); $userInfo = $this->msg( 'contributions-edit-count' ) ->params( $userName ) ->numParams( $editCount ) ->escaped(); $accountCreationDate = $userObj->getRegistration(); if ( $accountCreationDate ) { $date = $this->getLanguage()->date( $accountCreationDate, true ); $userInfo .= $this->msg( 'word-separator' ) ->escaped(); $userInfo .= $this->msg( 'contributions-account-creation-date' ) ->plaintextParams( $date ) ->escaped(); } $subHeadingsHtml .= Html::rawElement( 'div', [ 'class' => 'mw-contributions-editor-info' ], $userInfo ); } return $subHeadingsHtml; } /** * Links to different places. * * @note This function is also called in DeletedContributionsPage * @param SpecialPage $sp SpecialPage instance, for context * @param User $target Target user object * @return array */ protected function getUserLinks( SpecialPage $sp, User $target ) { $id = $target->getId(); $username = $target->getName(); $userpage = $target->getUserPage(); $talkpage = $target->getTalkPage(); $isIP = IPUtils::isValid( $username ); $isRange = IPUtils::isValidRange( $username ); $linkRenderer = $sp->getLinkRenderer(); $tools = []; # No talk pages for IP ranges. if ( !$isRange ) { $tools['user-talk'] = $linkRenderer->makeLink( $talkpage, $sp->msg( 'sp-contributions-talk' )->text(), [ 'class' => 'mw-contributions-link-talk' ] ); } # Block / Change block / Unblock links if ( $this->permissionManager->userHasRight( $sp->getUser(), 'block' ) ) { if ( $target->getBlock() && $target->getBlock()->getType() != Block::TYPE_AUTO ) { $tools['block'] = $linkRenderer->makeKnownLink( # Change block link SpecialPage::getTitleFor( 'Block', $username ), $sp->msg( 'change-blocklink' )->text(), [ 'class' => 'mw-contributions-link-change-block' ] ); $tools['unblock'] = $linkRenderer->makeKnownLink( # Unblock link SpecialPage::getTitleFor( 'Unblock', $username ), $sp->msg( 'unblocklink' )->text(), [ 'class' => 'mw-contributions-link-unblock' ] ); } else { # User is not blocked $tools['block'] = $linkRenderer->makeKnownLink( # Block link SpecialPage::getTitleFor( 'Block', $username ), $sp->msg( 'blocklink' )->text(), [ 'class' => 'mw-contributions-link-block' ] ); } } # Block log link $tools['log-block'] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Log', 'block' ), $sp->msg( 'sp-contributions-blocklog' )->text(), [ 'class' => 'mw-contributions-link-block-log' ], [ 'page' => $userpage->getPrefixedText() ] ); # Suppression log link (T61120) if ( $this->permissionManager->userHasRight( $sp->getUser(), 'suppressionlog' ) ) { $tools['log-suppression'] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Log', 'suppress' ), $sp->msg( 'sp-contributions-suppresslog', $username )->text(), [ 'class' => 'mw-contributions-link-suppress-log' ], [ 'offender' => $username ] ); } # Don't show some links for IP ranges if ( !$isRange ) { # Uploads: hide if IPs cannot upload (T220674) if ( !$isIP || $this->permissionManager->userHasRight( $target, 'upload' ) ) { $tools['uploads'] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Listfiles', $username ), $sp->msg( 'sp-contributions-uploads' )->text(), [ 'class' => 'mw-contributions-link-uploads' ] ); } # Other logs link # Todo: T146628 $tools['logs'] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Log', $username ), $sp->msg( 'sp-contributions-logs' )->text(), [ 'class' => 'mw-contributions-link-logs' ] ); # Add link to deleted user contributions for privileged users # Todo: T183457 if ( $this->permissionManager->userHasRight( $sp->getUser(), 'deletedhistory' ) ) { $tools['deletedcontribs'] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'DeletedContributions', $username ), $sp->msg( 'sp-contributions-deleted', $username )->text(), [ 'class' => 'mw-contributions-link-deleted-contribs' ] ); } } # (T373988) Don't show some links for temporary accounts if ( !$target->isTemp() ) { # Add a link to change user rights for privileged users $userrightsPage = new SpecialUserRights(); $userrightsPage->setContext( $sp->getContext() ); if ( $userrightsPage->userCanChangeRights( $target ) ) { $tools['userrights'] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Userrights', $username ), $sp->msg( 'sp-contributions-userrights', $username )->text(), [ 'class' => 'mw-contributions-link-user-rights' ] ); } # Add a link to rename the user if ( $id && $this->permissionManager->userHasRight( $sp->getUser(), 'renameuser' ) ) { $tools['renameuser'] = $sp->getLinkRenderer()->makeKnownLink( SpecialPage::getTitleFor( 'Renameuser' ), $sp->msg( 'renameuser-linkoncontribs', $userpage->getText() )->text(), [ 'title' => $sp->msg( 'renameuser-linkoncontribs-text', $userpage->getText() )->parse() ], [ 'oldusername' => $userpage->getText() ] ); } } $this->getHookRunner()->onContributionsToolLinks( $id, $userpage, $tools, $sp ); return $tools; } /** * Get the target field for the form * * @param string $target * @return array */ protected function getTargetField( $target ) { return [ 'type' => 'user', 'default' => str_replace( '_', ' ', $target ), 'label' => $this->msg( 'sp-contributions-username' )->text(), 'name' => 'target', 'id' => 'mw-target-user-or-ip', 'size' => 40, 'autofocus' => $target === '', 'section' => 'contribs-top', 'ipallowed' => true, 'usemodwiki-ipallowed' => true, 'iprange' => true, 'external' => true, 'required' => true, ]; } /** * Generates the namespace selector form with hidden attributes. * @param array $pagerOptions with keys contribs, user, deletedOnly, limit, target, topOnly, * newOnly, hideMinor, namespace, associated, nsInvert, tagfilter, tagInvert, year, start, end * @return string HTML fragment */ protected function getForm( array $pagerOptions ) { if ( $this->including() ) { // Do not show a form when special page is included in wikitext return ''; } // Modules required only for the form $this->getOutput()->addModules( [ 'mediawiki.special.contributions', ] ); $this->getOutput()->enableOOUI(); $fields = []; # Add hidden params for tracking except for parameters in $skipParameters $skipParameters = [ 'namespace', 'nsInvert', 'deletedOnly', 'target', 'year', 'month', 'start', 'end', 'topOnly', 'newOnly', 'hideMinor', 'associated', 'tagfilter', 'tagInvert', 'title', ]; foreach ( $this->opts as $name => $value ) { if ( in_array( $name, $skipParameters ) ) { continue; } $fields[$name] = [ 'name' => $name, 'type' => 'hidden', 'default' => $value, ]; } $target = $this->opts['target'] ?? ''; $fields['target'] = $this->getTargetField( $target ); $ns = $this->opts['namespace'] ?? null; $fields['namespace'] = [ 'type' => 'namespaceselect', 'label' => $this->msg( 'namespace' )->text(), 'name' => 'namespace', 'cssclass' => 'namespaceselector', 'default' => $ns, 'id' => 'namespace', 'section' => 'contribs-top', ]; $fields['nsFilters'] = [ 'class' => HTMLMultiSelectField::class, 'label' => '', 'name' => 'wpfilters', 'flatlist' => true, // Only shown when namespaces are selected. 'hide-if' => [ '===', 'namespace', 'all' ], 'options-messages' => [ 'invert' => 'nsInvert', 'namespace_association' => 'associated', ], 'section' => 'contribs-top', ]; $fields['tagfilter'] = [ 'type' => 'tagfilter', 'cssclass' => 'mw-tagfilter-input', 'id' => 'tagfilter', 'label-message' => [ 'tag-filter', 'parse' ], 'name' => 'tagfilter', 'size' => 20, 'section' => 'contribs-top', ]; $fields['tagInvert'] = [ 'type' => 'check', 'id' => 'tagInvert', 'label' => $this->msg( 'invert' ), 'name' => 'tagInvert', 'hide-if' => [ '===', 'tagfilter', '' ], 'section' => 'contribs-top', ]; if ( $this->permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) { $fields['deletedOnly'] = [ 'type' => 'check', 'id' => 'mw-show-deleted-only', 'label' => $this->msg( 'history-show-deleted' )->text(), 'name' => 'deletedOnly', 'section' => 'contribs-top', ]; } if ( !$this->isArchive() ) { $fields['topOnly'] = [ 'type' => 'check', 'id' => 'mw-show-top-only', 'label' => $this->msg( 'sp-contributions-toponly' )->text(), 'name' => 'topOnly', 'section' => 'contribs-top', ]; } $fields['newOnly'] = [ 'type' => 'check', 'id' => 'mw-show-new-only', 'label' => $this->msg( 'sp-contributions-newonly' )->text(), 'name' => 'newOnly', 'section' => 'contribs-top', ]; $fields['hideMinor'] = [ 'type' => 'check', 'cssclass' => 'mw-hide-minor-edits', 'id' => 'mw-show-new-only', 'label' => $this->msg( 'sp-contributions-hideminor' )->text(), 'name' => 'hideMinor', 'section' => 'contribs-top', ]; // Allow additions at this point to the filters. $rawFilters = []; $this->getHookRunner()->onSpecialContributions__getForm__filters( $this, $rawFilters ); foreach ( $rawFilters as $filter ) { // Backwards compatibility support for previous hook function signature. if ( is_string( $filter ) ) { $fields[] = [ 'type' => 'info', 'default' => $filter, 'raw' => true, 'section' => 'contribs-top', ]; wfDeprecatedMsg( 'A SpecialContributions::getForm::filters hook handler returned ' . 'an array of strings, this is deprecated since MediaWiki 1.33', '1.33', false, false ); } else { // Preferred append method. $fields[] = $filter; } } $fields['start'] = [ 'type' => 'date', 'default' => '', 'id' => 'mw-date-start', 'label' => $this->msg( 'date-range-from' )->text(), 'name' => 'start', 'section' => 'contribs-date', ]; $fields['end'] = [ 'type' => 'date', 'default' => '', 'id' => 'mw-date-end', 'label' => $this->msg( 'date-range-to' )->text(), 'name' => 'end', 'section' => 'contribs-date', ]; $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() ); $htmlForm ->setMethod( 'get' ) ->setTitle( $this->getPageTitle() ) // When offset is defined, the user is paging through results // so we hide the form by default to allow users to focus on browsing // rather than defining search parameters ->setCollapsibleOptions( ( $pagerOptions['target'] ?? null ) || ( $pagerOptions['start'] ?? null ) || ( $pagerOptions['end'] ?? null ) ) ->setAction( wfScript() ) ->setSubmitTextMsg( 'sp-contributions-submit' ) ->setWrapperLegendMsg( $this->getFormWrapperLegendMessageKey() ); $htmlForm->prepareForm(); // Submission is handled elsewhere, but do this to check for and display errors $htmlForm->setSubmitCallback( static function () { return true; } ); $result = $htmlForm->tryAuthorizedSubmit(); if ( !( $result === true || ( $result instanceof Status && $result->isGood() ) ) ) { // Uncollapse if there are errors $htmlForm->setCollapsibleOptions( false ); $this->formErrors = true; } return $htmlForm->getHTML( $result ); } /** * Return an array of subpages beginning with $search that this special page will accept. * * @param string $search Prefix to search for * @param int $limit Maximum number of results to return (usually 10) * @param int $offset Number of results to skip (usually 0) * @return string[] Matching subpages */ public function prefixSearchSubpages( $search, $limit, $offset ) { $search = $this->userNameUtils->getCanonical( $search ); if ( !$search ) { // No prefix suggestion for invalid user return []; } // Autocomplete subpage as user list - public to allow caching return $this->userNamePrefixSearch ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset ); } /** * @return bool This SpecialPage provides syndication feeds. */ protected function providesFeeds() { return true; } /** * Define whether this page shows existing revisions (from the revision table) or * revisions of deleted pages (from the archive table). * * @return bool This page shows existing revisions */ protected function isArchive() { return false; } /** * @param UserIdentity $targetUser The normalized target user identity * @return ContributionsPager */ protected function getPager( $targetUser ) { // TODO: This class and the classes it extends should be abstract, and this // method should be abstract. throw new \LogicException( __METHOD__ . " must be overridden" ); } /** * @inheritDoc */ protected function getGroupName() { return 'users'; } /** * @return string Message key for the fieldset wrapping the form */ protected function getFormWrapperLegendMessageKey() { return 'sp-contributions-search'; } /** * @param UserIdentity $target The target of the search that produced the results page * @return string Message key for the results page title */ protected function getResultsPageTitleMessageKey( UserIdentity $target ) { return 'contributions-title'; } /** * Whether the block log extract should be shown on the special page. This is public to allow extensions which * add block log entries to skip adding them when this returns false. * * @since 1.43 * @param UserIdentity $target The target of the search that produced the results page * @return bool Whether the block log extract should be shown if the target is blocked. */ public function shouldShowBlockLogExtract( UserIdentity $target ): bool { return !$this->including(); } } PK ! �vo� � RedirectSpecialArticle.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\SpecialPage; use MediaWiki\Title\Title; /** * Helper for any RedirectSpecialPage which redirects the user * to a particular article (as opposed to user contributions, logs, etc.). * * This is used by subclasses to create user-independent URLs pointing to * pages about the current user (user page, talk page, contributions, etc.). * This can let us link it statically and cache-safe within wikitext, * e.g. on help pages. * * For security reasons these special pages are restricted to only preserve * the following subset of GET parameters to the target page, while * removing and/or ignoring all others. * * - useskin, uselang, printable: to alter the appearance of the resulting page * * - redirect: allows viewing one's user page or talk page even if it is a * redirect. * * - rdfrom: allows redirecting to one's user page or talk page from an * external wiki with the "Redirect from..." notice. * * - limit, offset: Useful for linking to history of one's own user page or * user talk page. For example, this would be a link to "the last edit to your * user talk page in the year 2010": * https://en.wikipedia.org/wiki/Special:MyPage?offset=20110000000000&limit=1&action=history * * - feed: would allow linking to the current user's RSS feed for their user * talk page: * https://en.wikipedia.org/w/index.php?title=Special:MyTalk&action=history&feed=rss * * - preloadtitle: Can be used to provide a default section title for a * preloaded new comment on one's own talk page. * * - summary : Can be used to provide a default edit summary for a preloaded * edit to one's own user page or talk page. * * - preview: Allows showing/hiding preview on first edit regardless of user * preference, useful for preloaded edits where you know preview wouldn't be * useful. * * - redlink: Affects the message the user sees if their talk page/user talk * page does not currently exist. Avoids confusion for newbies with no user * pages over why they got a "permission error" following this link: * https://en.wikipedia.org/w/index.php?title=Special:MyPage&redlink=1 * * - debug: determines whether the debug parameter is passed to load.php, * which disables reformatting and allows scripts to be debugged. Useful * when debugging scripts that manipulate one's own user page or talk page. * * @par Hook extension: * Extensions can add to the redirect parameters list by using the hook * RedirectSpecialArticleRedirectParams * * This hook allows extensions which add GET parameters like FlaggedRevs to * retain those parameters when redirecting using special pages. * * @par Hook extension example: * @code * $wgHooks['RedirectSpecialArticleRedirectParams'][] = * 'MyExtensionHooks::onRedirectSpecialArticleRedirectParams'; * public static function onRedirectSpecialArticleRedirectParams( &$redirectParams ) { * $redirectParams[] = 'stable'; * return true; * } * @endcode * * @stable to extend * @ingroup SpecialPage */ abstract class RedirectSpecialArticle extends RedirectSpecialPage { /** * @stable to call * * @param string $name */ public function __construct( $name ) { parent::__construct( $name ); $redirectParams = [ 'action', 'redirect', 'rdfrom', # Options for preloaded edits 'preload', 'preloadparams', 'editintro', 'preloadtitle', 'summary', 'nosummary', # Options for overriding user settings 'preview', 'minor', 'watchthis', # Options for history/diffs 'section', 'oldid', 'diff', 'dir', 'limit', 'offset', 'feed', # Misc options 'redlink', # Options for action=raw; missing ctype can break JS or CSS in some browsers 'ctype', 'maxage', 'smaxage', ]; $this->getHookRunner()->onRedirectSpecialArticleRedirectParams( $redirectParams ); $this->mAllowedRedirectParams = $redirectParams; } /** * @inheritDoc */ public function getRedirectQuery( $subpage ) { $query = parent::getRedirectQuery( $subpage ); $title = $this->getRedirect( $subpage ); // Avoid double redirect for action=edit&redlink=1 for existing pages // (compare to the check in EditPage::edit) if ( $query && isset( $query['action'] ) && isset( $query['redlink'] ) && ( $query['action'] === 'edit' || $query['action'] === 'submit' ) && (bool)$query['redlink'] && $title instanceof Title && $title->exists() ) { return false; } return $query; } } /** @deprecated class alias since 1.41 */ class_alias( RedirectSpecialArticle::class, 'RedirectSpecialArticle' ); PK ! ���� � DisabledSpecialPage.phpnu �Iw�� <?php /** * Special page for replacing manually disabled special pages * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage */ namespace MediaWiki\SpecialPage; use Closure; use MediaWiki\Html\Html; use MediaWiki\Message\Message; /** * This class is a drop-in replacement for other special pages that need to be manually * disabled. To use it, just put something like * * $wgSpecialPages['Name'] = DisabledSpecialPage::getCallback( 'Name', 'message' ); * * in the local configuration (where 'Name' is the canonical name of the special page * to be disabled, and 'message' is a message key for explaining the reason for disabling). * * @since 1.33 */ class DisabledSpecialPage extends UnlistedSpecialPage { /** @var Message */ protected $errorMessage; /** * Create a callback suitable for use in $wgSpecialPages. * @param string $name Canonical name of the special page that's being replaced. * @param Message|string|null $errorMessage Error message to show when users try to use the page. * @return Closure */ public static function getCallback( $name, $errorMessage = null ) { return static function () use ( $name, $errorMessage ) { return new DisabledSpecialPage( $name, $errorMessage ); }; } /** * @param string $name Canonical name of the special page that's being replaced. * @param Message|string|null $errorMessage Error message to show when users try to use the page. */ public function __construct( $name, $errorMessage = null ) { parent::__construct( $name ); $this->errorMessage = $errorMessage ?: 'disabledspecialpage-disabled'; } public function execute( $subPage ) { $this->setHeaders(); $this->outputHeader(); $error = Html::rawElement( 'div', [ 'class' => 'error', ], $this->msg( $this->errorMessage )->parseAsBlock() ); $this->getOutput()->addHTML( $error ); } } /** @deprecated class alias since 1.41 */ class_alias( DisabledSpecialPage::class, 'DisabledSpecialPage' ); PK ! k4� � ImageQueryPage.phpnu �Iw�� <?php /** * Variant of QueryPage which uses a gallery to output results. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage */ namespace MediaWiki\SpecialPage; use ImageGalleryBase; use MediaWiki\Output\OutputPage; use MediaWiki\Title\Title; use Skin; use stdClass; use Wikimedia\Rdbms\IReadableDatabase; use Wikimedia\Rdbms\IResultWrapper; /** * Variant of QueryPage which uses a gallery to output results, thus * suited for reports generating images * * @stable to extend * * @ingroup SpecialPage * @author Rob Church <robchur@gmail.com> */ abstract class ImageQueryPage extends QueryPage { /** * Format and output report results using the given information plus * OutputPage * * @stable to override * * @param OutputPage $out OutputPage to print to * @param Skin $skin User skin to use [unused] * @param IReadableDatabase $dbr (read) connection to use * @param IResultWrapper $res Result pointer * @param int $num Number of available result rows * @param int $offset Paging offset */ protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) { if ( $num > 0 ) { $gallery = ImageGalleryBase::factory( false, $this->getContext() ); // $res might contain the whole 1,000 rows, so we read up to // $num [should update this to use a Pager] $i = 0; foreach ( $res as $row ) { $i++; $namespace = $row->namespace ?? NS_FILE; $title = Title::makeTitleSafe( $namespace, $row->title ); if ( $title instanceof Title && $title->inNamespace( NS_FILE ) ) { $gallery->add( $title, $this->getCellHtml( $row ), '', '', [], ImageGalleryBase::LOADING_LAZY ); } if ( $i === $num ) { break; } } $out->addHTML( $gallery->toHTML() ); } } /** * @stable to override * * @param Skin $skin * @param stdClass $result * * @return bool|string */ protected function formatResult( $skin, $result ) { return false; } /** * Get additional HTML to be shown in a results' cell * * @stable to override * * @param stdClass $row Result row * @return string */ protected function getCellHtml( $row ) { return ''; } } /** @deprecated class alias since 1.41 */ class_alias( ImageQueryPage::class, 'ImageQueryPage' ); PK ! Be� � � ChangesListSpecialPage.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\SpecialPage; use ChangesListBooleanFilter; use ChangesListBooleanFilterGroup; use ChangesListFilterGroup; use ChangesListStringOptionsFilterGroup; use ChangeTags; use MediaWiki\Context\IContextSource; use MediaWiki\Html\FormOptions; use MediaWiki\Html\Html; use MediaWiki\Json\FormatJson; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Parser\Sanitizer; use MediaWiki\ResourceLoader as RL; use MediaWiki\User\TempUser\TempUserConfig; use MediaWiki\User\UserArray; use MediaWiki\User\UserIdentity; use MediaWiki\User\UserIdentityUtils; use MWExceptionHandler; use OOUI\IconWidget; use RecentChange; use Wikimedia\Rdbms\DBQueryTimeoutError; use Wikimedia\Rdbms\FakeResultWrapper; use Wikimedia\Rdbms\IExpression; use Wikimedia\Rdbms\IReadableDatabase; use Wikimedia\Rdbms\IResultWrapper; use Wikimedia\Rdbms\RawSQLValue; use Wikimedia\Timestamp\ConvertibleTimestamp; /** * Special page which uses a ChangesList to show query results. * * @todo Most of the functions here should be protected instead of public. * * @ingroup RecentChanges * @ingroup SpecialPage */ abstract class ChangesListSpecialPage extends SpecialPage { /** @var string */ protected $rcSubpage; /** @var FormOptions */ protected $rcOptions; protected UserIdentityUtils $userIdentityUtils; protected TempUserConfig $tempUserConfig; // Order of both groups and filters is significant; first is top-most priority, // descending from there. // 'showHideSuffix' is a shortcut to and avoid spelling out // details specific to subclasses here. /** * Definition information for the filters and their groups * * The value is $groupDefinition, a parameter to the ChangesListFilterGroup constructor. * However, priority is dynamically added for the core groups, to ease maintenance. * * Groups are displayed to the user in the structured UI. However, if necessary, * all of the filters in a group can be configured to only display on the * unstuctured UI, in which case you don't need a group title. * * @var array */ private $filterGroupDefinitions; /** * @var array Same format as filterGroupDefinitions, but for a single group (reviewStatus) * that is registered conditionally. */ private $legacyReviewStatusFilterGroupDefinition; /** @var array Single filter group registered conditionally */ private $reviewStatusFilterGroupDefinition; /** @var array Single filter group registered conditionally */ private $hideCategorizationFilterDefinition; /** * Filter groups, and their contained filters * This is an associative array (with group name as key) of ChangesListFilterGroup objects. * * @var ChangesListFilterGroup[] */ protected $filterGroups = []; /** * @param string $name * @param string $restriction * @param UserIdentityUtils $userIdentityUtils * @param TempUserConfig $tempUserConfig */ public function __construct( $name, $restriction, UserIdentityUtils $userIdentityUtils, TempUserConfig $tempUserConfig ) { parent::__construct( $name, $restriction ); $this->userIdentityUtils = $userIdentityUtils; $this->tempUserConfig = $tempUserConfig; $nonRevisionTypes = [ RC_LOG ]; $this->getHookRunner()->onSpecialWatchlistGetNonRevisionTypes( $nonRevisionTypes ); $this->filterGroupDefinitions = [ [ 'name' => 'registration', 'title' => 'rcfilters-filtergroup-registration', 'class' => ChangesListBooleanFilterGroup::class, 'filters' => [ [ 'name' => 'hideliu', // rcshowhideliu-show, rcshowhideliu-hide, // wlshowhideliu 'showHideSuffix' => 'showhideliu', 'default' => false, 'queryCallable' => function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds[] = $this->getRegisteredExpr( false, $dbr ); $join_conds['recentchanges_actor'] = [ 'JOIN', 'actor_id=rc_actor' ]; }, 'isReplacedInStructuredUi' => true, ], [ 'name' => 'hideanons', // rcshowhideanons-show, rcshowhideanons-hide, // wlshowhideanons 'showHideSuffix' => 'showhideanons', 'default' => false, 'queryCallable' => function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds[] = $this->getRegisteredExpr( true, $dbr ); $join_conds['recentchanges_actor'] = [ 'JOIN', 'actor_id=rc_actor' ]; }, 'isReplacedInStructuredUi' => true, ] ], ], [ 'name' => 'userExpLevel', 'title' => 'rcfilters-filtergroup-user-experience-level', 'class' => ChangesListStringOptionsFilterGroup::class, 'isFullCoverage' => true, 'filters' => [ [ 'name' => 'unregistered', 'label' => 'rcfilters-filter-user-experience-level-unregistered-label', 'description' => $this->tempUserConfig->isKnown() ? 'rcfilters-filter-user-experience-level-unregistered-description-temp' : 'rcfilters-filter-user-experience-level-unregistered-description', 'cssClassSuffix' => 'user-unregistered', 'isRowApplicableCallable' => function ( IContextSource $ctx, RecentChange $rc ) { return !$this->userIdentityUtils->isNamed( $rc->getPerformerIdentity() ); } ], [ 'name' => 'registered', 'label' => 'rcfilters-filter-user-experience-level-registered-label', 'description' => 'rcfilters-filter-user-experience-level-registered-description', 'cssClassSuffix' => 'user-registered', 'isRowApplicableCallable' => function ( IContextSource $ctx, RecentChange $rc ) { return $this->userIdentityUtils->isNamed( $rc->getPerformerIdentity() ); } ], [ 'name' => 'newcomer', 'label' => 'rcfilters-filter-user-experience-level-newcomer-label', 'description' => 'rcfilters-filter-user-experience-level-newcomer-description', 'cssClassSuffix' => 'user-newcomer', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { $performer = $rc->getPerformerIdentity(); return $performer->isRegistered() && MediaWikiServices::getInstance() ->getUserFactory() ->newFromUserIdentity( $performer ) ->getExperienceLevel() === 'newcomer'; } ], [ 'name' => 'learner', 'label' => 'rcfilters-filter-user-experience-level-learner-label', 'description' => 'rcfilters-filter-user-experience-level-learner-description', 'cssClassSuffix' => 'user-learner', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { $performer = $rc->getPerformerIdentity(); return $performer->isRegistered() && MediaWikiServices::getInstance() ->getUserFactory() ->newFromUserIdentity( $performer ) ->getExperienceLevel() === 'learner'; }, ], [ 'name' => 'experienced', 'label' => 'rcfilters-filter-user-experience-level-experienced-label', 'description' => 'rcfilters-filter-user-experience-level-experienced-description', 'cssClassSuffix' => 'user-experienced', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { $performer = $rc->getPerformerIdentity(); return $performer->isRegistered() && MediaWikiServices::getInstance() ->getUserFactory() ->newFromUserIdentity( $performer ) ->getExperienceLevel() === 'experienced'; }, ] ], 'default' => ChangesListStringOptionsFilterGroup::NONE, 'queryCallable' => [ $this, 'filterOnUserExperienceLevel' ], ], [ 'name' => 'authorship', 'title' => 'rcfilters-filtergroup-authorship', 'class' => ChangesListBooleanFilterGroup::class, 'filters' => [ [ 'name' => 'hidemyself', 'label' => 'rcfilters-filter-editsbyself-label', 'description' => 'rcfilters-filter-editsbyself-description', // rcshowhidemine-show, rcshowhidemine-hide, // wlshowhidemine 'showHideSuffix' => 'showhidemine', 'default' => false, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $user = $ctx->getUser(); $conds[] = $dbr->expr( 'actor_name', '!=', $user->getName() ); $join_conds['recentchanges_actor'] = [ 'JOIN', 'actor_id=rc_actor' ]; }, 'cssClassSuffix' => 'self', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $ctx->getUser()->equals( $rc->getPerformerIdentity() ); }, ], [ 'name' => 'hidebyothers', 'label' => 'rcfilters-filter-editsbyother-label', 'description' => 'rcfilters-filter-editsbyother-description', 'default' => false, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $user = $ctx->getUser(); if ( $user->isAnon() ) { $conds['actor_name'] = $user->getName(); } else { $conds['actor_user'] = $user->getId(); } $join_conds['recentchanges_actor'] = [ 'JOIN', 'actor_id=rc_actor' ]; }, 'cssClassSuffix' => 'others', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return !$ctx->getUser()->equals( $rc->getPerformerIdentity() ); }, ] ] ], [ 'name' => 'automated', 'title' => 'rcfilters-filtergroup-automated', 'class' => ChangesListBooleanFilterGroup::class, 'filters' => [ [ 'name' => 'hidebots', 'label' => 'rcfilters-filter-bots-label', 'description' => 'rcfilters-filter-bots-description', // rcshowhidebots-show, rcshowhidebots-hide, // wlshowhidebots 'showHideSuffix' => 'showhidebots', 'default' => false, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds['rc_bot'] = 0; }, 'cssClassSuffix' => 'bot', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_bot' ); }, ], [ 'name' => 'hidehumans', 'label' => 'rcfilters-filter-humans-label', 'description' => 'rcfilters-filter-humans-description', 'default' => false, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds['rc_bot'] = 1; }, 'cssClassSuffix' => 'human', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return !$rc->getAttribute( 'rc_bot' ); }, ] ] ], // significance (conditional) [ 'name' => 'significance', 'title' => 'rcfilters-filtergroup-significance', 'class' => ChangesListBooleanFilterGroup::class, 'priority' => -6, 'filters' => [ [ 'name' => 'hideminor', 'label' => 'rcfilters-filter-minor-label', 'description' => 'rcfilters-filter-minor-description', // rcshowhideminor-show, rcshowhideminor-hide, // wlshowhideminor 'showHideSuffix' => 'showhideminor', 'default' => false, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds[] = $dbr->expr( 'rc_minor', '=', 0 ); }, 'cssClassSuffix' => 'minor', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_minor' ); } ], [ 'name' => 'hidemajor', 'label' => 'rcfilters-filter-major-label', 'description' => 'rcfilters-filter-major-description', 'default' => false, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds[] = $dbr->expr( 'rc_minor', '=', 1 ); }, 'cssClassSuffix' => 'major', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return !$rc->getAttribute( 'rc_minor' ); } ] ] ], [ 'name' => 'lastRevision', 'title' => 'rcfilters-filtergroup-lastrevision', 'class' => ChangesListBooleanFilterGroup::class, 'priority' => -7, 'filters' => [ [ 'name' => 'hidelastrevision', 'label' => 'rcfilters-filter-lastrevision-label', 'description' => 'rcfilters-filter-lastrevision-description', 'default' => false, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) use ( $nonRevisionTypes ) { $conds[] = $dbr->expr( 'rc_this_oldid', '!=', new RawSQLValue( 'page_latest' ) ) ->or( 'rc_type', '=', $nonRevisionTypes ); }, 'cssClassSuffix' => 'last', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_this_oldid' ) === $rc->getAttribute( 'page_latest' ); } ], [ 'name' => 'hidepreviousrevisions', 'label' => 'rcfilters-filter-previousrevision-label', 'description' => 'rcfilters-filter-previousrevision-description', 'default' => false, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) use ( $nonRevisionTypes ) { $conds[] = $dbr->expr( 'rc_this_oldid', '=', new RawSQLValue( 'page_latest' ) ) ->or( 'rc_type', '=', $nonRevisionTypes ); }, 'cssClassSuffix' => 'previous', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_this_oldid' ) !== $rc->getAttribute( 'page_latest' ); } ] ] ], // With extensions, there can be change types that will not be hidden by any of these. [ 'name' => 'changeType', 'title' => 'rcfilters-filtergroup-changetype', 'class' => ChangesListBooleanFilterGroup::class, 'priority' => -8, 'filters' => [ [ 'name' => 'hidepageedits', 'label' => 'rcfilters-filter-pageedits-label', 'description' => 'rcfilters-filter-pageedits-description', 'default' => false, 'priority' => -2, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds[] = $dbr->expr( 'rc_type', '!=', RC_EDIT ); }, 'cssClassSuffix' => 'src-mw-edit', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_EDIT; }, ], [ 'name' => 'hidenewpages', 'label' => 'rcfilters-filter-newpages-label', 'description' => 'rcfilters-filter-newpages-description', 'default' => false, 'priority' => -3, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds[] = $dbr->expr( 'rc_type', '!=', RC_NEW ); }, 'cssClassSuffix' => 'src-mw-new', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_NEW; }, ], // hidecategorization [ 'name' => 'hidelog', 'label' => 'rcfilters-filter-logactions-label', 'description' => 'rcfilters-filter-logactions-description', 'default' => false, 'priority' => -5, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds[] = $dbr->expr( 'rc_type', '!=', RC_LOG ); }, 'cssClassSuffix' => 'src-mw-log', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_LOG; } ], [ 'name' => 'hidenewuserlog', 'label' => 'rcfilters-filter-accountcreations-label', 'description' => 'rcfilters-filter-accountcreations-description', 'default' => false, 'priority' => -6, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds[] = $dbr->expr( 'rc_log_type', '!=', 'newusers' ) ->or( 'rc_log_type', '=', null ); }, 'cssClassSuffix' => 'src-mw-newuserlog', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_log_type' ) === "newusers"; }, ], ], ], ]; $this->legacyReviewStatusFilterGroupDefinition = [ [ 'name' => 'legacyReviewStatus', 'title' => 'rcfilters-filtergroup-reviewstatus', 'class' => ChangesListBooleanFilterGroup::class, 'filters' => [ [ 'name' => 'hidepatrolled', // rcshowhidepatr-show, rcshowhidepatr-hide // wlshowhidepatr 'showHideSuffix' => 'showhidepatr', 'default' => false, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED; }, 'isReplacedInStructuredUi' => true, ], [ 'name' => 'hideunpatrolled', 'default' => false, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds[] = $dbr->expr( 'rc_patrolled', '!=', RecentChange::PRC_UNPATROLLED ); }, 'isReplacedInStructuredUi' => true, ], ], ] ]; $this->reviewStatusFilterGroupDefinition = [ [ 'name' => 'reviewStatus', 'title' => 'rcfilters-filtergroup-reviewstatus', 'class' => ChangesListStringOptionsFilterGroup::class, 'isFullCoverage' => true, 'priority' => -5, 'filters' => [ [ 'name' => 'unpatrolled', 'label' => 'rcfilters-filter-reviewstatus-unpatrolled-label', 'description' => 'rcfilters-filter-reviewstatus-unpatrolled-description', 'cssClassSuffix' => 'reviewstatus-unpatrolled', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED; }, ], [ 'name' => 'manual', 'label' => 'rcfilters-filter-reviewstatus-manual-label', 'description' => 'rcfilters-filter-reviewstatus-manual-description', 'cssClassSuffix' => 'reviewstatus-manual', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_PATROLLED; }, ], [ 'name' => 'auto', 'label' => 'rcfilters-filter-reviewstatus-auto-label', 'description' => 'rcfilters-filter-reviewstatus-auto-description', 'cssClassSuffix' => 'reviewstatus-auto', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_AUTOPATROLLED; }, ], ], 'default' => ChangesListStringOptionsFilterGroup::NONE, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected ) { if ( $selected === [] ) { return; } $rcPatrolledValues = [ 'unpatrolled' => RecentChange::PRC_UNPATROLLED, 'manual' => RecentChange::PRC_PATROLLED, 'auto' => RecentChange::PRC_AUTOPATROLLED, ]; // e.g. rc_patrolled IN (0, 2) $conds['rc_patrolled'] = array_map( static function ( $s ) use ( $rcPatrolledValues ) { return $rcPatrolledValues[ $s ]; }, $selected ); } ] ]; $this->hideCategorizationFilterDefinition = [ 'name' => 'hidecategorization', 'label' => 'rcfilters-filter-categorization-label', 'description' => 'rcfilters-filter-categorization-description', // rcshowhidecategorization-show, rcshowhidecategorization-hide. // wlshowhidecategorization 'showHideSuffix' => 'showhidecategorization', 'default' => false, 'priority' => -4, 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds ) { $conds[] = $dbr->expr( 'rc_type', '!=', RC_CATEGORIZE ); }, 'cssClassSuffix' => 'src-mw-categorize', 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE; }, ]; } /** * Check if filters are in conflict and guaranteed to return no results. * * @return bool */ protected function areFiltersInConflict() { $opts = $this->getOptions(); foreach ( $this->getFilterGroups() as $group ) { if ( $group->getConflictingGroups() ) { wfLogWarning( $group->getName() . " specifies conflicts with other groups but these are not supported yet." ); } foreach ( $group->getConflictingFilters() as $conflictingFilter ) { if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) { return true; } } foreach ( $group->getFilters() as $filter ) { foreach ( $filter->getConflictingFilters() as $conflictingFilter ) { if ( $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) && $filter->activelyInConflictWithFilter( $conflictingFilter, $opts ) ) { return true; } } } } return false; } /** * @param string|null $subpage */ public function execute( $subpage ) { $this->rcSubpage = $subpage; if ( $this->considerActionsForDefaultSavedQuery( $subpage ) ) { // Don't bother rendering the page if we'll be performing a redirect (T330100). return; } // Enable OOUI and module for the clock icon. if ( $this->getConfig()->get( MainConfigNames::WatchlistExpiry ) && !$this->including() ) { $this->getOutput()->enableOOUI(); $this->getOutput()->addModules( 'mediawiki.special.changeslist.watchlistexpiry' ); } $opts = $this->getOptions(); try { $rows = $this->getRows(); if ( $rows === false ) { $rows = new FakeResultWrapper( [] ); } // Used by Structured UI app to get results without MW chrome if ( $this->getRequest()->getRawVal( 'action' ) === 'render' ) { $this->getOutput()->setArticleBodyOnly( true ); } // Used by "live update" and "view newest" to check // if there's new changes with minimal data transfer if ( $this->getRequest()->getBool( 'peek' ) ) { $code = $rows->numRows() > 0 ? 200 : 204; $this->getOutput()->setStatusCode( $code ); if ( $this->getUser()->isAnon() !== $this->getRequest()->getFuzzyBool( 'isAnon' ) ) { $this->getOutput()->setStatusCode( 205 ); } return; } $services = MediaWikiServices::getInstance(); $logFormatterFactory = $services->getLogFormatterFactory(); $linkBatchFactory = $services->getLinkBatchFactory(); $batch = $linkBatchFactory->newLinkBatch(); $userNames = []; foreach ( $rows as $row ) { $batch->add( NS_USER, $row->rc_user_text ); $batch->add( NS_USER_TALK, $row->rc_user_text ); $userNames[] = $row->rc_user_text; $batch->add( $row->rc_namespace, $row->rc_title ); if ( $row->rc_source === RecentChange::SRC_LOG ) { $formatter = $logFormatterFactory->newFromRow( $row ); foreach ( $formatter->getPreloadTitles() as $title ) { $batch->addObj( $title ); if ( $title->inNamespace( NS_USER ) || $title->inNamespace( NS_USER_TALK ) ) { $userNames[] = $title->getText(); } } } } $batch->execute(); foreach ( UserArray::newFromNames( $userNames ) as $_ ) { // Trigger UserEditTracker::setCachedUserEditCount via User::loadFromRow // Preloads edit count for User::getExperienceLevel() and Linker::userToolLinks() } $this->setHeaders(); $this->outputHeader(); $this->addModules(); $this->webOutput( $rows, $opts ); $rows->free(); } catch ( DBQueryTimeoutError $timeoutException ) { MWExceptionHandler::logException( $timeoutException ); $this->setHeaders(); $this->outputHeader(); $this->addModules(); $this->getOutput()->setStatusCode( 500 ); $this->webOutputHeader( 0, $opts ); $this->outputTimeout(); } $this->includeRcFiltersApp(); } /** * Set the temp user config. * * @internal * @param TempUserConfig $tempUserConfig * @since 1.42 */ public function setTempUserConfig( TempUserConfig $tempUserConfig ) { $this->tempUserConfig = $tempUserConfig; } /** * Check whether or not the page should load defaults, and if so, whether * a default saved query is relevant to be redirected to. If it is relevant, * redirect properly with all necessary query parameters. * * @param string $subpage * @return bool Whether a redirect will be performed. */ protected function considerActionsForDefaultSavedQuery( $subpage ) { if ( !$this->isStructuredFilterUiEnabled() || $this->including() ) { return false; } $knownParams = $this->getRequest()->getValues( ...array_keys( $this->getOptions()->getAllValues() ) ); // HACK: Temporarily until we can properly define "sticky" filters and parameters, // we need to exclude several parameters we know should not be counted towards preventing // the loading of defaults. $excludedParams = [ 'limit' => '', 'days' => '', 'enhanced' => '', 'from' => '' ]; $knownParams = array_diff_key( $knownParams, $excludedParams ); if ( // If there are NO known parameters in the URL request // (that are not excluded) then we need to check into loading // the default saved query count( $knownParams ) === 0 ) { $prefJson = MediaWikiServices::getInstance() ->getUserOptionsLookup() ->getOption( $this->getUser(), $this->getSavedQueriesPreferenceName() ); // Get the saved queries data and parse it $savedQueries = $prefJson ? FormatJson::decode( $prefJson, true ) : false; if ( $savedQueries && isset( $savedQueries[ 'default' ] ) ) { // Only load queries that are 'version' 2, since those // have parameter representation if ( isset( $savedQueries[ 'version' ] ) && $savedQueries[ 'version' ] === '2' ) { $savedQueryDefaultID = $savedQueries[ 'default' ]; $defaultQuery = $savedQueries[ 'queries' ][ $savedQueryDefaultID ][ 'data' ]; // Build the entire parameter list $query = array_merge( $defaultQuery[ 'params' ], $defaultQuery[ 'highlights' ], [ 'urlversion' => '2', ] ); // Add to the query any parameters that we may have ignored before // but are still valid and requested in the URL $query = array_merge( $this->getRequest()->getValues(), $query ); unset( $query[ 'title' ] ); $this->getOutput()->redirect( $this->getPageTitle( $subpage )->getCanonicalURL( $query ) ); // Signal that we only need to redirect to the full URL // and can skip rendering the actual page (T330100). return true; } else { // There's a default, but the version is not 2, and the server can't // actually recognize the query itself. This happens if it is before // the conversion, so we need to tell the UI to reload saved query as // it does the conversion to version 2 $this->getOutput()->addJsConfigVars( 'wgStructuredChangeFiltersDefaultSavedQueryExists', true ); // Add the class that tells the frontend it is still loading // another query $this->getOutput()->addBodyClasses( 'mw-rcfilters-ui-loading' ); } } } return false; } /** * @see \MediaWiki\MainConfigSchema::RCLinkDays and \MediaWiki\MainConfigSchema::RCFilterByAge. * @return int[] */ protected function getLinkDays() { $linkDays = $this->getConfig()->get( MainConfigNames::RCLinkDays ); $filterByAge = $this->getConfig()->get( MainConfigNames::RCFilterByAge ); $maxAge = $this->getConfig()->get( MainConfigNames::RCMaxAge ); if ( $filterByAge ) { // Trim it to only links which are within $wgRCMaxAge. // Note that we allow one link higher than the max for things like // "age 56 days" being accessible through the "60 days" link. sort( $linkDays ); $maxAgeDays = $maxAge / ( 3600 * 24 ); foreach ( $linkDays as $i => $days ) { if ( $days >= $maxAgeDays ) { array_splice( $linkDays, $i + 1 ); break; } } } return $linkDays; } /** * Include the modules and configuration for the RCFilters app. * Conditional on the user having the feature enabled. * * If it is disabled, add a <body> class marking that */ protected function includeRcFiltersApp() { $out = $this->getOutput(); if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) { $jsData = $this->getStructuredFilterJsData(); $messages = []; foreach ( $jsData['messageKeys'] as $key ) { $messages[$key] = $this->msg( $key )->plain(); } $out->addBodyClasses( 'mw-rcfilters-enabled' ); $collapsed = MediaWikiServices::getInstance()->getUserOptionsLookup() ->getBoolOption( $this->getUser(), $this->getCollapsedPreferenceName() ); if ( $collapsed ) { $out->addBodyClasses( 'mw-rcfilters-collapsed' ); } // These config and message exports should be moved into a ResourceLoader data module (T201574) $out->addJsConfigVars( 'wgStructuredChangeFilters', $jsData['groups'] ); $out->addJsConfigVars( 'wgStructuredChangeFiltersMessages', $messages ); $out->addJsConfigVars( 'wgStructuredChangeFiltersCollapsedState', $collapsed ); $out->addJsConfigVars( 'StructuredChangeFiltersDisplayConfig', [ 'maxDays' => // Translate to days (int)$this->getConfig()->get( MainConfigNames::RCMaxAge ) / ( 24 * 3600 ), 'limitArray' => $this->getConfig()->get( MainConfigNames::RCLinkLimits ), 'limitDefault' => $this->getDefaultLimit(), 'daysArray' => $this->getLinkDays(), 'daysDefault' => $this->getDefaultDays(), ] ); $out->addJsConfigVars( 'wgStructuredChangeFiltersSavedQueriesPreferenceName', $this->getSavedQueriesPreferenceName() ); $out->addJsConfigVars( 'wgStructuredChangeFiltersLimitPreferenceName', $this->getLimitPreferenceName() ); $out->addJsConfigVars( 'wgStructuredChangeFiltersDaysPreferenceName', $this->getDefaultDaysPreferenceName() ); $out->addJsConfigVars( 'wgStructuredChangeFiltersCollapsedPreferenceName', $this->getCollapsedPreferenceName() ); } else { $out->addBodyClasses( 'mw-rcfilters-disabled' ); } } /** * Get essential data about getRcFiltersConfigVars() for change detection. * * @internal For use by Resources.php only. * @see Module::getDefinitionSummary() and Module::getVersionHash() * @param RL\Context $context * @return array */ public static function getRcFiltersConfigSummary( RL\Context $context ) { $lang = MediaWikiServices::getInstance()->getLanguageFactory() ->getLanguage( $context->getLanguage() ); return [ // Reduce version computation by avoiding Message parsing 'RCFiltersChangeTags' => ChangeTags::getChangeTagListSummary( $context, $lang ), 'StructuredChangeFiltersEditWatchlistUrl' => SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL() ]; } /** * Get config vars to export with the mediawiki.rcfilters.filters.ui module. * * @internal For use by Resources.php only. * @param RL\Context $context * @return array */ public static function getRcFiltersConfigVars( RL\Context $context ) { $lang = MediaWikiServices::getInstance()->getLanguageFactory() ->getLanguage( $context->getLanguage() ); return [ 'RCFiltersChangeTags' => ChangeTags::getChangeTagList( $context, $lang ), 'StructuredChangeFiltersEditWatchlistUrl' => SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL() ]; } /** * Add the "no results" message to the output */ protected function outputNoResults() { $this->getOutput()->addHTML( Html::rawElement( 'div', [ 'class' => 'mw-changeslist-empty' ], $this->msg( 'recentchanges-noresult' )->parse() ) ); } /** * Add the "timeout" message to the output */ protected function outputTimeout() { $this->getOutput()->addHTML( '<div class="mw-changeslist-empty mw-changeslist-timeout">' . $this->msg( 'recentchanges-timeout' )->parse() . '</div>' ); } /** * Get the database result for this special page instance. Used by ApiFeedRecentChanges. * * @return IResultWrapper|false */ public function getRows() { $opts = $this->getOptions(); $tables = []; $fields = []; $conds = []; $query_options = []; $join_conds = []; $this->buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts ); return $this->doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts ); } /** * Get the current FormOptions for this request * * @return FormOptions */ public function getOptions() { if ( $this->rcOptions === null ) { $this->rcOptions = $this->setup( $this->rcSubpage ); } return $this->rcOptions; } /** * Register all filters and their groups (including those from hooks), plus handle * conflicts and defaults. * * You might want to customize these in the same method, in subclasses. You can * call getFilterGroup to access a group, and (on the group) getFilter to access a * filter, then make necessary modfications to the filter or group (e.g. with * setDefault). */ protected function registerFilters() { $this->registerFiltersFromDefinitions( $this->filterGroupDefinitions ); // Make sure this is not being transcluded (we don't want to show this // information to all users just because the user that saves the edit can // patrol or is logged in) if ( !$this->including() && $this->getUser()->useRCPatrol() ) { $this->registerFiltersFromDefinitions( $this->legacyReviewStatusFilterGroupDefinition ); $this->registerFiltersFromDefinitions( $this->reviewStatusFilterGroupDefinition ); } $changeTypeGroup = $this->getFilterGroup( 'changeType' ); $categoryFilter = null; if ( $this->getConfig()->get( MainConfigNames::RCWatchCategoryMembership ) ) { $transformedHideCategorizationDef = $this->transformFilterDefinition( $this->hideCategorizationFilterDefinition ); $transformedHideCategorizationDef['group'] = $changeTypeGroup; $categoryFilter = new ChangesListBooleanFilter( $transformedHideCategorizationDef ); } $this->getHookRunner()->onChangesListSpecialPageStructuredFilters( $this ); $this->registerFiltersFromDefinitions( [] ); $userExperienceLevel = $this->getFilterGroup( 'userExpLevel' ); $registered = $userExperienceLevel->getFilter( 'registered' ); $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'newcomer' ) ); $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'learner' ) ); $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'experienced' ) ); $logactionsFilter = $changeTypeGroup->getFilter( 'hidelog' ); $lognewuserFilter = $changeTypeGroup->getFilter( 'hidenewuserlog' ); $pagecreationFilter = $changeTypeGroup->getFilter( 'hidenewpages' ); $significanceTypeGroup = $this->getFilterGroup( 'significance' ); $hideMinorFilter = $significanceTypeGroup->getFilter( 'hideminor' ); if ( $categoryFilter !== null ) { $hideMinorFilter->conflictsWith( $categoryFilter, 'rcfilters-hideminor-conflicts-typeofchange-global', 'rcfilters-hideminor-conflicts-typeofchange', 'rcfilters-typeofchange-conflicts-hideminor' ); } $hideMinorFilter->conflictsWith( $logactionsFilter, 'rcfilters-hideminor-conflicts-typeofchange-global', 'rcfilters-hideminor-conflicts-typeofchange', 'rcfilters-typeofchange-conflicts-hideminor' ); $hideMinorFilter->conflictsWith( $lognewuserFilter, 'rcfilters-hideminor-conflicts-typeofchange-global', 'rcfilters-hideminor-conflicts-typeofchange', 'rcfilters-typeofchange-conflicts-hideminor' ); $hideMinorFilter->conflictsWith( $pagecreationFilter, 'rcfilters-hideminor-conflicts-typeofchange-global', 'rcfilters-hideminor-conflicts-typeofchange', 'rcfilters-typeofchange-conflicts-hideminor' ); } /** * Transforms filter definition to prepare it for constructor. * * See overrides of this method as well. * * @param array $filterDefinition Original filter definition * * @return array Transformed definition */ protected function transformFilterDefinition( array $filterDefinition ) { return $filterDefinition; } /** * Register filters from a definition object * * Array specifying groups and their filters; see Filter and * ChangesListFilterGroup constructors. * * There is light processing to simplify core maintenance. * @param array $definition * @phan-param array<int,array{class:class-string<ChangesListFilterGroup>,filters:array}> $definition */ protected function registerFiltersFromDefinitions( array $definition ) { $autoFillPriority = -1; foreach ( $definition as $groupDefinition ) { if ( !isset( $groupDefinition['priority'] ) ) { $groupDefinition['priority'] = $autoFillPriority; } else { // If it's explicitly specified, start over the auto-fill $autoFillPriority = $groupDefinition['priority']; } $autoFillPriority--; $className = $groupDefinition['class']; unset( $groupDefinition['class'] ); foreach ( $groupDefinition['filters'] as &$filterDefinition ) { $filterDefinition = $this->transformFilterDefinition( $filterDefinition ); } $this->registerFilterGroup( new $className( $groupDefinition ) ); } } /** * @return ChangesListBooleanFilter[] The legacy show/hide toggle filters */ protected function getLegacyShowHideFilters() { $filters = []; foreach ( $this->filterGroups as $group ) { if ( $group instanceof ChangesListBooleanFilterGroup ) { foreach ( $group->getFilters() as $key => $filter ) { if ( $filter->displaysOnUnstructuredUi() ) { $filters[ $key ] = $filter; } } } } return $filters; } /** * Register all the filters, including legacy hook-driven ones. * Then create a FormOptions object with options as specified by the user * * @param string $parameters * * @return FormOptions */ public function setup( $parameters ) { $this->registerFilters(); $opts = $this->getDefaultOptions(); $opts = $this->fetchOptionsFromRequest( $opts ); // Give precedence to subpage syntax if ( $parameters !== null ) { $this->parseParameters( $parameters, $opts ); } $this->validateOptions( $opts ); return $opts; } /** * Get a FormOptions object containing the default options. By default, returns * some basic options. The filters listed explicitly here are overridden in this * method, in subclasses, but most filters (e.g. hideminor, userExpLevel filters, * and more) are structured. Structured filters are overridden in registerFilters. * not here. * * @return FormOptions */ public function getDefaultOptions() { $opts = new FormOptions(); $structuredUI = $this->isStructuredFilterUiEnabled(); // If urlversion=2 is set, ignore the filter defaults and set them all to false/empty $useDefaults = $this->getRequest()->getInt( 'urlversion' ) !== 2; /** @var ChangesListFilterGroup $filterGroup */ foreach ( $this->filterGroups as $filterGroup ) { $filterGroup->addOptions( $opts, $useDefaults, $structuredUI ); } $opts->add( 'namespace', '', FormOptions::STRING ); // TODO: Rename this option to 'invertnamespaces'? $opts->add( 'invert', false ); $opts->add( 'associated', false ); $opts->add( 'urlversion', 1 ); $opts->add( 'tagfilter', '' ); $opts->add( 'inverttags', false ); $opts->add( 'days', $this->getDefaultDays(), FormOptions::FLOAT ); $opts->add( 'limit', $this->getDefaultLimit(), FormOptions::INT ); $opts->add( 'from', '' ); return $opts; } /** * Register a structured changes list filter group * * @param ChangesListFilterGroup $group */ public function registerFilterGroup( ChangesListFilterGroup $group ) { $groupName = $group->getName(); $this->filterGroups[$groupName] = $group; } /** * Gets the currently registered filters groups * * @return ChangesListFilterGroup[] Associative array of ChangesListFilterGroup objects, with group name as key */ protected function getFilterGroups() { return $this->filterGroups; } /** * Gets a specified ChangesListFilterGroup by name * * @param string $groupName Name of group * * @return ChangesListFilterGroup|null Group, or null if not registered */ public function getFilterGroup( $groupName ) { return $this->filterGroups[$groupName] ?? null; } // Currently, this intentionally only includes filters that display // in the structured UI. This can be changed easily, though, if we want // to include data on filters that use the unstructured UI. messageKeys is a // special top-level value, with the value being an array of the message keys to // send to the client. /** * Gets structured filter information needed by JS * * @return array Associative array * * array $return['groups'] Group data * * array $return['messageKeys'] Array of message keys */ public function getStructuredFilterJsData() { $output = [ 'groups' => [], 'messageKeys' => [], ]; usort( $this->filterGroups, static function ( ChangesListFilterGroup $a, ChangesListFilterGroup $b ) { return $b->getPriority() <=> $a->getPriority(); } ); foreach ( $this->filterGroups as $group ) { $groupOutput = $group->getJsData(); if ( $groupOutput !== null ) { $output['messageKeys'] = array_merge( $output['messageKeys'], $groupOutput['messageKeys'] ); unset( $groupOutput['messageKeys'] ); $output['groups'][] = $groupOutput; } } return $output; } /** * Fetch values for a FormOptions object from the WebRequest associated with this instance. * * Intended for subclassing, e.g. to add a backwards-compatibility layer. * * @param FormOptions $opts * @return FormOptions */ protected function fetchOptionsFromRequest( $opts ) { $opts->fetchValuesFromRequest( $this->getRequest() ); return $opts; } /** * Process $par and put options found in $opts. Used when including the page. * * @param string $par * @param FormOptions $opts */ public function parseParameters( $par, FormOptions $opts ) { $stringParameterNameSet = []; $hideParameterNameSet = []; // URL parameters can be per-group, like 'userExpLevel', // or per-filter, like 'hideminor'. foreach ( $this->filterGroups as $filterGroup ) { if ( $filterGroup instanceof ChangesListStringOptionsFilterGroup ) { $stringParameterNameSet[$filterGroup->getName()] = true; } elseif ( $filterGroup instanceof ChangesListBooleanFilterGroup ) { foreach ( $filterGroup->getFilters() as $filter ) { $hideParameterNameSet[$filter->getName()] = true; } } } $bits = preg_split( '/\s*,\s*/', trim( $par ) ); foreach ( $bits as $bit ) { $m = []; if ( isset( $hideParameterNameSet[$bit] ) ) { // hidefoo => hidefoo=true $opts[$bit] = true; } elseif ( isset( $hideParameterNameSet["hide$bit"] ) ) { // foo => hidefoo=false $opts["hide$bit"] = false; } elseif ( preg_match( '/^(.*)=(.*)$/', $bit, $m ) ) { if ( isset( $stringParameterNameSet[$m[1]] ) ) { $opts[$m[1]] = $m[2]; } } } } /** * Validate a FormOptions object generated by getDefaultOptions() with values already populated. * * @param FormOptions $opts */ public function validateOptions( FormOptions $opts ) { $isContradictory = $this->fixContradictoryOptions( $opts ); $isReplaced = $this->replaceOldOptions( $opts ); if ( $isContradictory || $isReplaced ) { $query = wfArrayToCgi( $this->convertParamsForLink( $opts->getChangedValues() ) ); $this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) ); } $opts->validateIntBounds( 'limit', 0, 5000 ); $opts->validateBounds( 'days', 0, $this->getConfig()->get( MainConfigNames::RCMaxAge ) / ( 3600 * 24 ) ); } /** * Fix invalid options by resetting pairs that should never appear together. * * @param FormOptions $opts * @return bool True if any option was reset */ private function fixContradictoryOptions( FormOptions $opts ) { $fixed = $this->fixBackwardsCompatibilityOptions( $opts ); foreach ( $this->filterGroups as $filterGroup ) { if ( $filterGroup instanceof ChangesListBooleanFilterGroup ) { $filters = $filterGroup->getFilters(); if ( count( $filters ) === 1 ) { // legacy boolean filters should not be considered continue; } $allInGroupEnabled = array_reduce( $filters, static function ( bool $carry, ChangesListBooleanFilter $filter ) use ( $opts ) { return $carry && $opts[ $filter->getName() ]; }, /* initialValue */ count( $filters ) > 0 ); if ( $allInGroupEnabled ) { foreach ( $filters as $filter ) { $opts[ $filter->getName() ] = false; } $fixed = true; } } } return $fixed; } /** * Fix a special case (hideanons=1 and hideliu=1) in a special way, for backwards * compatibility. * * This is deprecated and may be removed. * * @param FormOptions $opts * @return bool True if this change was mode */ private function fixBackwardsCompatibilityOptions( FormOptions $opts ) { if ( $opts['hideanons'] && $opts['hideliu'] ) { $opts->reset( 'hideanons' ); if ( !$opts['hidebots'] ) { $opts->reset( 'hideliu' ); $opts['hidehumans'] = 1; } return true; } return false; } /** * Replace old options with their structured UI equivalents * * @param FormOptions $opts * @return bool True if the change was made */ public function replaceOldOptions( FormOptions $opts ) { if ( !$this->isStructuredFilterUiEnabled() ) { return false; } $changed = false; // At this point 'hideanons' and 'hideliu' cannot be both true, // because fixBackwardsCompatibilityOptions resets (at least) 'hideanons' in such case if ( $opts[ 'hideanons' ] ) { $opts->reset( 'hideanons' ); $opts[ 'userExpLevel' ] = 'registered'; $changed = true; } if ( $opts[ 'hideliu' ] ) { $opts->reset( 'hideliu' ); $opts[ 'userExpLevel' ] = 'unregistered'; $changed = true; } if ( $this->getFilterGroup( 'legacyReviewStatus' ) ) { if ( $opts[ 'hidepatrolled' ] ) { $opts->reset( 'hidepatrolled' ); $opts[ 'reviewStatus' ] = 'unpatrolled'; $changed = true; } if ( $opts[ 'hideunpatrolled' ] ) { $opts->reset( 'hideunpatrolled' ); $opts[ 'reviewStatus' ] = implode( ChangesListStringOptionsFilterGroup::SEPARATOR, [ 'manual', 'auto' ] ); $changed = true; } } return $changed; } /** * Convert parameters values from true/false to 1/0 * so they are not omitted by wfArrayToCgi() * T38524 * * @param array $params * @return array */ protected function convertParamsForLink( $params ) { foreach ( $params as &$value ) { if ( $value === false ) { $value = '0'; } } unset( $value ); return $params; } /** * Sets appropriate tables, fields, conditions, etc. depending on which filters * the user requested. * * @param array &$tables Array of tables; see IReadableDatabase::select $table * @param array &$fields Array of fields; see IReadableDatabase::select $vars * @param array &$conds Array of conditions; see IReadableDatabase::select $conds * @param array &$query_options Array of query options; see IReadableDatabase::select $options * @param array &$join_conds Array of join conditions; see IReadableDatabase::select $join_conds * @param FormOptions $opts */ protected function buildQuery( &$tables, &$fields, &$conds, &$query_options, &$join_conds, FormOptions $opts ) { $dbr = $this->getDB(); $isStructuredUI = $this->isStructuredFilterUiEnabled(); /** @var ChangesListFilterGroup $filterGroup */ foreach ( $this->filterGroups as $filterGroup ) { $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds, $query_options, $join_conds, $opts, $isStructuredUI ); } // Namespace filtering if ( $opts[ 'namespace' ] !== '' ) { $namespaces = explode( ';', $opts[ 'namespace' ] ); $namespaces = $this->expandSymbolicNamespaceFilters( $namespaces ); $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); $namespaces = array_filter( $namespaces, [ $namespaceInfo, 'exists' ] ); if ( $namespaces !== [] ) { // Namespaces are just ints, use them as int when acting with the database $namespaces = array_map( 'intval', $namespaces ); if ( $opts[ 'associated' ] ) { $associatedNamespaces = array_map( [ $namespaceInfo, 'getAssociated' ], array_filter( $namespaces, [ $namespaceInfo, 'hasTalkNamespace' ] ) ); $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) ); } $operator = $opts[ 'invert' ] ? '!=' : '='; sort( $namespaces ); $conds[] = $dbr->expr( 'rc_namespace', $operator, $namespaces ); } } // Calculate cutoff $cutoff_unixtime = ConvertibleTimestamp::time() - $opts['days'] * 3600 * 24; $cutoff = $dbr->timestamp( $cutoff_unixtime ); $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] ); if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) { $cutoff = $dbr->timestamp( $opts['from'] ); } else { $opts->reset( 'from' ); } $conds[] = $dbr->expr( 'rc_timestamp', '>=', $cutoff ); } /** * Process the query * * @param array $tables Array of tables; see IReadableDatabase::select $table * @param array $fields Array of fields; see IReadableDatabase::select $vars * @param array $conds Array of conditions; see IReadableDatabase::select $conds * @param array $query_options Array of query options; see IReadableDatabase::select $options * @param array $join_conds Array of join conditions; see IReadableDatabase::select $join_conds * @param FormOptions $opts * @return bool|IResultWrapper Result or false */ protected function doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts ) { $rcQuery = RecentChange::getQueryInfo(); $tables = array_merge( $tables, $rcQuery['tables'] ); $fields = array_merge( $rcQuery['fields'], $fields ); $join_conds = array_merge( $join_conds, $rcQuery['joins'] ); MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQuery( $tables, $fields, $conds, $join_conds, $query_options, '', $opts[ 'inverttags' ] ); if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts ) ) { return false; } $dbr = $this->getDB(); return $dbr->newSelectQueryBuilder() ->tables( $tables ) ->fields( $fields ) ->conds( $conds ) ->caller( __METHOD__ ) ->options( $query_options ) ->joinConds( $join_conds ) ->fetchResultSet(); } protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ) { return $this->getHookRunner()->onChangesListSpecialPageQuery( $this->getName(), $tables, $fields, $conds, $query_options, $join_conds, $opts ); } /** * Which database to use for read queries * * @return IReadableDatabase */ protected function getDB(): IReadableDatabase { return MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); } /** * Send header output to the OutputPage object, only called if not using feeds * * @param int $rowCount Number of database rows * @param FormOptions $opts */ private function webOutputHeader( $rowCount, $opts ) { if ( !$this->including() ) { $this->outputFeedLinks(); $this->doHeader( $opts, $rowCount ); } } /** * Send output to the OutputPage object, only called if not used feeds * * @param IResultWrapper $rows Database rows * @param FormOptions $opts */ public function webOutput( $rows, $opts ) { $this->webOutputHeader( $rows->numRows(), $opts ); $this->outputChangesList( $rows, $opts ); } public function outputFeedLinks() { // nothing by default } /** * Build and output the actual changes list. * * @param IResultWrapper $rows Database rows * @param FormOptions $opts */ abstract public function outputChangesList( $rows, $opts ); /** * Set the text to be displayed above the changes * * @param FormOptions $opts * @param int $numRows Number of rows in the result to show after this header */ public function doHeader( $opts, $numRows ) { $this->setTopText( $opts ); // @todo Lots of stuff should be done here. $this->setBottomText( $opts ); } /** * Send the text to be displayed before the options. * Should use $this->getOutput()->addWikiTextAsInterface() * or similar methods to print the text. * * @param FormOptions $opts */ public function setTopText( FormOptions $opts ) { // nothing by default } /** * Send the text to be displayed after the options. * Should use $this->getOutput()->addWikiTextAsInterface() * or similar methods to print the text. * * @param FormOptions $opts */ public function setBottomText( FormOptions $opts ) { // nothing by default } /** * Get options to be displayed in a form * @todo This should handle options returned by getDefaultOptions(). * @todo Not called by anything in this class (but is in subclasses), should be * called by something… doHeader() maybe? * * @param FormOptions $opts * @return array */ public function getExtraOptions( $opts ) { return []; } /** * Return the legend displayed within the fieldset * * @return string */ public function makeLegend() { $context = $this->getContext(); $user = $context->getUser(); # The legend showing what the letters and stuff mean $legend = Html::openElement( 'dl' ) . "\n"; # Iterates through them and gets the messages for both letter and tooltip $legendItems = $context->getConfig()->get( MainConfigNames::RecentChangesFlags ); if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) { unset( $legendItems['unpatrolled'] ); } foreach ( $legendItems as $key => $item ) { # generate items of the legend $label = $item['legend'] ?? $item['title']; $letter = $item['letter']; $cssClass = $item['class'] ?? $key; $legend .= Html::element( 'dt', [ 'class' => $cssClass ], $context->msg( $letter )->text() ) . "\n" . Html::rawElement( 'dd', [ 'class' => Sanitizer::escapeClass( 'mw-changeslist-legend-' . $key ) ], $context->msg( $label )->parse() ) . "\n"; } # (+-123) $legend .= Html::rawElement( 'dt', [ 'class' => 'mw-plusminus-pos' ], $context->msg( 'recentchanges-legend-plusminus' )->parse() ) . "\n"; $legend .= Html::element( 'dd', [ 'class' => 'mw-changeslist-legend-plusminus' ], $context->msg( 'recentchanges-label-plusminus' )->text() ) . "\n"; // Watchlist expiry clock icon. if ( $context->getConfig()->get( MainConfigNames::WatchlistExpiry ) && !$this->including() ) { $widget = new IconWidget( [ 'icon' => 'clock', 'classes' => [ 'mw-changesList-watchlistExpiry' ], ] ); // Link the image to its label for assistive technologies. $watchlistLabelId = 'mw-changeslist-watchlistExpiry-label'; $widget->getIconElement()->setAttributes( [ 'role' => 'img', 'aria-labelledby' => $watchlistLabelId, ] ); $legend .= Html::rawElement( 'dt', [ 'class' => 'mw-changeslist-legend-watchlistexpiry' ], $widget ); $legend .= Html::element( 'dd', [ 'class' => 'mw-changeslist-legend-watchlistexpiry', 'id' => $watchlistLabelId ], $context->msg( 'recentchanges-legend-watchlistexpiry' )->text() ); } $legend .= Html::closeElement( 'dl' ) . "\n"; $legendHeading = $this->isStructuredFilterUiEnabled() ? $context->msg( 'rcfilters-legend-heading' )->parse() : $context->msg( 'recentchanges-legend-heading' )->parse(); # Collapsible $collapsedState = $this->getRequest()->getCookie( 'changeslist-state' ); $legend = Html::rawElement( 'details', [ 'class' => 'mw-changeslist-legend', 'open' => $collapsedState !== 'collapsed' ? 'open' : null, ], Html::rawElement( 'summary', [], $legendHeading ) . $legend ); return $legend; } /** * Add page-specific modules. */ protected function addModules() { $out = $this->getOutput(); // Styles and behavior for the legend box (see makeLegend()) $out->addModuleStyles( [ 'mediawiki.interface.helpers.styles', 'mediawiki.special.changeslist.legend', 'mediawiki.special.changeslist', ] ); $out->addModules( 'mediawiki.special.changeslist.legend.js' ); if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) { $out->addModules( 'mediawiki.rcfilters.filters.ui' ); $out->addModuleStyles( 'mediawiki.rcfilters.filters.base.styles' ); } } protected function getGroupName() { return 'changes'; } /** * Return expression that is true when the user is or isn't registered. * @param bool $isRegistered * @param IReadableDatabase $dbr * @return IExpression */ private function getRegisteredExpr( $isRegistered, $dbr ): IExpression { $expr = $dbr->expr( 'actor_user', $isRegistered ? '!=' : '=', null ); if ( !$this->tempUserConfig->isKnown() ) { return $expr; } if ( $isRegistered ) { return $expr->andExpr( $this->tempUserConfig->getMatchCondition( $dbr, 'actor_name', IExpression::NOT_LIKE ) ); } else { return $expr->orExpr( $this->tempUserConfig->getMatchCondition( $dbr, 'actor_name', IExpression::LIKE ) ); } } /** * Return expression that is true when the user has reached the given experience level. * @param string $level 'learner' or 'experienced' * @param int $now Current time as UNIX timestamp (if 0, uses actual time) * @param IReadableDatabase $dbr * @param bool $asNotCondition * @return IExpression */ private function getExperienceExpr( $level, $now, IReadableDatabase $dbr, $asNotCondition = false ): IExpression { $config = $this->getConfig(); $configSince = [ 'learner' => $config->get( MainConfigNames::LearnerMemberSince ), 'experienced' => $config->get( MainConfigNames::ExperiencedUserMemberSince ), ][$level]; if ( $now === 0 ) { $now = ConvertibleTimestamp::time(); } $secondsPerDay = 86400; $timeCutoff = $now - $configSince * $secondsPerDay; $editCutoff = [ 'learner' => $config->get( MainConfigNames::LearnerEdits ), 'experienced' => $config->get( MainConfigNames::ExperiencedUserEdits ), ][$level]; if ( $asNotCondition ) { return $dbr->expr( 'user_editcount', '<', intval( $editCutoff ) ) ->or( 'user_registration', '>', $dbr->timestamp( $timeCutoff ) ); } return $dbr->expr( 'user_editcount', '>=', intval( $editCutoff ) )->andExpr( // Users who don't have user_registration set are very old, so we assume they're above any cutoff $dbr->expr( 'user_registration', '=', null ) ->or( 'user_registration', '<=', $dbr->timestamp( $timeCutoff ) ) ); } /** * Filter on users' experience levels; this will not be called if nothing is * selected. * * @param string $specialPageClassName Class name of current special page * @param IContextSource $context Context, for e.g. user * @param IReadableDatabase $dbr Database, for addQuotes, makeList, and similar * @param array &$tables Array of tables; see IReadableDatabase::select $table * @param array &$fields Array of fields; see IReadableDatabase::select $vars * @param array &$conds Array of conditions; see IReadableDatabase::select $conds * @param array &$query_options Array of query options; see IReadableDatabase::select $options * @param array &$join_conds Array of join conditions; see IReadableDatabase::select $join_conds * @param array $selectedExpLevels The allowed active values, sorted * @param int $now Current time as UNIX timestamp (if 0, uses actual time) */ public function filterOnUserExperienceLevel( $specialPageClassName, $context, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0 ) { $selected = array_fill_keys( $selectedExpLevels, true ); $isUnregistered = $this->getRegisteredExpr( false, $dbr ); $isRegistered = $this->getRegisteredExpr( true, $dbr ); $aboveNewcomer = $this->getExperienceExpr( 'learner', $now, $dbr ); $notAboveNewcomer = $this->getExperienceExpr( 'learner', $now, $dbr, true ); $aboveLearner = $this->getExperienceExpr( 'experienced', $now, $dbr ); $notAboveLearner = $this->getExperienceExpr( 'experienced', $now, $dbr, true ); // We need to select some range of user experience levels, from the following table: // | Unregistered | --------- Registered --------- | // | | Newcomers | Learners | Experienced | // |<------------>|<----------->|<---------->|<----------->| // We just need to define a condition for each of the columns, figure out which are selected, // and then OR them together. $columnConds = [ 'unregistered' => $isUnregistered, 'registered' => $isRegistered, 'newcomer' => $dbr->andExpr( [ $isRegistered, $notAboveNewcomer ] ), 'learner' => $dbr->andExpr( [ $isRegistered, $aboveNewcomer, $notAboveLearner ] ), 'experienced' => $dbr->andExpr( [ $isRegistered, $aboveLearner ] ), ]; // There are some cases where we can easily optimize away some queries: // | Unregistered | --------- Registered --------- | // | | Newcomers | Learners | Experienced | // | |<-------------------------------------->| (1) // |<----------------------------------------------------->| (2) // (1) Selecting all of "Newcomers", "Learners" and "Experienced users" is the same as "Registered". if ( isset( $selected['registered'] ) || ( isset( $selected['newcomer'] ) && isset( $selected['learner'] ) && isset( $selected['experienced'] ) ) ) { unset( $selected['newcomer'], $selected['learner'], $selected['experienced'] ); $selected['registered'] = true; } // (2) Selecting "Unregistered" and "Registered" covers all users. if ( isset( $selected['registered'] ) && isset( $selected['unregistered'] ) ) { unset( $selected['registered'], $selected['unregistered'] ); } // Combine the conditions for the selected columns. if ( !$selected ) { return; } $selectedColumnConds = array_values( array_intersect_key( $columnConds, $selected ) ); $conds[] = $dbr->orExpr( $selectedColumnConds ); // Add necessary tables to the queries. $join_conds['recentchanges_actor'] = [ 'JOIN', 'actor_id=rc_actor' ]; if ( isset( $selected['newcomer'] ) || isset( $selected['learner'] ) || isset( $selected['experienced'] ) ) { $tables[] = 'user'; $join_conds['user'] = [ 'LEFT JOIN', 'actor_user=user_id' ]; } } /** * Check whether the structured filter UI is enabled * * @return bool */ public function isStructuredFilterUiEnabled() { if ( $this->getRequest()->getBool( 'rcfilters' ) ) { return true; } return static::checkStructuredFilterUiEnabled( $this->getUser() ); } /** * Static method to check whether StructuredFilter UI is enabled for the given user * * @since 1.31 * @param UserIdentity $user * @return bool */ public static function checkStructuredFilterUiEnabled( UserIdentity $user ) { return !MediaWikiServices::getInstance() ->getUserOptionsLookup() ->getOption( $user, 'rcenhancedfilters-disable' ); } /** * Get the default value of the number of changes to display when loading * the result set. * * @since 1.30 * @return int */ public function getDefaultLimit() { return MediaWikiServices::getInstance() ->getUserOptionsLookup() ->getIntOption( $this->getUser(), $this->getLimitPreferenceName() ); } /** * Get the default value of the number of days to display when loading * the result set. * Supports fractional values, and should be cast to a float. * * @since 1.30 * @return float */ public function getDefaultDays() { return floatval( MediaWikiServices::getInstance() ->getUserOptionsLookup() ->getOption( $this->getUser(), $this->getDefaultDaysPreferenceName() ) ); } /** * Getting the preference name for 'limit'. * * @since 1.37 * @return string */ abstract protected function getLimitPreferenceName(): string; /** * Preference name for saved queries. * * @since 1.38 * @return string */ abstract protected function getSavedQueriesPreferenceName(): string; /** * Preference name for 'days'. * * @since 1.38 * @return string */ abstract protected function getDefaultDaysPreferenceName(): string; /** * Preference name for collapsing the active filter display. * * @since 1.38 * @return string */ abstract protected function getCollapsedPreferenceName(): string; /** * @param array $namespaces * @return array */ private function expandSymbolicNamespaceFilters( array $namespaces ) { $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); $symbolicFilters = [ 'all-contents' => $nsInfo->getSubjectNamespaces(), 'all-discussions' => $nsInfo->getTalkNamespaces(), ]; $additionalNamespaces = []; foreach ( $symbolicFilters as $name => $values ) { if ( in_array( $name, $namespaces ) ) { $additionalNamespaces = array_merge( $additionalNamespaces, $values ); } } $namespaces = array_diff( $namespaces, array_keys( $symbolicFilters ) ); $namespaces = array_merge( $namespaces, $additionalNamespaces ); return array_unique( $namespaces ); } } /** @deprecated class alias since 1.41 */ class_alias( ChangesListSpecialPage::class, 'ChangesListSpecialPage' ); PK ! �a��� � SpecialRedirectToSpecial.phpnu �Iw�� <?php /** * Shortcuts to construct a special page alias. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage */ namespace MediaWiki\SpecialPage; use MediaWiki\Title\Title; /** * @stable to extend * * @ingroup SpecialPage */ abstract class SpecialRedirectToSpecial extends RedirectSpecialPage { /** @var string Name of redirect target */ protected $redirName; /** @var string|false Name of subpage of redirect target */ protected $redirSubpage; /** * @stable to call * * @param string $name * @param string $redirName * @param string|false $redirSubpage * @param array $allowedRedirectParams * @param array $addedRedirectParams */ public function __construct( $name, $redirName, $redirSubpage = false, $allowedRedirectParams = [], $addedRedirectParams = [] ) { parent::__construct( $name ); $this->redirName = $redirName; $this->redirSubpage = $redirSubpage; $this->mAllowedRedirectParams = $allowedRedirectParams; $this->mAddedRedirectParams = $addedRedirectParams; } /** * @param string|null $subpage * @return Title|bool */ public function getRedirect( $subpage ) { if ( $this->redirSubpage === false ) { return SpecialPage::getTitleFor( $this->redirName, $subpage ); } return SpecialPage::getTitleFor( $this->redirName, $this->redirSubpage ); } } /** @deprecated class alias since 1.41 */ class_alias( SpecialRedirectToSpecial::class, 'SpecialRedirectToSpecial' ); PK ! �8(� � SpecialRedirectWithAction.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage; use MediaWiki\HTMLForm\HTMLForm; use MediaWiki\MediaWikiServices; use MediaWiki\Status\Status; use MediaWiki\Title\MalformedTitleException; use MediaWiki\Title\Title; use SearchEngineFactory; /** * Abstract to simplify creation of redirect special pages * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @stable to extend * * @file * @ingroup SpecialPage * @author DannyS712 */ abstract class SpecialRedirectWithAction extends RedirectSpecialPage { /** @var string */ protected $action; /** @var string */ protected $msgPrefix; /** @var SearchEngineFactory */ private $searchEngineFactory; /** * @stable to call * @since 1.39 SearchEngineFactory added * * @param string $name * @param string $action * @param string $msgPrefix * @param SearchEngineFactory|null $searchEngineFactory Not providing this param is deprecated since 1.39 */ public function __construct( $name, $action, $msgPrefix, ?SearchEngineFactory $searchEngineFactory = null ) { parent::__construct( $name ); $this->action = $action; $this->msgPrefix = $msgPrefix; if ( !$searchEngineFactory ) { // Fallback to global state if the new parameter was not provided wfDeprecated( __METHOD__ . ' without providing SearchEngineFactory', '1.39' ); $searchEngineFactory = MediaWikiServices::getInstance()->getSearchEngineFactory(); } $this->searchEngineFactory = $searchEngineFactory; } /** * @inheritDoc */ public function getRedirect( $subpage ) { if ( $subpage === null || $subpage === '' ) { return false; } $this->mAddedRedirectParams['title'] = $subpage; $this->mAddedRedirectParams['action'] = $this->action; return true; } /** * @stable to override */ protected function showNoRedirectPage() { $this->setHeaders(); $this->outputHeader(); $this->showForm(); } private function showForm() { // Dynamic messages used: // 'special' . $this->msgPrefix . '-page' // 'special' . $this->msgPrefix . '-submit' // Each special page that extends this should include those as comments for grep $form = HTMLForm::factory( 'ooui', [ 'page' => [ 'type' => 'text', 'name' => 'page', 'label-message' => 'special' . $this->msgPrefix . '-page', 'required' => true, ], ], $this->getContext(), $this->msgPrefix ); $form->setSubmitTextMsg( 'special' . $this->msgPrefix . '-submit' ); $form->setSubmitCallback( [ $this, 'onFormSubmit' ] ); $form->show(); } /** * @stable to override * * @param array $formData * * @return Status|null */ public function onFormSubmit( $formData ) { $title = $formData['page']; try { $page = Title::newFromTextThrow( $title ); } catch ( MalformedTitleException $e ) { return Status::newFatal( $e->getMessageObject() ); } $query = [ 'action' => $this->action ]; $url = $page->getFullUrlForRedirect( $query ); $this->getOutput()->redirect( $url ); } /** * @stable to override * @return bool */ public function isListed() { return true; } /** * Return an array of subpages beginning with $search that this special page will accept. * * @param string $search Prefix to search for * @param int $limit Maximum number of results to return (usually 10) * @param int $offset Number of results to skip (usually 0) * @return string[] Matching subpages */ public function prefixSearchSubpages( $search, $limit, $offset ) { return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory ); } /** * @stable to override * @return string */ protected function getGroupName() { return 'redirects'; } } /** @deprecated class alias since 1.41 */ class_alias( SpecialRedirectWithAction::class, 'SpecialRedirectWithAction' ); PK ! ��{�^� ^� AuthManagerSpecialPage.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage; use ErrorPageError; use InvalidArgumentException; use LogicException; use MediaWiki\Auth\AuthenticationRequest; use MediaWiki\Auth\AuthenticationResponse; use MediaWiki\Auth\AuthManager; use MediaWiki\Context\DerivativeContext; use MediaWiki\HTMLForm\Field\HTMLInfoField; use MediaWiki\HTMLForm\HTMLForm; use MediaWiki\Language\RawMessage; use MediaWiki\Logger\LoggerFactory; use MediaWiki\Message\Message; use MediaWiki\Request\DerivativeRequest; use MediaWiki\Request\WebRequest; use MediaWiki\Session\Token; use MediaWiki\Status\Status; use MWCryptRand; use StatusValue; use UnexpectedValueException; /** * A special page subclass for authentication-related special pages. It generates a form from * a set of AuthenticationRequest objects, submits the result to AuthManager and * partially handles the response. * * @note Call self::setAuthManager from special page constructor when extending * * @stable to extend * @ingroup Auth */ abstract class AuthManagerSpecialPage extends SpecialPage { /** @var string[] The list of actions this special page deals with. Subclasses should override * this. */ protected static $allowedActions = [ AuthManager::ACTION_LOGIN, AuthManager::ACTION_LOGIN_CONTINUE, AuthManager::ACTION_CREATE, AuthManager::ACTION_CREATE_CONTINUE, AuthManager::ACTION_LINK, AuthManager::ACTION_LINK_CONTINUE, AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK, ]; /** @var array Customized messages */ protected static $messages = []; /** @var string one of the AuthManager::ACTION_* constants. */ protected $authAction; /** @var AuthenticationRequest[] */ protected $authRequests; /** @var string Subpage of the special page. */ protected $subPage; /** @var bool True if the current request is a result of returning from a redirect flow. */ protected $isReturn; /** @var WebRequest|null If set, will be used instead of the real request. Used for redirection. */ protected $savedRequest; /** * Change the form descriptor that determines how a field will look in the authentication form. * Called from fieldInfoToFormDescriptor(). * @stable to override * * @param AuthenticationRequest[] $requests * @param array $fieldInfo Field information array (union of all * AuthenticationRequest::getFieldInfo() responses). * @param array &$formDescriptor HTMLForm descriptor. The special key 'weight' can be set to * change the order of the fields. * @param string $action Authentication type (one of the AuthManager::ACTION_* constants) */ public function onAuthChangeFormFields( array $requests, array $fieldInfo, array &$formDescriptor, $action ) { } /** * @stable to override * @return bool|string */ protected function getLoginSecurityLevel() { return $this->getName(); } public function getRequest() { return $this->savedRequest ?: $this->getContext()->getRequest(); } /** * Override the POST data, GET data from the real request is preserved. * * Used to preserve POST data over a HTTP redirect. * * @stable to override * * @param array $data * @param bool|null $wasPosted */ protected function setRequest( array $data, $wasPosted = null ) { $request = $this->getContext()->getRequest(); $this->savedRequest = new DerivativeRequest( $request, $data + $request->getQueryValues(), $wasPosted ?? $request->wasPosted() ); } /** * @stable to override * @param string|null $subPage * * @return bool|void */ protected function beforeExecute( $subPage ) { $this->getOutput()->disallowUserJs(); return $this->handleReturnBeforeExecute( $subPage ) && $this->handleReauthBeforeExecute( $subPage ); } /** * Handle redirection from the /return subpage. * * This is used in the redirect flow where we need * to be able to process data that was sent via a GET request. We set the /return subpage as * the reentry point, so we know we need to treat GET as POST, but we don't want to handle all * future GETs requests as POSTs, so we need to normalize the URL. (Also, we don't want to show any * received parameters around in the URL; they are ugly and might be sensitive.) * * Thus, when on the /return subpage, we stash the request data in the session, redirect, then * use the session to detect that we have been redirected, recover the data and replace the * real WebRequest with a fake one that contains the saved data. * * @param string $subPage * @return bool False if execution should be stopped. */ protected function handleReturnBeforeExecute( $subPage ) { $authManager = $this->getAuthManager(); $key = 'AuthManagerSpecialPage:return:' . $this->getName(); if ( $subPage === 'return' ) { $this->loadAuth( $subPage ); $preservedParams = $this->getPreservedParams(); // FIXME save POST values only from request $authData = array_diff_key( $this->getRequest()->getValues(), $preservedParams, [ 'title' => 1 ] ); $authManager->setAuthenticationSessionData( $key, $authData ); $url = $this->getPageTitle()->getFullURL( $preservedParams, false, PROTO_HTTPS ); $this->getOutput()->redirect( $url ); return false; } $authData = $authManager->getAuthenticationSessionData( $key ); if ( $authData ) { $authManager->removeAuthenticationSessionData( $key ); $this->isReturn = true; $this->setRequest( $authData, true ); } return true; } /** * Handle redirection when the user needs to (re)authenticate. * * Send the user to the login form if needed; in case the request was a POST, stash in the * session and simulate it once the user gets back. * * @param string $subPage * @return bool False if execution should be stopped. * @throws ErrorPageError When the user is not allowed to use this page. */ protected function handleReauthBeforeExecute( $subPage ) { $authManager = $this->getAuthManager(); $request = $this->getRequest(); $key = 'AuthManagerSpecialPage:reauth:' . $this->getName(); $securityLevel = $this->getLoginSecurityLevel(); if ( $securityLevel ) { $securityStatus = $authManager->securitySensitiveOperationStatus( $securityLevel ); if ( $securityStatus === AuthManager::SEC_REAUTH ) { $queryParams = array_diff_key( $request->getQueryValues(), [ 'title' => true ] ); if ( $request->wasPosted() ) { // unique ID in case the same special page is open in multiple browser tabs $uniqueId = MWCryptRand::generateHex( 6 ); $key .= ':' . $uniqueId; $queryParams = [ 'authUniqueId' => $uniqueId ] + $queryParams; $authData = array_diff_key( $request->getValues(), $this->getPreservedParams(), [ 'title' => 1 ] ); $authManager->setAuthenticationSessionData( $key, $authData ); } $title = SpecialPage::getTitleFor( 'Userlogin' ); $url = $title->getFullURL( [ 'returnto' => $this->getFullTitle()->getPrefixedDBkey(), 'returntoquery' => wfArrayToCgi( $queryParams ), 'force' => $securityLevel, ], false, PROTO_HTTPS ); $this->getOutput()->redirect( $url ); return false; } if ( $securityStatus !== AuthManager::SEC_OK ) { throw new ErrorPageError( 'cannotauth-not-allowed-title', 'cannotauth-not-allowed' ); } } $uniqueId = $request->getVal( 'authUniqueId' ); if ( $uniqueId ) { $key .= ':' . $uniqueId; $authData = $authManager->getAuthenticationSessionData( $key ); if ( $authData ) { $authManager->removeAuthenticationSessionData( $key ); $this->setRequest( $authData, true ); } } return true; } /** * Get the default action for this special page if none is given via URL/POST data. * Subclasses should override this (or override loadAuth() so this is never called). * @stable to override * @param string $subPage Subpage of the special page. * @return string an AuthManager::ACTION_* constant. */ abstract protected function getDefaultAction( $subPage ); /** * Return custom message key. * Allows subclasses to customize messages. * @param string $defaultKey * @return string */ protected function messageKey( $defaultKey ) { return array_key_exists( $defaultKey, static::$messages ) ? static::$messages[$defaultKey] : $defaultKey; } /** * Allows blacklisting certain request types. * @stable to override * @return array A list of AuthenticationRequest subclass names */ protected function getRequestBlacklist() { return []; } /** * Load or initialize $authAction, $authRequests and $subPage. * Subclasses should call this from execute() or otherwise ensure the variables are initialized. * @stable to override * @param string $subPage Subpage of the special page. * @param string|null $authAction Override auth action specified in request (this is useful * when the form needs to be changed from <action> to <action>_CONTINUE after a successful * authentication step) * @param bool $reset Regenerate the requests even if a cached version is available */ protected function loadAuth( $subPage, $authAction = null, $reset = false ) { // Do not load if already loaded, to cut down on the number of getAuthenticationRequests // calls. This is important for requests which have hidden information, so any // getAuthenticationRequests call would mean putting data into some cache. if ( !$reset && $this->subPage === $subPage && $this->authAction && ( !$authAction || $authAction === $this->authAction ) ) { return; } $request = $this->getRequest(); $this->subPage = $subPage; $this->authAction = $authAction ?: $request->getText( 'authAction' ); if ( !in_array( $this->authAction, static::$allowedActions, true ) ) { $this->authAction = $this->getDefaultAction( $subPage ); if ( $request->wasPosted() ) { $continueAction = $this->getContinueAction( $this->authAction ); if ( in_array( $continueAction, static::$allowedActions, true ) ) { $this->authAction = $continueAction; } } } $allReqs = $this->getAuthManager()->getAuthenticationRequests( $this->authAction, $this->getUser() ); $this->authRequests = array_filter( $allReqs, function ( $req ) { return !in_array( get_class( $req ), $this->getRequestBlacklist(), true ); } ); } /** * Returns true if this is not the first step of the authentication. * @return bool */ protected function isContinued() { return in_array( $this->authAction, [ AuthManager::ACTION_LOGIN_CONTINUE, AuthManager::ACTION_CREATE_CONTINUE, AuthManager::ACTION_LINK_CONTINUE, ], true ); } /** * Gets the _CONTINUE version of an action. * @param string $action An AuthManager::ACTION_* constant. * @return string An AuthManager::ACTION_*_CONTINUE constant. */ protected function getContinueAction( $action ) { switch ( $action ) { case AuthManager::ACTION_LOGIN: $action = AuthManager::ACTION_LOGIN_CONTINUE; break; case AuthManager::ACTION_CREATE: $action = AuthManager::ACTION_CREATE_CONTINUE; break; case AuthManager::ACTION_LINK: $action = AuthManager::ACTION_LINK_CONTINUE; break; } return $action; } /** * Checks whether AuthManager is ready to perform the action. * ACTION_CHANGE needs special verification (AuthManager::allowsAuthenticationData*) which is * the caller's responsibility. * @param string $action One of the AuthManager::ACTION_* constants in static::$allowedActions * @return bool */ protected function isActionAllowed( $action ) { $authManager = $this->getAuthManager(); if ( !in_array( $action, static::$allowedActions, true ) ) { throw new InvalidArgumentException( 'invalid action: ' . $action ); } // calling getAuthenticationRequests can be expensive, avoid if possible $requests = ( $action === $this->authAction ) ? $this->authRequests : $authManager->getAuthenticationRequests( $action ); if ( !$requests ) { // no provider supports this action in the current state return false; } switch ( $action ) { case AuthManager::ACTION_LOGIN: case AuthManager::ACTION_LOGIN_CONTINUE: return $authManager->canAuthenticateNow(); case AuthManager::ACTION_CREATE: case AuthManager::ACTION_CREATE_CONTINUE: return $authManager->canCreateAccounts(); case AuthManager::ACTION_LINK: case AuthManager::ACTION_LINK_CONTINUE: return $authManager->canLinkAccounts(); case AuthManager::ACTION_CHANGE: case AuthManager::ACTION_REMOVE: case AuthManager::ACTION_UNLINK: return true; default: // should never reach here but makes static code analyzers happy throw new InvalidArgumentException( 'invalid action: ' . $action ); } } /** * @param string $action One of the AuthManager::ACTION_* constants * @param AuthenticationRequest[] $requests * @return AuthenticationResponse * @throws LogicException if $action is invalid */ protected function performAuthenticationStep( $action, array $requests ) { if ( !in_array( $action, static::$allowedActions, true ) ) { throw new InvalidArgumentException( 'invalid action: ' . $action ); } $authManager = $this->getAuthManager(); $returnToUrl = $this->getPageTitle( 'return' ) ->getFullURL( $this->getPreservedParams( [ 'withToken' => true ] ), false, PROTO_HTTPS ); switch ( $action ) { case AuthManager::ACTION_LOGIN: return $authManager->beginAuthentication( $requests, $returnToUrl ); case AuthManager::ACTION_LOGIN_CONTINUE: return $authManager->continueAuthentication( $requests ); case AuthManager::ACTION_CREATE: return $authManager->beginAccountCreation( $this->getAuthority(), $requests, $returnToUrl ); case AuthManager::ACTION_CREATE_CONTINUE: return $authManager->continueAccountCreation( $requests ); case AuthManager::ACTION_LINK: return $authManager->beginAccountLink( $this->getUser(), $requests, $returnToUrl ); case AuthManager::ACTION_LINK_CONTINUE: return $authManager->continueAccountLink( $requests ); case AuthManager::ACTION_CHANGE: case AuthManager::ACTION_REMOVE: case AuthManager::ACTION_UNLINK: if ( count( $requests ) > 1 ) { throw new InvalidArgumentException( 'only one auth request can be changed at a time' ); } if ( !$requests ) { throw new InvalidArgumentException( 'no auth request' ); } $req = reset( $requests ); $status = $authManager->allowsAuthenticationDataChange( $req ); $this->getHookRunner()->onChangeAuthenticationDataAudit( $req, $status ); if ( !$status->isGood() ) { return AuthenticationResponse::newFail( $status->getMessage() ); } $authManager->changeAuthenticationData( $req ); return AuthenticationResponse::newPass(); default: // should never reach here but makes static code analyzers happy throw new InvalidArgumentException( 'invalid action: ' . $action ); } } /** * Attempts to do an authentication step with the submitted data. * Subclasses should probably call this from execute(). * @return false|Status * - false if there was no submit at all * - a good Status wrapping an AuthenticationResponse if the form submit was successful. * This does not necessarily mean that the authentication itself was successful; see the * response for that. * - a bad Status for form errors. */ protected function trySubmit() { $status = false; $form = $this->getAuthForm( $this->authRequests, $this->authAction ); $form->setSubmitCallback( [ $this, 'handleFormSubmit' ] ); if ( $this->getRequest()->wasPosted() ) { // handle tokens manually; $form->tryAuthorizedSubmit only works for logged-in users $requestTokenValue = $this->getRequest()->getVal( $this->getTokenName() ); $sessionToken = $this->getToken(); if ( $sessionToken->wasNew() ) { return Status::newFatal( $this->messageKey( 'authform-newtoken' ) ); } elseif ( !$requestTokenValue ) { return Status::newFatal( $this->messageKey( 'authform-notoken' ) ); } elseif ( !$sessionToken->match( $requestTokenValue ) ) { return Status::newFatal( $this->messageKey( 'authform-wrongtoken' ) ); } $form->prepareForm(); $status = $form->trySubmit(); // HTMLForm submit return values are a mess; let's ensure it is false or a Status // FIXME this probably should be in HTMLForm if ( $status === true ) { // not supposed to happen since our submit handler should always return a Status throw new UnexpectedValueException( 'HTMLForm::trySubmit() returned true' ); } elseif ( $status === false ) { // form was not submitted; nothing to do } elseif ( $status instanceof Status ) { // already handled by the form; nothing to do } elseif ( $status instanceof StatusValue ) { // in theory not an allowed return type but nothing stops the submit handler from // accidentally returning it so best check and fix $status = Status::wrap( $status ); } elseif ( is_string( $status ) ) { $status = Status::newFatal( new RawMessage( '$1', [ $status ] ) ); } elseif ( is_array( $status ) ) { if ( is_string( reset( $status ) ) ) { // @phan-suppress-next-line PhanParamTooFewUnpack $status = Status::newFatal( ...$status ); } elseif ( is_array( reset( $status ) ) ) { $ret = Status::newGood(); foreach ( $status as $message ) { // @phan-suppress-next-line PhanParamTooFewUnpack $ret->fatal( ...$message ); } $status = $ret; } else { throw new UnexpectedValueException( 'invalid HTMLForm::trySubmit() return value: ' . 'first element of array is ' . get_debug_type( reset( $status ) ) ); } } else { // not supposed to happen, but HTMLForm does not verify the return type // from the submit callback; better safe then sorry! throw new UnexpectedValueException( 'invalid HTMLForm::trySubmit() return type: ' . get_debug_type( $status ) ); } if ( ( !$status || !$status->isOK() ) && $this->isReturn ) { // This is awkward. There was a form validation error, which means the data was not // passed to AuthManager. Normally we would display the form with an error message, // but for the data we received via the redirect flow that would not be helpful at all. // Let's just submit the data to AuthManager directly instead. LoggerFactory::getInstance( 'authentication' ) ->warning( 'Validation error on return', [ 'data' => $form->mFieldData, 'status' => $status->getWikiText( false, false, 'en' ) ] ); $status = $this->handleFormSubmit( $form->mFieldData ); } } $changeActions = [ AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK ]; if ( in_array( $this->authAction, $changeActions, true ) && $status && !$status->isOK() ) { $this->getHookRunner()->onChangeAuthenticationDataAudit( reset( $this->authRequests ), $status ); } return $status; } /** * Submit handler callback for HTMLForm * @internal * @param array $data Submitted data * @return Status */ public function handleFormSubmit( $data ) { $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data ); $response = $this->performAuthenticationStep( $this->authAction, $requests ); // we can't handle FAIL or similar as failure here since it might require changing the form return Status::newGood( $response ); } /** * Returns URL query parameters which should be preserved between authentication requests. * These should be used when generating links such as form submit or language switch. * * These parameters will be preserved in: * - successive authentication steps (the form submit URL and the return URL for redirecting * providers); * - links that reload the same form somehow (e.g. language switcher links); * - links for switching between the login and create account forms. * * @stable to override * @param array $options (since 1.43) * - reset (bool, default false): Reset the authentication process, i.e. omit parameters * which are related to continuing in-progress authentication. * - withToken (bool, default false): Include CSRF token * Before 1.43, this was a boolean flag identical to the current 'withToken' option. * That usage is deprecated. * @phan-param array{reset?: bool, withToken?: bool}|bool $options * @return array Array of parameter name => parameter value. */ protected function getPreservedParams( $options = [] ) { if ( is_bool( $options ) ) { wfDeprecated( __METHOD__ . ' boolean $options', '1.43' ); $options = [ 'withToken' => $options ]; } $options += [ 'reset' => false, 'withToken' => false, ]; // Help Phan figure out that these fields are now definitely set - https://github.com/phan/phan/issues/4864 '@phan-var array{reset: bool, withToken: bool} $options'; $params = []; $request = $this->getRequest(); $params += [ 'uselang' => $request->getVal( 'uselang' ), 'variant' => $request->getVal( 'variant' ), 'returnto' => $request->getVal( 'returnto' ), 'returntoquery' => $request->getVal( 'returntoquery' ), 'returntoanchor' => $request->getVal( 'returntoanchor' ), ]; if ( !$options['reset'] && $this->authAction !== $this->getDefaultAction( $this->subPage ) ) { $params['authAction'] = $this->getContinueAction( $this->authAction ); } if ( $options['withToken'] ) { $params[$this->getTokenName()] = $this->getToken()->toString(); } // Allow authentication extensions like CentralAuth to preserve their own // query params during and after the authentication process. $this->getHookRunner()->onAuthPreserveQueryParams( $params, [ 'reset' => $options['reset'] ] ); return array_filter( $params, fn ( $val ) => $val !== null ); } /** * Generates a HTMLForm descriptor array from a set of authentication requests. * @stable to override * @param AuthenticationRequest[] $requests * @param string $action AuthManager action name (one of the AuthManager::ACTION_* constants) * @return array[] */ protected function getAuthFormDescriptor( $requests, $action ) { $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests ); $formDescriptor = $this->fieldInfoToFormDescriptor( $requests, $fieldInfo, $action ); $this->addTabIndex( $formDescriptor ); return $formDescriptor; } /** * @stable to override * @param AuthenticationRequest[] $requests * @param string $action AuthManager action name (one of the AuthManager::ACTION_* constants) * @return HTMLForm */ protected function getAuthForm( array $requests, $action ) { $formDescriptor = $this->getAuthFormDescriptor( $requests, $action ); $context = $this->getContext(); if ( $context->getRequest() !== $this->getRequest() ) { // We have overridden the request, need to make sure the form uses that too. $context = new DerivativeContext( $this->getContext() ); $context->setRequest( $this->getRequest() ); } $form = HTMLForm::factory( 'ooui', $formDescriptor, $context ); $form->setAction( $this->getFullTitle()->getFullURL( $this->getPreservedParams() ) ); $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() ); $form->addHiddenField( 'authAction', $this->authAction ); $form->suppressDefaultSubmit( !$this->needsSubmitButton( $requests ) ); return $form; } /** * Display the form. * @param false|Status|StatusValue $status A form submit status, as in HTMLForm::trySubmit() */ protected function displayForm( $status ) { if ( $status instanceof StatusValue ) { $status = Status::wrap( $status ); } $form = $this->getAuthForm( $this->authRequests, $this->authAction ); $form->prepareForm()->displayForm( $status ); } /** * Returns true if the form built from the given AuthenticationRequests needs a submit button. * Providers using redirect flow (e.g. Google login) need their own submit buttons; if using * one of those custom buttons is the only way to proceed, there is no point in displaying the * default button which won't do anything useful. * @stable to override * * @param AuthenticationRequest[] $requests An array of AuthenticationRequests from which the * form will be built * @return bool */ protected function needsSubmitButton( array $requests ) { $customSubmitButtonPresent = false; // Secondary and preauth providers always need their data; they will not care what button // is used, so they can be ignored. So can OPTIONAL buttons createdby primary providers; // that's the point in being optional. Se we need to check whether all primary providers // have their own buttons and whether there is at least one button present. foreach ( $requests as $req ) { if ( $req->required === AuthenticationRequest::PRIMARY_REQUIRED ) { if ( $this->hasOwnSubmitButton( $req ) ) { $customSubmitButtonPresent = true; } else { return true; } } } return !$customSubmitButtonPresent; } /** * Checks whether the given AuthenticationRequest has its own submit button. * @param AuthenticationRequest $req * @return bool */ protected function hasOwnSubmitButton( AuthenticationRequest $req ) { foreach ( $req->getFieldInfo() as $info ) { if ( $info['type'] === 'button' ) { return true; } } return false; } /** * Adds a sequential tabindex starting from 1 to all form elements. This way the user can * use the tab key to traverse the form without having to step through all links and such. * @param array[] &$formDescriptor */ protected function addTabIndex( &$formDescriptor ) { $i = 1; foreach ( $formDescriptor as &$definition ) { $class = false; if ( array_key_exists( 'class', $definition ) ) { $class = $definition['class']; } elseif ( array_key_exists( 'type', $definition ) ) { $class = HTMLForm::$typeMappings[$definition['type']]; } if ( $class !== HTMLInfoField::class ) { $definition['tabindex'] = $i; $i++; } } } /** * Returns the CSRF token. * @stable to override * @return Token */ protected function getToken() { return $this->getRequest()->getSession()->getToken( 'AuthManagerSpecialPage:' . $this->getName() ); } /** * Returns the name of the CSRF token (under which it should be found in the POST or GET data). * @stable to override * @return string */ protected function getTokenName() { return 'wpAuthToken'; } /** * Turns a field info array into a form descriptor. Behavior can be modified by the * AuthChangeFormFields hook. * @param AuthenticationRequest[] $requests * @param array $fieldInfo Field information, in the format used by * AuthenticationRequest::getFieldInfo() * @param string $action One of the AuthManager::ACTION_* constants * @return array A form descriptor that can be passed to HTMLForm */ protected function fieldInfoToFormDescriptor( array $requests, array $fieldInfo, $action ) { $formDescriptor = []; foreach ( $fieldInfo as $fieldName => $singleFieldInfo ) { $formDescriptor[$fieldName] = self::mapSingleFieldInfo( $singleFieldInfo, $fieldName ); } $requestSnapshot = serialize( $requests ); $this->onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action ); $this->getHookRunner()->onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action ); if ( $requestSnapshot !== serialize( $requests ) ) { LoggerFactory::getInstance( 'authentication' )->warning( 'AuthChangeFormFields hook changed auth requests' ); } // Process the special 'weight' property, which is a way for AuthChangeFormFields hook // subscribers (who only see one field at a time) to influence ordering. self::sortFormDescriptorFields( $formDescriptor ); return $formDescriptor; } /** * Maps an authentication field configuration for a single field (as returned by * AuthenticationRequest::getFieldInfo()) to a HTMLForm field descriptor. * @param array $singleFieldInfo * @param string $fieldName * @return array */ protected static function mapSingleFieldInfo( $singleFieldInfo, $fieldName ) { $type = self::mapFieldInfoTypeToFormDescriptorType( $singleFieldInfo['type'] ); $descriptor = [ 'type' => $type, // Do not prefix input name with 'wp'. This is important for the redirect flow. 'name' => $fieldName, ]; if ( $type === 'submit' && isset( $singleFieldInfo['label'] ) ) { $descriptor['default'] = $singleFieldInfo['label']->plain(); } elseif ( $type !== 'submit' ) { $descriptor += array_filter( [ // help-message is omitted as it is usually not really useful for a web interface 'label-message' => self::getField( $singleFieldInfo, 'label' ), ] ); if ( isset( $singleFieldInfo['options'] ) ) { $descriptor['options'] = array_flip( array_map( static function ( $message ) { /** @var Message $message */ return $message->parse(); }, $singleFieldInfo['options'] ) ); } if ( isset( $singleFieldInfo['value'] ) ) { $descriptor['default'] = $singleFieldInfo['value']; } if ( empty( $singleFieldInfo['optional'] ) ) { $descriptor['required'] = true; } } return $descriptor; } /** * Sort the fields of a form descriptor by their 'weight' property. (Fields with higher weight * are shown closer to the bottom; weight defaults to 0. Negative weight is allowed.) * Keep order if weights are equal. * @param array &$formDescriptor */ protected static function sortFormDescriptorFields( array &$formDescriptor ) { $i = 0; foreach ( $formDescriptor as &$field ) { $field['__index'] = $i++; } unset( $field ); uasort( $formDescriptor, static function ( $first, $second ) { return self::getField( $first, 'weight', 0 ) <=> self::getField( $second, 'weight', 0 ) ?: $first['__index'] <=> $second['__index']; } ); foreach ( $formDescriptor as &$field ) { unset( $field['__index'] ); } } /** * Get an array value, or a default if it does not exist. * @param array $array * @param string $fieldName * @param mixed|null $default * @return mixed */ protected static function getField( array $array, $fieldName, $default = null ) { if ( array_key_exists( $fieldName, $array ) ) { return $array[$fieldName]; } else { return $default; } } /** * Maps AuthenticationRequest::getFieldInfo() types to HTMLForm types * * @param string $type * * @return string */ protected static function mapFieldInfoTypeToFormDescriptorType( $type ) { $map = [ 'string' => 'text', 'password' => 'password', 'select' => 'select', 'checkbox' => 'check', 'multiselect' => 'multiselect', 'button' => 'submit', 'hidden' => 'hidden', 'null' => 'info', ]; if ( !array_key_exists( $type, $map ) ) { throw new InvalidArgumentException( 'invalid field type: ' . $type ); } return $map[$type]; } /** * Apply defaults to a form descriptor, without creating non-existent fields. * * Overrides $formDescriptor fields with their $defaultFormDescriptor equivalent, but * only if the field is defined in $fieldInfo, uses the special 'basefield' property to * refer to a $fieldInfo field, or it is not a real field (e.g. help text). Applies some * common-sense behaviors to ensure related fields are overridden in a consistent manner. * @param array $fieldInfo * @param array $formDescriptor * @param array $defaultFormDescriptor * @return array */ protected static function mergeDefaultFormDescriptor( array $fieldInfo, array $formDescriptor, array $defaultFormDescriptor ) { // keep the ordering from $defaultFormDescriptor where there is no explicit weight foreach ( $defaultFormDescriptor as $fieldName => $defaultField ) { // remove everything that is not in the fieldinfo, is not marked as a supplemental field // to something in the fieldinfo, and is not an info field or a submit button if ( !isset( $fieldInfo[$fieldName] ) && ( !isset( $defaultField['baseField'] ) || !isset( $fieldInfo[$defaultField['baseField']] ) ) && ( !isset( $defaultField['type'] ) || !in_array( $defaultField['type'], [ 'submit', 'info' ], true ) ) ) { $defaultFormDescriptor[$fieldName] = null; continue; } // default message labels should always take priority $requestField = $formDescriptor[$fieldName] ?? []; if ( isset( $defaultField['label'] ) || isset( $defaultField['label-message'] ) || isset( $defaultField['label-raw'] ) ) { unset( $requestField['label'], $requestField['label-message'], $defaultField['label-raw'] ); } $defaultFormDescriptor[$fieldName] += $requestField; } return array_filter( $defaultFormDescriptor + $formDescriptor ); } } /** @deprecated class alias since 1.41 */ class_alias( AuthManagerSpecialPage::class, 'AuthManagerSpecialPage' ); PK ! ���� � ! Hook/AuthChangeFormFieldsHook.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage\Hook; use MediaWiki\Auth\AuthenticationRequest; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "AuthChangeFormFields" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface AuthChangeFormFieldsHook { /** * This hook is called after converting a field information array obtained * from a set of AuthenticationRequest classes into a form descriptor; hooks * can tweak the array to change how login etc. forms should look. * * @since 1.35 * * @param AuthenticationRequest[] $requests Array of AuthenticationRequests the fields * are created from * @param array $fieldInfo Field information array (union of all * AuthenticationRequest::getFieldInfo() responses) * @param array &$formDescriptor HTMLForm descriptor. The special key 'weight' can be set * to change the order of the fields. * @param string $action One of the AuthManager::ACTION_* constants * @return bool|void True or no return value to continue or false to abort */ public function onAuthChangeFormFields( $requests, $fieldInfo, &$formDescriptor, $action ); } PK ! ���c9 9 * Hook/ChangeAuthenticationDataAuditHook.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage\Hook; use MediaWiki\Auth\AuthenticationRequest; use StatusValue; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "ChangeAuthenticationDataAudit" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface ChangeAuthenticationDataAuditHook { /** * This hook is called when a user changes their password. * No return data is accepted; this hook is for auditing only. * * @since 1.35 * * @param AuthenticationRequest $req Object describing the change (and target user) * @param StatusValue $status StatusValue with the result of the action * @return bool|void True or no return value to continue or false to abort */ public function onChangeAuthenticationDataAudit( $req, $status ); } PK ! ���N N ! Hook/SpecialPage_initListHook.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage\Hook; // phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "SpecialPage_initList" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface SpecialPage_initListHook { /** * This hook is called when setting up SpecialPageFactory::$list. Use * this hook to remove a core special page or conditionally register special pages. * * @note The existence of a special page must not depend on the request context (e.g. current user or current * title). Possible red/blue links from wiki pages are cached in the parser cache and must be stable across * the requests from different users or for different pages. * Use only site config or checks if extensions are loaded to add or remove special pages from the list. * Override SpecialPage::userCanExecute or set a user right when calling SpecialPage::__construct, * after registering the special page for all users, to restrict the access for users allowed to use the page. * When replacing a (core) special page in the list, it is possible to depend on the request context, * but this hook is also called from Setup and the user is not always safe to load, * call User::isSafeToLoad before using any User class function or one of the user services like * UserOptionsLookup/UserOptionsManager. * Also the title may not be set for all requests (for e.g. api.php or load.php), * so checks for title should be avoided. * * @since 1.35 * * @param array &$list List of core special pages, * mapping of (canonical) page name to class name, factory callback or to ObjectFactory spec * @return bool|void True or no return value to continue or false to abort */ public function onSpecialPage_initList( &$list ); } PK ! n�� � � % Hook/SpecialPageBeforeExecuteHook.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage\Hook; use MediaWiki\SpecialPage\SpecialPage; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "SpecialPageBeforeExecute" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface SpecialPageBeforeExecuteHook { /** * This hook is called before SpecialPage::execute. * * @since 1.35 * * @param SpecialPage $special * @param string|null $subPage Subpage string, or null if no subpage was specified * @return bool|void True or no return value to continue or false to prevent execution */ public function onSpecialPageBeforeExecute( $special, $subPage ); } PK ! ) �5 5 4 Hook/ChangesListSpecialPageStructuredFiltersHook.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage\Hook; use MediaWiki\SpecialPage\ChangesListSpecialPage; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "ChangesListSpecialPageStructuredFilters" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface ChangesListSpecialPageStructuredFiltersHook { /** * Use this hook to register filters for pages inheriting from ChangesListSpecialPage * (in core: RecentChanges, RecentChangesLinked, and Watchlist). Generally, you will * want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects. * When constructing them, you specify which group they belong to. You can reuse * existing groups (accessed through $special->getFilterGroup), or create your own * (ChangesListBooleanFilterGroup or ChangesListStringOptionsFilterGroup). * If you create new groups, you must register them with $special->registerFilterGroup. * Note that this is called regardless of whether the user is currently using * the new (structured) or old (unstructured) filter UI. If you want your boolean * filter to show on both the new and old UI, specify all the supported fields. * These include showHide, label, and description. * See the constructor of each ChangesList* class for documentation of supported * fields. * * @since 1.35 * * @param ChangesListSpecialPage $special * @return bool|void True or no return value to continue or false to abort */ public function onChangesListSpecialPageStructuredFilters( $special ); } PK ! ��G�& & ( Hook/ChangesListSpecialPageQueryHook.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage\Hook; use MediaWiki\Html\FormOptions; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "ChangesListSpecialPageQuery" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface ChangesListSpecialPageQueryHook { /** * This hook is called when building an SQL query on pages inheriting from * ChangesListSpecialPage (in core: RecentChanges, RecentChangesLinked and * Watchlist). Do not use this to implement individual filters if they are * compatible with the ChangesListFilter and ChangesListFilterGroup structure. * Instead, use sub-classes of those classes in conjunction with the * ChangesListSpecialPageStructuredFilters hook. This hook can be used to * implement filters that do not implement that structure or custom behavior * that is not an individual filter. * * @since 1.35 * * @param string $name Name of the special page, e.g. 'Watchlist' * @param array &$tables Array of tables to be queried * @param array &$fields Array of columns to select * @param array &$conds Array of WHERE conditionals for query * @param array &$query_options Array of options for the database request * @param array &$join_conds Join conditions for the tables * @param FormOptions $opts FormOptions for this request * @return bool|void True or no return value to continue or false to abort */ public function onChangesListSpecialPageQuery( $name, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ); } PK ! �FSz 1 Hook/RedirectSpecialArticleRedirectParamsHook.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage\Hook; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "RedirectSpecialArticleRedirectParams" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface RedirectSpecialArticleRedirectParamsHook { /** * Use this hook to alter the set of parameter names such as "oldid" that * are preserved when using redirecting special pages such as Special:MyPage * and Special:MyTalk. * * @since 1.35 * * @param string[] &$redirectParams Array of parameters preserved by redirecting special pages * @return bool|void True or no return value to continue or false to abort */ public function onRedirectSpecialArticleRedirectParams( &$redirectParams ); } PK ! 7 � � � $ Hook/SpecialPageAfterExecuteHook.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage\Hook; use MediaWiki\SpecialPage\SpecialPage; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "SpecialPageAfterExecute" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface SpecialPageAfterExecuteHook { /** * This hook is called after SpecialPage::execute. * * @since 1.35 * * @param SpecialPage $special * @param string|null $subPage Subpage string, or null if no subpage was specified * @return bool|void True or no return value to continue or false to abort */ public function onSpecialPageAfterExecute( $special, $subPage ); } PK ! �H�: Hook/WgQueryPagesHook.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage\Hook; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "wgQueryPages" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface WgQueryPagesHook { /** * This hook is called when initialising list of QueryPage subclasses. Use this * hook to add new query pages to be updated with maintenance/updateSpecialPages.php. * * @since 1.35 * * @param array[] &$qp List of QueryPages * Format: [ string $class, string $specialPageName, ?int $limit (optional) ]. * Limit defaults to $wgQueryCacheLimit if not given. * @return bool|void True or no return value to continue or false to abort */ public function onWgQueryPages( &$qp ); } PK ! ,�P� � ) Hook/SpecialPageBeforeFormDisplayHook.phpnu �Iw�� <?php namespace MediaWiki\SpecialPage\Hook; use MediaWiki\HTMLForm\HTMLForm; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "SpecialPageBeforeFormDisplay" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface SpecialPageBeforeFormDisplayHook { /** * This hook is called before executing the HTMLForm object. * * @since 1.35 * * @param string $name Name of the special page * @param HTMLForm $form * @return bool|void True or no return value to continue or false to abort */ public function onSpecialPageBeforeFormDisplay( $name, $form ); } PK ! �V�� � RedirectSpecialPage.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\SpecialPage; use LogicException; use MediaWiki\Title\Title; /** * Shortcut to construct a special page alias. * * @stable to extend * * @ingroup SpecialPage */ abstract class RedirectSpecialPage extends UnlistedSpecialPage { /** @var array Query parameters that can be passed through redirects */ protected $mAllowedRedirectParams = []; /** @var array Query parameters added by redirects */ protected $mAddedRedirectParams = []; /** * @stable to override * @param string|null $subpage */ public function execute( $subpage ) { $redirect = $this->getRedirect( $subpage ); $query = $this->getRedirectQuery( $subpage ); if ( $redirect instanceof Title ) { // Redirect to a page title with possible query parameters $url = $redirect->getFullUrlForRedirect( $query ); $this->getOutput()->redirect( $url ); } elseif ( $redirect === true ) { // Redirect to index.php with query parameters $url = wfAppendQuery( wfScript( 'index' ), $query ); $this->getOutput()->redirect( $url ); } else { $this->showNoRedirectPage(); } } /** * If the special page is a redirect, then get the Title object it redirects to. * False otherwise. * * @param string|null $subpage * @return Title|bool */ abstract public function getRedirect( $subpage ); /** * Return part of the request string for a special redirect page * This allows passing, e.g. action=history to Special:Mypage, etc. * * @stable to override * @param string|null $subpage * @return array|false */ public function getRedirectQuery( $subpage ) { $params = []; $request = $this->getRequest(); foreach ( array_merge( $this->mAllowedRedirectParams, [ 'uselang', 'useskin', 'variant', 'debug', 'safemode' ] // parameters which can be passed to all pages ) as $arg ) { if ( $request->getVal( $arg, null ) !== null ) { $params[$arg] = $request->getVal( $arg ); } elseif ( $request->getArray( $arg, null ) !== null ) { $params[$arg] = $request->getArray( $arg ); } } foreach ( $this->mAddedRedirectParams as $arg => $val ) { $params[$arg] = $val; } return count( $params ) ? $params : false; } /** * Indicate if the target of this redirect can be used to identify * a particular user of this wiki (e.g., if the redirect is to the * user page of a User). See T109724. * * @stable to override * @since 1.27 * @return bool */ public function personallyIdentifiableTarget() { return false; } /** * @stable to override */ protected function showNoRedirectPage() { $class = static::class; throw new LogicException( "RedirectSpecialPage $class doesn't redirect!" ); } } /** @deprecated class alias since 1.41 */ class_alias( RedirectSpecialPage::class, 'RedirectSpecialPage' ); PK ! ���\� \� SpecialPage.phpnu �Iw�� <?php /** * Parent class for all special pages. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage */ namespace MediaWiki\SpecialPage; use ErrorPageError; use MediaWiki\Auth\AuthManager; use MediaWiki\Config\Config; use MediaWiki\Context\IContextSource; use MediaWiki\Context\RequestContext; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Language\Language; use MediaWiki\Language\RawMessage; use MediaWiki\Linker\LinkRenderer; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Message\Message; use MediaWiki\Navigation\PagerNavigationBuilder; use MediaWiki\Output\OutputPage; use MediaWiki\Permissions\Authority; use MediaWiki\Permissions\PermissionStatus; use MediaWiki\Request\WebRequest; use MediaWiki\Title\Title; use MediaWiki\Title\TitleValue; use MediaWiki\User\User; use MessageLocalizer; use MWCryptRand; use PermissionsError; use ReadOnlyError; use SearchEngineFactory; use Skin; use UserNotLoggedIn; use Wikimedia\Message\MessageSpecifier; /** * Parent class for all special pages. * * Includes some static functions for handling the special page list deprecated * in favor of SpecialPageFactory. * * @stable to extend * * @ingroup SpecialPage */ class SpecialPage implements MessageLocalizer { /** * @var string The canonical name of this special page * Also used as the message key for the default <h1> heading, * @see getDescription() */ protected $mName; /** @var string The local name of this special page */ private $mLocalName; /** * @var string Minimum user level required to access this page, or "" for anyone. * Also used to categorise the pages in Special:Specialpages */ protected $mRestriction; /** @var bool Listed in Special:Specialpages? */ private $mListed; /** @var bool Whether or not this special page is being included from an article */ protected $mIncluding; /** @var bool Whether the special page can be included in an article */ protected $mIncludable; /** * Current request context * @var IContextSource */ protected $mContext; /** @var Language|null */ private $contentLanguage; /** * @var LinkRenderer|null */ private $linkRenderer = null; /** @var HookContainer|null */ private $hookContainer; /** @var HookRunner|null */ private $hookRunner; /** @var AuthManager|null */ private $authManager = null; /** @var SpecialPageFactory */ private $specialPageFactory; /** * Get the users preferred search page. * * It will fall back to Special:Search if the preference points to a page * that doesn't exist or is not defined. * * @since 1.38 * @param User $user Search page can be customized by user preference. * @return Title */ public static function newSearchPage( User $user ) { // Try user preference first $userOptionsManager = MediaWikiServices::getInstance()->getUserOptionsManager(); $title = $userOptionsManager->getOption( $user, 'search-special-page' ); if ( $title ) { $page = self::getTitleFor( $title ); $factory = MediaWikiServices::getInstance()->getSpecialPageFactory(); if ( $factory->exists( $page->getText() ) ) { return $page; } } return self::getTitleFor( 'Search' ); } /** * Get a localised Title object for a specified special page name * If you don't need a full Title object, consider using TitleValue through * getTitleValueFor() below. * * @since 1.9 * @since 1.21 $fragment parameter added * * @param string $name * @param string|false|null $subpage Subpage string, or false/null to not use a subpage * @param string $fragment The link fragment (after the "#") * @return Title */ public static function getTitleFor( $name, $subpage = false, $fragment = '' ) { return Title::newFromLinkTarget( self::getTitleValueFor( $name, $subpage, $fragment ) ); } /** * Get a localised TitleValue object for a specified special page name * * @since 1.28 * @param string $name * @param string|false|null $subpage Subpage string, or false/null to not use a subpage * @param string $fragment The link fragment (after the "#") * @return TitleValue */ public static function getTitleValueFor( $name, $subpage = false, $fragment = '' ) { $name = MediaWikiServices::getInstance()->getSpecialPageFactory()-> getLocalNameFor( $name, $subpage ); return new TitleValue( NS_SPECIAL, $name, $fragment ); } /** * Get a localised Title object for a page name with a possibly unvalidated subpage * * @param string $name * @param string|false $subpage Subpage string, or false to not use a subpage * @return Title|null Title object or null if the page doesn't exist */ public static function getSafeTitleFor( $name, $subpage = false ) { $name = MediaWikiServices::getInstance()->getSpecialPageFactory()-> getLocalNameFor( $name, $subpage ); if ( $name ) { return Title::makeTitleSafe( NS_SPECIAL, $name ); } else { return null; } } /** * Default constructor for special pages * Derivative classes should call this from their constructor * Note that if the user does not have the required level, an error message will * be displayed by the default execute() method, without the global function ever * being called. * * If you override execute(), you can recover the default behavior with userCanExecute() * and displayRestrictionError() * * @stable to call * * @param string $name Name of the special page, as seen in links and URLs * @param string $restriction User right required, e.g. "block" or "delete" * @param bool $listed Whether the page is listed in Special:Specialpages * @param callable|bool $function Unused * @param string $file Unused * @param bool $includable Whether the page can be included in normal pages */ public function __construct( $name = '', $restriction = '', $listed = true, $function = false, $file = '', $includable = false ) { $this->mName = $name; $this->mRestriction = $restriction; $this->mListed = $listed; $this->mIncludable = $includable; } /** * Get the canonical, unlocalized name of this special page without namespace. * @return string */ public function getName() { return $this->mName; } /** * Get the permission that a user must have to execute this page * @return string */ public function getRestriction() { return $this->mRestriction; } // @todo FIXME: Decide which syntax to use for this, and stick to it /** * Whether this special page is listed in Special:SpecialPages * @stable to override * @since 1.3 (r3583) * @return bool */ public function isListed() { return $this->mListed; } /** * Whether it's allowed to transclude the special page via {{Special:Foo/params}} * @stable to override * @return bool */ public function isIncludable() { return $this->mIncludable; } /** * How long to cache page when it is being included. * * @note If cache time is not 0, then the current user becomes an anon * if you want to do any per-user customizations, than this method * must be overridden to return 0. * @since 1.26 * @stable to override * @return int Time in seconds, 0 to disable caching altogether, * false to use the parent page's cache settings */ public function maxIncludeCacheTime() { return $this->getConfig()->get( MainConfigNames::MiserMode ) ? $this->getCacheTTL() : 0; } /** * @stable to override * @return int Seconds that this page can be cached */ protected function getCacheTTL() { return 60 * 60; } /** * Whether the special page is being evaluated via transclusion * @param bool|null $x * @return bool */ public function including( $x = null ) { return wfSetVar( $this->mIncluding, $x ); } /** * Get the localised name of the special page * @stable to override * @return string */ public function getLocalName() { if ( !isset( $this->mLocalName ) ) { $this->mLocalName = $this->getSpecialPageFactory()->getLocalNameFor( $this->mName ); } return $this->mLocalName; } /** * Is this page expensive (for some definition of expensive)? * Expensive pages are disabled or cached in miser mode. Originally used * (and still overridden) by QueryPage and subclasses, moved here so that * Special:SpecialPages can safely call it for all special pages. * * @stable to override * @return bool */ public function isExpensive() { return false; } /** * Is this page cached? * Expensive pages are cached or disabled in miser mode. * Used by QueryPage and subclasses, moved here so that * Special:SpecialPages can safely call it for all special pages. * * @stable to override * @return bool * @since 1.21 */ public function isCached() { return false; } /** * Can be overridden by subclasses with more complicated permissions * schemes. * * @stable to override * @return bool Should the page be displayed with the restricted-access * pages? */ public function isRestricted() { // DWIM: If anons can do something, then it is not restricted return $this->mRestriction != '' && !MediaWikiServices::getInstance() ->getGroupPermissionsLookup() ->groupHasPermission( '*', $this->mRestriction ); } /** * Checks if the given user (identified by an object) can execute this * special page (as defined by $mRestriction). Can be overridden by sub- * classes with more complicated permissions schemes. * * @stable to override * @param User $user The user to check * @return bool Does the user have permission to view the page? */ public function userCanExecute( User $user ) { return MediaWikiServices::getInstance() ->getPermissionManager() ->userHasRight( $user, $this->mRestriction ); } /** * Utility function for authorizing an action to be performed by the special * page. User blocks and rate limits are enforced implicitly. * * @see Authority::authorizeAction. * * @param ?string $action If not given, the action returned by * getRestriction() will be used. * * @return PermissionStatus */ protected function authorizeAction( ?string $action = null ): PermissionStatus { $action ??= $this->getRestriction(); if ( !$action ) { return PermissionStatus::newGood(); } $status = PermissionStatus::newEmpty(); $this->getAuthority()->authorizeAction( $action, $status ); return $status; } /** * Output an error message telling the user what access level they have to have * @stable to override * @throws PermissionsError * @return never */ protected function displayRestrictionError() { throw new PermissionsError( $this->mRestriction ); } /** * Checks if userCanExecute, and if not throws a PermissionsError * * @stable to override * @since 1.19 * @return void * @throws PermissionsError */ public function checkPermissions() { if ( !$this->userCanExecute( $this->getUser() ) ) { $this->displayRestrictionError(); } } /** * If the wiki is currently in readonly mode, throws a ReadOnlyError * * @since 1.19 * @return void * @throws ReadOnlyError */ public function checkReadOnly() { // Can not inject the ReadOnlyMode as it would break the installer since // it instantiates SpecialPageFactory before the DB (via ParserFactory for message parsing) if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) { throw new ReadOnlyError; } } /** * If the user is not logged in, throws UserNotLoggedIn error * * The user will be redirected to Special:Userlogin with the given message as an error on * the form. * * @since 1.23 * @param string $reasonMsg [optional] Message key to be displayed on login page * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor * @throws UserNotLoggedIn */ public function requireLogin( $reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin' ) { if ( $this->getUser()->isAnon() ) { throw new UserNotLoggedIn( $reasonMsg, $titleMsg ); } } /** * If the user is not logged in or is a temporary user, throws UserNotLoggedIn * * @since 1.39 * @param string $reasonMsg [optional] Message key to be displayed on login page * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor. Default 'exception-nologin' * which is used when $titleMsg is null. * @param bool $alwaysRedirectToLoginPage [optional] Should the redirect always go to Special:UserLogin? * If false (the default), the redirect will be to Special:CreateAccount when the user is logged in to * a temporary account. * @throws UserNotLoggedIn */ public function requireNamedUser( $reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin', bool $alwaysRedirectToLoginPage = false ) { if ( !$this->getUser()->isNamed() ) { throw new UserNotLoggedIn( $reasonMsg, $titleMsg, [], $alwaysRedirectToLoginPage ); } } /** * Tells if the special page does something security-sensitive and needs extra defense against * a stolen account (e.g. a reauthentication). What exactly that will mean is decided by the * authentication framework. * @stable to override * @return string|false False or the argument for AuthManager::securitySensitiveOperationStatus(). * Typically a special page needing elevated security would return its name here. */ protected function getLoginSecurityLevel() { return false; } /** * Record preserved POST data after a reauthentication. * * This is called from checkLoginSecurityLevel() when returning from the * redirect for reauthentication, if the redirect had been served in * response to a POST request. * * The base SpecialPage implementation does nothing. If your subclass uses * getLoginSecurityLevel() or checkLoginSecurityLevel(), it should probably * implement this to do something with the data. * * @note Call self::setAuthManager from special page constructor when overriding * * @stable to override * @since 1.32 * @param array $data */ protected function setReauthPostData( array $data ) { } /** * Verifies that the user meets the security level, possibly reauthenticating them in the process. * * This should be used when the page does something security-sensitive and needs extra defense * against a stolen account (e.g. a reauthentication). The authentication framework will make * an extra effort to make sure the user account is not compromised. What that exactly means * will depend on the system and user settings; e.g. the user might be required to log in again * unless their last login happened recently, or they might be given a second-factor challenge. * * Calling this method will result in one if these actions: * - return true: all good. * - return false and set a redirect: caller should abort; the redirect will take the user * to the login page for reauthentication, and back. * - throw an exception if there is no way for the user to meet the requirements without using * a different access method (e.g. this functionality is only available from a specific IP). * * Note that this does not in any way check that the user is authorized to use this special page * (use checkPermissions() for that). * * @param string|null $level A security level. Can be an arbitrary string, defaults to the page * name. * @return bool False means a redirect to the reauthentication page has been set and processing * of the special page should be aborted. * @throws ErrorPageError If the security level cannot be met, even with reauthentication. */ protected function checkLoginSecurityLevel( $level = null ) { $level = $level ?: $this->getName(); $key = 'SpecialPage:reauth:' . $this->getName(); $request = $this->getRequest(); $securityStatus = $this->getAuthManager()->securitySensitiveOperationStatus( $level ); if ( $securityStatus === AuthManager::SEC_OK ) { $uniqueId = $request->getVal( 'postUniqueId' ); if ( $uniqueId ) { $key .= ':' . $uniqueId; $session = $request->getSession(); $data = $session->getSecret( $key ); if ( $data ) { $session->remove( $key ); $this->setReauthPostData( $data ); } } return true; } elseif ( $securityStatus === AuthManager::SEC_REAUTH ) { $title = self::getTitleFor( 'Userlogin' ); $queryParams = $request->getQueryValues(); if ( $request->wasPosted() ) { $data = array_diff_assoc( $request->getValues(), $request->getQueryValues() ); if ( $data ) { // unique ID in case the same special page is open in multiple browser tabs $uniqueId = MWCryptRand::generateHex( 6 ); $key .= ':' . $uniqueId; $queryParams['postUniqueId'] = $uniqueId; $session = $request->getSession(); $session->persist(); // Just in case $session->setSecret( $key, $data ); } } $query = [ 'returnto' => $this->getFullTitle()->getPrefixedDBkey(), 'returntoquery' => wfArrayToCgi( array_diff_key( $queryParams, [ 'title' => true ] ) ), 'force' => $level, ]; $url = $title->getFullURL( $query, false, PROTO_HTTPS ); $this->getOutput()->redirect( $url ); return false; } $titleMessage = wfMessage( 'specialpage-securitylevel-not-allowed-title' ); $errorMessage = wfMessage( 'specialpage-securitylevel-not-allowed' ); throw new ErrorPageError( $titleMessage, $errorMessage ); } /** * Set the injected AuthManager from the special page constructor * * @since 1.36 * @param AuthManager $authManager */ final protected function setAuthManager( AuthManager $authManager ) { $this->authManager = $authManager; } /** * @note Call self::setAuthManager from special page constructor when using * * @since 1.36 * @return AuthManager */ final protected function getAuthManager(): AuthManager { if ( $this->authManager === null ) { // Fallback if not provided // TODO Change to wfWarn in a future release $this->authManager = MediaWikiServices::getInstance()->getAuthManager(); } return $this->authManager; } /** * Return an array of subpages beginning with $search that this special page will accept. * * For example, if a page supports subpages "foo", "bar" and "baz" (as in Special:PageName/foo, * etc.): * * - `prefixSearchSubpages( "ba" )` should return `[ "bar", "baz" ]` * - `prefixSearchSubpages( "f" )` should return `[ "foo" ]` * - `prefixSearchSubpages( "z" )` should return `[]` * - `prefixSearchSubpages( "" )` should return `[ foo", "bar", "baz" ]` * * @stable to override * @param string $search Prefix to search for * @param int $limit Maximum number of results to return (usually 10) * @param int $offset Number of results to skip (usually 0) * @return string[] Matching subpages */ public function prefixSearchSubpages( $search, $limit, $offset ) { $subpages = $this->getSubpagesForPrefixSearch(); if ( !$subpages ) { return []; } return self::prefixSearchArray( $search, $limit, $subpages, $offset ); } /** * Return an array of subpages that this special page will accept for prefix * searches. If this method requires a query you might instead want to implement * prefixSearchSubpages() directly so you can support $limit and $offset. This * method is better for static-ish lists of things. * * @stable to override * @return string[] subpages to search from */ protected function getSubpagesForPrefixSearch() { return []; } /** * Return an array of strings representing page titles that are discoverable to end users via UI. * * @since 1.39 * @stable to call or override * @return string[] strings representing page titles that can be rendered by skins if required. */ public function getAssociatedNavigationLinks() { return []; } /** * Perform a regular substring search for prefixSearchSubpages * @since 1.36 Added $searchEngineFactory parameter * @param string $search Prefix to search for * @param int $limit Maximum number of results to return (usually 10) * @param int $offset Number of results to skip (usually 0) * @param SearchEngineFactory|null $searchEngineFactory Provide the service * @return string[] Matching subpages */ protected function prefixSearchString( $search, $limit, $offset, ?SearchEngineFactory $searchEngineFactory = null ) { $title = Title::newFromText( $search ); if ( !$title || !$title->canExist() ) { // No prefix suggestion in special and media namespace return []; } $searchEngine = $searchEngineFactory ? $searchEngineFactory->create() // Fallback if not provided // TODO Change to wfWarn in a future release : MediaWikiServices::getInstance()->newSearchEngine(); $searchEngine->setLimitOffset( $limit, $offset ); $searchEngine->setNamespaces( [] ); $result = $searchEngine->defaultPrefixSearch( $search ); return array_map( static function ( Title $t ) { return $t->getPrefixedText(); }, $result ); } /** * Helper function for implementations of prefixSearchSubpages() that * filter the values in memory (as opposed to making a query). * * @since 1.24 * @param string $search * @param int $limit * @param array $subpages * @param int $offset * @return string[] */ protected static function prefixSearchArray( $search, $limit, array $subpages, $offset ) { $escaped = preg_quote( $search, '/' ); return array_slice( preg_grep( "/^$escaped/i", array_slice( $subpages, $offset ) ), 0, $limit ); } /** * Sets headers - this should be called from the execute() method of all derived classes! * @stable to override */ protected function setHeaders() { $out = $this->getOutput(); $out->setArticleRelated( false ); $out->setRobotPolicy( $this->getRobotPolicy() ); $title = $this->getDescription(); // T343849 if ( is_string( $title ) ) { wfDeprecated( "string return from {$this->getName()}::getDescription()", '1.41' ); $title = ( new RawMessage( '$1' ) )->rawParams( $title ); } $out->setPageTitleMsg( $title ); } /** * Entry point. * * @since 1.20 * * @param string|null $subPage */ final public function run( $subPage ) { if ( !$this->getHookRunner()->onSpecialPageBeforeExecute( $this, $subPage ) ) { return; } if ( $this->beforeExecute( $subPage ) === false ) { return; } $this->execute( $subPage ); $this->afterExecute( $subPage ); $this->getHookRunner()->onSpecialPageAfterExecute( $this, $subPage ); } /** * Gets called before @see SpecialPage::execute. * Return false to prevent calling execute() (since 1.27+). * * @stable to override * @since 1.20 * * @param string|null $subPage * @return bool|void */ protected function beforeExecute( $subPage ) { // No-op } /** * Gets called after @see SpecialPage::execute. * * @stable to override * @since 1.20 * * @param string|null $subPage */ protected function afterExecute( $subPage ) { // No-op } /** * Default execute method * Checks user permissions * * This must be overridden by subclasses; it will be made abstract in a future version * * @stable to override * * @param string|null $subPage */ public function execute( $subPage ) { $this->setHeaders(); $this->checkPermissions(); $securityLevel = $this->getLoginSecurityLevel(); if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) { return; } $this->outputHeader(); } /** * Outputs a summary message on top of special pages * By default the message key is the canonical name of the special page * May be overridden, i.e. by extensions to stick with the naming conventions * for message keys: 'extensionname-xxx' * * @stable to override * * @param string $summaryMessageKey Message key of the summary */ protected function outputHeader( $summaryMessageKey = '' ) { if ( $summaryMessageKey == '' ) { $msg = strtolower( $this->getName() ) . '-summary'; } else { $msg = $summaryMessageKey; } if ( !$this->msg( $msg )->isDisabled() && !$this->including() ) { $this->getOutput()->wrapWikiMsg( "<div class='mw-specialpage-summary'>\n$1\n</div>", $msg ); } } /** * Returns the name that goes in the \<h1\> in the special page itself, and * also the name that will be listed in Special:Specialpages * * Derived classes can override this, but usually it is easier to keep the * default behavior. * * Returning a string from this method has been deprecated since 1.41. * * @stable to override * * @return string|Message */ public function getDescription() { return $this->msg( strtolower( $this->mName ) ); } /** * Similar to getDescription, but takes into account subpages and designed for display * in tabs. * * @since 1.39 * @stable to override if special page has complex parameter handling. Use default message keys * where possible. * * @param string $path (optional) * @return string */ public function getShortDescription( string $path = '' ): string { $lowerPath = strtolower( str_replace( '/', '-', $path ) ); $shortKey = 'special-tab-' . $lowerPath; $shortKey .= '-short'; $msgShort = $this->msg( $shortKey ); return $msgShort->text(); } /** * Get a self-referential title object * * @param string|false|null $subpage * @return Title * @since 1.23 */ public function getPageTitle( $subpage = false ) { return self::getTitleFor( $this->mName, $subpage ); } /** * Sets the context this SpecialPage is executed in * * @param IContextSource $context * @since 1.18 */ public function setContext( $context ) { $this->mContext = $context; } /** * Gets the context this SpecialPage is executed in * * @return IContextSource|RequestContext * @since 1.18 */ public function getContext() { if ( !( $this->mContext instanceof IContextSource ) ) { wfDebug( __METHOD__ . " called and \$mContext is null. " . "Using RequestContext::getMain()" ); $this->mContext = RequestContext::getMain(); } return $this->mContext; } /** * Get the WebRequest being used for this instance * * @return WebRequest * @since 1.18 */ public function getRequest() { return $this->getContext()->getRequest(); } /** * Get the OutputPage being used for this instance * * @return OutputPage * @since 1.18 */ public function getOutput() { return $this->getContext()->getOutput(); } /** * Shortcut to get the User executing this instance * * @return User * @since 1.18 */ public function getUser() { return $this->getContext()->getUser(); } /** * Shortcut to get the Authority executing this instance * * @return Authority * @since 1.36 */ public function getAuthority(): Authority { return $this->getContext()->getAuthority(); } /** * Shortcut to get the skin being used for this instance * * @return Skin * @since 1.18 */ public function getSkin() { return $this->getContext()->getSkin(); } /** * Shortcut to get user's language * * @return Language * @since 1.19 */ public function getLanguage() { return $this->getContext()->getLanguage(); } /** * Shortcut to get content language * * @return Language * @since 1.36 */ final public function getContentLanguage(): Language { if ( $this->contentLanguage === null ) { // Fallback if not provided // TODO Change to wfWarn in a future release $this->contentLanguage = MediaWikiServices::getInstance()->getContentLanguage(); } return $this->contentLanguage; } /** * Set content language * * @internal For factory only * @param Language $contentLanguage * @since 1.36 */ final public function setContentLanguage( Language $contentLanguage ) { $this->contentLanguage = $contentLanguage; } /** * Shortcut to get main config object * @return Config * @since 1.24 */ public function getConfig() { return $this->getContext()->getConfig(); } /** * Return the full title, including $par * * @return Title * @since 1.18 */ public function getFullTitle() { return $this->getContext()->getTitle(); } /** * Return the robot policy. Derived classes that override this can change * the robot policy set by setHeaders() from the default 'noindex,nofollow'. * * @return string * @since 1.23 */ protected function getRobotPolicy() { return 'noindex,nofollow'; } /** * Wrapper around wfMessage that sets the current context. * * @since 1.16 * @param string|string[]|MessageSpecifier $key * @param mixed ...$params * @return Message * @see wfMessage */ public function msg( $key, ...$params ) { $message = $this->getContext()->msg( $key, ...$params ); // RequestContext passes context to wfMessage, and the language is set from // the context, but setting the language for Message class removes the // interface message status, which breaks for example usernameless gender // invocations. Restore the flag when not including special page in content. if ( $this->including() ) { $message->setInterfaceMessageFlag( false ); } return $message; } /** * Adds RSS/atom links * * @param array $params */ protected function addFeedLinks( $params ) { $feedTemplate = wfScript( 'api' ); foreach ( $this->getConfig()->get( MainConfigNames::FeedClasses ) as $format => $class ) { $theseParams = $params + [ 'feedformat' => $format ]; $url = wfAppendQuery( $feedTemplate, $theseParams ); $this->getOutput()->addFeedLink( $format, $url ); } } /** * Adds help link with an icon via page indicators. * Link target can be overridden by a local message containing a wikilink: * the message key is: lowercase special page name + '-helppage'. * @param string $to Target MediaWiki.org page title or encoded URL. * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o. * @since 1.25 */ public function addHelpLink( $to, $overrideBaseUrl = false ) { if ( $this->including() ) { return; } $msg = $this->msg( strtolower( $this->getName() ) . '-helppage' ); if ( !$msg->isDisabled() ) { $title = Title::newFromText( $msg->plain() ); if ( $title instanceof Title ) { $this->getOutput()->addHelpLink( $title->getLocalURL(), true ); } } else { $this->getOutput()->addHelpLink( $to, $overrideBaseUrl ); } } /** * Get the group that the special page belongs in on Special:SpecialPage * Use this method, instead of getGroupName to allow customization * of the group name from the wiki side * * @return string Group of this special page * @since 1.21 */ public function getFinalGroupName() { $name = $this->getName(); // Allow overriding the group from the wiki side $msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage(); if ( !$msg->isBlank() ) { $group = $msg->text(); } else { // Than use the group from this object $group = $this->getGroupName(); } return $group; } /** * Indicates whether POST requests to this special page require write access to the wiki. * * Subclasses must override this method to return true if any of the operations that * they perform on POST requests are not "safe" per RFC 7231 section 4.2.1. A subclass's * operation is "safe" if it is essentially read-only, i.e. the client does not request * nor expect any state change that would be observable in the responses to future requests. * * Implementations of this method must always return the same value, regardless of the * parameters passed to the constructor or system state. * * When handling GET/HEAD requests, subclasses should only perform "safe" operations. * Note that some subclasses might only perform "safe" operations even for POST requests, * particularly in the case where large input parameters are required. * * @stable to override * * @return bool * @since 1.27 */ public function doesWrites() { return false; } /** * Under which header this special page is listed in Special:SpecialPages * See messages 'specialpages-group-*' for valid names * This method defaults to group 'other' * * @stable to override * * @return string * @since 1.21 */ protected function getGroupName() { return 'other'; } /** * Call wfTransactionalTimeLimit() if this request was POSTed * @since 1.26 */ protected function useTransactionalTimeLimit() { if ( $this->getRequest()->wasPosted() ) { wfTransactionalTimeLimit(); } } /** * @since 1.28 * @return LinkRenderer */ public function getLinkRenderer(): LinkRenderer { if ( $this->linkRenderer === null ) { // TODO Inject the service $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRendererFactory() ->create(); } return $this->linkRenderer; } /** * @since 1.28 * @param LinkRenderer $linkRenderer */ public function setLinkRenderer( LinkRenderer $linkRenderer ) { $this->linkRenderer = $linkRenderer; } /** * Generate (prev x| next x) (20|50|100...) type links for paging * * @param int $offset * @param int $limit * @param array $query Optional URL query parameter string * @param bool $atend Optional param for specified if this is the last page * @param string|false $subpage Optional param for specifying subpage * @return string */ protected function buildPrevNextNavigation( $offset, $limit, array $query = [], $atend = false, $subpage = false ) { $navBuilder = new PagerNavigationBuilder( $this ); $navBuilder ->setPage( $this->getPageTitle( $subpage ) ) ->setLinkQuery( [ 'limit' => $limit, 'offset' => $offset ] + $query ) ->setLimitLinkQueryParam( 'limit' ) ->setCurrentLimit( $limit ) ->setPrevTooltipMsg( 'prevn-title' ) ->setNextTooltipMsg( 'nextn-title' ) ->setLimitTooltipMsg( 'shown-title' ); if ( $offset > 0 ) { $navBuilder->setPrevLinkQuery( [ 'offset' => (string)max( $offset - $limit, 0 ) ] ); } if ( !$atend ) { $navBuilder->setNextLinkQuery( [ 'offset' => (string)( $offset + $limit ) ] ); } return $navBuilder->getHtml(); } /** * @since 1.35 * @internal * @param HookContainer $hookContainer */ public function setHookContainer( HookContainer $hookContainer ) { $this->hookContainer = $hookContainer; $this->hookRunner = new HookRunner( $hookContainer ); } /** * @since 1.35 * @return HookContainer */ protected function getHookContainer() { if ( !$this->hookContainer ) { $this->hookContainer = MediaWikiServices::getInstance()->getHookContainer(); } return $this->hookContainer; } /** * @internal This is for use by core only. Hook interfaces may be removed * without notice. * @since 1.35 * @return HookRunner */ protected function getHookRunner() { if ( !$this->hookRunner ) { $this->hookRunner = new HookRunner( $this->getHookContainer() ); } return $this->hookRunner; } /** * @internal For factory only * @since 1.36 * @param SpecialPageFactory $specialPageFactory */ final public function setSpecialPageFactory( SpecialPageFactory $specialPageFactory ) { $this->specialPageFactory = $specialPageFactory; } /** * @since 1.36 * @return SpecialPageFactory */ final protected function getSpecialPageFactory(): SpecialPageFactory { if ( !$this->specialPageFactory ) { // Fallback if not provided // TODO Change to wfWarn in a future release $this->specialPageFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); } return $this->specialPageFactory; } } /** @deprecated class alias since 1.41 */ class_alias( SpecialPage::class, 'SpecialPage' ); PK ! �v�$� $� LoginSignupSpecialPage.phpnu �Iw�� <?php /** * Holds shared logic for login and account creation pages. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage */ namespace MediaWiki\SpecialPage; use ErrorPageError; use Exception; use FatalError; use LogicException; use LoginHelper; use MediaWiki\Auth\AuthenticationRequest; use MediaWiki\Auth\AuthenticationResponse; use MediaWiki\Auth\AuthManager; use MediaWiki\Auth\PasswordAuthenticationRequest; use MediaWiki\Auth\UsernameAuthenticationRequest; use MediaWiki\Context\DerivativeContext; use MediaWiki\Context\RequestContext; use MediaWiki\Html\Html; use MediaWiki\HTMLForm\HTMLForm; use MediaWiki\Language\RawMessage; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Message\Message; use MediaWiki\Parser\Sanitizer; use MediaWiki\Session\SessionManager; use MediaWiki\Status\Status; use MediaWiki\Title\Title; use MediaWiki\User\User; use PermissionsError; use ReadOnlyError; use Skin; use StatusValue; use Wikimedia\ScopedCallback; /** * Holds shared logic for login and account creation pages. * * @ingroup SpecialPage * @ingroup Auth */ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage { /** * The title of the page to return to after authentication finishes, or the empty string * when there is no return target. * Typically comes from the 'returnto' URL parameter. Validating and normalizing is the * caller's responsibility. * @var string */ protected string $mReturnTo; /** * The query string part of the URL to return to after authentication finishes. * Typically comes from the 'returntoquery' URL parameter. * @var string */ protected string $mReturnToQuery; /** * The fragment part of the URL to return to after authentication finishes. * When not empty, should include the '#' character. * Typically comes from the 'returntoanchor' URL parameter. * @var string */ protected string $mReturnToAnchor; /** @var bool */ protected $mPosted; /** @var string|null */ protected $mAction; /** @var string */ protected $mToken; /** @var bool */ protected $mStickHTTPS; /** @var bool */ protected $mFromHTTP; /** @var string */ protected $mEntryError = ''; /** @var string */ protected $mEntryErrorType = 'error'; /** @var string */ protected $mDisplay = 'page'; /** @var bool */ protected $mLoaded = false; /** @var bool */ protected $mLoadedRequest = false; /** @var string|null */ protected $mSecureLoginUrl; /** @var string|true|null */ private $reasonValidatorResult = null; /** @var string */ protected $securityLevel; /** @var bool True if the user if creating an account for someone else. Flag used for internal * communication, only set at the very end. */ protected $proxyAccountCreation; /** @var User FIXME another flag for passing data. */ protected $targetUser; /** @var HTMLForm|null */ protected $authForm; abstract protected function isSignup(); /** * @param bool $direct True if the action was successful just now; false if that happened * pre-redirection (so this handler was called already) * @param StatusValue|null $extraMessages * @return void */ abstract protected function successfulAction( $direct = false, $extraMessages = null ); /** * Logs to the authmanager-stats channel. * @param bool $success * @param string|null $status Error message key */ abstract protected function logAuthResult( $success, $status = null ); protected function setRequest( array $data, $wasPosted = null ) { parent::setRequest( $data, $wasPosted ); $this->mLoadedRequest = false; } /** * Load basic request parameters for this Special page. */ private function loadRequestParameters() { if ( $this->mLoadedRequest ) { return; } $this->mLoadedRequest = true; $request = $this->getRequest(); $this->mPosted = $request->wasPosted(); $this->mAction = $request->getRawVal( 'action' ); $this->mFromHTTP = $request->getBool( 'fromhttp', false ) || $request->getBool( 'wpFromhttp', false ); $this->mStickHTTPS = $this->getConfig()->get( MainConfigNames::ForceHTTPS ) || ( !$this->mFromHTTP && $request->getProtocol() === 'https' ) || $request->getBool( 'wpForceHttps', false ); $this->mReturnTo = $request->getVal( 'returnto', '' ); $this->mReturnToQuery = $request->getVal( 'returntoquery', '' ); $this->mReturnToAnchor = $request->getVal( 'returntoanchor', '' ); if ( $request->getVal( 'display' ) === 'popup' ) { $this->mDisplay = 'popup'; } } /** * Load data from request. * @internal * @param string $subPage Subpage of Special:Userlogin */ protected function load( $subPage ) { $this->loadRequestParameters(); if ( $this->mLoaded ) { return; } $this->mLoaded = true; $request = $this->getRequest(); $securityLevel = $this->getRequest()->getText( 'force' ); if ( $securityLevel && MediaWikiServices::getInstance()->getAuthManager()->securitySensitiveOperationStatus( $securityLevel ) === AuthManager::SEC_REAUTH ) { $this->securityLevel = $securityLevel; } $this->loadAuth( $subPage ); $this->mToken = $request->getVal( $this->getTokenName() ); // Show an error or warning or a notice passed on from a previous page $entryError = $this->msg( $request->getVal( 'error', '' ) ); $entryWarning = $this->msg( $request->getVal( 'warning', '' ) ); $entryNotice = $this->msg( $request->getVal( 'notice', '' ) ); // bc: provide login link as a parameter for messages where the translation // was not updated $loginreqlink = $this->getLinkRenderer()->makeKnownLink( $this->getPageTitle(), $this->msg( 'loginreqlink' )->text(), [], $this->getPreservedParams( [ 'reset' => true ] ) ); // Only show valid error or warning messages. $validErrorMessages = LoginHelper::getValidErrorMessages(); if ( $entryError->exists() && in_array( $entryError->getKey(), $validErrorMessages, true ) ) { $this->mEntryErrorType = 'error'; $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse(); } elseif ( $entryWarning->exists() && in_array( $entryWarning->getKey(), $validErrorMessages, true ) ) { $this->mEntryErrorType = 'warning'; $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse(); } elseif ( $entryNotice->exists() && in_array( $entryNotice->getKey(), $validErrorMessages, true ) ) { $this->mEntryErrorType = 'notice'; $this->mEntryError = $entryNotice->parse(); } # 1. When switching accounts, it sucks to get automatically logged out # 2. Do not return to PasswordReset after a successful password change # but goto Wiki start page (Main_Page) instead ( T35997 ) $returnToTitle = Title::newFromText( $this->mReturnTo ); if ( is_object( $returnToTitle ) && ( $returnToTitle->isSpecial( 'Userlogout' ) || $returnToTitle->isSpecial( 'PasswordReset' ) ) ) { $this->mReturnTo = ''; $this->mReturnToQuery = ''; } } /** @inheritDoc */ protected function getPreservedParams( $options = [] ) { $params = parent::getPreservedParams( $options ); // Override returnto* with their property-based values, to account for the // special-casing in load(). $this->loadRequestParameters(); $properties = [ 'returnto' => 'mReturnTo', 'returntoquery' => 'mReturnToQuery', 'returntoanchor' => 'mReturnToAnchor', ]; foreach ( $properties as $key => $prop ) { $value = $this->$prop; if ( $value !== '' ) { $params[$key] = $value; } else { unset( $params[$key] ); } } if ( $this->getConfig()->get( MainConfigNames::SecureLogin ) && !$this->isSignup() ) { $params['fromhttp'] = $this->mFromHTTP ? '1' : null; } if ( $this->mDisplay !== 'page' ) { $params['display'] = $this->mDisplay; } return array_filter( $params, fn ( $val ) => $val !== null ); } protected function beforeExecute( $subPage ) { // finish initializing the class before processing the request - T135924 $this->loadRequestParameters(); return parent::beforeExecute( $subPage ); } /** * @param string|null $subPage * @suppress PhanTypeObjectUnsetDeclaredProperty */ public function execute( $subPage ) { if ( $this->mPosted ) { $time = microtime( true ); $profilingScope = new ScopedCallback( function () use ( $time ) { $time = microtime( true ) - $time; $stats = MediaWikiServices::getInstance()->getStatsFactory(); $stats->getTiming( 'auth_specialpage_executeTiming_seconds' ) ->setLabel( 'action', $this->authAction ) ->copyToStatsdAt( "timing.login.ui.{$this->authAction}" ) ->observe( $time * 1000 ); } ); } $authManager = MediaWikiServices::getInstance()->getAuthManager(); $session = SessionManager::getGlobalSession(); // Session data is used for various things in the authentication process, so we must make // sure a session cookie or some equivalent mechanism is set. $session->persist(); // Explicitly disable cache to ensure cookie blocks may be set (T152462). // (Technically redundant with sessions persisting from this page.) $this->getOutput()->disableClientCache(); $this->load( $subPage ); // Do this early, so that it affects how error pages are rendered too if ( $this->mDisplay === 'popup' ) { // Replace the default skin with a "micro-skin" that omits most of the interface. (T362706) // In the future, we might allow normal skins to serve this mode too, if they advise that // they support it by setting a skin option, so that colors and fonts could stay consistent. $skinFactory = MediaWikiServices::getInstance()->getSkinFactory(); $this->getContext()->setSkin( $skinFactory->makeSkin( 'authentication-popup' ) ); } $this->setHeaders(); $this->checkPermissions(); // Make sure the system configuration allows log in / sign up if ( !$this->isSignup() && !$authManager->canAuthenticateNow() ) { if ( !$session->canSetUser() ) { throw new ErrorPageError( 'cannotloginnow-title', 'cannotloginnow-text', [ $session->getProvider()->describe( $this->getLanguage() ) ] ); } throw new ErrorPageError( 'cannotlogin-title', 'cannotlogin-text' ); } elseif ( $this->isSignup() && !$authManager->canCreateAccounts() ) { throw new ErrorPageError( 'cannotcreateaccount-title', 'cannotcreateaccount-text' ); } /* * In the case where the user is already logged in, and was redirected to * the login form from a page that requires login, do not show the login * page. The use case scenario for this is when a user opens a large number * of tabs, is redirected to the login page on all of them, and then logs * in on one, expecting all the others to work properly. * * However, do show the form if it was visited intentionally (no 'returnto' * is present). People who often switch between several accounts have grown * accustomed to this behavior. * * For temporary users, the form is always shown, since the UI presents * temporary users as not logged in and offers to discard their temporary * account by logging in. * * Also make an exception when force=<level> is set in the URL, which means the user must * reauthenticate for security reasons. */ if ( !$this->isSignup() && !$this->mPosted && !$this->securityLevel && ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' ) && !$this->getUser()->isTemp() && $this->getUser()->isRegistered() ) { $this->successfulAction(); return; } // If logging in and not on HTTPS, either redirect to it or offer a link. if ( $this->getRequest()->getProtocol() !== 'https' ) { $title = $this->getFullTitle(); $query = $this->getPreservedParams() + [ 'title' => null, ( $this->mEntryErrorType === 'error' ? 'error' : 'warning' ) => $this->mEntryError, ] + $this->getRequest()->getQueryValues(); $url = $title->getFullURL( $query, false, PROTO_HTTPS ); if ( $this->getConfig()->get( MainConfigNames::SecureLogin ) && !$this->mFromHTTP ) { // Avoid infinite redirect $url = wfAppendQuery( $url, 'fromhttp=1' ); $this->getOutput()->redirect( $url ); // Since we only do this redir to change proto, always vary $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' ); return; } else { // A wiki without HTTPS login support should set $wgServer to // http://somehost, in which case the secure URL generated // above won't actually start with https:// if ( str_starts_with( $url, 'https://' ) ) { $this->mSecureLoginUrl = $url; } } } if ( !$this->isActionAllowed( $this->authAction ) ) { // FIXME how do we explain this to the user? can we handle session loss better? // messages used: authpage-cannot-login, authpage-cannot-login-continue, // authpage-cannot-create, authpage-cannot-create-continue $this->mainLoginForm( [], 'authpage-cannot-' . $this->authAction ); return; } if ( $this->canBypassForm( $button_name ) ) { $this->setRequest( [], true ); $this->getRequest()->setVal( $this->getTokenName(), $this->getToken() ); if ( $button_name ) { $this->getRequest()->setVal( $button_name, true ); } } $status = $this->trySubmit(); if ( !$status || !$status->isGood() ) { $this->mainLoginForm( $this->authRequests, $status ? $status->getMessage() : '', 'error' ); return; } /** @var AuthenticationResponse $response */ $response = $status->getValue(); $returnToUrl = $this->getPageTitle( 'return' ) ->getFullURL( $this->getPreservedParams( [ 'withToken' => true ] ), false, PROTO_HTTPS ); switch ( $response->status ) { case AuthenticationResponse::PASS: $this->logAuthResult( true ); $this->proxyAccountCreation = $this->isSignup() && $this->getUser()->isNamed(); $this->targetUser = User::newFromName( $response->username ); if ( !$this->proxyAccountCreation && $response->loginRequest && $authManager->canAuthenticateNow() ) { // successful registration; log the user in instantly $response2 = $authManager->beginAuthentication( [ $response->loginRequest ], $returnToUrl ); if ( $response2->status !== AuthenticationResponse::PASS ) { LoggerFactory::getInstance( 'login' ) ->error( 'Could not log in after account creation' ); $this->successfulAction( true, Status::newFatal( 'createacct-loginerror' ) ); break; } } if ( !$this->proxyAccountCreation ) { $context = RequestContext::getMain(); $localContext = $this->getContext(); if ( $context !== $localContext ) { // remove AuthManagerSpecialPage context hack $this->setContext( $context ); } // Ensure that the context user is the same as the session user. $this->getAuthManager()->setRequestContextUserFromSessionUser(); } $this->successfulAction( true ); break; case AuthenticationResponse::FAIL: // fall through case AuthenticationResponse::RESTART: unset( $this->authForm ); if ( $response->status === AuthenticationResponse::FAIL ) { $action = $this->getDefaultAction( $subPage ); $messageType = 'error'; } else { $action = $this->getContinueAction( $this->authAction ); $messageType = 'warning'; } $this->logAuthResult( false, $response->message ? $response->message->getKey() : '-' ); $this->loadAuth( $subPage, $action, true ); $this->mainLoginForm( $this->authRequests, $response->message, $messageType ); break; case AuthenticationResponse::REDIRECT: unset( $this->authForm ); $this->getOutput()->redirect( $response->redirectTarget ); break; case AuthenticationResponse::UI: unset( $this->authForm ); $this->authAction = $this->isSignup() ? AuthManager::ACTION_CREATE_CONTINUE : AuthManager::ACTION_LOGIN_CONTINUE; $this->authRequests = $response->neededRequests; $this->mainLoginForm( $response->neededRequests, $response->message, $response->messageType ); break; default: throw new LogicException( 'invalid AuthenticationResponse' ); } } /** * Determine if the login form can be bypassed. This will be the case when no more than one * button is present and no other user input fields that are not marked as 'skippable' are * present. If the login form were not bypassed, the user would be presented with a * superfluous page on which they must press the single button to proceed with login. * Not only does this cause an additional mouse click and page load, it confuses users, * especially since there are a help link and forgotten password link that are * provided on the login page that do not apply to this situation. * * @param string|null &$button_name if the form has a single button, returns * the name of the button; otherwise, returns null * @return bool */ private function canBypassForm( &$button_name ) { $button_name = null; if ( $this->isContinued() ) { return false; } $fields = AuthenticationRequest::mergeFieldInfo( $this->authRequests ); foreach ( $fields as $fieldname => $field ) { if ( !isset( $field['type'] ) ) { return false; } if ( !empty( $field['skippable'] ) ) { continue; } if ( $field['type'] === 'button' ) { if ( $button_name !== null ) { $button_name = null; return false; } else { $button_name = $fieldname; } } elseif ( $field['type'] !== 'null' ) { return false; } } return true; } /** * Show the success page. * * @param string $type Condition of return to; see `executeReturnTo` * @param string|Message $title Page's title * @param string $msgname * @param string $injected_html * @param StatusValue|null $extraMessages */ protected function showSuccessPage( $type, $title, $msgname, $injected_html, $extraMessages ) { $out = $this->getOutput(); if ( is_string( $title ) ) { wfDeprecated( __METHOD__ . ' with string title', '1.41' ); // T343849 $title = ( new RawMessage( '$1' ) )->rawParams( $title ); } $out->setPageTitleMsg( $title ); if ( $msgname ) { $out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) ); } if ( $extraMessages ) { $extraMessages = Status::wrap( $extraMessages ); $out->addWikiTextAsInterface( $extraMessages->getWikiText( false, false, $this->getLanguage() ) ); } $out->addHTML( $injected_html ); $helper = new LoginHelper( $this->getContext() ); $helper->showReturnToPage( $type, $this->mReturnTo, $this->mReturnToQuery, $this->mStickHTTPS, $this->mReturnToAnchor ); } /** * @param AuthenticationRequest[] $requests A list of AuthorizationRequest objects, * used to generate the form fields. An empty array means a fatal error * (authentication cannot continue). * @param string|Message $msg * @param string $msgtype * @throws ErrorPageError * @throws Exception * @throws FatalError * @throws PermissionsError * @throws ReadOnlyError * @internal */ protected function mainLoginForm( array $requests, $msg = '', $msgtype = 'error' ) { $user = $this->getUser(); $out = $this->getOutput(); // FIXME how to handle empty $requests - restart, or no form, just an error message? // no form would be better for no session type errors, restart is better when can* fails. if ( !$requests ) { $this->authAction = $this->getDefaultAction( $this->subPage ); $this->authForm = null; $requests = MediaWikiServices::getInstance()->getAuthManager() ->getAuthenticationRequests( $this->authAction, $user ); } // Generic styles and scripts for both login and signup form $out->addModuleStyles( [ 'mediawiki.special.userlogin.common.styles' ] ); if ( $this->isSignup() ) { // Additional styles and scripts for signup form $out->addModules( 'mediawiki.special.createaccount' ); $out->addModuleStyles( [ 'mediawiki.special.userlogin.signup.styles' ] ); } else { // Additional styles for login form $out->addModuleStyles( [ 'mediawiki.special.userlogin.login.styles' ] ); } $out->disallowUserJs(); // just in case... $form = $this->getAuthForm( $requests, $this->authAction ); $form->prepareForm(); $submitStatus = Status::newGood(); if ( $msg && $msgtype === 'warning' ) { $submitStatus->warning( $msg ); } elseif ( $msg && $msgtype === 'error' ) { $submitStatus->fatal( $msg ); } // warning header for non-standard workflows (e.g. security reauthentication) if ( !$this->isSignup() && $this->getUser()->isRegistered() && !$this->getUser()->isTemp() && $this->authAction !== AuthManager::ACTION_LOGIN_CONTINUE ) { $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin'; $submitStatus->warning( $reauthMessage, $this->getUser()->getName() ); } $formHtml = $form->getHTML( $submitStatus ); $out->addHTML( $this->getPageHtml( $formHtml ) ); } /** * Add page elements which are outside the form. * FIXME this should probably be a template, but use a sensible language (handlebars?) * @param string $formHtml * @return string */ protected function getPageHtml( $formHtml ) { $loginPrompt = $this->isSignup() ? '' : Html::rawElement( 'div', [ 'id' => 'userloginprompt' ], $this->msg( 'loginprompt' )->parseAsBlock() ); $languageLinks = $this->getConfig()->get( MainConfigNames::LoginLanguageSelector ) ? $this->makeLanguageSelector() : ''; $signupStartMsg = $this->msg( 'signupstart' ); $signupStart = ( $this->isSignup() && !$signupStartMsg->isDisabled() ) ? Html::rawElement( 'div', [ 'id' => 'signupstart' ], $signupStartMsg->parseAsBlock() ) : ''; if ( $languageLinks ) { $languageLinks = Html::rawElement( 'div', [ 'id' => 'languagelinks' ], Html::rawElement( 'p', [], $languageLinks ) ); } if ( $this->getUser()->isTemp() ) { $noticeHtml = $this->getNoticeHtml(); } else { $noticeHtml = ''; } $formBlock = Html::rawElement( 'div', [ 'id' => 'userloginForm' ], $formHtml ); $formAndBenefits = $formBlock; if ( $this->isSignup() && $this->showExtraInformation() ) { $benefitsContainerHtml = null; $info = [ 'context' => $this->getContext(), 'form' => $this->authForm, ]; $options = [ 'beforeForm' => false, ]; $this->getHookRunner()->onSpecialCreateAccountBenefits( $benefitsContainerHtml, $info, $options ); $benefitsContainerHtml ??= $this->getBenefitsContainerHtml(); $formAndBenefits = $options['beforeForm'] ? ( $benefitsContainerHtml . $formBlock ) : ( $formBlock . $benefitsContainerHtml ); } return $loginPrompt . $languageLinks . $signupStart . $noticeHtml . Html::rawElement( 'div', [ 'class' => 'mw-ui-container' ], $formAndBenefits ); } /** * The HTML to be shown in the "benefits to signing in / creating an account" section of the signup/login page. * * @unstable Experimental method added in 1.38. As noted in the comment from 2015 for getPageHtml, * this should use a template. * @return string */ protected function getBenefitsContainerHtml(): string { $benefitsContainer = ''; $this->getOutput()->addModuleStyles( [ 'oojs-ui.styles.icons-user' ] ); if ( $this->isSignup() && $this->showExtraInformation() ) { if ( !$this->getUser()->isTemp() ) { // The following messages are used here: // * createacct-benefit-icon1 createacct-benefit-head1 createacct-benefit-body1 // * createacct-benefit-icon2 createacct-benefit-head2 createacct-benefit-body2 // * createacct-benefit-icon3 createacct-benefit-head3 createacct-benefit-body3 $benefitCount = 3; $benefitList = ''; for ( $benefitIdx = 1; $benefitIdx <= $benefitCount; $benefitIdx++ ) { $headUnescaped = $this->msg( "createacct-benefit-head$benefitIdx" )->text(); $iconClass = $this->msg( "createacct-benefit-icon$benefitIdx" )->text(); $benefitList .= Html::rawElement( 'div', [ 'class' => "mw-number-text $iconClass" ], Html::rawElement( 'span', [], $this->msg( "createacct-benefit-head$benefitIdx" )->escaped() ) . Html::rawElement( 'p', [], $this->msg( "createacct-benefit-body$benefitIdx" )->params( $headUnescaped )->escaped() ) ); } $benefitsContainer = Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ], Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-heading' ], $this->msg( 'createacct-benefit-heading' )->escaped() ) . Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-list' ], $benefitList ) ); } else { $benefitList = ''; $this->getOutput()->addModuleStyles( [ 'oojs-ui.styles.icons-moderation', 'oojs-ui.styles.icons-interactions', ] ); $benefits = [ [ 'icon' => 'oo-ui-icon-unStar', 'description' => $this->msg( "benefit-1-description" )->escaped() ], [ 'icon' => 'oo-ui-icon-userContributions', 'description' => $this->msg( "benefit-2-description" )->escaped() ], [ 'icon' => 'oo-ui-icon-settings', 'description' => $this->msg( "benefit-3-description" )->escaped() ] ]; foreach ( $benefits as $benefit ) { $benefitContent = Html::rawElement( 'div', [ 'class' => 'mw-benefit-item' ], Html::rawElement( 'span', [ 'class' => $benefit[ 'icon' ] ] ) . Html::rawElement( 'p', [], $benefit['description'] ) ); $benefitList .= Html::rawElement( 'div', [ 'class' => 'mw-benefit-item-wrapper' ], $benefitContent ); } $benefitsListWrapper = Html::rawElement( 'div', [ 'class' => 'mw-benefit-list-wrapper' ], $benefitList ); $headingSubheadingWrapper = Html::rawElement( 'div', [ 'class' => 'mw-heading-subheading-wrapper' ], Html::rawElement( 'h2', [], $this->msg( 'createacct-benefit-heading-temp-user' )->escaped() ) . Html::rawElement( 'p', [ 'class' => 'mw-benefit-subheading' ], $this->msg( 'createacct-benefit-subheading-temp-user' )->escaped() ) ); $benefitsContainer = Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ], $headingSubheadingWrapper . $benefitsListWrapper ); } } return $benefitsContainer; } /** * Generates a form from the given request. * @param AuthenticationRequest[] $requests * @param string $action AuthManager action name * @return HTMLForm */ protected function getAuthForm( array $requests, $action ) { // FIXME merge this with parent if ( isset( $this->authForm ) ) { return $this->authForm; } $usingHTTPS = $this->getRequest()->getProtocol() === 'https'; // get basic form description from the auth logic $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests ); // this will call onAuthChangeFormFields() $formDescriptor = $this->fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction ); $this->postProcessFormDescriptor( $formDescriptor, $requests ); $context = $this->getContext(); if ( $context->getRequest() !== $this->getRequest() ) { // We have overridden the request, need to make sure the form uses that too. $context = new DerivativeContext( $this->getContext() ); $context->setRequest( $this->getRequest() ); } $form = HTMLForm::factory( 'codex', $formDescriptor, $context ); $form->addHiddenField( 'authAction', $this->authAction ); $form->addHiddenField( 'force', $this->securityLevel ); $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() ); $config = $this->getConfig(); if ( $config->get( MainConfigNames::SecureLogin ) && !$config->get( MainConfigNames::ForceHTTPS ) ) { // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved if ( !$this->isSignup() ) { $form->addHiddenField( 'wpForceHttps', (int)$this->mStickHTTPS ); $form->addHiddenField( 'wpFromhttp', $usingHTTPS ); } } $form->setAction( $this->getPageTitle()->getLocalURL( $this->getPreservedParams( // We have manually set authAction above, so we don't need it in the action URL. [ 'reset' => true ] ) ) ); $form->setName( 'userlogin' . ( $this->isSignup() ? '2' : '' ) ); if ( $this->isSignup() ) { $form->setId( 'userlogin2' ); } $form->suppressDefaultSubmit(); $this->authForm = $form; return $form; } /** @inheritDoc */ public function onAuthChangeFormFields( array $requests, array $fieldInfo, array &$formDescriptor, $action ) { $formDescriptor = self::mergeDefaultFormDescriptor( $fieldInfo, $formDescriptor, $this->getFieldDefinitions( $fieldInfo, $requests ) ); } /** * Show extra information such as password recovery information, link from login to signup, * CTA etc? Such information should only be shown on the "landing page", ie. when the user * is at the first step of the authentication process. * @return bool */ protected function showExtraInformation() { return $this->authAction !== $this->getContinueAction( $this->authAction ) && !$this->securityLevel; } /** * Create a HTMLForm descriptor for the core login fields. * * @param array $fieldInfo * @param array $requests * * @return array */ protected function getFieldDefinitions( array $fieldInfo, array $requests ) { $isLoggedIn = $this->getUser()->isRegistered(); $continuePart = $this->isContinued() ? 'continue-' : ''; $anotherPart = $isLoggedIn ? 'another-' : ''; // @phan-suppress-next-line PhanUndeclaredMethod $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration(); $expirationDays = ceil( $expiration / ( 3600 * 24 ) ); $secureLoginLink = ''; if ( $this->mSecureLoginUrl ) { $secureLoginLink = Html::rawElement( 'a', [ 'href' => $this->mSecureLoginUrl, 'class' => 'mw-login-flush-right mw-secure', ], Html::element( 'span', [ 'class' => 'mw-secure--icon' ] ) . $this->msg( 'userlogin-signwithsecure' )->parse() ); } $usernameHelpLink = ''; if ( !$this->msg( 'createacct-helpusername' )->isDisabled() ) { $usernameHelpLink = Html::rawElement( 'span', [ 'class' => 'mw-login-flush-right', ], $this->msg( 'createacct-helpusername' )->parse() ); } if ( $this->isSignup() ) { $config = $this->getConfig(); $hideIf = isset( $fieldInfo['mailpassword'] ) ? [ 'hide-if' => [ '===', 'mailpassword', '1' ] ] : []; $fieldDefinitions = [ 'username' => [ 'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $usernameHelpLink, 'id' => 'wpName2', 'placeholder-message' => $isLoggedIn ? 'createacct-another-username-ph' : 'userlogin-yourname-ph', ], 'mailpassword' => [ // create account without providing password, a temporary one will be mailed 'type' => 'check', 'label-message' => 'createaccountmail', 'name' => 'wpCreateaccountMail', 'id' => 'wpCreateaccountMail', ], 'password' => [ 'id' => 'wpPassword2', 'autocomplete' => 'new-password', 'placeholder-message' => 'createacct-yourpassword-ph', 'help-message' => 'createacct-useuniquepass', ] + $hideIf, 'domain' => [], 'retype' => [ 'type' => 'password', 'label-message' => 'createacct-yourpasswordagain', 'id' => 'wpRetype', 'cssclass' => 'loginPassword', 'size' => 20, 'autocomplete' => 'new-password', 'validation-callback' => function ( $value, $alldata ) { if ( empty( $alldata['mailpassword'] ) && !empty( $alldata['password'] ) ) { if ( !$value ) { return $this->msg( 'htmlform-required' ); } elseif ( $value !== $alldata['password'] ) { return $this->msg( 'badretype' ); } } return true; }, 'placeholder-message' => 'createacct-yourpasswordagain-ph', ] + $hideIf, 'email' => [ 'type' => 'email', 'label-message' => $config->get( MainConfigNames::EmailConfirmToEdit ) ? 'createacct-emailrequired' : 'createacct-emailoptional', 'id' => 'wpEmail', 'cssclass' => 'loginText', 'size' => '20', 'maxlength' => 255, 'autocomplete' => 'email', // FIXME will break non-standard providers 'required' => $config->get( MainConfigNames::EmailConfirmToEdit ), 'validation-callback' => function ( $value, $alldata ) { // AuthManager will check most of these, but that will make the auth // session fail and this won't, so nicer to do it this way if ( !$value && $this->getConfig()->get( MainConfigNames::EmailConfirmToEdit ) ) { // no point in allowing registration without email when email is // required to edit return $this->msg( 'noemailtitle' ); } elseif ( !$value && !empty( $alldata['mailpassword'] ) ) { // cannot send password via email when there is no email address return $this->msg( 'noemailcreate' ); } elseif ( $value && !Sanitizer::validateEmail( $value ) ) { return $this->msg( 'invalidemailaddress' ); } elseif ( is_string( $value ) && strlen( $value ) > 255 ) { return $this->msg( 'changeemail-maxlength' ); } return true; }, // The following messages are used here: // * createacct-email-ph // * createacct-another-email-ph 'placeholder-message' => 'createacct-' . $anotherPart . 'email-ph', ], 'realname' => [ 'type' => 'text', 'help-message' => $isLoggedIn ? 'createacct-another-realname-tip' : 'prefs-help-realname', 'label-message' => 'createacct-realname', 'cssclass' => 'loginText', 'size' => 20, 'placeholder-message' => 'createacct-realname', 'id' => 'wpRealName', 'autocomplete' => 'name', ], 'reason' => [ // comment for the user creation log 'type' => 'text', 'label-message' => 'createacct-reason', 'cssclass' => 'loginText', 'id' => 'wpReason', 'size' => '20', 'validation-callback' => function ( $value, $alldata ) { // if the user sets an email address as the user creation reason, confirm that // that was their intent if ( $value && Sanitizer::validateEmail( $value ) ) { if ( $this->reasonValidatorResult !== null ) { return $this->reasonValidatorResult; } $this->reasonValidatorResult = true; $authManager = MediaWikiServices::getInstance()->getAuthManager(); if ( !$authManager->getAuthenticationSessionData( 'reason-retry', false ) ) { $authManager->setAuthenticationSessionData( 'reason-retry', true ); $this->reasonValidatorResult = $this->msg( 'createacct-reason-confirm' ); } return $this->reasonValidatorResult; } return true; }, 'placeholder-message' => 'createacct-reason-ph', ], 'createaccount' => [ // submit button 'type' => 'submit', // The following messages are used here: // * createacct-submit // * createacct-another-submit // * createacct-continue-submit // * createacct-another-continue-submit 'default' => $this->msg( 'createacct-' . $anotherPart . $continuePart . 'submit' )->text(), 'name' => 'wpCreateaccount', 'id' => 'wpCreateaccount', 'weight' => 100, ], ]; if ( !$this->msg( 'createacct-username-help' )->isDisabled() ) { $fieldDefinitions['username']['help-message'] = 'createacct-username-help'; } } else { // When the user's password is too weak, they might be asked to provide a stronger one // as a followup step. That is a form with only two fields, 'password' and 'retype', // and they should behave more like account creation. $passwordRequest = AuthenticationRequest::getRequestByClass( $this->authRequests, PasswordAuthenticationRequest::class ); $changePassword = $passwordRequest && $passwordRequest->action == AuthManager::ACTION_CHANGE; $fieldDefinitions = [ 'username' => ( [ 'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $secureLoginLink, 'id' => 'wpName1', 'placeholder-message' => 'userlogin-yourname-ph', ] + ( $changePassword ? [ // There is no username field on the AuthManager level when changing // passwords. Fake one because password 'baseField' => 'password', 'nodata' => true, 'readonly' => true, 'cssclass' => 'mw-htmlform-hidden-field', ] : [] ) ), 'password' => ( $changePassword ? [ 'autocomplete' => 'new-password', 'placeholder-message' => 'createacct-yourpassword-ph', 'help-message' => 'createacct-useuniquepass', ] : [ 'id' => 'wpPassword1', 'autocomplete' => 'current-password', 'placeholder-message' => 'userlogin-yourpassword-ph', ] ), 'retype' => [ 'type' => 'password', 'autocomplete' => 'new-password', 'placeholder-message' => 'createacct-yourpasswordagain-ph', ], 'domain' => [], 'rememberMe' => [ // option for saving the user token to a cookie 'type' => 'check', 'cssclass' => 'mw-userlogin-rememberme', 'name' => 'wpRemember', 'label-message' => $this->msg( 'userlogin-remembermypassword' ) ->numParams( $expirationDays ), 'id' => 'wpRemember', ], 'loginattempt' => [ // submit button 'type' => 'submit', // The following messages are used here: // * pt-login-button // * pt-login-continue-button 'default' => $this->msg( 'pt-login-' . $continuePart . 'button' )->text(), 'id' => 'wpLoginAttempt', 'weight' => 100, ], 'linkcontainer' => [ // help link 'type' => 'info', 'cssclass' => 'mw-form-related-link-container mw-userlogin-help', // 'id' => 'mw-userlogin-help', // FIXME HTMLInfoField ignores this 'raw' => true, 'default' => Html::element( 'a', [ 'href' => Skin::makeInternalOrExternalUrl( $this->msg( 'helplogin-url' ) ->inContentLanguage() ->text() ), ], $this->msg( 'userlogin-helplink2' )->text() ), 'weight' => 200, ], // button for ResetPasswordSecondaryAuthenticationProvider 'skipReset' => [ 'weight' => 110, 'flags' => [], ], ]; } // T369641: We want to ensure that this transformation to the username and/or // password fields are applied only when we have matching requests within the // authentication manager. $isUsernameOrPasswordRequest = AuthenticationRequest::getRequestByClass( $requests, UsernameAuthenticationRequest::class ) || AuthenticationRequest::getRequestByClass( $requests, PasswordAuthenticationRequest::class ); if ( $isUsernameOrPasswordRequest ) { $fieldDefinitions['username'] += [ 'type' => 'text', 'name' => 'wpName', 'cssclass' => 'loginText mw-userlogin-username', 'size' => 20, 'autocomplete' => 'username', // 'required' => true, ]; $fieldDefinitions['password'] += [ 'type' => 'password', // 'label-message' => 'userlogin-yourpassword', // would override the changepassword label 'name' => 'wpPassword', 'cssclass' => 'loginPassword mw-userlogin-password', 'size' => 20, // 'required' => true, ]; } if ( $this->mEntryError ) { $defaultHtml = ''; if ( $this->mEntryErrorType === 'error' ) { $defaultHtml = Html::errorBox( $this->mEntryError ); } elseif ( $this->mEntryErrorType === 'warning' ) { $defaultHtml = Html::warningBox( $this->mEntryError ); } elseif ( $this->mEntryErrorType === 'notice' ) { $defaultHtml = Html::noticeBox( $this->mEntryError, '' ); } $fieldDefinitions['entryError'] = [ 'type' => 'info', 'default' => $defaultHtml, 'raw' => true, 'rawrow' => true, 'weight' => -100, ]; } if ( !$this->showExtraInformation() ) { unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] ); } if ( $this->isSignup() && $this->showExtraInformation() ) { // blank signup footer for site customization // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise $signupendMsg = $this->msg( 'signupend' ); $signupendHttpsMsg = $this->msg( 'signupend-https' ); if ( !$signupendMsg->isDisabled() ) { $usingHTTPS = $this->getRequest()->getProtocol() === 'https'; $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() ) ? $signupendHttpsMsg->parse() : $signupendMsg->parse(); $fieldDefinitions['signupend'] = [ 'type' => 'info', 'raw' => true, 'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ), 'weight' => 225, ]; } } if ( !$this->isSignup() && $this->showExtraInformation() ) { $passwordReset = MediaWikiServices::getInstance()->getPasswordReset(); if ( $passwordReset->isAllowed( $this->getUser() )->isGood() ) { $fieldDefinitions['passwordReset'] = [ 'type' => 'info', 'raw' => true, 'cssclass' => 'mw-form-related-link-container', 'default' => $this->getLinkRenderer()->makeLink( SpecialPage::getTitleFor( 'PasswordReset' ), $this->msg( 'userlogin-resetpassword-link' )->text() ), 'weight' => 230, ]; } // Don't show a "create account" link if the user can't. if ( $this->showCreateAccountLink() ) { // link to the other action $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' : 'CreateAccount' ); $linkq = wfArrayToCgi( $this->getPreservedParams( [ 'reset' => true ] ) ); $isLoggedIn = $this->getUser()->isRegistered() && !$this->getUser()->isTemp(); $fieldDefinitions['createOrLogin'] = [ 'type' => 'info', 'raw' => true, 'linkQuery' => $linkq, 'default' => function ( $params ) use ( $isLoggedIn, $linkTitle ) { $buttonClasses = 'cdx-button cdx-button--action-progressive ' . 'cdx-button--fake-button cdx-button--fake-button--enabled'; return Html::rawElement( 'div', // The following element IDs are used here: // mw-createaccount, mw-createaccount-cta [ 'id' => 'mw-createaccount' . ( !$isLoggedIn ? '-cta' : '' ), 'class' => ( $isLoggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ], ( $isLoggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() ) . Html::element( 'a', [ // The following element IDs are used here: // mw-createaccount-join, mw-createaccount-join-loggedin 'id' => 'mw-createaccount-join' . ( $isLoggedIn ? '-loggedin' : '' ), 'href' => $linkTitle->getLocalURL( $params['linkQuery'] ), 'class' => [ 'mw-authentication-popup-link', $buttonClasses => !$isLoggedIn ], 'target' => '_self', 'tabindex' => 100, ], $this->msg( $isLoggedIn ? 'userlogin-createanother' : 'userlogin-joinproject' )->text() ) ); }, 'weight' => 235, ]; } } return $fieldDefinitions; } /** * Check if a session cookie is present. * * This will not pick up a cookie set during _this_ request, but is meant * to ensure that the client is returning the cookie which was set on a * previous pass through the system. * * @return bool */ protected function hasSessionCookie() { global $wgInitialSessionId; return $wgInitialSessionId && $this->getRequest()->getSession()->getId() === (string)$wgInitialSessionId; } /** * Whether the login/create account form should display a link to the * other form (in addition to whatever the skin provides). * @return bool */ private function showCreateAccountLink() { return $this->isSignup() || $this->getContext()->getAuthority()->isAllowed( 'createaccount' ); } protected function getTokenName() { return $this->isSignup() ? 'wpCreateaccountToken' : 'wpLoginToken'; } /** * Produce a bar of links which allow the user to select another language * during login/registration but retain "returnto" * * @return string */ protected function makeLanguageSelector() { $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage(); if ( $msg->isBlank() ) { return ''; } $langs = explode( "\n", $msg->text() ); $links = []; foreach ( $langs as $lang ) { $lang = trim( $lang, '* ' ); $parts = explode( '|', $lang ); if ( count( $parts ) >= 2 ) { $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) ); } } return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams( $this->getLanguage()->pipeList( $links ) )->escaped() : ''; } /** * Create a language selector link for a particular language * Links back to this page preserving type and returnto * * @param string $text Link text * @param string $lang Language code * @return string */ protected function makeLanguageSelectorLink( $text, $lang ) { $services = MediaWikiServices::getInstance(); if ( $this->getLanguage()->getCode() == $lang || !$services->getLanguageNameUtils()->isValidCode( $lang ) ) { // no link for currently used language // or invalid language code return htmlspecialchars( $text ); } $query = $this->getPreservedParams(); $query['uselang'] = $lang; $attr = []; $targetLanguage = $services->getLanguageFactory()->getLanguage( $lang ); $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode(); $attr['class'] = 'mw-authentication-popup-link'; return $this->getLinkRenderer()->makeKnownLink( $this->getPageTitle(), $text, $attr, $query ); } protected function getGroupName() { return 'login'; } /** * @param array &$formDescriptor * @param array $requests */ protected function postProcessFormDescriptor( &$formDescriptor, $requests ) { // Pre-fill username (if not creating an account, T46775). if ( isset( $formDescriptor['username'] ) && !isset( $formDescriptor['username']['default'] ) && !$this->isSignup() ) { $user = $this->getUser(); if ( $user->isRegistered() && !$user->isTemp() ) { $formDescriptor['username']['default'] = $user->getName(); } else { $formDescriptor['username']['default'] = $this->getRequest()->getSession()->suggestLoginUsername(); } } // don't show a submit button if there is nothing to submit (i.e. the only form content // is other submit buttons, for redirect flows) if ( !$this->needsSubmitButton( $requests ) ) { unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] ); } if ( !$this->isSignup() ) { // FIXME HACK don't focus on non-empty field // maybe there should be an autofocus-if similar to hide-if? if ( isset( $formDescriptor['username'] ) && empty( $formDescriptor['username']['default'] ) && !$this->getRequest()->getCheck( 'wpName' ) ) { $formDescriptor['username']['autofocus'] = true; } elseif ( isset( $formDescriptor['password'] ) ) { $formDescriptor['password']['autofocus'] = true; } } $this->addTabIndex( $formDescriptor ); } /** * Generates the HTML for a notice box to be displayed to a temporary user. * * @return string HTML representing the notice box */ protected function getNoticeHtml() { $noticeContent = $this->msg( 'createacct-temp-warning', $this->getUser()->getName() )->parse(); return Html::noticeBox( $noticeContent, 'mw-createaccount-temp-warning', '', 'mw-userLogin-icon--user-temporary' ); } } /** @deprecated class alias since 1.41 */ class_alias( LoginSignupSpecialPage::class, 'LoginSignupSpecialPage' ); PK ! D��'$ '$ FormSpecialPage.phpnu �Iw�� <?php /** * Special page which uses an HTMLForm to handle processing. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage */ namespace MediaWiki\SpecialPage; use MediaWiki\Context\DerivativeContext; use MediaWiki\Debug\MWDebug; use MediaWiki\HTMLForm\HTMLForm; use MediaWiki\Request\DerivativeRequest; use MediaWiki\Status\Status; use MediaWiki\User\User; use UserBlockedError; /** * Special page which uses an HTMLForm to handle processing. This is mostly a * clone of FormAction. More special pages should be built this way; maybe this could be * a new structure for SpecialPages. * * @ingroup SpecialPage */ abstract class FormSpecialPage extends SpecialPage { /** * The subpage of the special page. * @var string|null */ protected $par = null; /** * @var array|null POST data preserved across re-authentication * @since 1.32 */ protected $reauthPostData = null; /** * Get an HTMLForm descriptor array * @return array */ abstract protected function getFormFields(); /** * Add pre-HTML to the form * @return string HTML which will be sent to $form->addPreHtml() * @since 1.38 */ protected function preHtml() { return ''; } /** * Add post-HTML to the form * @return string HTML which will be sent to $form->addPostHtml() * @since 1.38 */ protected function postHtml() { return ''; } /** * Add pre-text to the form * @return string HTML which will be sent to $form->addPreHtml() * @deprecated since 1.38, use preHtml() instead, hard-deprecated since 1.43 */ protected function preText() { wfDeprecated( __METHOD__, '1.38' ); return $this->preHtml(); } /** * Add post-text to the form * @return string HTML which will be sent to $form->addPostHtml() * @deprecated since 1.38, use postHtml() instead, hard-deprecated since 1.43 */ protected function postText() { wfDeprecated( __METHOD__, '1.38' ); return $this->postHtml(); } /** * Play with the HTMLForm if you need to more substantially * @param HTMLForm $form */ protected function alterForm( HTMLForm $form ) { } /** * Get message prefix for HTMLForm * * @since 1.21 * @return string */ protected function getMessagePrefix() { return strtolower( $this->getName() ); } /** * Get display format for the form. See HTMLForm documentation for available values. * * @since 1.25 * @return string */ protected function getDisplayFormat() { return 'table'; } /** * Get the HTMLForm to control behavior * @return HTMLForm|null */ protected function getForm() { $context = $this->getContext(); $onSubmit = [ $this, 'onSubmit' ]; if ( $this->reauthPostData ) { // Restore POST data $context = new DerivativeContext( $context ); $oldRequest = $this->getRequest(); $context->setRequest( new DerivativeRequest( $oldRequest, $this->reauthPostData + $oldRequest->getQueryValues(), true ) ); // But don't treat it as a "real" submission just in case of some // crazy kind of CSRF. $onSubmit = static function () { return false; }; } $form = HTMLForm::factory( $this->getDisplayFormat(), $this->getFormFields(), $context, $this->getMessagePrefix() ); if ( !$this->requiresPost() ) { $form->setMethod( 'get' ); } $form->setSubmitCallback( $onSubmit ); if ( $this->getDisplayFormat() !== 'ooui' ) { // No legend and wrapper by default in OOUI forms, but can be set manually // from alterForm() $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' ); } $headerMsg = $this->msg( $this->getMessagePrefix() . '-text' ); if ( !$headerMsg->isDisabled() ) { $form->addHeaderHtml( $headerMsg->parseAsBlock() ); } // preText / postText are deprecated, but we need to keep calling them until the end of // the deprecation process so a subclass overriding *Text and *Html both work $form->addPreHtml( MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'preText', '1.38' ) ? $this->preText() : $this->preHtml() ); $form->addPostHtml( MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'postText', '1.38' ) ? $this->postText() : $this->postHtml() ); // Give precedence to subpage syntax $field = $this->getSubpageField(); // cast to string so that "0" is not thrown away if ( strval( $this->par ) !== '' && $field ) { $this->getRequest()->setVal( $form->getField( $field )->getName(), $this->par ); $form->setTitle( $this->getPageTitle() ); } $this->alterForm( $form ); if ( $form->getMethod() == 'post' ) { // Retain query parameters (uselang etc) on POST requests $params = array_diff_key( $this->getRequest()->getQueryValues(), [ 'title' => null ] ); $form->addHiddenField( 'redirectparams', wfArrayToCgi( $params ) ); } // Give hooks a chance to alter the form, adding extra fields or text etc $this->getHookRunner()->onSpecialPageBeforeFormDisplay( $this->getName(), $form ); return $form; } /** * Process the form on submission. * @phpcs:disable MediaWiki.Commenting.FunctionComment.ExtraParamComment * @param array $data * @param HTMLForm|null $form * @suppress PhanCommentParamWithoutRealParam Many implementations don't have $form * @return bool|string|array|Status As documented for HTMLForm::trySubmit. * @phpcs:enable MediaWiki.Commenting.FunctionComment.ExtraParamComment */ abstract public function onSubmit( array $data /* HTMLForm $form = null */ ); /** * Do something exciting on successful processing of the form, most likely to show a * confirmation message * @since 1.22 Default is to do nothing */ public function onSuccess() { } /** * Basic SpecialPage workflow: get a form, send it to the user; get some data back, * * @param string|null $par Subpage string if one was specified */ public function execute( $par ) { $this->setParameter( $par ); $this->setHeaders(); $this->outputHeader(); // This will throw exceptions if there's a problem $this->checkExecutePermissions( $this->getUser() ); $securityLevel = $this->getLoginSecurityLevel(); if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) { return; } $form = $this->getForm(); // GET forms can be set as includable if ( !$this->including() ) { $result = $this->getShowAlways() ? $form->showAlways() : $form->show(); } else { $result = $form->prepareForm()->tryAuthorizedSubmit(); } if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) { $this->onSuccess(); } } /** * Whether the form should always be shown despite the success of submission. * @since 1.40 * @return bool */ protected function getShowAlways() { return false; } /** * Maybe do something interesting with the subpage parameter * @param string|null $par */ protected function setParameter( $par ) { $this->par = $par; } /** * Override this function to set the field name used in the subpage syntax. * @since 1.40 * @return false|string */ protected function getSubpageField() { return false; } /** * Called from execute() to check if the given user can perform this action. * Failures here must throw subclasses of ErrorPageError. * @param User $user * @throws UserBlockedError */ protected function checkExecutePermissions( User $user ) { $this->checkPermissions(); if ( $this->requiresUnblock() ) { $block = $user->getBlock(); if ( $block && $block->isSitewide() ) { throw new UserBlockedError( $block, $user, $this->getLanguage(), $this->getRequest()->getIP() ); } } if ( $this->requiresWrite() ) { $this->checkReadOnly(); } } /** * Whether this action should using POST method to submit, default to true * @since 1.40 * @return bool */ public function requiresPost() { return true; } /** * Whether this action requires the wiki not to be locked, default to requiresPost() * @return bool */ public function requiresWrite() { return $this->requiresPost(); } /** * Whether this action cannot be executed by a blocked user, default to requiresPost() * @return bool */ public function requiresUnblock() { return $this->requiresPost(); } /** * Preserve POST data across reauthentication * * @since 1.32 * @param array $data */ protected function setReauthPostData( array $data ) { $this->reauthPostData = $data; } } /** @deprecated class alias since 1.41 */ class_alias( FormSpecialPage::class, 'FormSpecialPage' ); PK ! Ż]� WantedQueryPage.phpnu �Iw�� <?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\SpecialPage; use MediaWiki\Title\Title; use Skin; use stdClass; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\IResultWrapper; /** * Base class for a "wanted" query page like WantedPages, WantedTemplates, etc * * @stable to extend * @ingroup SpecialPage */ abstract class WantedQueryPage extends QueryPage { public function isExpensive() { return true; } public function isSyndicated() { return false; } /** * Cache page existence for performance * @stable to override * @param IDatabase $db * @param IResultWrapper $res */ protected function preprocessResults( $db, $res ) { $this->executeLBFromResultWrapper( $res ); } /** * Should formatResult() always check page existence, even if * the results are fresh? This is a (hopefully temporary) * kluge for Special:WantedFiles, which may contain false * positives for files that exist e.g. in a shared repo (bug * 6220). * @stable to override * @return bool */ protected function forceExistenceCheck() { return false; } /** * Format an individual result * * @stable to override * * @param Skin $skin Skin to use for UI elements * @param stdClass $result Result row * @return string */ public function formatResult( $skin, $result ) { $linkRenderer = $this->getLinkRenderer(); $title = Title::makeTitleSafe( $result->namespace, $result->title ); if ( $title instanceof Title ) { if ( $this->isCached() || $this->forceExistenceCheck() ) { $pageLink = $this->existenceCheck( $title ) ? '<del>' . $linkRenderer->makeLink( $title ) . '</del>' : $linkRenderer->makeLink( $title ); } else { $pageLink = $linkRenderer->makeBrokenLink( $title ); } return $this->getLanguage()->specialList( $pageLink, $this->makeWlhLink( $title, $result ) ); } else { return $this->msg( 'wantedpages-badtitle', $result->title )->escaped(); } } /** * Does the Title currently exists * * This method allows a subclass to override this check * (For example, wantedfiles, would want to check if the file exists * not just that a page in the file namespace exists). * * This will only control if the link is crossed out. Whether or not the link * is blue vs red is controlled by if the title exists. * * @note This will only be run if the page is cached (ie $wgMiserMode = true) * unless forceExistenceCheck() is true. * @since 1.24 * @stable to override * * @param Title $title * @return bool */ protected function existenceCheck( Title $title ) { return $title->isKnown(); } /** * Make a "what links here" link for a given title * * @param Title $title Title to make the link for * @param stdClass $result Result row * @return string */ protected function makeWlhLink( $title, $result ) { $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() ); $label = $this->msg( 'nlinks' )->numParams( $result->value )->text(); return $this->getLinkRenderer()->makeLink( $wlh, $label ); } /** * Order by title for pages with the same number of links to them * * @stable to override * @return array * @since 1.29 */ protected function getOrderFields() { return [ 'value DESC', 'namespace', 'title' ]; } /** * Do not order descending for all order fields. We will use DESC only on one field, see * getOrderFields above. This overwrites sortDescending from QueryPage::getOrderFields(). * Do NOT change this to true unless you remove the phrase DESC in getOrderFields above. * If you do a database error will be thrown due to double adding DESC to query! * * @stable to override * @return bool * @since 1.29 */ protected function sortDescending() { return false; } /** * Also use the order fields returned by getOrderFields when fetching from the cache. * @stable to override * @return array * @since 1.29 */ protected function getCacheOrderFields() { return $this->getOrderFields(); } } /** @deprecated class alias since 1.41 */ class_alias( WantedQueryPage::class, 'WantedQueryPage' ); PK ! �+BA UnlistedSpecialPage.phpnu �Iw�� <?php /** * Shortcut to construct a special page which is unlisted by default. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup SpecialPage */ namespace MediaWiki\SpecialPage; /** * Shortcut to construct a special page which is unlisted by default. * * @stable to extend * * @ingroup SpecialPage */ class UnlistedSpecialPage extends SpecialPage { /** * @stable to call * * @param string $name * @param string $restriction * @param bool $function * @param string $file */ public function __construct( $name, $restriction = '', $function = false, $file = 'default' ) { parent::__construct( $name, $restriction, false, $function, $file ); } public function isListed() { return false; } } /** @deprecated class alias since 1.41 */ class_alias( UnlistedSpecialPage::class, 'UnlistedSpecialPage' ); PK ! �}q�* * IncludableSpecialPage.phpnu �Iw�� PK ! ,�I�� � s SpecialPageFactory.phpnu �Iw�� PK ! �,=]�u �u �� QueryPage.phpnu �Iw�� PK ! �)- �7 PageQueryPage.phpnu �Iw�� PK ! "� r$� $� E ContributionsSpecialPage.phpnu �Iw�� PK ! �vo� � �� RedirectSpecialArticle.phpnu �Iw�� PK ! ���� � �� DisabledSpecialPage.phpnu �Iw�� PK ! k4� � }� ImageQueryPage.phpnu �Iw�� PK ! Be� � � h� ChangesListSpecialPage.phpnu �Iw�� PK ! �a��� � �� SpecialRedirectToSpecial.phpnu �Iw�� PK ! �8(� � | SpecialRedirectWithAction.phpnu �Iw�� PK ! ��{�^� ^� M AuthManagerSpecialPage.phpnu �Iw�� PK ! ���� � ! �� Hook/AuthChangeFormFieldsHook.phpnu �Iw�� PK ! ���c9 9 * � Hook/ChangeAuthenticationDataAuditHook.phpnu �Iw�� PK ! ���N N ! �� Hook/SpecialPage_initListHook.phpnu �Iw�� PK ! n�� � � % � Hook/SpecialPageBeforeExecuteHook.phpnu �Iw�� PK ! ) �5 5 4 +� Hook/ChangesListSpecialPageStructuredFiltersHook.phpnu �Iw�� PK ! ��G�& & ( IJ Hook/ChangesListSpecialPageQueryHook.phpnu �Iw�� PK ! �FSz 1 B� Hook/RedirectSpecialArticleRedirectParamsHook.phpnu �Iw�� PK ! 7 � � � $ �� Hook/SpecialPageAfterExecuteHook.phpnu �Iw�� PK ! �H�: �� Hook/WgQueryPagesHook.phpnu �Iw�� PK ! ,�P� � ) �� Hook/SpecialPageBeforeFormDisplayHook.phpnu �Iw�� PK ! �V�� � �� RedirectSpecialPage.phpnu �Iw�� PK ! ���\� \� � SpecialPage.phpnu �Iw�� PK ! �v�$� $� �b LoginSignupSpecialPage.phpnu �Iw�� PK ! D��'$ '$ % FormSpecialPage.phpnu �Iw�� PK ! Ż]� �C WantedQueryPage.phpnu �Iw�� PK ! �+BA �V UnlistedSpecialPage.phpnu �Iw�� PK 5]
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0.02 |
proxy
|
phpinfo
|
ÐаÑтройка