From f51b1cd9ec1d70fa070a90fa6412ff774251959c Mon Sep 17 00:00:00 2001 From: addshore Date: Thu, 24 Mar 2016 11:25:40 +0000 Subject: [PATCH] Split Pager classes out of SpecialPage files Change-Id: I6c8dbc9084040dd3b8a5e19caaf076bd14b77762 --- autoload.php | 24 +- includes/specials/SpecialActiveusers.php | 230 ------- includes/specials/SpecialAllMessages.php | 400 ------------ includes/specials/SpecialBlockList.php | 241 ------- includes/specials/SpecialCategories.php | 106 --- includes/specials/SpecialContributions.php | 505 --------------- .../specials/SpecialDeletedContributions.php | 331 ---------- includes/specials/SpecialListfiles.php | 581 ----------------- includes/specials/SpecialListusers.php | 370 ----------- includes/specials/SpecialMergeHistory.php | 75 --- includes/specials/SpecialNewimages.php | 186 ------ includes/specials/SpecialNewpages.php | 127 ---- includes/specials/SpecialProtectedtitles.php | 71 --- includes/specials/pagers/ActiveUsersPager.php | 254 ++++++++ .../specials/pagers/AllMessagesTablePager.php | 424 ++++++++++++ includes/specials/pagers/BlockListPager.php | 266 ++++++++ includes/specials/pagers/CategoryPager.php | 126 ++++ includes/specials/pagers/ContribsPager.php | 526 +++++++++++++++ .../specials/pagers/DeletedContribsPager.php | 355 +++++++++++ includes/specials/pagers/ImageListPager.php | 602 ++++++++++++++++++ .../specials/pagers/MergeHistoryPager.php | 99 +++ includes/specials/pagers/NewFilesPager.php | 207 ++++++ includes/specials/pagers/NewPagesPager.php | 148 +++++ .../specials/pagers/ProtectedTitlesPager.php | 91 +++ includes/specials/pagers/UsersPager.php | 395 ++++++++++++ 25 files changed, 3505 insertions(+), 3235 deletions(-) create mode 100644 includes/specials/pagers/ActiveUsersPager.php create mode 100644 includes/specials/pagers/AllMessagesTablePager.php create mode 100644 includes/specials/pagers/BlockListPager.php create mode 100644 includes/specials/pagers/CategoryPager.php create mode 100644 includes/specials/pagers/ContribsPager.php create mode 100644 includes/specials/pagers/DeletedContribsPager.php create mode 100644 includes/specials/pagers/ImageListPager.php create mode 100644 includes/specials/pagers/MergeHistoryPager.php create mode 100644 includes/specials/pagers/NewFilesPager.php create mode 100644 includes/specials/pagers/NewPagesPager.php create mode 100644 includes/specials/pagers/ProtectedTitlesPager.php create mode 100644 includes/specials/pagers/UsersPager.php diff --git a/autoload.php b/autoload.php index e74df0aa1f..8e1276f5f5 100644 --- a/autoload.php +++ b/autoload.php @@ -7,11 +7,11 @@ $wgAutoloadLocalClasses = [ 'APCBagOStuff' => __DIR__ . '/includes/libs/objectcache/APCBagOStuff.php', 'AbstractContent' => __DIR__ . '/includes/content/AbstractContent.php', 'Action' => __DIR__ . '/includes/actions/Action.php', - 'ActiveUsersPager' => __DIR__ . '/includes/specials/SpecialActiveusers.php', + 'ActiveUsersPager' => __DIR__ . '/includes/specials/pagers/ActiveUsersPager.php', 'ActivityUpdateJob' => __DIR__ . '/includes/jobqueue/jobs/ActivityUpdateJob.php', 'AjaxDispatcher' => __DIR__ . '/includes/AjaxDispatcher.php', 'AjaxResponse' => __DIR__ . '/includes/AjaxResponse.php', - 'AllMessagesTablePager' => __DIR__ . '/includes/specials/SpecialAllMessages.php', + 'AllMessagesTablePager' => __DIR__ . '/includes/specials/pagers/AllMessagesTablePager.php', 'AllTrans' => __DIR__ . '/maintenance/language/alltrans.php', 'AlphabeticPager' => __DIR__ . '/includes/pager/AlphabeticPager.php', 'AlterSharedConstraints' => __DIR__ . '/maintenance/oracle/alterSharedConstraints.php', @@ -180,7 +180,7 @@ $wgAutoloadLocalClasses = [ 'BitmapMetadataHandler' => __DIR__ . '/includes/media/BitmapMetadataHandler.php', 'Blob' => __DIR__ . '/includes/db/DatabaseUtility.php', 'Block' => __DIR__ . '/includes/Block.php', - 'BlockListPager' => __DIR__ . '/includes/specials/SpecialBlockList.php', + 'BlockListPager' => __DIR__ . '/includes/specials/pagers/BlockListPager.php', 'BlockLogFormatter' => __DIR__ . '/includes/logging/BlockLogFormatter.php', 'BmpHandler' => __DIR__ . '/includes/media/BMP.php', 'BotPassword' => __DIR__ . '/includes/user/BotPassword.php', @@ -200,7 +200,7 @@ $wgAutoloadLocalClasses = [ 'CategoryMembershipChange' => __DIR__ . '/includes/changes/CategoryMembershipChange.php', 'CategoryMembershipChangeJob' => __DIR__ . '/includes/jobqueue/jobs/CategoryMembershipChangeJob.php', 'CategoryPage' => __DIR__ . '/includes/page/CategoryPage.php', - 'CategoryPager' => __DIR__ . '/includes/specials/SpecialCategories.php', + 'CategoryPager' => __DIR__ . '/includes/specials/pagers/CategoryPager.php', 'CategoryViewer' => __DIR__ . '/includes/CategoryViewer.php', 'CdbException' => __DIR__ . '/includes/compat/CdbCompat.php', 'CdbReader' => __DIR__ . '/includes/compat/CdbCompat.php', @@ -263,7 +263,7 @@ $wgAutoloadLocalClasses = [ 'ContentHandler' => __DIR__ . '/includes/content/ContentHandler.php', 'ContentModelLogFormatter' => __DIR__ . '/includes/logging/ContentModelLogFormatter.php', 'ContextSource' => __DIR__ . '/includes/context/ContextSource.php', - 'ContribsPager' => __DIR__ . '/includes/specials/SpecialContributions.php', + 'ContribsPager' => __DIR__ . '/includes/specials/pagers/ContribsPager.php', 'ConvertExtensionToRegistration' => __DIR__ . '/maintenance/convertExtensionToRegistration.php', 'ConvertLinks' => __DIR__ . '/maintenance/convertLinks.php', 'ConvertUserOptions' => __DIR__ . '/maintenance/convertUserOptions.php', @@ -332,7 +332,7 @@ $wgAutoloadLocalClasses = [ 'DeleteOrphanedRevisions' => __DIR__ . '/maintenance/deleteOrphanedRevisions.php', 'DeleteRevision' => __DIR__ . '/maintenance/deleteRevision.php', 'DeleteSelfExternals' => __DIR__ . '/maintenance/deleteSelfExternals.php', - 'DeletedContribsPager' => __DIR__ . '/includes/specials/SpecialDeletedContributions.php', + 'DeletedContribsPager' => __DIR__ . '/includes/specials/pagers/DeletedContribsPager.php', 'DeletedContributionsPage' => __DIR__ . '/includes/specials/SpecialDeletedContributions.php', 'DependencyWrapper' => __DIR__ . '/includes/cache/CacheDependency.php', 'DeprecatedGlobal' => __DIR__ . '/includes/DeprecatedGlobal.php', @@ -573,7 +573,7 @@ $wgAutoloadLocalClasses = [ 'ImageHandler' => __DIR__ . '/includes/media/ImageHandler.php', 'ImageHistoryList' => __DIR__ . '/includes/page/ImageHistoryList.php', 'ImageHistoryPseudoPager' => __DIR__ . '/includes/page/ImageHistoryPseudoPager.php', - 'ImageListPager' => __DIR__ . '/includes/specials/SpecialListfiles.php', + 'ImageListPager' => __DIR__ . '/includes/specials/pagers/ImageListPager.php', 'ImagePage' => __DIR__ . '/includes/page/ImagePage.php', 'ImageQueryPage' => __DIR__ . '/includes/specialpage/ImageQueryPage.php', 'ImportLogFormatter' => __DIR__ . '/includes/logging/ImportLogFormatter.php', @@ -833,7 +833,7 @@ $wgAutoloadLocalClasses = [ 'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php', 'MemoryFileBackend' => __DIR__ . '/includes/filebackend/MemoryFileBackend.php', 'MergeHistory' => __DIR__ . '/includes/MergeHistory.php', - 'MergeHistoryPager' => __DIR__ . '/includes/specials/SpecialMergeHistory.php', + 'MergeHistoryPager' => __DIR__ . '/includes/specials/pagers/MergeHistoryPager.php', 'MergeLogFormatter' => __DIR__ . '/includes/logging/MergeLogFormatter.php', 'MergeMessageFileList' => __DIR__ . '/maintenance/mergeMessageFileList.php', 'MergeableUpdate' => __DIR__ . '/includes/deferred/MergeableUpdate.php', @@ -880,8 +880,8 @@ $wgAutoloadLocalClasses = [ 'NamespaceAwareForeignTitleFactory' => __DIR__ . '/includes/title/NamespaceAwareForeignTitleFactory.php', 'NamespaceConflictChecker' => __DIR__ . '/maintenance/namespaceDupes.php', 'NamespaceImportTitleFactory' => __DIR__ . '/includes/title/NamespaceImportTitleFactory.php', - 'NewFilesPager' => __DIR__ . '/includes/specials/SpecialNewimages.php', - 'NewPagesPager' => __DIR__ . '/includes/specials/SpecialNewpages.php', + 'NewFilesPager' => __DIR__ . '/includes/specials/pagers/NewFilesPager.php', + 'NewPagesPager' => __DIR__ . '/includes/specials/pagers/NewPagesPager.php', 'NewUsersLogFormatter' => __DIR__ . '/includes/logging/NewUsersLogFormatter.php', 'NolinesImageGallery' => __DIR__ . '/includes/gallery/NolinesImageGallery.php', 'NotRecursiveIterator' => __DIR__ . '/includes/utils/iterators/NotRecursiveIterator.php', @@ -1006,7 +1006,7 @@ $wgAutoloadLocalClasses = [ 'ProtectAction' => __DIR__ . '/includes/actions/ProtectAction.php', 'ProtectLogFormatter' => __DIR__ . '/includes/logging/ProtectLogFormatter.php', 'ProtectedPagesPager' => __DIR__ . '/includes/specials/SpecialProtectedpages.php', - 'ProtectedTitlesPager' => __DIR__ . '/includes/specials/SpecialProtectedtitles.php', + 'ProtectedTitlesPager' => __DIR__ . '/includes/specials/pagers/ProtectedTitlesPager.php', 'ProtectionForm' => __DIR__ . '/includes/ProtectionForm.php', 'PruneFileCache' => __DIR__ . '/maintenance/pruneFileCache.php', 'PublishStashedFileJob' => __DIR__ . '/includes/jobqueue/jobs/PublishStashedFileJob.php', @@ -1387,7 +1387,7 @@ $wgAutoloadLocalClasses = [ 'UsercreateTemplate' => __DIR__ . '/includes/templates/Usercreate.php', 'UserloginTemplate' => __DIR__ . '/includes/templates/Userlogin.php', 'UserrightsPage' => __DIR__ . '/includes/specials/SpecialUserrights.php', - 'UsersPager' => __DIR__ . '/includes/specials/SpecialListusers.php', + 'UsersPager' => __DIR__ . '/includes/specials/pagers/UsersPager.php', 'UtfNormal' => __DIR__ . '/includes/compat/normal/UtfNormal.php', 'UzConverter' => __DIR__ . '/languages/classes/LanguageUz.php', 'VFormHTMLForm' => __DIR__ . '/includes/htmlform/VFormHTMLForm.php', diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php index 9198c1ebaa..d6d4500972 100644 --- a/includes/specials/SpecialActiveusers.php +++ b/includes/specials/SpecialActiveusers.php @@ -23,236 +23,6 @@ * @ingroup SpecialPage */ -/** - * This class is used to get a list of active users. The ones with specials - * rights (sysop, bureaucrat, developer) will have them displayed - * next to their names. - * - * @ingroup SpecialPage - */ -class ActiveUsersPager extends UsersPager { - /** - * @var FormOptions - */ - protected $opts; - - /** - * @var array - */ - protected $hideGroups = []; - - /** - * @var array - */ - protected $hideRights = []; - - /** - * @var array - */ - private $blockStatusByUid; - - /** - * @param IContextSource $context - * @param null $group Unused - * @param string $par Parameter passed to the page - */ - function __construct( IContextSource $context = null, $group = null, $par = null ) { - parent::__construct( $context ); - - $this->RCMaxAge = $this->getConfig()->get( 'ActiveUserDays' ); - $un = $this->getRequest()->getText( 'username', $par ); - $this->requestedUser = ''; - if ( $un != '' ) { - $username = Title::makeTitleSafe( NS_USER, $un ); - if ( !is_null( $username ) ) { - $this->requestedUser = $username->getText(); - } - } - - $this->setupOptions(); - } - - public function setupOptions() { - $this->opts = new FormOptions(); - - $this->opts->add( 'hidebots', false, FormOptions::BOOL ); - $this->opts->add( 'hidesysops', false, FormOptions::BOOL ); - - $this->opts->fetchValuesFromRequest( $this->getRequest() ); - - if ( $this->opts->getValue( 'hidebots' ) == 1 ) { - $this->hideRights[] = 'bot'; - } - if ( $this->opts->getValue( 'hidesysops' ) == 1 ) { - $this->hideGroups[] = 'sysop'; - } - } - - function getIndexField() { - return 'qcc_title'; - } - - function getQueryInfo() { - $dbr = $this->getDatabase(); - - $activeUserSeconds = $this->getConfig()->get( 'ActiveUserDays' ) * 86400; - $timestamp = $dbr->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds ); - $conds = [ - 'qcc_type' => 'activeusers', - 'qcc_namespace' => NS_USER, - 'user_name = qcc_title', - 'rc_user_text = qcc_title', - 'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata. - 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ), // Don't count categorization changes. - 'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ), - 'rc_timestamp >= ' . $dbr->addQuotes( $timestamp ), - ]; - if ( $this->requestedUser != '' ) { - $conds[] = 'qcc_title >= ' . $dbr->addQuotes( $this->requestedUser ); - } - if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { - $conds[] = 'NOT EXISTS (' . $dbr->selectSQLText( - 'ipblocks', '1', [ 'ipb_user=user_id', 'ipb_deleted' => 1 ] - ) . ')'; - } - - if ( $dbr->implicitGroupby() ) { - $options = [ 'GROUP BY' => [ 'qcc_title' ] ]; - } else { - $options = [ 'GROUP BY' => [ 'user_name', 'user_id', 'qcc_title' ] ]; - } - - return [ - 'tables' => [ 'querycachetwo', 'user', 'recentchanges' ], - 'fields' => [ 'user_name', 'user_id', 'recentedits' => 'COUNT(*)', 'qcc_title' ], - 'options' => $options, - 'conds' => $conds - ]; - } - - function doBatchLookups() { - parent::doBatchLookups(); - - $uids = []; - foreach ( $this->mResult as $row ) { - $uids[] = $row->user_id; - } - // Fetch the block status of the user for showing "(blocked)" text and for - // striking out names of suppressed users when privileged user views the list. - // Although the first query already hits the block table for un-privileged, this - // is done in two queries to avoid huge quicksorts and to make COUNT(*) correct. - $dbr = $this->getDatabase(); - $res = $dbr->select( 'ipblocks', - [ 'ipb_user', 'MAX(ipb_deleted) AS block_status' ], - [ 'ipb_user' => $uids ], - __METHOD__, - [ 'GROUP BY' => [ 'ipb_user' ] ] - ); - $this->blockStatusByUid = []; - foreach ( $res as $row ) { - $this->blockStatusByUid[$row->ipb_user] = $row->block_status; // 0 or 1 - } - $this->mResult->seek( 0 ); - } - - function formatRow( $row ) { - $userName = $row->user_name; - - $ulinks = Linker::userLink( $row->user_id, $userName ); - $ulinks .= Linker::userToolLinks( $row->user_id, $userName ); - - $lang = $this->getLanguage(); - - $list = []; - $user = User::newFromId( $row->user_id ); - - // User right filter - foreach ( $this->hideRights as $right ) { - // Calling User::getRights() within the loop so that - // if the hideRights() filter is empty, we don't have to - // trigger the lazy-init of the big userrights array in the - // User object - if ( in_array( $right, $user->getRights() ) ) { - return ''; - } - } - - // User group filter - // Note: This is a different loop than for user rights, - // because we're reusing it to build the group links - // at the same time - $groups_list = self::getGroups( intval( $row->user_id ), $this->userGroupCache ); - foreach ( $groups_list as $group ) { - if ( in_array( $group, $this->hideGroups ) ) { - return ''; - } - $list[] = self::buildGroupLink( $group, $userName ); - } - - $groups = $lang->commaList( $list ); - - $item = $lang->specialList( $ulinks, $groups ); - - $isBlocked = isset( $this->blockStatusByUid[$row->user_id] ); - if ( $isBlocked && $this->blockStatusByUid[$row->user_id] == 1 ) { - $item = "$item"; - } - $count = $this->msg( 'activeusers-count' )->numParams( $row->recentedits ) - ->params( $userName )->numParams( $this->RCMaxAge )->escaped(); - $blocked = $isBlocked ? ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : ''; - - return Html::rawElement( 'li', [], "{$item} [{$count}]{$blocked}" ); - } - - function getPageHeader() { - $self = $this->getTitle(); - $limit = $this->mLimit ? Html::hidden( 'limit', $this->mLimit ) : ''; - - # Form tag - $out = Xml::openElement( 'form', [ 'method' => 'get', 'action' => wfScript() ] ); - $out .= Xml::fieldset( $this->msg( 'activeusers' )->text() ) . "\n"; - $out .= Html::hidden( 'title', $self->getPrefixedDBkey() ) . $limit . "\n"; - - # Username field (with autocompletion support) - $this->getOutput()->addModules( 'mediawiki.userSuggest' ); - $out .= Xml::inputLabel( - $this->msg( 'activeusers-from' )->text(), - 'username', - 'offset', - 20, - $this->requestedUser, - [ - 'class' => 'mw-ui-input-inline mw-autocomplete-user', - 'tabindex' => 1, - ] + ( - // Set autofocus on blank input - $this->requestedUser === '' ? [ 'autofocus' => '' ] : [] - ) - ) . '
'; - - $out .= Xml::checkLabel( $this->msg( 'activeusers-hidebots' )->text(), - 'hidebots', 'hidebots', $this->opts->getValue( 'hidebots' ), [ 'tabindex' => 2 ] ); - - $out .= Xml::checkLabel( - $this->msg( 'activeusers-hidesysops' )->text(), - 'hidesysops', - 'hidesysops', - $this->opts->getValue( 'hidesysops' ), - [ 'tabindex' => 3 ] - ) . '
'; - - # Submit button and form bottom - $out .= Xml::submitButton( - $this->msg( 'activeusers-submit' )->text(), - [ 'tabindex' => 4 ] - ) . "\n"; - $out .= Xml::closeElement( 'fieldset' ); - $out .= Xml::closeElement( 'form' ); - - return $out; - } -} - /** * @ingroup SpecialPage */ diff --git a/includes/specials/SpecialAllMessages.php b/includes/specials/SpecialAllMessages.php index 49d5d6e596..49ca9f45de 100644 --- a/includes/specials/SpecialAllMessages.php +++ b/includes/specials/SpecialAllMessages.php @@ -77,403 +77,3 @@ class SpecialAllMessages extends SpecialPage { return 'wiki'; } } - -/** - * Use TablePager for prettified output. We have to pretend that we're - * getting data from a table when in fact not all of it comes from the database. - */ -class AllMessagesTablePager extends TablePager { - protected $filter, $prefix, $langcode, $displayPrefix; - - public $mLimitsShown; - - /** - * @var Language - */ - public $lang; - - /** - * @var null|bool - */ - public $custom; - - function __construct( $page, $conds, $langObj = null ) { - parent::__construct( $page->getContext() ); - $this->mIndexField = 'am_title'; - $this->mPage = $page; - $this->mConds = $conds; - // FIXME: Why does this need to be set to DIR_DESCENDING to produce ascending ordering? - $this->mDefaultDirection = IndexPager::DIR_DESCENDING; - $this->mLimitsShown = [ 20, 50, 100, 250, 500, 5000 ]; - - global $wgContLang; - - $this->talk = $this->msg( 'talkpagelinktext' )->escaped(); - - $this->lang = ( $langObj ? $langObj : $wgContLang ); - $this->langcode = $this->lang->getCode(); - $this->foreign = $this->langcode !== $wgContLang->getCode(); - - $request = $this->getRequest(); - - $this->filter = $request->getVal( 'filter', 'all' ); - if ( $this->filter === 'all' ) { - $this->custom = null; // So won't match in either case - } else { - $this->custom = ( $this->filter === 'unmodified' ); - } - - $prefix = $this->getLanguage()->ucfirst( $request->getVal( 'prefix', '' ) ); - $prefix = $prefix !== '' ? - Title::makeTitleSafe( NS_MEDIAWIKI, $request->getVal( 'prefix', null ) ) : - null; - - if ( $prefix !== null ) { - $this->displayPrefix = $prefix->getDBkey(); - $this->prefix = '/^' . preg_quote( $this->displayPrefix, '/' ) . '/i'; - } else { - $this->displayPrefix = false; - $this->prefix = false; - } - - // The suffix that may be needed for message names if we're in a - // different language (eg [[MediaWiki:Foo/fr]]: $suffix = '/fr' - if ( $this->foreign ) { - $this->suffix = '/' . $this->langcode; - } else { - $this->suffix = ''; - } - } - - function buildForm() { - $attrs = [ 'id' => 'mw-allmessages-form-lang', 'name' => 'lang' ]; - $msg = wfMessage( 'allmessages-language' ); - $langSelect = Xml::languageSelector( $this->langcode, false, null, $attrs, $msg ); - - $out = Xml::openElement( 'form', [ - 'method' => 'get', - 'action' => $this->getConfig()->get( 'Script' ), - 'id' => 'mw-allmessages-form' - ] ) . - Xml::fieldset( $this->msg( 'allmessages-filter-legend' )->text() ) . - Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . - Xml::openElement( 'table', [ 'class' => 'mw-allmessages-table' ] ) . "\n" . - ' - ' . - Xml::label( $this->msg( 'allmessages-prefix' )->text(), 'mw-allmessages-form-prefix' ) . - "\n - " . - Xml::input( - 'prefix', - 20, - str_replace( '_', ' ', $this->displayPrefix ), - [ 'id' => 'mw-allmessages-form-prefix' ] - ) . - "\n - - \n - " . - $this->msg( 'allmessages-filter' )->escaped() . - "\n - " . - Xml::radioLabel( $this->msg( 'allmessages-filter-unmodified' )->text(), - 'filter', - 'unmodified', - 'mw-allmessages-form-filter-unmodified', - ( $this->filter === 'unmodified' ) - ) . - Xml::radioLabel( $this->msg( 'allmessages-filter-all' )->text(), - 'filter', - 'all', - 'mw-allmessages-form-filter-all', - ( $this->filter === 'all' ) - ) . - Xml::radioLabel( $this->msg( 'allmessages-filter-modified' )->text(), - 'filter', - 'modified', - 'mw-allmessages-form-filter-modified', - ( $this->filter === 'modified' ) - ) . - "\n - - \n - " . $langSelect[0] . "\n - " . $langSelect[1] . "\n - " . - - ' - ' . - Xml::label( $this->msg( 'table_pager_limit_label' )->text(), 'mw-table_pager_limit_label' ) . - ' - ' . - $this->getLimitSelect( [ 'id' => 'mw-table_pager_limit_label' ] ) . - ' - - - ' . - Xml::submitButton( $this->msg( 'allmessages-filter-submit' )->text() ) . - "\n - " . - - Xml::closeElement( 'table' ) . - $this->getHiddenFields( [ 'title', 'prefix', 'filter', 'lang', 'limit' ] ) . - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ); - - return $out; - } - - function getAllMessages( $descending ) { - $messageNames = Language::getLocalisationCache()->getSubitemList( 'en', 'messages' ); - - // Normalise message names so they look like page titles and sort correctly - T86139 - $messageNames = array_map( [ $this->lang, 'ucfirst' ], $messageNames ); - - if ( $descending ) { - rsort( $messageNames ); - } else { - asort( $messageNames ); - } - - return $messageNames; - } - - /** - * Determine which of the MediaWiki and MediaWiki_talk namespace pages exist. - * Returns array( 'pages' => ..., 'talks' => ... ), where the subarrays have - * an entry for each existing page, with the key being the message name and - * value arbitrary. - * - * @param array $messageNames - * @param string $langcode What language code - * @param bool $foreign Whether the $langcode is not the content language - * @return array A 'pages' and 'talks' array with the keys of existing pages - */ - public static function getCustomisedStatuses( $messageNames, $langcode = 'en', $foreign = false ) { - // FIXME: This function should be moved to Language:: or something. - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'page', - [ 'page_namespace', 'page_title' ], - [ 'page_namespace' => [ NS_MEDIAWIKI, NS_MEDIAWIKI_TALK ] ], - __METHOD__, - [ 'USE INDEX' => 'name_title' ] - ); - $xNames = array_flip( $messageNames ); - - $pageFlags = $talkFlags = []; - - foreach ( $res as $s ) { - $exists = false; - - if ( $foreign ) { - $titleParts = explode( '/', $s->page_title ); - if ( count( $titleParts ) === 2 && - $langcode === $titleParts[1] && - isset( $xNames[$titleParts[0]] ) - ) { - $exists = $titleParts[0]; - } - } elseif ( isset( $xNames[$s->page_title] ) ) { - $exists = $s->page_title; - } - - $title = Title::newFromRow( $s ); - if ( $exists && $title->inNamespace( NS_MEDIAWIKI ) ) { - $pageFlags[$exists] = true; - } elseif ( $exists && $title->inNamespace( NS_MEDIAWIKI_TALK ) ) { - $talkFlags[$exists] = true; - } - } - - return [ 'pages' => $pageFlags, 'talks' => $talkFlags ]; - } - - /** - * This function normally does a database query to get the results; we need - * to make a pretend result using a FakeResultWrapper. - * @param string $offset - * @param int $limit - * @param bool $descending - * @return FakeResultWrapper - */ - function reallyDoQuery( $offset, $limit, $descending ) { - $result = new FakeResultWrapper( [] ); - - $messageNames = $this->getAllMessages( $descending ); - $statuses = self::getCustomisedStatuses( $messageNames, $this->langcode, $this->foreign ); - - $count = 0; - foreach ( $messageNames as $key ) { - $customised = isset( $statuses['pages'][$key] ); - if ( $customised !== $this->custom && - ( $descending && ( $key < $offset || !$offset ) || !$descending && $key > $offset ) && - ( ( $this->prefix && preg_match( $this->prefix, $key ) ) || $this->prefix === false ) - ) { - $actual = wfMessage( $key )->inLanguage( $this->langcode )->plain(); - $default = wfMessage( $key )->inLanguage( $this->langcode )->useDatabase( false )->plain(); - $result->result[] = [ - 'am_title' => $key, - 'am_actual' => $actual, - 'am_default' => $default, - 'am_customised' => $customised, - 'am_talk_exists' => isset( $statuses['talks'][$key] ) - ]; - $count++; - } - - if ( $count === $limit ) { - break; - } - } - - return $result; - } - - function getStartBody() { - $tableClass = $this->getTableClass(); - return Xml::openElement( 'table', [ - 'class' => "mw-datatable $tableClass", - 'id' => 'mw-allmessagestable' - ] ) . - "\n" . - " - " . - $this->msg( 'allmessagesname' )->escaped() . " - - " . - $this->msg( 'allmessagesdefault' )->escaped() . - " - \n - - " . - $this->msg( 'allmessagescurrent' )->escaped() . - " - \n"; - } - - function formatValue( $field, $value ) { - switch ( $field ) { - case 'am_title' : - $title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix ); - $talk = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix ); - $translation = Linker::makeExternalLink( - 'https://translatewiki.net/w/i.php?' . wfArrayToCgi( [ - 'title' => 'Special:SearchTranslations', - 'group' => 'mediawiki', - 'grouppath' => 'mediawiki', - 'query' => 'language:' . $this->getLanguage()->getCode() . '^25 ' . - 'messageid:"MediaWiki:' . $value . '"^10 "' . - $this->msg( $value )->inLanguage( 'en' )->plain() . '"' - ] ), - $this->msg( 'allmessages-filter-translate' )->text() - ); - - if ( $this->mCurrentRow->am_customised ) { - $title = Linker::linkKnown( $title, $this->getLanguage()->lcfirst( $value ) ); - } else { - $title = Linker::link( - $title, - $this->getLanguage()->lcfirst( $value ), - [], - [], - [ 'broken' ] - ); - } - if ( $this->mCurrentRow->am_talk_exists ) { - $talk = Linker::linkKnown( $talk, $this->talk ); - } else { - $talk = Linker::link( - $talk, - $this->talk, - [], - [], - [ 'broken' ] - ); - } - - return $title . ' ' . - $this->msg( 'parentheses' )->rawParams( $talk )->escaped() . - ' ' . - $this->msg( 'parentheses' )->rawParams( $translation )->escaped(); - - case 'am_default' : - case 'am_actual' : - return Sanitizer::escapeHtmlAllowEntities( $value ); - } - - return ''; - } - - function formatRow( $row ) { - // Do all the normal stuff - $s = parent::formatRow( $row ); - - // But if there's a customised message, add that too. - if ( $row->am_customised ) { - $s .= Xml::openElement( 'tr', $this->getRowAttrs( $row, true ) ); - $formatted = strval( $this->formatValue( 'am_actual', $row->am_actual ) ); - - if ( $formatted === '' ) { - $formatted = ' '; - } - - $s .= Xml::tags( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted ) - . "\n"; - } - - return $s; - } - - function getRowAttrs( $row, $isSecond = false ) { - $arr = []; - - if ( $row->am_customised ) { - $arr['class'] = 'allmessages-customised'; - } - - if ( !$isSecond ) { - $arr['id'] = Sanitizer::escapeId( 'msg_' . $this->getLanguage()->lcfirst( $row->am_title ) ); - } - - return $arr; - } - - function getCellAttrs( $field, $value ) { - if ( $this->mCurrentRow->am_customised && $field === 'am_title' ) { - return [ 'rowspan' => '2', 'class' => $field ]; - } elseif ( $field === 'am_title' ) { - return [ 'class' => $field ]; - } else { - return [ - 'lang' => $this->lang->getHtmlCode(), - 'dir' => $this->lang->getDir(), - 'class' => $field - ]; - } - } - - // This is not actually used, as getStartBody is overridden above - function getFieldNames() { - return [ - 'am_title' => $this->msg( 'allmessagesname' )->text(), - 'am_default' => $this->msg( 'allmessagesdefault' )->text() - ]; - } - - function getTitle() { - return SpecialPage::getTitleFor( 'Allmessages', false ); - } - - function isFieldSortable( $x ) { - return false; - } - - function getDefaultSort() { - return ''; - } - - function getQueryInfo() { - return ''; - } -} diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php index e589ecb00a..dbbee71453 100644 --- a/includes/specials/SpecialBlockList.php +++ b/includes/specials/SpecialBlockList.php @@ -222,244 +222,3 @@ class SpecialBlockList extends SpecialPage { return 'users'; } } - -class BlockListPager extends TablePager { - protected $conds; - protected $page; - - /** - * @param SpecialPage $page - * @param array $conds - */ - function __construct( $page, $conds ) { - $this->page = $page; - $this->conds = $conds; - $this->mDefaultDirection = IndexPager::DIR_DESCENDING; - parent::__construct( $page->getContext() ); - } - - function getFieldNames() { - static $headers = null; - - if ( $headers === null ) { - $headers = [ - 'ipb_timestamp' => 'blocklist-timestamp', - 'ipb_target' => 'blocklist-target', - 'ipb_expiry' => 'blocklist-expiry', - 'ipb_by' => 'blocklist-by', - 'ipb_params' => 'blocklist-params', - 'ipb_reason' => 'blocklist-reason', - ]; - foreach ( $headers as $key => $val ) { - $headers[$key] = $this->msg( $val )->text(); - } - } - - return $headers; - } - - function formatValue( $name, $value ) { - static $msg = null; - if ( $msg === null ) { - $keys = [ - 'anononlyblock', - 'createaccountblock', - 'noautoblockblock', - 'emailblock', - 'blocklist-nousertalk', - 'unblocklink', - 'change-blocklink', - ]; - - foreach ( $keys as $key ) { - $msg[$key] = $this->msg( $key )->escaped(); - } - } - - /** @var $row object */ - $row = $this->mCurrentRow; - - $language = $this->getLanguage(); - - $formatted = ''; - - switch ( $name ) { - case 'ipb_timestamp': - $formatted = htmlspecialchars( $language->userTimeAndDate( $value, $this->getUser() ) ); - break; - - case 'ipb_target': - if ( $row->ipb_auto ) { - $formatted = $this->msg( 'autoblockid', $row->ipb_id )->parse(); - } else { - list( $target, $type ) = Block::parseTarget( $row->ipb_address ); - switch ( $type ) { - case Block::TYPE_USER: - case Block::TYPE_IP: - $formatted = Linker::userLink( $target->getId(), $target ); - $formatted .= Linker::userToolLinks( - $target->getId(), - $target, - false, - Linker::TOOL_LINKS_NOBLOCK - ); - break; - case Block::TYPE_RANGE: - $formatted = htmlspecialchars( $target ); - } - } - break; - - case 'ipb_expiry': - $formatted = htmlspecialchars( $language->formatExpiry( - $value, - /* User preference timezone */true - ) ); - if ( $this->getUser()->isAllowed( 'block' ) ) { - if ( $row->ipb_auto ) { - $links[] = Linker::linkKnown( - SpecialPage::getTitleFor( 'Unblock' ), - $msg['unblocklink'], - [], - [ 'wpTarget' => "#{$row->ipb_id}" ] - ); - } else { - $links[] = Linker::linkKnown( - SpecialPage::getTitleFor( 'Unblock', $row->ipb_address ), - $msg['unblocklink'] - ); - $links[] = Linker::linkKnown( - SpecialPage::getTitleFor( 'Block', $row->ipb_address ), - $msg['change-blocklink'] - ); - } - $formatted .= ' ' . Html::rawElement( - 'span', - [ 'class' => 'mw-blocklist-actions' ], - $this->msg( 'parentheses' )->rawParams( - $language->pipeList( $links ) )->escaped() - ); - } - break; - - case 'ipb_by': - if ( isset( $row->by_user_name ) ) { - $formatted = Linker::userLink( $value, $row->by_user_name ); - $formatted .= Linker::userToolLinks( $value, $row->by_user_name ); - } else { - $formatted = htmlspecialchars( $row->ipb_by_text ); // foreign user? - } - break; - - case 'ipb_reason': - $formatted = Linker::formatComment( $value ); - break; - - case 'ipb_params': - $properties = []; - if ( $row->ipb_anon_only ) { - $properties[] = $msg['anononlyblock']; - } - if ( $row->ipb_create_account ) { - $properties[] = $msg['createaccountblock']; - } - if ( $row->ipb_user && !$row->ipb_enable_autoblock ) { - $properties[] = $msg['noautoblockblock']; - } - - if ( $row->ipb_block_email ) { - $properties[] = $msg['emailblock']; - } - - if ( !$row->ipb_allow_usertalk ) { - $properties[] = $msg['blocklist-nousertalk']; - } - - $formatted = $language->commaList( $properties ); - break; - - default: - $formatted = "Unable to format $name"; - break; - } - - return $formatted; - } - - function getQueryInfo() { - $info = [ - 'tables' => [ 'ipblocks', 'user' ], - 'fields' => [ - 'ipb_id', - 'ipb_address', - 'ipb_user', - 'ipb_by', - 'ipb_by_text', - 'by_user_name' => 'user_name', - 'ipb_reason', - 'ipb_timestamp', - 'ipb_auto', - 'ipb_anon_only', - 'ipb_create_account', - 'ipb_enable_autoblock', - 'ipb_expiry', - 'ipb_range_start', - 'ipb_range_end', - 'ipb_deleted', - 'ipb_block_email', - 'ipb_allow_usertalk', - ], - 'conds' => $this->conds, - 'join_conds' => [ 'user' => [ 'LEFT JOIN', 'user_id = ipb_by' ] ] - ]; - - # Filter out any expired blocks - $db = $this->getDatabase(); - $info['conds'][] = 'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ); - - # Is the user allowed to see hidden blocks? - if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { - $info['conds']['ipb_deleted'] = 0; - } - - return $info; - } - - public function getTableClass() { - return parent::getTableClass() . ' mw-blocklist'; - } - - function getIndexField() { - return 'ipb_timestamp'; - } - - function getDefaultSort() { - return 'ipb_timestamp'; - } - - function isFieldSortable( $name ) { - return false; - } - - /** - * Do a LinkBatch query to minimise database load when generating all these links - * @param ResultWrapper $result - */ - function preprocessResults( $result ) { - # Do a link batch query - $lb = new LinkBatch; - $lb->setCaller( __METHOD__ ); - - foreach ( $result as $row ) { - $lb->add( NS_USER, $row->ipb_address ); - $lb->add( NS_USER_TALK, $row->ipb_address ); - - if ( isset( $row->by_user_name ) ) { - $lb->add( NS_USER, $row->by_user_name ); - $lb->add( NS_USER_TALK, $row->by_user_name ); - } - } - - $lb->execute(); - } -} diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php index 5314f63489..d7d338ccf9 100644 --- a/includes/specials/SpecialCategories.php +++ b/includes/specials/SpecialCategories.php @@ -92,109 +92,3 @@ class SpecialCategories extends SpecialPage { return 'pages'; } } - -/** - * TODO: Allow sorting by count. We need to have a unique index to do this - * properly. - * - * @ingroup SpecialPage Pager - */ -class CategoryPager extends AlphabeticPager { - - /** - * @var PageLinkRenderer - */ - protected $linkRenderer; - - /** - * @param IContextSource $context - * @param string $from - * @param PageLinkRenderer $linkRenderer - */ - public function __construct( IContextSource $context, $from, PageLinkRenderer $linkRenderer - ) { - parent::__construct( $context ); - $from = str_replace( ' ', '_', $from ); - if ( $from !== '' ) { - $from = Title::capitalize( $from, NS_CATEGORY ); - $this->setOffset( $from ); - $this->setIncludeOffset( true ); - } - - $this->linkRenderer = $linkRenderer; - } - - function getQueryInfo() { - return [ - 'tables' => [ 'category' ], - 'fields' => [ 'cat_title', 'cat_pages' ], - 'conds' => [ 'cat_pages > 0' ], - 'options' => [ 'USE INDEX' => 'cat_title' ], - ]; - } - - function getIndexField() { -# return array( 'abc' => 'cat_title', 'count' => 'cat_pages' ); - return 'cat_title'; - } - - function getDefaultQuery() { - parent::getDefaultQuery(); - unset( $this->mDefaultQuery['from'] ); - - return $this->mDefaultQuery; - } - -# protected function getOrderTypeMessages() { -# return array( 'abc' => 'special-categories-sort-abc', -# 'count' => 'special-categories-sort-count' ); -# } - - protected function getDefaultDirections() { -# return array( 'abc' => false, 'count' => true ); - return false; - } - - /* Override getBody to apply LinksBatch on resultset before actually outputting anything. */ - public function getBody() { - $batch = new LinkBatch; - - $this->mResult->rewind(); - - foreach ( $this->mResult as $row ) { - $batch->addObj( Title::makeTitleSafe( NS_CATEGORY, $row->cat_title ) ); - } - $batch->execute(); - $this->mResult->rewind(); - - return parent::getBody(); - } - - function formatRow( $result ) { - $title = new TitleValue( NS_CATEGORY, $result->cat_title ); - $text = $title->getText(); - $link = $this->linkRenderer->renderHtmlLink( $title, $text ); - - $count = $this->msg( 'nmembers' )->numParams( $result->cat_pages )->escaped(); - return Html::rawElement( 'li', null, $this->getLanguage()->specialList( $link, $count ) ) . "\n"; - } - - public function getStartForm( $from ) { - return Xml::tags( - 'form', - [ 'method' => 'get', 'action' => wfScript() ], - Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . - Xml::fieldset( - $this->msg( 'categories' )->text(), - Xml::inputLabel( - $this->msg( 'categoriesfrom' )->text(), - 'from', 'from', 20, $from, [ 'class' => 'mw-ui-input-inline' ] ) . - ' ' . - Html::submitButton( - $this->msg( 'categories-submit' )->text(), - [], [ 'mw-ui-progressive' ] - ) - ) - ); - } -} diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index 7b8aa4c5f8..431b556cb3 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -673,508 +673,3 @@ class SpecialContributions extends IncludableSpecialPage { return 'users'; } } - -/** - * Pager for Special:Contributions - * @ingroup SpecialPage Pager - */ -class ContribsPager extends ReverseChronologicalPager { - public $mDefaultDirection = IndexPager::DIR_DESCENDING; - public $messages; - public $target; - public $namespace = ''; - public $mDb; - public $preventClickjacking = false; - - /** @var IDatabase */ - public $mDbSecondary; - - /** - * @var array - */ - protected $mParentLens; - - function __construct( IContextSource $context, array $options ) { - parent::__construct( $context ); - - $msgs = [ - 'diff', - 'hist', - 'pipe-separator', - 'uctop' - ]; - - foreach ( $msgs as $msg ) { - $this->messages[$msg] = $this->msg( $msg )->escaped(); - } - - $this->target = isset( $options['target'] ) ? $options['target'] : ''; - $this->contribs = isset( $options['contribs'] ) ? $options['contribs'] : 'users'; - $this->namespace = isset( $options['namespace'] ) ? $options['namespace'] : ''; - $this->tagFilter = isset( $options['tagfilter'] ) ? $options['tagfilter'] : false; - $this->nsInvert = isset( $options['nsInvert'] ) ? $options['nsInvert'] : false; - $this->associated = isset( $options['associated'] ) ? $options['associated'] : false; - - $this->deletedOnly = !empty( $options['deletedOnly'] ); - $this->topOnly = !empty( $options['topOnly'] ); - $this->newOnly = !empty( $options['newOnly'] ); - - $year = isset( $options['year'] ) ? $options['year'] : false; - $month = isset( $options['month'] ) ? $options['month'] : false; - $this->getDateCond( $year, $month ); - - // Most of this code will use the 'contributions' group DB, which can map to slaves - // with extra user based indexes or partioning by user. The additional metadata - // queries should use a regular slave since the lookup pattern is not all by user. - $this->mDbSecondary = wfGetDB( DB_SLAVE ); // any random slave - $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); - } - - function getDefaultQuery() { - $query = parent::getDefaultQuery(); - $query['target'] = $this->target; - - return $query; - } - - /** - * This method basically executes the exact same code as the parent class, though with - * a hook added, to allow extensions to add additional queries. - * - * @param string $offset Index offset, inclusive - * @param int $limit Exact query limit - * @param bool $descending Query direction, false for ascending, true for descending - * @return ResultWrapper - */ - function reallyDoQuery( $offset, $limit, $descending ) { - list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( - $offset, - $limit, - $descending - ); - - /* - * This hook will allow extensions to add in additional queries, so they can get their data - * in My Contributions as well. Extensions should append their results to the $data array. - * - * Extension queries have to implement the navbar requirement as well. They should - * - have a column aliased as $pager->getIndexField() - * - have LIMIT set - * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset - * - have the ORDER BY specified based upon the details provided by the navbar - * - * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY - * - * &$data: an array of results of all contribs queries - * $pager: the ContribsPager object hooked into - * $offset: see phpdoc above - * $limit: see phpdoc above - * $descending: see phpdoc above - */ - $data = [ $this->mDb->select( - $tables, $fields, $conds, $fname, $options, $join_conds - ) ]; - Hooks::run( - 'ContribsPager::reallyDoQuery', - [ &$data, $this, $offset, $limit, $descending ] - ); - - $result = []; - - // loop all results and collect them in an array - foreach ( $data as $query ) { - foreach ( $query as $i => $row ) { - // use index column as key, allowing us to easily sort in PHP - $result[$row->{$this->getIndexField()} . "-$i"] = $row; - } - } - - // sort results - if ( $descending ) { - ksort( $result ); - } else { - krsort( $result ); - } - - // enforce limit - $result = array_slice( $result, 0, $limit ); - - // get rid of array keys - $result = array_values( $result ); - - return new FakeResultWrapper( $result ); - } - - function getQueryInfo() { - list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond(); - - $user = $this->getUser(); - $conds = array_merge( $userCond, $this->getNamespaceCond() ); - - // Paranoia: avoid brute force searches (bug 17342) - if ( !$user->isAllowed( 'deletedhistory' ) ) { - $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::SUPPRESSED_USER ) . - ' != ' . Revision::SUPPRESSED_USER; - } - - # Don't include orphaned revisions - $join_cond['page'] = Revision::pageJoinCond(); - # Get the current user name for accounts - $join_cond['user'] = Revision::userJoinCond(); - - $options = []; - if ( $index ) { - $options['USE INDEX'] = [ 'revision' => $index ]; - } - - $queryInfo = [ - 'tables' => $tables, - 'fields' => array_merge( - Revision::selectFields(), - Revision::selectUserFields(), - [ 'page_namespace', 'page_title', 'page_is_new', - 'page_latest', 'page_is_redirect', 'page_len' ] - ), - 'conds' => $conds, - 'options' => $options, - 'join_conds' => $join_cond - ]; - - ChangeTags::modifyDisplayQuery( - $queryInfo['tables'], - $queryInfo['fields'], - $queryInfo['conds'], - $queryInfo['join_conds'], - $queryInfo['options'], - $this->tagFilter - ); - - Hooks::run( 'ContribsPager::getQueryInfo', [ &$this, &$queryInfo ] ); - - return $queryInfo; - } - - function getUserCond() { - $condition = []; - $join_conds = []; - $tables = [ 'revision', 'page', 'user' ]; - $index = false; - if ( $this->contribs == 'newbie' ) { - $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ ); - $condition[] = 'rev_user >' . (int)( $max - $max / 100 ); - # ignore local groups with the bot right - # @todo FIXME: Global groups may have 'bot' rights - $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); - if ( count( $groupsWithBotPermission ) ) { - $tables[] = 'user_groups'; - $condition[] = 'ug_group IS NULL'; - $join_conds['user_groups'] = [ - 'LEFT JOIN', [ - 'ug_user = rev_user', - 'ug_group' => $groupsWithBotPermission - ] - ]; - } - } else { - $uid = User::idFromName( $this->target ); - if ( $uid ) { - $condition['rev_user'] = $uid; - $index = 'user_timestamp'; - } else { - $condition['rev_user_text'] = $this->target; - $index = 'usertext_timestamp'; - } - } - - if ( $this->deletedOnly ) { - $condition[] = 'rev_deleted != 0'; - } - - if ( $this->topOnly ) { - $condition[] = 'rev_id = page_latest'; - } - - if ( $this->newOnly ) { - $condition[] = 'rev_parent_id = 0'; - } - - return [ $tables, $index, $condition, $join_conds ]; - } - - function getNamespaceCond() { - if ( $this->namespace !== '' ) { - $selectedNS = $this->mDb->addQuotes( $this->namespace ); - $eq_op = $this->nsInvert ? '!=' : '='; - $bool_op = $this->nsInvert ? 'AND' : 'OR'; - - if ( !$this->associated ) { - return [ "page_namespace $eq_op $selectedNS" ]; - } - - $associatedNS = $this->mDb->addQuotes( - MWNamespace::getAssociated( $this->namespace ) - ); - - return [ - "page_namespace $eq_op $selectedNS " . - $bool_op . - " page_namespace $eq_op $associatedNS" - ]; - } - - return []; - } - - function getIndexField() { - return 'rev_timestamp'; - } - - function doBatchLookups() { - # Do a link batch query - $this->mResult->seek( 0 ); - $parentRevIds = []; - $this->mParentLens = []; - $batch = new LinkBatch(); - # Give some pointers to make (last) links - foreach ( $this->mResult as $row ) { - if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) { - $parentRevIds[] = $row->rev_parent_id; - } - if ( isset( $row->rev_id ) ) { - $this->mParentLens[$row->rev_id] = $row->rev_len; - if ( $this->contribs === 'newbie' ) { // multiple users - $batch->add( NS_USER, $row->user_name ); - $batch->add( NS_USER_TALK, $row->user_name ); - } - $batch->add( $row->page_namespace, $row->page_title ); - } - } - # Fetch rev_len for revisions not already scanned above - $this->mParentLens += Revision::getParentLengths( - $this->mDbSecondary, - array_diff( $parentRevIds, array_keys( $this->mParentLens ) ) - ); - $batch->execute(); - $this->mResult->seek( 0 ); - } - - /** - * @return string - */ - function getStartBody() { - return "\n"; - } - - /** - * Generates each row in the contributions list. - * - * Contributions which are marked "top" are currently on top of the history. - * For these contributions, a [rollback] link is shown for users with roll- - * back privileges. The rollback link restores the most recent version that - * was not written by the target user. - * - * @todo This would probably look a lot nicer in a table. - * @param object $row - * @return string - */ - function formatRow( $row ) { - - $ret = ''; - $classes = []; - - /* - * There may be more than just revision rows. To make sure that we'll only be processing - * revisions here, let's _try_ to build a revision out of our row (without displaying - * notices though) and then trying to grab data from the built object. If we succeed, - * we're definitely dealing with revision data and we may proceed, if not, we'll leave it - * to extensions to subscribe to the hook to parse the row. - */ - MediaWiki\suppressWarnings(); - try { - $rev = new Revision( $row ); - $validRevision = (bool)$rev->getId(); - } catch ( Exception $e ) { - $validRevision = false; - } - MediaWiki\restoreWarnings(); - - if ( $validRevision ) { - $classes = []; - - $page = Title::newFromRow( $row ); - $link = Linker::link( - $page, - htmlspecialchars( $page->getPrefixedText() ), - [ 'class' => 'mw-contributions-title' ], - $page->isRedirect() ? [ 'redirect' => 'no' ] : [] - ); - # Mark current revisions - $topmarktext = ''; - $user = $this->getUser(); - if ( $row->rev_id == $row->page_latest ) { - $topmarktext .= '' . $this->messages['uctop'] . ''; - # Add rollback link - if ( !$row->page_is_new && $page->quickUserCan( 'rollback', $user ) - && $page->quickUserCan( 'edit', $user ) - ) { - $this->preventClickjacking(); - $topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext() ); - } - } - # Is there a visible previous revision? - if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) { - $difftext = Linker::linkKnown( - $page, - $this->messages['diff'], - [], - [ - 'diff' => 'prev', - 'oldid' => $row->rev_id - ] - ); - } else { - $difftext = $this->messages['diff']; - } - $histlink = Linker::linkKnown( - $page, - $this->messages['hist'], - [], - [ 'action' => 'history' ] - ); - - if ( $row->rev_parent_id === null ) { - // For some reason rev_parent_id isn't populated for this row. - // Its rumoured this is true on wikipedia for some revisions (bug 34922). - // Next best thing is to have the total number of bytes. - $chardiff = ' . . '; - $chardiff .= Linker::formatRevisionSize( $row->rev_len ); - $chardiff .= ' . . '; - } else { - $parentLen = 0; - if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) { - $parentLen = $this->mParentLens[$row->rev_parent_id]; - } - - $chardiff = ' . . '; - $chardiff .= ChangesList::showCharacterDifference( - $parentLen, - $row->rev_len, - $this->getContext() - ); - $chardiff .= ' . . '; - } - - $lang = $this->getLanguage(); - $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true ); - $date = $lang->userTimeAndDate( $row->rev_timestamp, $user ); - if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { - $d = Linker::linkKnown( - $page, - htmlspecialchars( $date ), - [ 'class' => 'mw-changeslist-date' ], - [ 'oldid' => intval( $row->rev_id ) ] - ); - } else { - $d = htmlspecialchars( $date ); - } - if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { - $d = '' . $d . ''; - } - - # Show user names for /newbies as there may be different users. - # Note that we already excluded rows with hidden user names. - if ( $this->contribs == 'newbie' ) { - $userlink = ' . . ' . $lang->getDirMark() - . Linker::userLink( $rev->getUser(), $rev->getUserText() ); - $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( - Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' '; - } else { - $userlink = ''; - } - - if ( $rev->getParentId() === 0 ) { - $nflag = ChangesList::flag( 'newpage' ); - } else { - $nflag = ''; - } - - if ( $rev->isMinor() ) { - $mflag = ChangesList::flag( 'minor' ); - } else { - $mflag = ''; - } - - $del = Linker::getRevDeleteLink( $user, $rev, $page ); - if ( $del !== '' ) { - $del .= ' '; - } - - $diffHistLinks = $this->msg( 'parentheses' ) - ->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink ) - ->escaped(); - $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} "; - $ret .= "{$link}{$userlink} {$comment} {$topmarktext}"; - - # Denote if username is redacted for this edit - if ( $rev->isDeleted( Revision::DELETED_USER ) ) { - $ret .= " " . - $this->msg( 'rev-deleted-user-contribs' )->escaped() . - ""; - } - - # Tags, if any. - list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( - $row->ts_tags, - 'contributions', - $this->getContext() - ); - $classes = array_merge( $classes, $newClasses ); - $ret .= " $tagSummary"; - } - - // Let extensions add data - Hooks::run( 'ContributionsLineEnding', [ $this, &$ret, $row, &$classes ] ); - - if ( $classes === [] && $ret === '' ) { - wfDebug( "Dropping Special:Contribution row that could not be formatted\n" ); - $ret = "\n"; - } else { - $ret = Html::rawElement( 'li', [ 'class' => $classes ], $ret ) . "\n"; - } - - return $ret; - } - - /** - * Overwrite Pager function and return a helpful comment - * @return string - */ - function getSqlComment() { - if ( $this->namespace || $this->deletedOnly ) { - // potentially slow, see CR r58153 - return 'contributions page filtered for namespace or RevisionDeleted edits'; - } else { - return 'contributions page unfiltered'; - } - } - - protected function preventClickjacking() { - $this->preventClickjacking = true; - } - - /** - * @return bool - */ - public function getPreventClickjacking() { - return $this->preventClickjacking; - } -} diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php index 6256bbf21a..190bf9f981 100644 --- a/includes/specials/SpecialDeletedContributions.php +++ b/includes/specials/SpecialDeletedContributions.php @@ -25,337 +25,6 @@ * Implements Special:DeletedContributions to display archived revisions * @ingroup SpecialPage */ -class DeletedContribsPager extends IndexPager { - public $mDefaultDirection = IndexPager::DIR_DESCENDING; - public $messages; - public $target; - public $namespace = ''; - public $mDb; - - /** - * @var string Navigation bar with paging links. - */ - protected $mNavigationBar; - - function __construct( IContextSource $context, $target, $namespace = false ) { - parent::__construct( $context ); - $msgs = [ 'deletionlog', 'undeleteviewlink', 'diff' ]; - foreach ( $msgs as $msg ) { - $this->messages[$msg] = $this->msg( $msg )->escaped(); - } - $this->target = $target; - $this->namespace = $namespace; - $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); - } - - function getDefaultQuery() { - $query = parent::getDefaultQuery(); - $query['target'] = $this->target; - - return $query; - } - - function getQueryInfo() { - list( $index, $userCond ) = $this->getUserCond(); - $conds = array_merge( $userCond, $this->getNamespaceCond() ); - $user = $this->getUser(); - // Paranoia: avoid brute force searches (bug 17792) - if ( !$user->isAllowed( 'deletedhistory' ) ) { - $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::DELETED_USER ) . ' = 0'; - } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::SUPPRESSED_USER ) . - ' != ' . Revision::SUPPRESSED_USER; - } - - return [ - 'tables' => [ 'archive' ], - 'fields' => [ - 'ar_rev_id', 'ar_namespace', 'ar_title', 'ar_timestamp', 'ar_comment', - 'ar_minor_edit', 'ar_user', 'ar_user_text', 'ar_deleted' - ], - 'conds' => $conds, - 'options' => [ 'USE INDEX' => $index ] - ]; - } - - /** - * This method basically executes the exact same code as the parent class, though with - * a hook added, to allow extensions to add additional queries. - * - * @param string $offset Index offset, inclusive - * @param int $limit Exact query limit - * @param bool $descending Query direction, false for ascending, true for descending - * @return ResultWrapper - */ - function reallyDoQuery( $offset, $limit, $descending ) { - $data = [ parent::reallyDoQuery( $offset, $limit, $descending ) ]; - - // This hook will allow extensions to add in additional queries, nearly - // identical to ContribsPager::reallyDoQuery. - Hooks::run( - 'DeletedContribsPager::reallyDoQuery', - [ &$data, $this, $offset, $limit, $descending ] - ); - - $result = []; - - // loop all results and collect them in an array - foreach ( $data as $query ) { - foreach ( $query as $i => $row ) { - // use index column as key, allowing us to easily sort in PHP - $result[$row->{$this->getIndexField()} . "-$i"] = $row; - } - } - - // sort results - if ( $descending ) { - ksort( $result ); - } else { - krsort( $result ); - } - - // enforce limit - $result = array_slice( $result, 0, $limit ); - - // get rid of array keys - $result = array_values( $result ); - - return new FakeResultWrapper( $result ); - } - - function getUserCond() { - $condition = []; - - $condition['ar_user_text'] = $this->target; - $index = 'usertext_timestamp'; - - return [ $index, $condition ]; - } - - function getIndexField() { - return 'ar_timestamp'; - } - - function getStartBody() { - return "\n"; - } - - function getNavigationBar() { - if ( isset( $this->mNavigationBar ) ) { - return $this->mNavigationBar; - } - - $linkTexts = [ - 'prev' => $this->msg( 'pager-newer-n' )->numParams( $this->mLimit )->escaped(), - 'next' => $this->msg( 'pager-older-n' )->numParams( $this->mLimit )->escaped(), - 'first' => $this->msg( 'histlast' )->escaped(), - 'last' => $this->msg( 'histfirst' )->escaped() - ]; - - $pagingLinks = $this->getPagingLinks( $linkTexts ); - $limitLinks = $this->getLimitLinks(); - $lang = $this->getLanguage(); - $limits = $lang->pipeList( $limitLinks ); - - $firstLast = $lang->pipeList( [ $pagingLinks['first'], $pagingLinks['last'] ] ); - $firstLast = $this->msg( 'parentheses' )->rawParams( $firstLast )->escaped(); - $prevNext = $this->msg( 'viewprevnext' ) - ->rawParams( - $pagingLinks['prev'], - $pagingLinks['next'], - $limits - )->escaped(); - $separator = $this->msg( 'word-separator' )->escaped(); - $this->mNavigationBar = $firstLast . $separator . $prevNext; - - return $this->mNavigationBar; - } - - function getNamespaceCond() { - if ( $this->namespace !== '' ) { - return [ 'ar_namespace' => (int)$this->namespace ]; - } else { - return []; - } - } - - /** - * Generates each row in the contributions list. - * - * @todo This would probably look a lot nicer in a table. - * @param stdClass $row - * @return string - */ - function formatRow( $row ) { - $ret = ''; - $classes = []; - - /* - * There may be more than just revision rows. To make sure that we'll only be processing - * revisions here, let's _try_ to build a revision out of our row (without displaying - * notices though) and then trying to grab data from the built object. If we succeed, - * we're definitely dealing with revision data and we may proceed, if not, we'll leave it - * to extensions to subscribe to the hook to parse the row. - */ - MediaWiki\suppressWarnings(); - try { - $rev = Revision::newFromArchiveRow( $row ); - $validRevision = (bool)$rev->getId(); - } catch ( Exception $e ) { - $validRevision = false; - } - MediaWiki\restoreWarnings(); - - if ( $validRevision ) { - $ret = $this->formatRevisionRow( $row ); - } - - // Let extensions add data - Hooks::run( 'DeletedContributionsLineEnding', [ $this, &$ret, $row, &$classes ] ); - - if ( $classes === [] && $ret === '' ) { - wfDebug( "Dropping Special:DeletedContribution row that could not be formatted\n" ); - $ret = "\n"; - } else { - $ret = Html::rawElement( 'li', [ 'class' => $classes ], $ret ) . "\n"; - } - - return $ret; - } - - /** - * Generates each row in the contributions list for archive entries. - * - * Contributions which are marked "top" are currently on top of the history. - * For these contributions, a [rollback] link is shown for users with sysop - * privileges. The rollback link restores the most recent version that was not - * written by the target user. - * - * @todo This would probably look a lot nicer in a table. - * @param stdClass $row - * @return string - */ - function formatRevisionRow( $row ) { - $page = Title::makeTitle( $row->ar_namespace, $row->ar_title ); - - $rev = new Revision( [ - 'title' => $page, - 'id' => $row->ar_rev_id, - 'comment' => $row->ar_comment, - 'user' => $row->ar_user, - 'user_text' => $row->ar_user_text, - 'timestamp' => $row->ar_timestamp, - 'minor_edit' => $row->ar_minor_edit, - 'deleted' => $row->ar_deleted, - ] ); - - $undelete = SpecialPage::getTitleFor( 'Undelete' ); - - $logs = SpecialPage::getTitleFor( 'Log' ); - $dellog = Linker::linkKnown( - $logs, - $this->messages['deletionlog'], - [], - [ - 'type' => 'delete', - 'page' => $page->getPrefixedText() - ] - ); - - $reviewlink = Linker::linkKnown( - SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ), - $this->messages['undeleteviewlink'] - ); - - $user = $this->getUser(); - - if ( $user->isAllowed( 'deletedtext' ) ) { - $last = Linker::linkKnown( - $undelete, - $this->messages['diff'], - [], - [ - 'target' => $page->getPrefixedText(), - 'timestamp' => $rev->getTimestamp(), - 'diff' => 'prev' - ] - ); - } else { - $last = $this->messages['diff']; - } - - $comment = Linker::revComment( $rev ); - $date = $this->getLanguage()->userTimeAndDate( $rev->getTimestamp(), $user ); - $date = htmlspecialchars( $date ); - - if ( !$user->isAllowed( 'undelete' ) || !$rev->userCan( Revision::DELETED_TEXT, $user ) ) { - $link = $date; // unusable link - } else { - $link = Linker::linkKnown( - $undelete, - $date, - [ 'class' => 'mw-changeslist-date' ], - [ - 'target' => $page->getPrefixedText(), - 'timestamp' => $rev->getTimestamp() - ] - ); - } - // Style deleted items - if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { - $link = '' . $link . ''; - } - - $pagelink = Linker::link( - $page, - null, - [ 'class' => 'mw-changeslist-title' ] - ); - - if ( $rev->isMinor() ) { - $mflag = ChangesList::flag( 'minor' ); - } else { - $mflag = ''; - } - - // Revision delete link - $del = Linker::getRevDeleteLink( $user, $rev, $page ); - if ( $del ) { - $del .= ' '; - } - - $tools = Html::rawElement( - 'span', - [ 'class' => 'mw-deletedcontribs-tools' ], - $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList( - [ $last, $dellog, $reviewlink ] ) )->escaped() - ); - - $separator = '. .'; - $ret = "{$del}{$link} {$tools} {$separator} {$mflag} {$pagelink} {$comment}"; - - # Denote if username is redacted for this edit - if ( $rev->isDeleted( Revision::DELETED_USER ) ) { - $ret .= " " . $this->msg( 'rev-deleted-user-contribs' )->escaped() . ""; - } - - return $ret; - } - - /** - * Get the Database object in use - * - * @return IDatabase - */ - public function getDatabase() { - return $this->mDb; - } -} - class DeletedContributionsPage extends SpecialPage { function __construct() { parent::__construct( 'DeletedContributions', 'deletedhistory', diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php index 6c856e98af..e6e1048cd6 100644 --- a/includes/specials/SpecialListfiles.php +++ b/includes/specials/SpecialListfiles.php @@ -81,584 +81,3 @@ class SpecialListFiles extends IncludableSpecialPage { return 'media'; } } - -/** - * @ingroup SpecialPage Pager - */ -class ImageListPager extends TablePager { - protected $mFieldNames = null; - - // Subclasses should override buildQueryConds instead of using $mQueryConds variable. - protected $mQueryConds = []; - - protected $mUserName = null; - - /** - * The relevant user - * - * @var User|null - */ - protected $mUser = null; - - protected $mSearch = ''; - - protected $mIncluding = false; - - protected $mShowAll = false; - - protected $mTableName = 'image'; - - function __construct( IContextSource $context, $userName = null, $search = '', - $including = false, $showAll = false - ) { - $this->setContext( $context ); - $this->mIncluding = $including; - $this->mShowAll = $showAll; - - if ( $userName !== null && $userName !== '' ) { - $nt = Title::newFromText( $userName, NS_USER ); - if ( is_null( $nt ) ) { - $this->outputUserDoesNotExist( $userName ); - } else { - $this->mUserName = $nt->getText(); - $user = User::newFromName( $this->mUserName, false ); - if ( $user ) { - $this->mUser = $user; - } - if ( !$user || ( $user->isAnon() && !User::isIP( $user->getName() ) ) ) { - $this->outputUserDoesNotExist( $userName ); - } - } - } - - if ( $search !== '' && !$this->getConfig()->get( 'MiserMode' ) ) { - $this->mSearch = $search; - $nt = Title::newFromText( $this->mSearch ); - - if ( $nt ) { - $dbr = wfGetDB( DB_SLAVE ); - $this->mQueryConds[] = 'LOWER(img_name)' . - $dbr->buildLike( $dbr->anyString(), - strtolower( $nt->getDBkey() ), $dbr->anyString() ); - } - } - - if ( !$including ) { - if ( $this->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) { - $this->mDefaultDirection = IndexPager::DIR_DESCENDING; - } else { - $this->mDefaultDirection = IndexPager::DIR_ASCENDING; - } - } else { - $this->mDefaultDirection = IndexPager::DIR_DESCENDING; - } - - parent::__construct( $context ); - } - - /** - * Get the user relevant to the ImageList - * - * @return User|null - */ - function getRelevantUser() { - return $this->mUser; - } - - /** - * Add a message to the output stating that the user doesn't exist - * - * @param string $userName Unescaped user name - */ - protected function outputUserDoesNotExist( $userName ) { - $this->getOutput()->wrapWikiMsg( - "
\n$1\n
", - [ - 'listfiles-userdoesnotexist', - wfEscapeWikiText( $userName ), - ] - ); - } - - /** - * Build the where clause of the query. - * - * Replaces the older mQueryConds member variable. - * @param string $table Either "image" or "oldimage" - * @return array The query conditions. - */ - protected function buildQueryConds( $table ) { - $prefix = $table === 'image' ? 'img' : 'oi'; - $conds = []; - - if ( !is_null( $this->mUserName ) ) { - $conds[$prefix . '_user_text'] = $this->mUserName; - } - - if ( $this->mSearch !== '' ) { - $nt = Title::newFromText( $this->mSearch ); - if ( $nt ) { - $dbr = wfGetDB( DB_SLAVE ); - $conds[] = 'LOWER(' . $prefix . '_name)' . - $dbr->buildLike( $dbr->anyString(), - strtolower( $nt->getDBkey() ), $dbr->anyString() ); - } - } - - if ( $table === 'oldimage' ) { - // Don't want to deal with revdel. - // Future fixme: Show partial information as appropriate. - // Would have to be careful about filtering by username when username is deleted. - $conds['oi_deleted'] = 0; - } - - // Add mQueryConds in case anyone was subclassing and using the old variable. - return $conds + $this->mQueryConds; - } - - /** - * @return array - */ - function getFieldNames() { - if ( !$this->mFieldNames ) { - $this->mFieldNames = [ - 'img_timestamp' => $this->msg( 'listfiles_date' )->text(), - 'img_name' => $this->msg( 'listfiles_name' )->text(), - 'thumb' => $this->msg( 'listfiles_thumb' )->text(), - 'img_size' => $this->msg( 'listfiles_size' )->text(), - ]; - if ( is_null( $this->mUserName ) ) { - // Do not show username if filtering by username - $this->mFieldNames['img_user_text'] = $this->msg( 'listfiles_user' )->text(); - } - // img_description down here, in order so that its still after the username field. - $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text(); - - if ( !$this->getConfig()->get( 'MiserMode' ) && !$this->mShowAll ) { - $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text(); - } - if ( $this->mShowAll ) { - $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text(); - } - } - - return $this->mFieldNames; - } - - function isFieldSortable( $field ) { - if ( $this->mIncluding ) { - return false; - } - $sortable = [ 'img_timestamp', 'img_name', 'img_size' ]; - /* For reference, the indicies we can use for sorting are: - * On the image table: img_usertext_timestamp, img_size, img_timestamp - * On oldimage: oi_usertext_timestamp, oi_name_timestamp - * - * In particular that means we cannot sort by timestamp when not filtering - * by user and including old images in the results. Which is sad. - */ - if ( $this->getConfig()->get( 'MiserMode' ) && !is_null( $this->mUserName ) ) { - // If we're sorting by user, the index only supports sorting by time. - if ( $field === 'img_timestamp' ) { - return true; - } else { - return false; - } - } elseif ( $this->getConfig()->get( 'MiserMode' ) - && $this->mShowAll /* && mUserName === null */ - ) { - // no oi_timestamp index, so only alphabetical sorting in this case. - if ( $field === 'img_name' ) { - return true; - } else { - return false; - } - } - - return in_array( $field, $sortable ); - } - - function getQueryInfo() { - // Hacky Hacky Hacky - I want to get query info - // for two different tables, without reimplementing - // the pager class. - $qi = $this->getQueryInfoReal( $this->mTableName ); - - return $qi; - } - - /** - * Actually get the query info. - * - * This is to allow displaying both stuff from image and oldimage table. - * - * This is a bit hacky. - * - * @param string $table Either 'image' or 'oldimage' - * @return array Query info - */ - protected function getQueryInfoReal( $table ) { - $prefix = $table === 'oldimage' ? 'oi' : 'img'; - - $tables = [ $table ]; - $fields = array_keys( $this->getFieldNames() ); - - if ( $table === 'oldimage' ) { - foreach ( $fields as $id => &$field ) { - if ( substr( $field, 0, 4 ) !== 'img_' ) { - continue; - } - $field = $prefix . substr( $field, 3 ) . ' AS ' . $field; - } - $fields[array_search( 'top', $fields )] = "'no' AS top"; - } else { - if ( $this->mShowAll ) { - $fields[array_search( 'top', $fields )] = "'yes' AS top"; - } - } - $fields[] = $prefix . '_user AS img_user'; - $fields[array_search( 'thumb', $fields )] = $prefix . '_name AS thumb'; - - $options = $join_conds = []; - - # Depends on $wgMiserMode - # Will also not happen if mShowAll is true. - if ( isset( $this->mFieldNames['count'] ) ) { - $tables[] = 'oldimage'; - - # Need to rewrite this one - foreach ( $fields as &$field ) { - if ( $field == 'count' ) { - $field = 'COUNT(oi_archive_name) AS count'; - } - } - unset( $field ); - - $dbr = wfGetDB( DB_SLAVE ); - if ( $dbr->implicitGroupby() ) { - $options = [ 'GROUP BY' => 'img_name' ]; - } else { - $columnlist = preg_grep( '/^img/', array_keys( $this->getFieldNames() ) ); - $options = [ 'GROUP BY' => array_merge( [ 'img_user' ], $columnlist ) ]; - } - $join_conds = [ 'oldimage' => [ 'LEFT JOIN', 'oi_name = img_name' ] ]; - } - - return [ - 'tables' => $tables, - 'fields' => $fields, - 'conds' => $this->buildQueryConds( $table ), - 'options' => $options, - 'join_conds' => $join_conds - ]; - } - - /** - * Override reallyDoQuery to mix together two queries. - * - * @note $asc is named $descending in IndexPager base class. However - * it is true when the order is ascending, and false when the order - * is descending, so I renamed it to $asc here. - * @param int $offset - * @param int $limit - * @param bool $asc - * @return array - * @throws MWException - */ - function reallyDoQuery( $offset, $limit, $asc ) { - $prevTableName = $this->mTableName; - $this->mTableName = 'image'; - list( $tables, $fields, $conds, $fname, $options, $join_conds ) = - $this->buildQueryInfo( $offset, $limit, $asc ); - $imageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); - $this->mTableName = $prevTableName; - - if ( !$this->mShowAll ) { - return $imageRes; - } - - $this->mTableName = 'oldimage'; - - # Hacky... - $oldIndex = $this->mIndexField; - if ( substr( $this->mIndexField, 0, 4 ) !== 'img_' ) { - throw new MWException( "Expected to be sorting on an image table field" ); - } - $this->mIndexField = 'oi_' . substr( $this->mIndexField, 4 ); - - list( $tables, $fields, $conds, $fname, $options, $join_conds ) = - $this->buildQueryInfo( $offset, $limit, $asc ); - $oldimageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); - - $this->mTableName = $prevTableName; - $this->mIndexField = $oldIndex; - - return $this->combineResult( $imageRes, $oldimageRes, $limit, $asc ); - } - - /** - * Combine results from 2 tables. - * - * Note: This will throw away some results - * - * @param ResultWrapper $res1 - * @param ResultWrapper $res2 - * @param int $limit - * @param bool $ascending See note about $asc in $this->reallyDoQuery - * @return FakeResultWrapper $res1 and $res2 combined - */ - protected function combineResult( $res1, $res2, $limit, $ascending ) { - $res1->rewind(); - $res2->rewind(); - $topRes1 = $res1->next(); - $topRes2 = $res2->next(); - $resultArray = []; - for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) { - if ( strcmp( $topRes1->{$this->mIndexField}, $topRes2->{$this->mIndexField} ) > 0 ) { - if ( !$ascending ) { - $resultArray[] = $topRes1; - $topRes1 = $res1->next(); - } else { - $resultArray[] = $topRes2; - $topRes2 = $res2->next(); - } - } else { - if ( !$ascending ) { - $resultArray[] = $topRes2; - $topRes2 = $res2->next(); - } else { - $resultArray[] = $topRes1; - $topRes1 = $res1->next(); - } - } - } - - // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect - for ( ; $i < $limit && $topRes1; $i++ ) { - // @codingStandardsIgnoreEnd - $resultArray[] = $topRes1; - $topRes1 = $res1->next(); - } - - // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect - for ( ; $i < $limit && $topRes2; $i++ ) { - // @codingStandardsIgnoreEnd - $resultArray[] = $topRes2; - $topRes2 = $res2->next(); - } - - return new FakeResultWrapper( $resultArray ); - } - - function getDefaultSort() { - if ( $this->mShowAll && $this->getConfig()->get( 'MiserMode' ) && is_null( $this->mUserName ) ) { - // Unfortunately no index on oi_timestamp. - return 'img_name'; - } else { - return 'img_timestamp'; - } - } - - function doBatchLookups() { - $userIds = []; - $this->mResult->seek( 0 ); - foreach ( $this->mResult as $row ) { - $userIds[] = $row->img_user; - } - # Do a link batch query for names and userpages - UserCache::singleton()->doQuery( $userIds, [ 'userpage' ], __METHOD__ ); - } - - /** - * @param string $field - * @param string $value - * @return Message|string|int The return type depends on the value of $field: - * - thumb: string - * - img_timestamp: string - * - img_name: string - * - img_user_text: string - * - img_size: string - * - img_description: string - * - count: int - * - top: Message - * @throws MWException - */ - function formatValue( $field, $value ) { - switch ( $field ) { - case 'thumb': - $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ]; - $file = RepoGroup::singleton()->getLocalRepo()->findFile( $value, $opt ); - // If statement for paranoia - if ( $file ) { - $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] ); - - return $thumb->toHtml( [ 'desc-link' => true ] ); - } else { - return htmlspecialchars( $value ); - } - case 'img_timestamp': - // We may want to make this a link to the "old" version when displaying old files - return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) ); - case 'img_name': - static $imgfile = null; - if ( $imgfile === null ) { - $imgfile = $this->msg( 'imgfile' )->text(); - } - - // Weird files can maybe exist? Bug 22227 - $filePage = Title::makeTitleSafe( NS_FILE, $value ); - if ( $filePage ) { - $link = Linker::linkKnown( - $filePage, - htmlspecialchars( $filePage->getText() ) - ); - $download = Xml::element( 'a', - [ 'href' => wfLocalFile( $filePage )->getUrl() ], - $imgfile - ); - $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped(); - - // Add delete links if allowed - // From https://github.com/Wikia/app/pull/3859 - if ( $filePage->userCan( 'delete', $this->getUser() ) ) { - $deleteMsg = $this->msg( 'listfiles-delete' )->escaped(); - - $delete = Linker::linkKnown( - $filePage, $deleteMsg, [], [ 'action' => 'delete' ] - ); - $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped(); - - return "$link $download $delete"; - } - - return "$link $download"; - } else { - return htmlspecialchars( $value ); - } - case 'img_user_text': - if ( $this->mCurrentRow->img_user ) { - $name = User::whoIs( $this->mCurrentRow->img_user ); - $link = Linker::link( - Title::makeTitle( NS_USER, $name ), - htmlspecialchars( $name ) - ); - } else { - $link = htmlspecialchars( $value ); - } - - return $link; - case 'img_size': - return htmlspecialchars( $this->getLanguage()->formatSize( $value ) ); - case 'img_description': - return Linker::formatComment( $value ); - case 'count': - return intval( $value ) + 1; - case 'top': - // Messages: listfiles-latestversion-yes, listfiles-latestversion-no - return $this->msg( 'listfiles-latestversion-' . $value ); - default: - throw new MWException( "Unknown field '$field'" ); - } - } - - function getForm() { - $fields = []; - $fields['limit'] = [ - 'type' => 'select', - 'name' => 'limit', - 'label-message' => 'table_pager_limit_label', - 'options' => $this->getLimitSelectList(), - 'default' => $this->mLimit, - ]; - - if ( !$this->getConfig()->get( 'MiserMode' ) ) { - $fields['ilsearch'] = [ - 'type' => 'text', - 'name' => 'ilsearch', - 'id' => 'mw-ilsearch', - 'label-message' => 'listfiles_search_for', - 'default' => $this->mSearch, - 'size' => '40', - 'maxlength' => '255', - ]; - } - - $this->getOutput()->addModules( 'mediawiki.userSuggest' ); - $fields['user'] = [ - 'type' => 'text', - 'name' => 'user', - 'id' => 'mw-listfiles-user', - 'label-message' => 'username', - 'default' => $this->mUserName, - 'size' => '40', - 'maxlength' => '255', - 'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest - ]; - - $fields['ilshowall'] = [ - 'type' => 'check', - 'name' => 'ilshowall', - 'id' => 'mw-listfiles-show-all', - 'label-message' => 'listfiles-show-all', - 'default' => $this->mShowAll, - ]; - - $query = $this->getRequest()->getQueryValues(); - unset( $query['title'] ); - unset( $query['limit'] ); - unset( $query['ilsearch'] ); - unset( $query['ilshowall'] ); - unset( $query['user'] ); - - $form = new HTMLForm( $fields, $this->getContext() ); - - $form->setMethod( 'get' ); - $form->setTitle( $this->getTitle() ); - $form->setId( 'mw-listfiles-form' ); - $form->setWrapperLegendMsg( 'listfiles' ); - $form->setSubmitTextMsg( 'table_pager_limit_submit' ); - $form->addHiddenFields( $query ); - - $form->prepareForm(); - $form->displayForm( '' ); - } - - function getTableClass() { - return parent::getTableClass() . ' listfiles'; - } - - function getNavClass() { - return parent::getNavClass() . ' listfiles_nav'; - } - - function getSortHeaderClass() { - return parent::getSortHeaderClass() . ' listfiles_sort'; - } - - function getPagingQueries() { - $queries = parent::getPagingQueries(); - if ( !is_null( $this->mUserName ) ) { - # Append the username to the query string - foreach ( $queries as &$query ) { - if ( $query !== false ) { - $query['user'] = $this->mUserName; - } - } - } - - return $queries; - } - - function getDefaultQuery() { - $queries = parent::getDefaultQuery(); - if ( !isset( $queries['user'] ) && !is_null( $this->mUserName ) ) { - $queries['user'] = $this->mUserName; - } - - return $queries; - } - - function getTitle() { - return SpecialPage::getTitleFor( 'Listfiles' ); - } -} diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php index 7eb3757a3d..1a8dccf4de 100644 --- a/includes/specials/SpecialListusers.php +++ b/includes/specials/SpecialListusers.php @@ -25,376 +25,6 @@ * @ingroup SpecialPage */ -/** - * This class is used to get a list of user. The ones with specials - * rights (sysop, bureaucrat, developer) will have them displayed - * next to their names. - * - * @ingroup SpecialPage - */ -class UsersPager extends AlphabeticPager { - - /** - * @var array A array with user ids as key and a array of groups as value - */ - protected $userGroupCache; - - /** - * @param IContextSource $context - * @param array $par (Default null) - * @param bool $including Whether this page is being transcluded in - * another page - */ - function __construct( IContextSource $context = null, $par = null, $including = null ) { - if ( $context ) { - $this->setContext( $context ); - } - - $request = $this->getRequest(); - $par = ( $par !== null ) ? $par : ''; - $parms = explode( '/', $par ); - $symsForAll = [ '*', 'user' ]; - - if ( $parms[0] != '' && - ( in_array( $par, User::getAllGroups() ) || in_array( $par, $symsForAll ) ) - ) { - $this->requestedGroup = $par; - $un = $request->getText( 'username' ); - } elseif ( count( $parms ) == 2 ) { - $this->requestedGroup = $parms[0]; - $un = $parms[1]; - } else { - $this->requestedGroup = $request->getVal( 'group' ); - $un = ( $par != '' ) ? $par : $request->getText( 'username' ); - } - - if ( in_array( $this->requestedGroup, $symsForAll ) ) { - $this->requestedGroup = ''; - } - $this->editsOnly = $request->getBool( 'editsOnly' ); - $this->creationSort = $request->getBool( 'creationSort' ); - $this->including = $including; - $this->mDefaultDirection = $request->getBool( 'desc' ) - ? IndexPager::DIR_DESCENDING - : IndexPager::DIR_ASCENDING; - - $this->requestedUser = ''; - - if ( $un != '' ) { - $username = Title::makeTitleSafe( NS_USER, $un ); - - if ( !is_null( $username ) ) { - $this->requestedUser = $username->getText(); - } - } - - parent::__construct(); - } - - /** - * @return string - */ - function getIndexField() { - return $this->creationSort ? 'user_id' : 'user_name'; - } - - /** - * @return array - */ - function getQueryInfo() { - $dbr = wfGetDB( DB_SLAVE ); - $conds = []; - - // Don't show hidden names - if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { - $conds[] = 'ipb_deleted IS NULL OR ipb_deleted = 0'; - } - - $options = []; - - if ( $this->requestedGroup != '' ) { - $conds['ug_group'] = $this->requestedGroup; - } - - if ( $this->requestedUser != '' ) { - # Sorted either by account creation or name - if ( $this->creationSort ) { - $conds[] = 'user_id >= ' . intval( User::idFromName( $this->requestedUser ) ); - } else { - $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser ); - } - } - - if ( $this->editsOnly ) { - $conds[] = 'user_editcount > 0'; - } - - $options['GROUP BY'] = $this->creationSort ? 'user_id' : 'user_name'; - - $query = [ - 'tables' => [ 'user', 'user_groups', 'ipblocks' ], - 'fields' => [ - 'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name', - 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)', - 'edits' => 'MAX(user_editcount)', - 'creation' => 'MIN(user_registration)', - 'ipb_deleted' => 'MAX(ipb_deleted)' // block/hide status - ], - 'options' => $options, - 'join_conds' => [ - 'user_groups' => [ 'LEFT JOIN', 'user_id=ug_user' ], - 'ipblocks' => [ - 'LEFT JOIN', [ - 'user_id=ipb_user', - 'ipb_auto' => 0 - ] - ], - ], - 'conds' => $conds - ]; - - Hooks::run( 'SpecialListusersQueryInfo', [ $this, &$query ] ); - - return $query; - } - - /** - * @param stdClass $row - * @return string - */ - function formatRow( $row ) { - if ( $row->user_id == 0 ) { # Bug 16487 - return ''; - } - - $userName = $row->user_name; - - $ulinks = Linker::userLink( $row->user_id, $userName ); - $ulinks .= Linker::userToolLinksRedContribs( - $row->user_id, - $userName, - (int)$row->edits - ); - - $lang = $this->getLanguage(); - - $groups = ''; - $groups_list = self::getGroups( intval( $row->user_id ), $this->userGroupCache ); - - if ( !$this->including && count( $groups_list ) > 0 ) { - $list = []; - foreach ( $groups_list as $group ) { - $list[] = self::buildGroupLink( $group, $userName ); - } - $groups = $lang->commaList( $list ); - } - - $item = $lang->specialList( $ulinks, $groups ); - - if ( $row->ipb_deleted ) { - $item = "$item"; - } - - $edits = ''; - if ( !$this->including && $this->getConfig()->get( 'Edititis' ) ) { - $count = $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped(); - $edits = $this->msg( 'word-separator' )->escaped() . $this->msg( 'brackets', $count )->escaped(); - } - - $created = ''; - # Some rows may be null - if ( !$this->including && $row->creation ) { - $user = $this->getUser(); - $d = $lang->userDate( $row->creation, $user ); - $t = $lang->userTime( $row->creation, $user ); - $created = $this->msg( 'usercreated', $d, $t, $row->user_name )->escaped(); - $created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped(); - } - $blocked = !is_null( $row->ipb_deleted ) ? - ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : - ''; - - Hooks::run( 'SpecialListusersFormatRow', [ &$item, $row ] ); - - return Html::rawElement( 'li', [], "{$item}{$edits}{$created}{$blocked}" ); - } - - function doBatchLookups() { - $batch = new LinkBatch(); - $userIds = []; - # Give some pointers to make user links - foreach ( $this->mResult as $row ) { - $batch->add( NS_USER, $row->user_name ); - $batch->add( NS_USER_TALK, $row->user_name ); - $userIds[] = $row->user_id; - } - - // Lookup groups for all the users - $dbr = wfGetDB( DB_SLAVE ); - $groupRes = $dbr->select( - 'user_groups', - [ 'ug_user', 'ug_group' ], - [ 'ug_user' => $userIds ], - __METHOD__ - ); - $cache = []; - $groups = []; - foreach ( $groupRes as $row ) { - $cache[intval( $row->ug_user )][] = $row->ug_group; - $groups[$row->ug_group] = true; - } - $this->userGroupCache = $cache; - - // Add page of groups to link batch - foreach ( $groups as $group => $unused ) { - $groupPage = User::getGroupPage( $group ); - if ( $groupPage ) { - $batch->addObj( $groupPage ); - } - } - - $batch->execute(); - $this->mResult->rewind(); - } - - /** - * @return string - */ - function getPageHeader() { - list( $self ) = explode( '/', $this->getTitle()->getPrefixedDBkey() ); - - $this->getOutput()->addModules( 'mediawiki.userSuggest' ); - - # Form tag - $out = Xml::openElement( - 'form', - [ 'method' => 'get', 'action' => wfScript(), 'id' => 'mw-listusers-form' ] - ) . - Xml::fieldset( $this->msg( 'listusers' )->text() ) . - Html::hidden( 'title', $self ); - - # Username field (with autocompletion support) - $out .= Xml::label( $this->msg( 'listusersfrom' )->text(), 'offset' ) . ' ' . - Html::input( - 'username', - $this->requestedUser, - 'text', - [ - 'class' => 'mw-autocomplete-user', - 'id' => 'offset', - 'size' => 20, - 'autofocus' => $this->requestedUser === '' - ] - ) . ' '; - - # Group drop-down list - $sel = new XmlSelect( 'group', 'group', $this->requestedGroup ); - $sel->addOption( $this->msg( 'group-all' )->text(), '' ); - foreach ( $this->getAllGroups() as $group => $groupText ) { - $sel->addOption( $groupText, $group ); - } - - $out .= Xml::label( $this->msg( 'group' )->text(), 'group' ) . ' '; - $out .= $sel->getHTML() . '
'; - $out .= Xml::checkLabel( - $this->msg( 'listusers-editsonly' )->text(), - 'editsOnly', - 'editsOnly', - $this->editsOnly - ); - $out .= ' '; - $out .= Xml::checkLabel( - $this->msg( 'listusers-creationsort' )->text(), - 'creationSort', - 'creationSort', - $this->creationSort - ); - $out .= ' '; - $out .= Xml::checkLabel( - $this->msg( 'listusers-desc' )->text(), - 'desc', - 'desc', - $this->mDefaultDirection - ); - $out .= '
'; - - Hooks::run( 'SpecialListusersHeaderForm', [ $this, &$out ] ); - - # Submit button and form bottom - $out .= Html::hidden( 'limit', $this->mLimit ); - $out .= Xml::submitButton( $this->msg( 'listusers-submit' )->text() ); - Hooks::run( 'SpecialListusersHeader', [ $this, &$out ] ); - $out .= Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ); - - return $out; - } - - /** - * Get a list of all explicit groups - * @return array - */ - function getAllGroups() { - $result = []; - foreach ( User::getAllGroups() as $group ) { - $result[$group] = User::getGroupName( $group ); - } - asort( $result ); - - return $result; - } - - /** - * Preserve group and username offset parameters when paging - * @return array - */ - function getDefaultQuery() { - $query = parent::getDefaultQuery(); - if ( $this->requestedGroup != '' ) { - $query['group'] = $this->requestedGroup; - } - if ( $this->requestedUser != '' ) { - $query['username'] = $this->requestedUser; - } - Hooks::run( 'SpecialListusersDefaultQuery', [ $this, &$query ] ); - - return $query; - } - - /** - * Get a list of groups the specified user belongs to - * - * @param int $uid User id - * @param array|null $cache - * @return array - */ - protected static function getGroups( $uid, $cache = null ) { - if ( $cache === null ) { - $user = User::newFromId( $uid ); - $effectiveGroups = $user->getEffectiveGroups(); - } else { - $effectiveGroups = isset( $cache[$uid] ) ? $cache[$uid] : []; - } - $groups = array_diff( $effectiveGroups, User::getImplicitGroups() ); - - return $groups; - } - - /** - * Format a link to a group description page - * - * @param string $group Group name - * @param string $username Username - * @return string - */ - protected static function buildGroupLink( $group, $username ) { - return User::makeGroupLinkHTML( - $group, - User::getGroupMember( $group, $username ) - ); - } -} - /** * @ingroup SpecialPage */ diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php index 3310538a59..b916c1fc78 100644 --- a/includes/specials/SpecialMergeHistory.php +++ b/includes/specials/SpecialMergeHistory.php @@ -379,78 +379,3 @@ class SpecialMergeHistory extends SpecialPage { return 'pagetools'; } } - -class MergeHistoryPager extends ReverseChronologicalPager { - /** @var SpecialMergeHistory */ - public $mForm; - - /** @var array */ - public $mConds; - - function __construct( SpecialMergeHistory $form, $conds, Title $source, Title $dest ) { - $this->mForm = $form; - $this->mConds = $conds; - $this->title = $source; - $this->articleID = $source->getArticleID(); - - $dbr = wfGetDB( DB_SLAVE ); - $maxtimestamp = $dbr->selectField( - 'revision', - 'MIN(rev_timestamp)', - [ 'rev_page' => $dest->getArticleID() ], - __METHOD__ - ); - $this->maxTimestamp = $maxtimestamp; - - parent::__construct( $form->getContext() ); - } - - function getStartBody() { - # Do a link batch query - $this->mResult->seek( 0 ); - $batch = new LinkBatch(); - # Give some pointers to make (last) links - $this->mForm->prevId = []; - foreach ( $this->mResult as $row ) { - $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); - $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) ); - - $rev_id = isset( $rev_id ) ? $rev_id : $row->rev_id; - if ( $rev_id > $row->rev_id ) { - $this->mForm->prevId[$rev_id] = $row->rev_id; - } elseif ( $rev_id < $row->rev_id ) { - $this->mForm->prevId[$row->rev_id] = $rev_id; - } - - $rev_id = $row->rev_id; - } - - $batch->execute(); - $this->mResult->seek( 0 ); - - return ''; - } - - function formatRow( $row ) { - return $this->mForm->formatRevisionRow( $row ); - } - - function getQueryInfo() { - $conds = $this->mConds; - $conds['rev_page'] = $this->articleID; - $conds[] = "rev_timestamp < " . $this->mDb->addQuotes( $this->maxTimestamp ); - - return [ - 'tables' => [ 'revision', 'page', 'user' ], - 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ), - 'conds' => $conds, - 'join_conds' => [ - 'page' => Revision::pageJoinCond(), - 'user' => Revision::userJoinCond() ] - ]; - } - - function getIndexField() { - return 'rev_timestamp'; - } -} diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php index 629a50875b..14391d2459 100644 --- a/includes/specials/SpecialNewimages.php +++ b/includes/specials/SpecialNewimages.php @@ -71,189 +71,3 @@ class SpecialNewFiles extends IncludableSpecialPage { } } } - -/** - * @ingroup SpecialPage Pager - */ -class NewFilesPager extends ReverseChronologicalPager { - /** - * @var ImageGallery - */ - protected $gallery; - - /** - * @var bool - */ - protected $showBots; - - /** - * @var bool - */ - protected $hidePatrolled; - - function __construct( IContextSource $context, $par = null ) { - $this->like = $context->getRequest()->getText( 'like' ); - $this->showBots = $context->getRequest()->getBool( 'showbots', 0 ); - $this->hidePatrolled = $context->getRequest()->getBool( 'hidepatrolled', 0 ); - if ( is_numeric( $par ) ) { - $this->setLimit( $par ); - } - - parent::__construct( $context ); - } - - function getQueryInfo() { - $conds = $jconds = []; - $tables = [ 'image' ]; - $fields = [ 'img_name', 'img_user', 'img_timestamp' ]; - $options = []; - - if ( !$this->showBots ) { - $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); - - if ( count( $groupsWithBotPermission ) ) { - $tables[] = 'user_groups'; - $conds[] = 'ug_group IS NULL'; - $jconds['user_groups'] = [ - 'LEFT JOIN', - [ - 'ug_group' => $groupsWithBotPermission, - 'ug_user = img_user' - ] - ]; - } - } - - if ( $this->hidePatrolled ) { - $tables[] = 'recentchanges'; - $conds['rc_type'] = RC_LOG; - $conds['rc_log_type'] = 'upload'; - $conds['rc_patrolled'] = 0; - $conds['rc_namespace'] = NS_FILE; - $jconds['recentchanges'] = [ - 'INNER JOIN', - [ - 'rc_title = img_name', - 'rc_user = img_user', - 'rc_timestamp = img_timestamp' - ] - ]; - // We're ordering by img_timestamp, so we have to make sure MariaDB queries `image` first. - // It sometimes decides to query `recentchanges` first and filesort the result set later - // to get the right ordering. T124205 / https://mariadb.atlassian.net/browse/MDEV-8880 - $options[] = 'STRAIGHT_JOIN'; - } - - if ( !$this->getConfig()->get( 'MiserMode' ) && $this->like !== null ) { - $dbr = wfGetDB( DB_SLAVE ); - $likeObj = Title::newFromText( $this->like ); - if ( $likeObj instanceof Title ) { - $like = $dbr->buildLike( - $dbr->anyString(), - strtolower( $likeObj->getDBkey() ), - $dbr->anyString() - ); - $conds[] = "LOWER(img_name) $like"; - } - } - - $query = [ - 'tables' => $tables, - 'fields' => $fields, - 'join_conds' => $jconds, - 'conds' => $conds, - 'options' => $options, - ]; - - return $query; - } - - function getIndexField() { - return 'img_timestamp'; - } - - function getStartBody() { - if ( !$this->gallery ) { - // Note that null for mode is taken to mean use default. - $mode = $this->getRequest()->getVal( 'gallerymode', null ); - try { - $this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() ); - } catch ( Exception $e ) { - // User specified something invalid, fallback to default. - $this->gallery = ImageGalleryBase::factory( false, $this->getContext() ); - } - } - - return ''; - } - - function getEndBody() { - return $this->gallery->toHTML(); - } - - function formatRow( $row ) { - $name = $row->img_name; - $user = User::newFromId( $row->img_user ); - - $title = Title::makeTitle( NS_FILE, $name ); - $ul = Linker::link( $user->getUserPage(), $user->getName() ); - $time = $this->getLanguage()->userTimeAndDate( $row->img_timestamp, $this->getUser() ); - - $this->gallery->add( - $title, - "$ul
\n" - . htmlspecialchars( $time ) - . "
\n" - ); - } - - function getForm() { - $fields = [ - 'like' => [ - 'type' => 'text', - 'label-message' => 'newimages-label', - 'name' => 'like', - ], - 'showbots' => [ - 'type' => 'check', - 'label-message' => 'newimages-showbots', - 'name' => 'showbots', - ], - 'hidepatrolled' => [ - 'type' => 'check', - 'label-message' => 'newimages-hidepatrolled', - 'name' => 'hidepatrolled', - ], - 'limit' => [ - 'type' => 'hidden', - 'default' => $this->mLimit, - 'name' => 'limit', - ], - 'offset' => [ - 'type' => 'hidden', - 'default' => $this->getRequest()->getText( 'offset' ), - 'name' => 'offset', - ], - ]; - - if ( $this->getConfig()->get( 'MiserMode' ) ) { - unset( $fields['like'] ); - } - - if ( !$this->getUser()->useFilePatrol() ) { - unset( $fields['hidepatrolled'] ); - } - - $context = new DerivativeContext( $this->getContext() ); - $context->setTitle( $this->getTitle() ); // Remove subpage - $form = new HTMLForm( $fields, $context ); - - $form->setSubmitTextMsg( 'ilsubmit' ); - $form->setSubmitProgressive(); - - $form->setMethod( 'get' ); - $form->setWrapperLegendMsg( 'newimages-legend' ); - - return $form; - } -} diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index ab29d13794..c24b054750 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -479,130 +479,3 @@ class SpecialNewpages extends IncludableSpecialPage { return 'changes'; } } - -/** - * @ingroup SpecialPage Pager - */ -class NewPagesPager extends ReverseChronologicalPager { - // Stored opts - protected $opts; - - /** - * @var HtmlForm - */ - protected $mForm; - - function __construct( $form, FormOptions $opts ) { - parent::__construct( $form->getContext() ); - $this->mForm = $form; - $this->opts = $opts; - } - - function getQueryInfo() { - $conds = []; - $conds['rc_new'] = 1; - - $namespace = $this->opts->getValue( 'namespace' ); - $namespace = ( $namespace === 'all' ) ? false : intval( $namespace ); - - $username = $this->opts->getValue( 'username' ); - $user = Title::makeTitleSafe( NS_USER, $username ); - - $rcIndexes = []; - - if ( $namespace !== false ) { - if ( $this->opts->getValue( 'invert' ) ) { - $conds[] = 'rc_namespace != ' . $this->mDb->addQuotes( $namespace ); - } else { - $conds['rc_namespace'] = $namespace; - } - } - - if ( $user ) { - $conds['rc_user_text'] = $user->getText(); - $rcIndexes = 'rc_user_text'; - } elseif ( User::groupHasPermission( '*', 'createpage' ) && - $this->opts->getValue( 'hideliu' ) - ) { - # If anons cannot make new pages, don't "exclude logged in users"! - $conds['rc_user'] = 0; - } - - # If this user cannot see patrolled edits or they are off, don't do dumb queries! - if ( $this->opts->getValue( 'hidepatrolled' ) && $this->getUser()->useNPPatrol() ) { - $conds['rc_patrolled'] = 0; - } - - if ( $this->opts->getValue( 'hidebots' ) ) { - $conds['rc_bot'] = 0; - } - - if ( $this->opts->getValue( 'hideredirs' ) ) { - $conds['page_is_redirect'] = 0; - } - - // Allow changes to the New Pages query - $tables = [ 'recentchanges', 'page' ]; - $fields = [ - 'rc_namespace', 'rc_title', 'rc_cur_id', 'rc_user', 'rc_user_text', - 'rc_comment', 'rc_timestamp', 'rc_patrolled', 'rc_id', 'rc_deleted', - 'length' => 'page_len', 'rev_id' => 'page_latest', 'rc_this_oldid', - 'page_namespace', 'page_title' - ]; - $join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ]; - - Hooks::run( 'SpecialNewpagesConditions', - [ &$this, $this->opts, &$conds, &$tables, &$fields, &$join_conds ] ); - - $options = []; - - if ( $rcIndexes ) { - $options = [ 'USE INDEX' => [ 'recentchanges' => $rcIndexes ] ]; - } - - $info = [ - 'tables' => $tables, - 'fields' => $fields, - 'conds' => $conds, - 'options' => $options, - 'join_conds' => $join_conds - ]; - - // Modify query for tags - ChangeTags::modifyDisplayQuery( - $info['tables'], - $info['fields'], - $info['conds'], - $info['join_conds'], - $info['options'], - $this->opts['tagfilter'] - ); - - return $info; - } - - function getIndexField() { - return 'rc_timestamp'; - } - - function formatRow( $row ) { - return $this->mForm->formatRow( $row ); - } - - function getStartBody() { - # Do a batch existence check on pages - $linkBatch = new LinkBatch(); - foreach ( $this->mResult as $row ) { - $linkBatch->add( NS_USER, $row->rc_user_text ); - $linkBatch->add( NS_USER_TALK, $row->rc_user_text ); - $linkBatch->add( $row->page_namespace, $row->page_title ); - } - $linkBatch->execute(); - - return ''; - } -} diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php index 5df425a18c..c800d96c78 100644 --- a/includes/specials/SpecialProtectedtitles.php +++ b/includes/specials/SpecialProtectedtitles.php @@ -190,74 +190,3 @@ class SpecialProtectedtitles extends SpecialPage { return 'maintenance'; } } - -/** - * @todo document - * @ingroup Pager - */ -class ProtectedTitlesPager extends AlphabeticPager { - public $mForm, $mConds; - - function __construct( $form, $conds = [], $type, $level, $namespace, - $sizetype = '', $size = 0 - ) { - $this->mForm = $form; - $this->mConds = $conds; - $this->level = $level; - $this->namespace = $namespace; - $this->size = intval( $size ); - parent::__construct( $form->getContext() ); - } - - function getStartBody() { - # Do a link batch query - $this->mResult->seek( 0 ); - $lb = new LinkBatch; - - foreach ( $this->mResult as $row ) { - $lb->add( $row->pt_namespace, $row->pt_title ); - } - - $lb->execute(); - - return ''; - } - - /** - * @return Title - */ - function getTitle() { - return $this->mForm->getTitle(); - } - - function formatRow( $row ) { - return $this->mForm->formatRow( $row ); - } - - /** - * @return array - */ - function getQueryInfo() { - $conds = $this->mConds; - $conds[] = 'pt_expiry > ' . $this->mDb->addQuotes( $this->mDb->timestamp() ) . - ' OR pt_expiry IS NULL'; - if ( $this->level ) { - $conds['pt_create_perm'] = $this->level; - } - - if ( !is_null( $this->namespace ) ) { - $conds[] = 'pt_namespace=' . $this->mDb->addQuotes( $this->namespace ); - } - - return [ - 'tables' => 'protected_titles', - 'fields' => [ 'pt_namespace', 'pt_title', 'pt_create_perm', - 'pt_expiry', 'pt_timestamp' ], - 'conds' => $conds - ]; - } - - function getIndexField() { - return 'pt_timestamp'; - } -} diff --git a/includes/specials/pagers/ActiveUsersPager.php b/includes/specials/pagers/ActiveUsersPager.php new file mode 100644 index 0000000000..0d3bc9aeee --- /dev/null +++ b/includes/specials/pagers/ActiveUsersPager.php @@ -0,0 +1,254 @@ +RCMaxAge = $this->getConfig()->get( 'ActiveUserDays' ); + $un = $this->getRequest()->getText( 'username', $par ); + $this->requestedUser = ''; + if ( $un != '' ) { + $username = Title::makeTitleSafe( NS_USER, $un ); + if ( !is_null( $username ) ) { + $this->requestedUser = $username->getText(); + } + } + + $this->setupOptions(); + } + + public function setupOptions() { + $this->opts = new FormOptions(); + + $this->opts->add( 'hidebots', false, FormOptions::BOOL ); + $this->opts->add( 'hidesysops', false, FormOptions::BOOL ); + + $this->opts->fetchValuesFromRequest( $this->getRequest() ); + + if ( $this->opts->getValue( 'hidebots' ) == 1 ) { + $this->hideRights[] = 'bot'; + } + if ( $this->opts->getValue( 'hidesysops' ) == 1 ) { + $this->hideGroups[] = 'sysop'; + } + } + + function getIndexField() { + return 'qcc_title'; + } + + function getQueryInfo() { + $dbr = $this->getDatabase(); + + $activeUserSeconds = $this->getConfig()->get( 'ActiveUserDays' ) * 86400; + $timestamp = $dbr->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds ); + $conds = [ + 'qcc_type' => 'activeusers', + 'qcc_namespace' => NS_USER, + 'user_name = qcc_title', + 'rc_user_text = qcc_title', + 'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata. + 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ), // Don't count categorization changes. + 'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ), + 'rc_timestamp >= ' . $dbr->addQuotes( $timestamp ), + ]; + if ( $this->requestedUser != '' ) { + $conds[] = 'qcc_title >= ' . $dbr->addQuotes( $this->requestedUser ); + } + if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { + $conds[] = 'NOT EXISTS (' . $dbr->selectSQLText( + 'ipblocks', '1', [ 'ipb_user=user_id', 'ipb_deleted' => 1 ] + ) . ')'; + } + + if ( $dbr->implicitGroupby() ) { + $options = [ 'GROUP BY' => [ 'qcc_title' ] ]; + } else { + $options = [ 'GROUP BY' => [ 'user_name', 'user_id', 'qcc_title' ] ]; + } + + return [ + 'tables' => [ 'querycachetwo', 'user', 'recentchanges' ], + 'fields' => [ 'user_name', 'user_id', 'recentedits' => 'COUNT(*)', 'qcc_title' ], + 'options' => $options, + 'conds' => $conds + ]; + } + + function doBatchLookups() { + parent::doBatchLookups(); + + $uids = []; + foreach ( $this->mResult as $row ) { + $uids[] = $row->user_id; + } + // Fetch the block status of the user for showing "(blocked)" text and for + // striking out names of suppressed users when privileged user views the list. + // Although the first query already hits the block table for un-privileged, this + // is done in two queries to avoid huge quicksorts and to make COUNT(*) correct. + $dbr = $this->getDatabase(); + $res = $dbr->select( 'ipblocks', + [ 'ipb_user', 'MAX(ipb_deleted) AS block_status' ], + [ 'ipb_user' => $uids ], + __METHOD__, + [ 'GROUP BY' => [ 'ipb_user' ] ] + ); + $this->blockStatusByUid = []; + foreach ( $res as $row ) { + $this->blockStatusByUid[$row->ipb_user] = $row->block_status; // 0 or 1 + } + $this->mResult->seek( 0 ); + } + + function formatRow( $row ) { + $userName = $row->user_name; + + $ulinks = Linker::userLink( $row->user_id, $userName ); + $ulinks .= Linker::userToolLinks( $row->user_id, $userName ); + + $lang = $this->getLanguage(); + + $list = []; + $user = User::newFromId( $row->user_id ); + + // User right filter + foreach ( $this->hideRights as $right ) { + // Calling User::getRights() within the loop so that + // if the hideRights() filter is empty, we don't have to + // trigger the lazy-init of the big userrights array in the + // User object + if ( in_array( $right, $user->getRights() ) ) { + return ''; + } + } + + // User group filter + // Note: This is a different loop than for user rights, + // because we're reusing it to build the group links + // at the same time + $groups_list = self::getGroups( intval( $row->user_id ), $this->userGroupCache ); + foreach ( $groups_list as $group ) { + if ( in_array( $group, $this->hideGroups ) ) { + return ''; + } + $list[] = self::buildGroupLink( $group, $userName ); + } + + $groups = $lang->commaList( $list ); + + $item = $lang->specialList( $ulinks, $groups ); + + $isBlocked = isset( $this->blockStatusByUid[$row->user_id] ); + if ( $isBlocked && $this->blockStatusByUid[$row->user_id] == 1 ) { + $item = "$item"; + } + $count = $this->msg( 'activeusers-count' )->numParams( $row->recentedits ) + ->params( $userName )->numParams( $this->RCMaxAge )->escaped(); + $blocked = $isBlocked ? ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : ''; + + return Html::rawElement( 'li', [], "{$item} [{$count}]{$blocked}" ); + } + + function getPageHeader() { + $self = $this->getTitle(); + $limit = $this->mLimit ? Html::hidden( 'limit', $this->mLimit ) : ''; + + # Form tag + $out = Xml::openElement( 'form', [ 'method' => 'get', 'action' => wfScript() ] ); + $out .= Xml::fieldset( $this->msg( 'activeusers' )->text() ) . "\n"; + $out .= Html::hidden( 'title', $self->getPrefixedDBkey() ) . $limit . "\n"; + + # Username field (with autocompletion support) + $this->getOutput()->addModules( 'mediawiki.userSuggest' ); + $out .= Xml::inputLabel( + $this->msg( 'activeusers-from' )->text(), + 'username', + 'offset', + 20, + $this->requestedUser, + [ + 'class' => 'mw-ui-input-inline mw-autocomplete-user', + 'tabindex' => 1, + ] + ( + // Set autofocus on blank input + $this->requestedUser === '' ? [ 'autofocus' => '' ] : [] + ) + ) . '
'; + + $out .= Xml::checkLabel( $this->msg( 'activeusers-hidebots' )->text(), + 'hidebots', 'hidebots', $this->opts->getValue( 'hidebots' ), [ 'tabindex' => 2 ] ); + + $out .= Xml::checkLabel( + $this->msg( 'activeusers-hidesysops' )->text(), + 'hidesysops', + 'hidesysops', + $this->opts->getValue( 'hidesysops' ), + [ 'tabindex' => 3 ] + ) . '
'; + + # Submit button and form bottom + $out .= Xml::submitButton( + $this->msg( 'activeusers-submit' )->text(), + [ 'tabindex' => 4 ] + ) . "\n"; + $out .= Xml::closeElement( 'fieldset' ); + $out .= Xml::closeElement( 'form' ); + + return $out; + } + +} diff --git a/includes/specials/pagers/AllMessagesTablePager.php b/includes/specials/pagers/AllMessagesTablePager.php new file mode 100644 index 0000000000..2f2cbc2be8 --- /dev/null +++ b/includes/specials/pagers/AllMessagesTablePager.php @@ -0,0 +1,424 @@ +getContext() ); + $this->mIndexField = 'am_title'; + $this->mPage = $page; + $this->mConds = $conds; + // FIXME: Why does this need to be set to DIR_DESCENDING to produce ascending ordering? + $this->mDefaultDirection = IndexPager::DIR_DESCENDING; + $this->mLimitsShown = [ 20, 50, 100, 250, 500, 5000 ]; + + global $wgContLang; + + $this->talk = $this->msg( 'talkpagelinktext' )->escaped(); + + $this->lang = ( $langObj ? $langObj : $wgContLang ); + $this->langcode = $this->lang->getCode(); + $this->foreign = $this->langcode !== $wgContLang->getCode(); + + $request = $this->getRequest(); + + $this->filter = $request->getVal( 'filter', 'all' ); + if ( $this->filter === 'all' ) { + $this->custom = null; // So won't match in either case + } else { + $this->custom = ( $this->filter === 'unmodified' ); + } + + $prefix = $this->getLanguage()->ucfirst( $request->getVal( 'prefix', '' ) ); + $prefix = $prefix !== '' ? + Title::makeTitleSafe( NS_MEDIAWIKI, $request->getVal( 'prefix', null ) ) : + null; + + if ( $prefix !== null ) { + $this->displayPrefix = $prefix->getDBkey(); + $this->prefix = '/^' . preg_quote( $this->displayPrefix, '/' ) . '/i'; + } else { + $this->displayPrefix = false; + $this->prefix = false; + } + + // The suffix that may be needed for message names if we're in a + // different language (eg [[MediaWiki:Foo/fr]]: $suffix = '/fr' + if ( $this->foreign ) { + $this->suffix = '/' . $this->langcode; + } else { + $this->suffix = ''; + } + } + + function buildForm() { + $attrs = [ 'id' => 'mw-allmessages-form-lang', 'name' => 'lang' ]; + $msg = wfMessage( 'allmessages-language' ); + $langSelect = Xml::languageSelector( $this->langcode, false, null, $attrs, $msg ); + + $out = Xml::openElement( 'form', [ + 'method' => 'get', + 'action' => $this->getConfig()->get( 'Script' ), + 'id' => 'mw-allmessages-form' + ] ) . + Xml::fieldset( $this->msg( 'allmessages-filter-legend' )->text() ) . + Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . + Xml::openElement( 'table', [ 'class' => 'mw-allmessages-table' ] ) . "\n" . + ' + ' . + Xml::label( $this->msg( 'allmessages-prefix' )->text(), 'mw-allmessages-form-prefix' ) . + "\n + " . + Xml::input( + 'prefix', + 20, + str_replace( '_', ' ', $this->displayPrefix ), + [ 'id' => 'mw-allmessages-form-prefix' ] + ) . + "\n + + \n + " . + $this->msg( 'allmessages-filter' )->escaped() . + "\n + " . + Xml::radioLabel( $this->msg( 'allmessages-filter-unmodified' )->text(), + 'filter', + 'unmodified', + 'mw-allmessages-form-filter-unmodified', + ( $this->filter === 'unmodified' ) + ) . + Xml::radioLabel( $this->msg( 'allmessages-filter-all' )->text(), + 'filter', + 'all', + 'mw-allmessages-form-filter-all', + ( $this->filter === 'all' ) + ) . + Xml::radioLabel( $this->msg( 'allmessages-filter-modified' )->text(), + 'filter', + 'modified', + 'mw-allmessages-form-filter-modified', + ( $this->filter === 'modified' ) + ) . + "\n + + \n + " . $langSelect[0] . "\n + " . $langSelect[1] . "\n + " . + + ' + ' . + Xml::label( $this->msg( 'table_pager_limit_label' )->text(), 'mw-table_pager_limit_label' ) . + ' + ' . + $this->getLimitSelect( [ 'id' => 'mw-table_pager_limit_label' ] ) . + ' + + + ' . + Xml::submitButton( $this->msg( 'allmessages-filter-submit' )->text() ) . + "\n + " . + + Xml::closeElement( 'table' ) . + $this->getHiddenFields( [ 'title', 'prefix', 'filter', 'lang', 'limit' ] ) . + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ); + + return $out; + } + + function getAllMessages( $descending ) { + $messageNames = Language::getLocalisationCache()->getSubitemList( 'en', 'messages' ); + + // Normalise message names so they look like page titles and sort correctly - T86139 + $messageNames = array_map( [ $this->lang, 'ucfirst' ], $messageNames ); + + if ( $descending ) { + rsort( $messageNames ); + } else { + asort( $messageNames ); + } + + return $messageNames; + } + + /** + * Determine which of the MediaWiki and MediaWiki_talk namespace pages exist. + * Returns array( 'pages' => ..., 'talks' => ... ), where the subarrays have + * an entry for each existing page, with the key being the message name and + * value arbitrary. + * + * @param array $messageNames + * @param string $langcode What language code + * @param bool $foreign Whether the $langcode is not the content language + * @return array A 'pages' and 'talks' array with the keys of existing pages + */ + public static function getCustomisedStatuses( $messageNames, $langcode = 'en', $foreign = false ) { + // FIXME: This function should be moved to Language:: or something. + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'page', + [ 'page_namespace', 'page_title' ], + [ 'page_namespace' => [ NS_MEDIAWIKI, NS_MEDIAWIKI_TALK ] ], + __METHOD__, + [ 'USE INDEX' => 'name_title' ] + ); + $xNames = array_flip( $messageNames ); + + $pageFlags = $talkFlags = []; + + foreach ( $res as $s ) { + $exists = false; + + if ( $foreign ) { + $titleParts = explode( '/', $s->page_title ); + if ( count( $titleParts ) === 2 && + $langcode === $titleParts[1] && + isset( $xNames[$titleParts[0]] ) + ) { + $exists = $titleParts[0]; + } + } elseif ( isset( $xNames[$s->page_title] ) ) { + $exists = $s->page_title; + } + + $title = Title::newFromRow( $s ); + if ( $exists && $title->inNamespace( NS_MEDIAWIKI ) ) { + $pageFlags[$exists] = true; + } elseif ( $exists && $title->inNamespace( NS_MEDIAWIKI_TALK ) ) { + $talkFlags[$exists] = true; + } + } + + return [ 'pages' => $pageFlags, 'talks' => $talkFlags ]; + } + + /** + * This function normally does a database query to get the results; we need + * to make a pretend result using a FakeResultWrapper. + * @param string $offset + * @param int $limit + * @param bool $descending + * @return FakeResultWrapper + */ + function reallyDoQuery( $offset, $limit, $descending ) { + $result = new FakeResultWrapper( [] ); + + $messageNames = $this->getAllMessages( $descending ); + $statuses = self::getCustomisedStatuses( $messageNames, $this->langcode, $this->foreign ); + + $count = 0; + foreach ( $messageNames as $key ) { + $customised = isset( $statuses['pages'][$key] ); + if ( $customised !== $this->custom && + ( $descending && ( $key < $offset || !$offset ) || !$descending && $key > $offset ) && + ( ( $this->prefix && preg_match( $this->prefix, $key ) ) || $this->prefix === false ) + ) { + $actual = wfMessage( $key )->inLanguage( $this->langcode )->plain(); + $default = wfMessage( $key )->inLanguage( $this->langcode )->useDatabase( false )->plain(); + $result->result[] = [ + 'am_title' => $key, + 'am_actual' => $actual, + 'am_default' => $default, + 'am_customised' => $customised, + 'am_talk_exists' => isset( $statuses['talks'][$key] ) + ]; + $count++; + } + + if ( $count === $limit ) { + break; + } + } + + return $result; + } + + function getStartBody() { + $tableClass = $this->getTableClass(); + return Xml::openElement( 'table', [ + 'class' => "mw-datatable $tableClass", + 'id' => 'mw-allmessagestable' + ] ) . + "\n" . + " + " . + $this->msg( 'allmessagesname' )->escaped() . " + + " . + $this->msg( 'allmessagesdefault' )->escaped() . + " + \n + + " . + $this->msg( 'allmessagescurrent' )->escaped() . + " + \n"; + } + + function formatValue( $field, $value ) { + switch ( $field ) { + case 'am_title' : + $title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix ); + $talk = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix ); + $translation = Linker::makeExternalLink( + 'https://translatewiki.net/w/i.php?' . wfArrayToCgi( [ + 'title' => 'Special:SearchTranslations', + 'group' => 'mediawiki', + 'grouppath' => 'mediawiki', + 'query' => 'language:' . $this->getLanguage()->getCode() . '^25 ' . + 'messageid:"MediaWiki:' . $value . '"^10 "' . + $this->msg( $value )->inLanguage( 'en' )->plain() . '"' + ] ), + $this->msg( 'allmessages-filter-translate' )->text() + ); + + if ( $this->mCurrentRow->am_customised ) { + $title = Linker::linkKnown( $title, $this->getLanguage()->lcfirst( $value ) ); + } else { + $title = Linker::link( + $title, + $this->getLanguage()->lcfirst( $value ), + [], + [], + [ 'broken' ] + ); + } + if ( $this->mCurrentRow->am_talk_exists ) { + $talk = Linker::linkKnown( $talk, $this->talk ); + } else { + $talk = Linker::link( + $talk, + $this->talk, + [], + [], + [ 'broken' ] + ); + } + + return $title . ' ' . + $this->msg( 'parentheses' )->rawParams( $talk )->escaped() . + ' ' . + $this->msg( 'parentheses' )->rawParams( $translation )->escaped(); + + case 'am_default' : + case 'am_actual' : + return Sanitizer::escapeHtmlAllowEntities( $value ); + } + + return ''; + } + + function formatRow( $row ) { + // Do all the normal stuff + $s = parent::formatRow( $row ); + + // But if there's a customised message, add that too. + if ( $row->am_customised ) { + $s .= Xml::openElement( 'tr', $this->getRowAttrs( $row, true ) ); + $formatted = strval( $this->formatValue( 'am_actual', $row->am_actual ) ); + + if ( $formatted === '' ) { + $formatted = ' '; + } + + $s .= Xml::tags( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted ) + . "\n"; + } + + return $s; + } + + function getRowAttrs( $row, $isSecond = false ) { + $arr = []; + + if ( $row->am_customised ) { + $arr['class'] = 'allmessages-customised'; + } + + if ( !$isSecond ) { + $arr['id'] = Sanitizer::escapeId( 'msg_' . $this->getLanguage()->lcfirst( $row->am_title ) ); + } + + return $arr; + } + + function getCellAttrs( $field, $value ) { + if ( $this->mCurrentRow->am_customised && $field === 'am_title' ) { + return [ 'rowspan' => '2', 'class' => $field ]; + } elseif ( $field === 'am_title' ) { + return [ 'class' => $field ]; + } else { + return [ + 'lang' => $this->lang->getHtmlCode(), + 'dir' => $this->lang->getDir(), + 'class' => $field + ]; + } + } + + // This is not actually used, as getStartBody is overridden above + function getFieldNames() { + return [ + 'am_title' => $this->msg( 'allmessagesname' )->text(), + 'am_default' => $this->msg( 'allmessagesdefault' )->text() + ]; + } + + function getTitle() { + return SpecialPage::getTitleFor( 'Allmessages', false ); + } + + function isFieldSortable( $x ) { + return false; + } + + function getDefaultSort() { + return ''; + } + + function getQueryInfo() { + return ''; + } + +} diff --git a/includes/specials/pagers/BlockListPager.php b/includes/specials/pagers/BlockListPager.php new file mode 100644 index 0000000000..8857907712 --- /dev/null +++ b/includes/specials/pagers/BlockListPager.php @@ -0,0 +1,266 @@ +page = $page; + $this->conds = $conds; + $this->mDefaultDirection = IndexPager::DIR_DESCENDING; + parent::__construct( $page->getContext() ); + } + + function getFieldNames() { + static $headers = null; + + if ( $headers === null ) { + $headers = [ + 'ipb_timestamp' => 'blocklist-timestamp', + 'ipb_target' => 'blocklist-target', + 'ipb_expiry' => 'blocklist-expiry', + 'ipb_by' => 'blocklist-by', + 'ipb_params' => 'blocklist-params', + 'ipb_reason' => 'blocklist-reason', + ]; + foreach ( $headers as $key => $val ) { + $headers[$key] = $this->msg( $val )->text(); + } + } + + return $headers; + } + + function formatValue( $name, $value ) { + static $msg = null; + if ( $msg === null ) { + $keys = [ + 'anononlyblock', + 'createaccountblock', + 'noautoblockblock', + 'emailblock', + 'blocklist-nousertalk', + 'unblocklink', + 'change-blocklink', + ]; + + foreach ( $keys as $key ) { + $msg[$key] = $this->msg( $key )->escaped(); + } + } + + /** @var $row object */ + $row = $this->mCurrentRow; + + $language = $this->getLanguage(); + + $formatted = ''; + + switch ( $name ) { + case 'ipb_timestamp': + $formatted = htmlspecialchars( $language->userTimeAndDate( $value, $this->getUser() ) ); + break; + + case 'ipb_target': + if ( $row->ipb_auto ) { + $formatted = $this->msg( 'autoblockid', $row->ipb_id )->parse(); + } else { + list( $target, $type ) = Block::parseTarget( $row->ipb_address ); + switch ( $type ) { + case Block::TYPE_USER: + case Block::TYPE_IP: + $formatted = Linker::userLink( $target->getId(), $target ); + $formatted .= Linker::userToolLinks( + $target->getId(), + $target, + false, + Linker::TOOL_LINKS_NOBLOCK + ); + break; + case Block::TYPE_RANGE: + $formatted = htmlspecialchars( $target ); + } + } + break; + + case 'ipb_expiry': + $formatted = htmlspecialchars( $language->formatExpiry( + $value, + /* User preference timezone */true + ) ); + if ( $this->getUser()->isAllowed( 'block' ) ) { + if ( $row->ipb_auto ) { + $links[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Unblock' ), + $msg['unblocklink'], + [], + [ 'wpTarget' => "#{$row->ipb_id}" ] + ); + } else { + $links[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Unblock', $row->ipb_address ), + $msg['unblocklink'] + ); + $links[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Block', $row->ipb_address ), + $msg['change-blocklink'] + ); + } + $formatted .= ' ' . Html::rawElement( + 'span', + [ 'class' => 'mw-blocklist-actions' ], + $this->msg( 'parentheses' )->rawParams( + $language->pipeList( $links ) )->escaped() + ); + } + break; + + case 'ipb_by': + if ( isset( $row->by_user_name ) ) { + $formatted = Linker::userLink( $value, $row->by_user_name ); + $formatted .= Linker::userToolLinks( $value, $row->by_user_name ); + } else { + $formatted = htmlspecialchars( $row->ipb_by_text ); // foreign user? + } + break; + + case 'ipb_reason': + $formatted = Linker::formatComment( $value ); + break; + + case 'ipb_params': + $properties = []; + if ( $row->ipb_anon_only ) { + $properties[] = $msg['anononlyblock']; + } + if ( $row->ipb_create_account ) { + $properties[] = $msg['createaccountblock']; + } + if ( $row->ipb_user && !$row->ipb_enable_autoblock ) { + $properties[] = $msg['noautoblockblock']; + } + + if ( $row->ipb_block_email ) { + $properties[] = $msg['emailblock']; + } + + if ( !$row->ipb_allow_usertalk ) { + $properties[] = $msg['blocklist-nousertalk']; + } + + $formatted = $language->commaList( $properties ); + break; + + default: + $formatted = "Unable to format $name"; + break; + } + + return $formatted; + } + + function getQueryInfo() { + $info = [ + 'tables' => [ 'ipblocks', 'user' ], + 'fields' => [ + 'ipb_id', + 'ipb_address', + 'ipb_user', + 'ipb_by', + 'ipb_by_text', + 'by_user_name' => 'user_name', + 'ipb_reason', + 'ipb_timestamp', + 'ipb_auto', + 'ipb_anon_only', + 'ipb_create_account', + 'ipb_enable_autoblock', + 'ipb_expiry', + 'ipb_range_start', + 'ipb_range_end', + 'ipb_deleted', + 'ipb_block_email', + 'ipb_allow_usertalk', + ], + 'conds' => $this->conds, + 'join_conds' => [ 'user' => [ 'LEFT JOIN', 'user_id = ipb_by' ] ] + ]; + + # Filter out any expired blocks + $db = $this->getDatabase(); + $info['conds'][] = 'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ); + + # Is the user allowed to see hidden blocks? + if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { + $info['conds']['ipb_deleted'] = 0; + } + + return $info; + } + + public function getTableClass() { + return parent::getTableClass() . ' mw-blocklist'; + } + + function getIndexField() { + return 'ipb_timestamp'; + } + + function getDefaultSort() { + return 'ipb_timestamp'; + } + + function isFieldSortable( $name ) { + return false; + } + + /** + * Do a LinkBatch query to minimise database load when generating all these links + * @param ResultWrapper $result + */ + function preprocessResults( $result ) { + # Do a link batch query + $lb = new LinkBatch; + $lb->setCaller( __METHOD__ ); + + foreach ( $result as $row ) { + $lb->add( NS_USER, $row->ipb_address ); + $lb->add( NS_USER_TALK, $row->ipb_address ); + + if ( isset( $row->by_user_name ) ) { + $lb->add( NS_USER, $row->by_user_name ); + $lb->add( NS_USER_TALK, $row->by_user_name ); + } + } + + $lb->execute(); + } + +} diff --git a/includes/specials/pagers/CategoryPager.php b/includes/specials/pagers/CategoryPager.php new file mode 100644 index 0000000000..fd2ac1f75f --- /dev/null +++ b/includes/specials/pagers/CategoryPager.php @@ -0,0 +1,126 @@ +setOffset( $from ); + $this->setIncludeOffset( true ); + } + + $this->linkRenderer = $linkRenderer; + } + + function getQueryInfo() { + return [ + 'tables' => [ 'category' ], + 'fields' => [ 'cat_title', 'cat_pages' ], + 'conds' => [ 'cat_pages > 0' ], + 'options' => [ 'USE INDEX' => 'cat_title' ], + ]; + } + + function getIndexField() { +# return array( 'abc' => 'cat_title', 'count' => 'cat_pages' ); + return 'cat_title'; + } + + function getDefaultQuery() { + parent::getDefaultQuery(); + unset( $this->mDefaultQuery['from'] ); + + return $this->mDefaultQuery; + } + +# protected function getOrderTypeMessages() { +# return array( 'abc' => 'special-categories-sort-abc', +# 'count' => 'special-categories-sort-count' ); +# } + + protected function getDefaultDirections() { +# return array( 'abc' => false, 'count' => true ); + return false; + } + + /* Override getBody to apply LinksBatch on resultset before actually outputting anything. */ + public function getBody() { + $batch = new LinkBatch; + + $this->mResult->rewind(); + + foreach ( $this->mResult as $row ) { + $batch->addObj( Title::makeTitleSafe( NS_CATEGORY, $row->cat_title ) ); + } + $batch->execute(); + $this->mResult->rewind(); + + return parent::getBody(); + } + + function formatRow( $result ) { + $title = new TitleValue( NS_CATEGORY, $result->cat_title ); + $text = $title->getText(); + $link = $this->linkRenderer->renderHtmlLink( $title, $text ); + + $count = $this->msg( 'nmembers' )->numParams( $result->cat_pages )->escaped(); + return Html::rawElement( 'li', null, $this->getLanguage()->specialList( $link, $count ) ) . "\n"; + } + + public function getStartForm( $from ) { + return Xml::tags( + 'form', + [ 'method' => 'get', 'action' => wfScript() ], + Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . + Xml::fieldset( + $this->msg( 'categories' )->text(), + Xml::inputLabel( + $this->msg( 'categoriesfrom' )->text(), + 'from', 'from', 20, $from, [ 'class' => 'mw-ui-input-inline' ] ) . + ' ' . + Html::submitButton( + $this->msg( 'categories-submit' )->text(), + [], [ 'mw-ui-progressive' ] + ) + ) + ); + } +} diff --git a/includes/specials/pagers/ContribsPager.php b/includes/specials/pagers/ContribsPager.php new file mode 100644 index 0000000000..d90c37bab7 --- /dev/null +++ b/includes/specials/pagers/ContribsPager.php @@ -0,0 +1,526 @@ +messages[$msg] = $this->msg( $msg )->escaped(); + } + + $this->target = isset( $options['target'] ) ? $options['target'] : ''; + $this->contribs = isset( $options['contribs'] ) ? $options['contribs'] : 'users'; + $this->namespace = isset( $options['namespace'] ) ? $options['namespace'] : ''; + $this->tagFilter = isset( $options['tagfilter'] ) ? $options['tagfilter'] : false; + $this->nsInvert = isset( $options['nsInvert'] ) ? $options['nsInvert'] : false; + $this->associated = isset( $options['associated'] ) ? $options['associated'] : false; + + $this->deletedOnly = !empty( $options['deletedOnly'] ); + $this->topOnly = !empty( $options['topOnly'] ); + $this->newOnly = !empty( $options['newOnly'] ); + + $year = isset( $options['year'] ) ? $options['year'] : false; + $month = isset( $options['month'] ) ? $options['month'] : false; + $this->getDateCond( $year, $month ); + + // Most of this code will use the 'contributions' group DB, which can map to slaves + // with extra user based indexes or partioning by user. The additional metadata + // queries should use a regular slave since the lookup pattern is not all by user. + $this->mDbSecondary = wfGetDB( DB_SLAVE ); // any random slave + $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); + } + + function getDefaultQuery() { + $query = parent::getDefaultQuery(); + $query['target'] = $this->target; + + return $query; + } + + /** + * This method basically executes the exact same code as the parent class, though with + * a hook added, to allow extensions to add additional queries. + * + * @param string $offset Index offset, inclusive + * @param int $limit Exact query limit + * @param bool $descending Query direction, false for ascending, true for descending + * @return ResultWrapper + */ + function reallyDoQuery( $offset, $limit, $descending ) { + list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( + $offset, + $limit, + $descending + ); + + /* + * This hook will allow extensions to add in additional queries, so they can get their data + * in My Contributions as well. Extensions should append their results to the $data array. + * + * Extension queries have to implement the navbar requirement as well. They should + * - have a column aliased as $pager->getIndexField() + * - have LIMIT set + * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset + * - have the ORDER BY specified based upon the details provided by the navbar + * + * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY + * + * &$data: an array of results of all contribs queries + * $pager: the ContribsPager object hooked into + * $offset: see phpdoc above + * $limit: see phpdoc above + * $descending: see phpdoc above + */ + $data = [ $this->mDb->select( + $tables, $fields, $conds, $fname, $options, $join_conds + ) ]; + Hooks::run( + 'ContribsPager::reallyDoQuery', + [ &$data, $this, $offset, $limit, $descending ] + ); + + $result = []; + + // loop all results and collect them in an array + foreach ( $data as $query ) { + foreach ( $query as $i => $row ) { + // use index column as key, allowing us to easily sort in PHP + $result[$row->{$this->getIndexField()} . "-$i"] = $row; + } + } + + // sort results + if ( $descending ) { + ksort( $result ); + } else { + krsort( $result ); + } + + // enforce limit + $result = array_slice( $result, 0, $limit ); + + // get rid of array keys + $result = array_values( $result ); + + return new FakeResultWrapper( $result ); + } + + function getQueryInfo() { + list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond(); + + $user = $this->getUser(); + $conds = array_merge( $userCond, $this->getNamespaceCond() ); + + // Paranoia: avoid brute force searches (bug 17342) + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::SUPPRESSED_USER ) . + ' != ' . Revision::SUPPRESSED_USER; + } + + # Don't include orphaned revisions + $join_cond['page'] = Revision::pageJoinCond(); + # Get the current user name for accounts + $join_cond['user'] = Revision::userJoinCond(); + + $options = []; + if ( $index ) { + $options['USE INDEX'] = [ 'revision' => $index ]; + } + + $queryInfo = [ + 'tables' => $tables, + 'fields' => array_merge( + Revision::selectFields(), + Revision::selectUserFields(), + [ 'page_namespace', 'page_title', 'page_is_new', + 'page_latest', 'page_is_redirect', 'page_len' ] + ), + 'conds' => $conds, + 'options' => $options, + 'join_conds' => $join_cond + ]; + + ChangeTags::modifyDisplayQuery( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + $queryInfo['join_conds'], + $queryInfo['options'], + $this->tagFilter + ); + + Hooks::run( 'ContribsPager::getQueryInfo', [ &$this, &$queryInfo ] ); + + return $queryInfo; + } + + function getUserCond() { + $condition = []; + $join_conds = []; + $tables = [ 'revision', 'page', 'user' ]; + $index = false; + if ( $this->contribs == 'newbie' ) { + $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ ); + $condition[] = 'rev_user >' . (int)( $max - $max / 100 ); + # ignore local groups with the bot right + # @todo FIXME: Global groups may have 'bot' rights + $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); + if ( count( $groupsWithBotPermission ) ) { + $tables[] = 'user_groups'; + $condition[] = 'ug_group IS NULL'; + $join_conds['user_groups'] = [ + 'LEFT JOIN', [ + 'ug_user = rev_user', + 'ug_group' => $groupsWithBotPermission + ] + ]; + } + } else { + $uid = User::idFromName( $this->target ); + if ( $uid ) { + $condition['rev_user'] = $uid; + $index = 'user_timestamp'; + } else { + $condition['rev_user_text'] = $this->target; + $index = 'usertext_timestamp'; + } + } + + if ( $this->deletedOnly ) { + $condition[] = 'rev_deleted != 0'; + } + + if ( $this->topOnly ) { + $condition[] = 'rev_id = page_latest'; + } + + if ( $this->newOnly ) { + $condition[] = 'rev_parent_id = 0'; + } + + return [ $tables, $index, $condition, $join_conds ]; + } + + function getNamespaceCond() { + if ( $this->namespace !== '' ) { + $selectedNS = $this->mDb->addQuotes( $this->namespace ); + $eq_op = $this->nsInvert ? '!=' : '='; + $bool_op = $this->nsInvert ? 'AND' : 'OR'; + + if ( !$this->associated ) { + return [ "page_namespace $eq_op $selectedNS" ]; + } + + $associatedNS = $this->mDb->addQuotes( + MWNamespace::getAssociated( $this->namespace ) + ); + + return [ + "page_namespace $eq_op $selectedNS " . + $bool_op . + " page_namespace $eq_op $associatedNS" + ]; + } + + return []; + } + + function getIndexField() { + return 'rev_timestamp'; + } + + function doBatchLookups() { + # Do a link batch query + $this->mResult->seek( 0 ); + $parentRevIds = []; + $this->mParentLens = []; + $batch = new LinkBatch(); + # Give some pointers to make (last) links + foreach ( $this->mResult as $row ) { + if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) { + $parentRevIds[] = $row->rev_parent_id; + } + if ( isset( $row->rev_id ) ) { + $this->mParentLens[$row->rev_id] = $row->rev_len; + if ( $this->contribs === 'newbie' ) { // multiple users + $batch->add( NS_USER, $row->user_name ); + $batch->add( NS_USER_TALK, $row->user_name ); + } + $batch->add( $row->page_namespace, $row->page_title ); + } + } + # Fetch rev_len for revisions not already scanned above + $this->mParentLens += Revision::getParentLengths( + $this->mDbSecondary, + array_diff( $parentRevIds, array_keys( $this->mParentLens ) ) + ); + $batch->execute(); + $this->mResult->seek( 0 ); + } + + /** + * @return string + */ + function getStartBody() { + return "\n"; + } + + /** + * Generates each row in the contributions list. + * + * Contributions which are marked "top" are currently on top of the history. + * For these contributions, a [rollback] link is shown for users with roll- + * back privileges. The rollback link restores the most recent version that + * was not written by the target user. + * + * @todo This would probably look a lot nicer in a table. + * @param object $row + * @return string + */ + function formatRow( $row ) { + + $ret = ''; + $classes = []; + + /* + * There may be more than just revision rows. To make sure that we'll only be processing + * revisions here, let's _try_ to build a revision out of our row (without displaying + * notices though) and then trying to grab data from the built object. If we succeed, + * we're definitely dealing with revision data and we may proceed, if not, we'll leave it + * to extensions to subscribe to the hook to parse the row. + */ + MediaWiki\suppressWarnings(); + try { + $rev = new Revision( $row ); + $validRevision = (bool)$rev->getId(); + } catch ( Exception $e ) { + $validRevision = false; + } + MediaWiki\restoreWarnings(); + + if ( $validRevision ) { + $classes = []; + + $page = Title::newFromRow( $row ); + $link = Linker::link( + $page, + htmlspecialchars( $page->getPrefixedText() ), + [ 'class' => 'mw-contributions-title' ], + $page->isRedirect() ? [ 'redirect' => 'no' ] : [] + ); + # Mark current revisions + $topmarktext = ''; + $user = $this->getUser(); + if ( $row->rev_id == $row->page_latest ) { + $topmarktext .= '' . $this->messages['uctop'] . ''; + # Add rollback link + if ( !$row->page_is_new && $page->quickUserCan( 'rollback', $user ) + && $page->quickUserCan( 'edit', $user ) + ) { + $this->preventClickjacking(); + $topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext() ); + } + } + # Is there a visible previous revision? + if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) { + $difftext = Linker::linkKnown( + $page, + $this->messages['diff'], + [], + [ + 'diff' => 'prev', + 'oldid' => $row->rev_id + ] + ); + } else { + $difftext = $this->messages['diff']; + } + $histlink = Linker::linkKnown( + $page, + $this->messages['hist'], + [], + [ 'action' => 'history' ] + ); + + if ( $row->rev_parent_id === null ) { + // For some reason rev_parent_id isn't populated for this row. + // Its rumoured this is true on wikipedia for some revisions (bug 34922). + // Next best thing is to have the total number of bytes. + $chardiff = ' . . '; + $chardiff .= Linker::formatRevisionSize( $row->rev_len ); + $chardiff .= ' . . '; + } else { + $parentLen = 0; + if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) { + $parentLen = $this->mParentLens[$row->rev_parent_id]; + } + + $chardiff = ' . . '; + $chardiff .= ChangesList::showCharacterDifference( + $parentLen, + $row->rev_len, + $this->getContext() + ); + $chardiff .= ' . . '; + } + + $lang = $this->getLanguage(); + $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true ); + $date = $lang->userTimeAndDate( $row->rev_timestamp, $user ); + if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { + $d = Linker::linkKnown( + $page, + htmlspecialchars( $date ), + [ 'class' => 'mw-changeslist-date' ], + [ 'oldid' => intval( $row->rev_id ) ] + ); + } else { + $d = htmlspecialchars( $date ); + } + if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $d = '' . $d . ''; + } + + # Show user names for /newbies as there may be different users. + # Note that we already excluded rows with hidden user names. + if ( $this->contribs == 'newbie' ) { + $userlink = ' . . ' . $lang->getDirMark() + . Linker::userLink( $rev->getUser(), $rev->getUserText() ); + $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( + Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' '; + } else { + $userlink = ''; + } + + if ( $rev->getParentId() === 0 ) { + $nflag = ChangesList::flag( 'newpage' ); + } else { + $nflag = ''; + } + + if ( $rev->isMinor() ) { + $mflag = ChangesList::flag( 'minor' ); + } else { + $mflag = ''; + } + + $del = Linker::getRevDeleteLink( $user, $rev, $page ); + if ( $del !== '' ) { + $del .= ' '; + } + + $diffHistLinks = $this->msg( 'parentheses' ) + ->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink ) + ->escaped(); + $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} "; + $ret .= "{$link}{$userlink} {$comment} {$topmarktext}"; + + # Denote if username is redacted for this edit + if ( $rev->isDeleted( Revision::DELETED_USER ) ) { + $ret .= " " . + $this->msg( 'rev-deleted-user-contribs' )->escaped() . + ""; + } + + # Tags, if any. + list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( + $row->ts_tags, + 'contributions', + $this->getContext() + ); + $classes = array_merge( $classes, $newClasses ); + $ret .= " $tagSummary"; + } + + // Let extensions add data + Hooks::run( 'ContributionsLineEnding', [ $this, &$ret, $row, &$classes ] ); + + if ( $classes === [] && $ret === '' ) { + wfDebug( "Dropping Special:Contribution row that could not be formatted\n" ); + $ret = "\n"; + } else { + $ret = Html::rawElement( 'li', [ 'class' => $classes ], $ret ) . "\n"; + } + + return $ret; + } + + /** + * Overwrite Pager function and return a helpful comment + * @return string + */ + function getSqlComment() { + if ( $this->namespace || $this->deletedOnly ) { + // potentially slow, see CR r58153 + return 'contributions page filtered for namespace or RevisionDeleted edits'; + } else { + return 'contributions page unfiltered'; + } + } + + protected function preventClickjacking() { + $this->preventClickjacking = true; + } + + /** + * @return bool + */ + public function getPreventClickjacking() { + return $this->preventClickjacking; + } +} diff --git a/includes/specials/pagers/DeletedContribsPager.php b/includes/specials/pagers/DeletedContribsPager.php new file mode 100644 index 0000000000..f2421f871a --- /dev/null +++ b/includes/specials/pagers/DeletedContribsPager.php @@ -0,0 +1,355 @@ +messages[$msg] = $this->msg( $msg )->escaped(); + } + $this->target = $target; + $this->namespace = $namespace; + $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); + } + + function getDefaultQuery() { + $query = parent::getDefaultQuery(); + $query['target'] = $this->target; + + return $query; + } + + function getQueryInfo() { + list( $index, $userCond ) = $this->getUserCond(); + $conds = array_merge( $userCond, $this->getNamespaceCond() ); + $user = $this->getUser(); + // Paranoia: avoid brute force searches (bug 17792) + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::DELETED_USER ) . ' = 0'; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::SUPPRESSED_USER ) . + ' != ' . Revision::SUPPRESSED_USER; + } + + return [ + 'tables' => [ 'archive' ], + 'fields' => [ + 'ar_rev_id', 'ar_namespace', 'ar_title', 'ar_timestamp', 'ar_comment', + 'ar_minor_edit', 'ar_user', 'ar_user_text', 'ar_deleted' + ], + 'conds' => $conds, + 'options' => [ 'USE INDEX' => $index ] + ]; + } + + /** + * This method basically executes the exact same code as the parent class, though with + * a hook added, to allow extensions to add additional queries. + * + * @param string $offset Index offset, inclusive + * @param int $limit Exact query limit + * @param bool $descending Query direction, false for ascending, true for descending + * @return ResultWrapper + */ + function reallyDoQuery( $offset, $limit, $descending ) { + $data = [ parent::reallyDoQuery( $offset, $limit, $descending ) ]; + + // This hook will allow extensions to add in additional queries, nearly + // identical to ContribsPager::reallyDoQuery. + Hooks::run( + 'DeletedContribsPager::reallyDoQuery', + [ &$data, $this, $offset, $limit, $descending ] + ); + + $result = []; + + // loop all results and collect them in an array + foreach ( $data as $query ) { + foreach ( $query as $i => $row ) { + // use index column as key, allowing us to easily sort in PHP + $result[$row->{$this->getIndexField()} . "-$i"] = $row; + } + } + + // sort results + if ( $descending ) { + ksort( $result ); + } else { + krsort( $result ); + } + + // enforce limit + $result = array_slice( $result, 0, $limit ); + + // get rid of array keys + $result = array_values( $result ); + + return new FakeResultWrapper( $result ); + } + + function getUserCond() { + $condition = []; + + $condition['ar_user_text'] = $this->target; + $index = 'usertext_timestamp'; + + return [ $index, $condition ]; + } + + function getIndexField() { + return 'ar_timestamp'; + } + + function getStartBody() { + return "\n"; + } + + function getNavigationBar() { + if ( isset( $this->mNavigationBar ) ) { + return $this->mNavigationBar; + } + + $linkTexts = [ + 'prev' => $this->msg( 'pager-newer-n' )->numParams( $this->mLimit )->escaped(), + 'next' => $this->msg( 'pager-older-n' )->numParams( $this->mLimit )->escaped(), + 'first' => $this->msg( 'histlast' )->escaped(), + 'last' => $this->msg( 'histfirst' )->escaped() + ]; + + $pagingLinks = $this->getPagingLinks( $linkTexts ); + $limitLinks = $this->getLimitLinks(); + $lang = $this->getLanguage(); + $limits = $lang->pipeList( $limitLinks ); + + $firstLast = $lang->pipeList( [ $pagingLinks['first'], $pagingLinks['last'] ] ); + $firstLast = $this->msg( 'parentheses' )->rawParams( $firstLast )->escaped(); + $prevNext = $this->msg( 'viewprevnext' ) + ->rawParams( + $pagingLinks['prev'], + $pagingLinks['next'], + $limits + )->escaped(); + $separator = $this->msg( 'word-separator' )->escaped(); + $this->mNavigationBar = $firstLast . $separator . $prevNext; + + return $this->mNavigationBar; + } + + function getNamespaceCond() { + if ( $this->namespace !== '' ) { + return [ 'ar_namespace' => (int)$this->namespace ]; + } else { + return []; + } + } + + /** + * Generates each row in the contributions list. + * + * @todo This would probably look a lot nicer in a table. + * @param stdClass $row + * @return string + */ + function formatRow( $row ) { + $ret = ''; + $classes = []; + + /* + * There may be more than just revision rows. To make sure that we'll only be processing + * revisions here, let's _try_ to build a revision out of our row (without displaying + * notices though) and then trying to grab data from the built object. If we succeed, + * we're definitely dealing with revision data and we may proceed, if not, we'll leave it + * to extensions to subscribe to the hook to parse the row. + */ + MediaWiki\suppressWarnings(); + try { + $rev = Revision::newFromArchiveRow( $row ); + $validRevision = (bool)$rev->getId(); + } catch ( Exception $e ) { + $validRevision = false; + } + MediaWiki\restoreWarnings(); + + if ( $validRevision ) { + $ret = $this->formatRevisionRow( $row ); + } + + // Let extensions add data + Hooks::run( 'DeletedContributionsLineEnding', [ $this, &$ret, $row, &$classes ] ); + + if ( $classes === [] && $ret === '' ) { + wfDebug( "Dropping Special:DeletedContribution row that could not be formatted\n" ); + $ret = "\n"; + } else { + $ret = Html::rawElement( 'li', [ 'class' => $classes ], $ret ) . "\n"; + } + + return $ret; + } + + /** + * Generates each row in the contributions list for archive entries. + * + * Contributions which are marked "top" are currently on top of the history. + * For these contributions, a [rollback] link is shown for users with sysop + * privileges. The rollback link restores the most recent version that was not + * written by the target user. + * + * @todo This would probably look a lot nicer in a table. + * @param stdClass $row + * @return string + */ + function formatRevisionRow( $row ) { + $page = Title::makeTitle( $row->ar_namespace, $row->ar_title ); + + $rev = new Revision( [ + 'title' => $page, + 'id' => $row->ar_rev_id, + 'comment' => $row->ar_comment, + 'user' => $row->ar_user, + 'user_text' => $row->ar_user_text, + 'timestamp' => $row->ar_timestamp, + 'minor_edit' => $row->ar_minor_edit, + 'deleted' => $row->ar_deleted, + ] ); + + $undelete = SpecialPage::getTitleFor( 'Undelete' ); + + $logs = SpecialPage::getTitleFor( 'Log' ); + $dellog = Linker::linkKnown( + $logs, + $this->messages['deletionlog'], + [], + [ + 'type' => 'delete', + 'page' => $page->getPrefixedText() + ] + ); + + $reviewlink = Linker::linkKnown( + SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ), + $this->messages['undeleteviewlink'] + ); + + $user = $this->getUser(); + + if ( $user->isAllowed( 'deletedtext' ) ) { + $last = Linker::linkKnown( + $undelete, + $this->messages['diff'], + [], + [ + 'target' => $page->getPrefixedText(), + 'timestamp' => $rev->getTimestamp(), + 'diff' => 'prev' + ] + ); + } else { + $last = $this->messages['diff']; + } + + $comment = Linker::revComment( $rev ); + $date = $this->getLanguage()->userTimeAndDate( $rev->getTimestamp(), $user ); + $date = htmlspecialchars( $date ); + + if ( !$user->isAllowed( 'undelete' ) || !$rev->userCan( Revision::DELETED_TEXT, $user ) ) { + $link = $date; // unusable link + } else { + $link = Linker::linkKnown( + $undelete, + $date, + [ 'class' => 'mw-changeslist-date' ], + [ + 'target' => $page->getPrefixedText(), + 'timestamp' => $rev->getTimestamp() + ] + ); + } + // Style deleted items + if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $link = '' . $link . ''; + } + + $pagelink = Linker::link( + $page, + null, + [ 'class' => 'mw-changeslist-title' ] + ); + + if ( $rev->isMinor() ) { + $mflag = ChangesList::flag( 'minor' ); + } else { + $mflag = ''; + } + + // Revision delete link + $del = Linker::getRevDeleteLink( $user, $rev, $page ); + if ( $del ) { + $del .= ' '; + } + + $tools = Html::rawElement( + 'span', + [ 'class' => 'mw-deletedcontribs-tools' ], + $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList( + [ $last, $dellog, $reviewlink ] ) )->escaped() + ); + + $separator = '. .'; + $ret = "{$del}{$link} {$tools} {$separator} {$mflag} {$pagelink} {$comment}"; + + # Denote if username is redacted for this edit + if ( $rev->isDeleted( Revision::DELETED_USER ) ) { + $ret .= " " . $this->msg( 'rev-deleted-user-contribs' )->escaped() . ""; + } + + return $ret; + } + + /** + * Get the Database object in use + * + * @return IDatabase + */ + public function getDatabase() { + return $this->mDb; + } +} diff --git a/includes/specials/pagers/ImageListPager.php b/includes/specials/pagers/ImageListPager.php new file mode 100644 index 0000000000..48f60caf15 --- /dev/null +++ b/includes/specials/pagers/ImageListPager.php @@ -0,0 +1,602 @@ +setContext( $context ); + $this->mIncluding = $including; + $this->mShowAll = $showAll; + + if ( $userName !== null && $userName !== '' ) { + $nt = Title::newFromText( $userName, NS_USER ); + if ( is_null( $nt ) ) { + $this->outputUserDoesNotExist( $userName ); + } else { + $this->mUserName = $nt->getText(); + $user = User::newFromName( $this->mUserName, false ); + if ( $user ) { + $this->mUser = $user; + } + if ( !$user || ( $user->isAnon() && !User::isIP( $user->getName() ) ) ) { + $this->outputUserDoesNotExist( $userName ); + } + } + } + + if ( $search !== '' && !$this->getConfig()->get( 'MiserMode' ) ) { + $this->mSearch = $search; + $nt = Title::newFromText( $this->mSearch ); + + if ( $nt ) { + $dbr = wfGetDB( DB_SLAVE ); + $this->mQueryConds[] = 'LOWER(img_name)' . + $dbr->buildLike( $dbr->anyString(), + strtolower( $nt->getDBkey() ), $dbr->anyString() ); + } + } + + if ( !$including ) { + if ( $this->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) { + $this->mDefaultDirection = IndexPager::DIR_DESCENDING; + } else { + $this->mDefaultDirection = IndexPager::DIR_ASCENDING; + } + } else { + $this->mDefaultDirection = IndexPager::DIR_DESCENDING; + } + + parent::__construct( $context ); + } + + /** + * Get the user relevant to the ImageList + * + * @return User|null + */ + function getRelevantUser() { + return $this->mUser; + } + + /** + * Add a message to the output stating that the user doesn't exist + * + * @param string $userName Unescaped user name + */ + protected function outputUserDoesNotExist( $userName ) { + $this->getOutput()->wrapWikiMsg( + "
\n$1\n
", + [ + 'listfiles-userdoesnotexist', + wfEscapeWikiText( $userName ), + ] + ); + } + + /** + * Build the where clause of the query. + * + * Replaces the older mQueryConds member variable. + * @param string $table Either "image" or "oldimage" + * @return array The query conditions. + */ + protected function buildQueryConds( $table ) { + $prefix = $table === 'image' ? 'img' : 'oi'; + $conds = []; + + if ( !is_null( $this->mUserName ) ) { + $conds[$prefix . '_user_text'] = $this->mUserName; + } + + if ( $this->mSearch !== '' ) { + $nt = Title::newFromText( $this->mSearch ); + if ( $nt ) { + $dbr = wfGetDB( DB_SLAVE ); + $conds[] = 'LOWER(' . $prefix . '_name)' . + $dbr->buildLike( $dbr->anyString(), + strtolower( $nt->getDBkey() ), $dbr->anyString() ); + } + } + + if ( $table === 'oldimage' ) { + // Don't want to deal with revdel. + // Future fixme: Show partial information as appropriate. + // Would have to be careful about filtering by username when username is deleted. + $conds['oi_deleted'] = 0; + } + + // Add mQueryConds in case anyone was subclassing and using the old variable. + return $conds + $this->mQueryConds; + } + + /** + * @return array + */ + function getFieldNames() { + if ( !$this->mFieldNames ) { + $this->mFieldNames = [ + 'img_timestamp' => $this->msg( 'listfiles_date' )->text(), + 'img_name' => $this->msg( 'listfiles_name' )->text(), + 'thumb' => $this->msg( 'listfiles_thumb' )->text(), + 'img_size' => $this->msg( 'listfiles_size' )->text(), + ]; + if ( is_null( $this->mUserName ) ) { + // Do not show username if filtering by username + $this->mFieldNames['img_user_text'] = $this->msg( 'listfiles_user' )->text(); + } + // img_description down here, in order so that its still after the username field. + $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text(); + + if ( !$this->getConfig()->get( 'MiserMode' ) && !$this->mShowAll ) { + $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text(); + } + if ( $this->mShowAll ) { + $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text(); + } + } + + return $this->mFieldNames; + } + + function isFieldSortable( $field ) { + if ( $this->mIncluding ) { + return false; + } + $sortable = [ 'img_timestamp', 'img_name', 'img_size' ]; + /* For reference, the indicies we can use for sorting are: + * On the image table: img_usertext_timestamp, img_size, img_timestamp + * On oldimage: oi_usertext_timestamp, oi_name_timestamp + * + * In particular that means we cannot sort by timestamp when not filtering + * by user and including old images in the results. Which is sad. + */ + if ( $this->getConfig()->get( 'MiserMode' ) && !is_null( $this->mUserName ) ) { + // If we're sorting by user, the index only supports sorting by time. + if ( $field === 'img_timestamp' ) { + return true; + } else { + return false; + } + } elseif ( $this->getConfig()->get( 'MiserMode' ) + && $this->mShowAll /* && mUserName === null */ + ) { + // no oi_timestamp index, so only alphabetical sorting in this case. + if ( $field === 'img_name' ) { + return true; + } else { + return false; + } + } + + return in_array( $field, $sortable ); + } + + function getQueryInfo() { + // Hacky Hacky Hacky - I want to get query info + // for two different tables, without reimplementing + // the pager class. + $qi = $this->getQueryInfoReal( $this->mTableName ); + + return $qi; + } + + /** + * Actually get the query info. + * + * This is to allow displaying both stuff from image and oldimage table. + * + * This is a bit hacky. + * + * @param string $table Either 'image' or 'oldimage' + * @return array Query info + */ + protected function getQueryInfoReal( $table ) { + $prefix = $table === 'oldimage' ? 'oi' : 'img'; + + $tables = [ $table ]; + $fields = array_keys( $this->getFieldNames() ); + + if ( $table === 'oldimage' ) { + foreach ( $fields as $id => &$field ) { + if ( substr( $field, 0, 4 ) !== 'img_' ) { + continue; + } + $field = $prefix . substr( $field, 3 ) . ' AS ' . $field; + } + $fields[array_search( 'top', $fields )] = "'no' AS top"; + } else { + if ( $this->mShowAll ) { + $fields[array_search( 'top', $fields )] = "'yes' AS top"; + } + } + $fields[] = $prefix . '_user AS img_user'; + $fields[array_search( 'thumb', $fields )] = $prefix . '_name AS thumb'; + + $options = $join_conds = []; + + # Depends on $wgMiserMode + # Will also not happen if mShowAll is true. + if ( isset( $this->mFieldNames['count'] ) ) { + $tables[] = 'oldimage'; + + # Need to rewrite this one + foreach ( $fields as &$field ) { + if ( $field == 'count' ) { + $field = 'COUNT(oi_archive_name) AS count'; + } + } + unset( $field ); + + $dbr = wfGetDB( DB_SLAVE ); + if ( $dbr->implicitGroupby() ) { + $options = [ 'GROUP BY' => 'img_name' ]; + } else { + $columnlist = preg_grep( '/^img/', array_keys( $this->getFieldNames() ) ); + $options = [ 'GROUP BY' => array_merge( [ 'img_user' ], $columnlist ) ]; + } + $join_conds = [ 'oldimage' => [ 'LEFT JOIN', 'oi_name = img_name' ] ]; + } + + return [ + 'tables' => $tables, + 'fields' => $fields, + 'conds' => $this->buildQueryConds( $table ), + 'options' => $options, + 'join_conds' => $join_conds + ]; + } + + /** + * Override reallyDoQuery to mix together two queries. + * + * @note $asc is named $descending in IndexPager base class. However + * it is true when the order is ascending, and false when the order + * is descending, so I renamed it to $asc here. + * @param int $offset + * @param int $limit + * @param bool $asc + * @return array + * @throws MWException + */ + function reallyDoQuery( $offset, $limit, $asc ) { + $prevTableName = $this->mTableName; + $this->mTableName = 'image'; + list( $tables, $fields, $conds, $fname, $options, $join_conds ) = + $this->buildQueryInfo( $offset, $limit, $asc ); + $imageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); + $this->mTableName = $prevTableName; + + if ( !$this->mShowAll ) { + return $imageRes; + } + + $this->mTableName = 'oldimage'; + + # Hacky... + $oldIndex = $this->mIndexField; + if ( substr( $this->mIndexField, 0, 4 ) !== 'img_' ) { + throw new MWException( "Expected to be sorting on an image table field" ); + } + $this->mIndexField = 'oi_' . substr( $this->mIndexField, 4 ); + + list( $tables, $fields, $conds, $fname, $options, $join_conds ) = + $this->buildQueryInfo( $offset, $limit, $asc ); + $oldimageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); + + $this->mTableName = $prevTableName; + $this->mIndexField = $oldIndex; + + return $this->combineResult( $imageRes, $oldimageRes, $limit, $asc ); + } + + /** + * Combine results from 2 tables. + * + * Note: This will throw away some results + * + * @param ResultWrapper $res1 + * @param ResultWrapper $res2 + * @param int $limit + * @param bool $ascending See note about $asc in $this->reallyDoQuery + * @return FakeResultWrapper $res1 and $res2 combined + */ + protected function combineResult( $res1, $res2, $limit, $ascending ) { + $res1->rewind(); + $res2->rewind(); + $topRes1 = $res1->next(); + $topRes2 = $res2->next(); + $resultArray = []; + for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) { + if ( strcmp( $topRes1->{$this->mIndexField}, $topRes2->{$this->mIndexField} ) > 0 ) { + if ( !$ascending ) { + $resultArray[] = $topRes1; + $topRes1 = $res1->next(); + } else { + $resultArray[] = $topRes2; + $topRes2 = $res2->next(); + } + } else { + if ( !$ascending ) { + $resultArray[] = $topRes2; + $topRes2 = $res2->next(); + } else { + $resultArray[] = $topRes1; + $topRes1 = $res1->next(); + } + } + } + + // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect + for ( ; $i < $limit && $topRes1; $i++ ) { + // @codingStandardsIgnoreEnd + $resultArray[] = $topRes1; + $topRes1 = $res1->next(); + } + + // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect + for ( ; $i < $limit && $topRes2; $i++ ) { + // @codingStandardsIgnoreEnd + $resultArray[] = $topRes2; + $topRes2 = $res2->next(); + } + + return new FakeResultWrapper( $resultArray ); + } + + function getDefaultSort() { + if ( $this->mShowAll && $this->getConfig()->get( 'MiserMode' ) && is_null( $this->mUserName ) ) { + // Unfortunately no index on oi_timestamp. + return 'img_name'; + } else { + return 'img_timestamp'; + } + } + + function doBatchLookups() { + $userIds = []; + $this->mResult->seek( 0 ); + foreach ( $this->mResult as $row ) { + $userIds[] = $row->img_user; + } + # Do a link batch query for names and userpages + UserCache::singleton()->doQuery( $userIds, [ 'userpage' ], __METHOD__ ); + } + + /** + * @param string $field + * @param string $value + * @return Message|string|int The return type depends on the value of $field: + * - thumb: string + * - img_timestamp: string + * - img_name: string + * - img_user_text: string + * - img_size: string + * - img_description: string + * - count: int + * - top: Message + * @throws MWException + */ + function formatValue( $field, $value ) { + switch ( $field ) { + case 'thumb': + $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ]; + $file = RepoGroup::singleton()->getLocalRepo()->findFile( $value, $opt ); + // If statement for paranoia + if ( $file ) { + $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] ); + + return $thumb->toHtml( [ 'desc-link' => true ] ); + } else { + return htmlspecialchars( $value ); + } + case 'img_timestamp': + // We may want to make this a link to the "old" version when displaying old files + return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) ); + case 'img_name': + static $imgfile = null; + if ( $imgfile === null ) { + $imgfile = $this->msg( 'imgfile' )->text(); + } + + // Weird files can maybe exist? Bug 22227 + $filePage = Title::makeTitleSafe( NS_FILE, $value ); + if ( $filePage ) { + $link = Linker::linkKnown( + $filePage, + htmlspecialchars( $filePage->getText() ) + ); + $download = Xml::element( 'a', + [ 'href' => wfLocalFile( $filePage )->getUrl() ], + $imgfile + ); + $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped(); + + // Add delete links if allowed + // From https://github.com/Wikia/app/pull/3859 + if ( $filePage->userCan( 'delete', $this->getUser() ) ) { + $deleteMsg = $this->msg( 'listfiles-delete' )->escaped(); + + $delete = Linker::linkKnown( + $filePage, $deleteMsg, [], [ 'action' => 'delete' ] + ); + $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped(); + + return "$link $download $delete"; + } + + return "$link $download"; + } else { + return htmlspecialchars( $value ); + } + case 'img_user_text': + if ( $this->mCurrentRow->img_user ) { + $name = User::whoIs( $this->mCurrentRow->img_user ); + $link = Linker::link( + Title::makeTitle( NS_USER, $name ), + htmlspecialchars( $name ) + ); + } else { + $link = htmlspecialchars( $value ); + } + + return $link; + case 'img_size': + return htmlspecialchars( $this->getLanguage()->formatSize( $value ) ); + case 'img_description': + return Linker::formatComment( $value ); + case 'count': + return intval( $value ) + 1; + case 'top': + // Messages: listfiles-latestversion-yes, listfiles-latestversion-no + return $this->msg( 'listfiles-latestversion-' . $value ); + default: + throw new MWException( "Unknown field '$field'" ); + } + } + + function getForm() { + $fields = []; + $fields['limit'] = [ + 'type' => 'select', + 'name' => 'limit', + 'label-message' => 'table_pager_limit_label', + 'options' => $this->getLimitSelectList(), + 'default' => $this->mLimit, + ]; + + if ( !$this->getConfig()->get( 'MiserMode' ) ) { + $fields['ilsearch'] = [ + 'type' => 'text', + 'name' => 'ilsearch', + 'id' => 'mw-ilsearch', + 'label-message' => 'listfiles_search_for', + 'default' => $this->mSearch, + 'size' => '40', + 'maxlength' => '255', + ]; + } + + $this->getOutput()->addModules( 'mediawiki.userSuggest' ); + $fields['user'] = [ + 'type' => 'text', + 'name' => 'user', + 'id' => 'mw-listfiles-user', + 'label-message' => 'username', + 'default' => $this->mUserName, + 'size' => '40', + 'maxlength' => '255', + 'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest + ]; + + $fields['ilshowall'] = [ + 'type' => 'check', + 'name' => 'ilshowall', + 'id' => 'mw-listfiles-show-all', + 'label-message' => 'listfiles-show-all', + 'default' => $this->mShowAll, + ]; + + $query = $this->getRequest()->getQueryValues(); + unset( $query['title'] ); + unset( $query['limit'] ); + unset( $query['ilsearch'] ); + unset( $query['ilshowall'] ); + unset( $query['user'] ); + + $form = new HTMLForm( $fields, $this->getContext() ); + + $form->setMethod( 'get' ); + $form->setTitle( $this->getTitle() ); + $form->setId( 'mw-listfiles-form' ); + $form->setWrapperLegendMsg( 'listfiles' ); + $form->setSubmitTextMsg( 'table_pager_limit_submit' ); + $form->addHiddenFields( $query ); + + $form->prepareForm(); + $form->displayForm( '' ); + } + + function getTableClass() { + return parent::getTableClass() . ' listfiles'; + } + + function getNavClass() { + return parent::getNavClass() . ' listfiles_nav'; + } + + function getSortHeaderClass() { + return parent::getSortHeaderClass() . ' listfiles_sort'; + } + + function getPagingQueries() { + $queries = parent::getPagingQueries(); + if ( !is_null( $this->mUserName ) ) { + # Append the username to the query string + foreach ( $queries as &$query ) { + if ( $query !== false ) { + $query['user'] = $this->mUserName; + } + } + } + + return $queries; + } + + function getDefaultQuery() { + $queries = parent::getDefaultQuery(); + if ( !isset( $queries['user'] ) && !is_null( $this->mUserName ) ) { + $queries['user'] = $this->mUserName; + } + + return $queries; + } + + function getTitle() { + return SpecialPage::getTitleFor( 'Listfiles' ); + } +} diff --git a/includes/specials/pagers/MergeHistoryPager.php b/includes/specials/pagers/MergeHistoryPager.php new file mode 100644 index 0000000000..0b9587c422 --- /dev/null +++ b/includes/specials/pagers/MergeHistoryPager.php @@ -0,0 +1,99 @@ +mForm = $form; + $this->mConds = $conds; + $this->title = $source; + $this->articleID = $source->getArticleID(); + + $dbr = wfGetDB( DB_SLAVE ); + $maxtimestamp = $dbr->selectField( + 'revision', + 'MIN(rev_timestamp)', + [ 'rev_page' => $dest->getArticleID() ], + __METHOD__ + ); + $this->maxTimestamp = $maxtimestamp; + + parent::__construct( $form->getContext() ); + } + + function getStartBody() { + # Do a link batch query + $this->mResult->seek( 0 ); + $batch = new LinkBatch(); + # Give some pointers to make (last) links + $this->mForm->prevId = []; + foreach ( $this->mResult as $row ) { + $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); + $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) ); + + $rev_id = isset( $rev_id ) ? $rev_id : $row->rev_id; + if ( $rev_id > $row->rev_id ) { + $this->mForm->prevId[$rev_id] = $row->rev_id; + } elseif ( $rev_id < $row->rev_id ) { + $this->mForm->prevId[$row->rev_id] = $rev_id; + } + + $rev_id = $row->rev_id; + } + + $batch->execute(); + $this->mResult->seek( 0 ); + + return ''; + } + + function formatRow( $row ) { + return $this->mForm->formatRevisionRow( $row ); + } + + function getQueryInfo() { + $conds = $this->mConds; + $conds['rev_page'] = $this->articleID; + $conds[] = "rev_timestamp < " . $this->mDb->addQuotes( $this->maxTimestamp ); + + return [ + 'tables' => [ 'revision', 'page', 'user' ], + 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ), + 'conds' => $conds, + 'join_conds' => [ + 'page' => Revision::pageJoinCond(), + 'user' => Revision::userJoinCond() ] + ]; + } + + function getIndexField() { + return 'rev_timestamp'; + } +} diff --git a/includes/specials/pagers/NewFilesPager.php b/includes/specials/pagers/NewFilesPager.php new file mode 100644 index 0000000000..ae5773617c --- /dev/null +++ b/includes/specials/pagers/NewFilesPager.php @@ -0,0 +1,207 @@ +like = $context->getRequest()->getText( 'like' ); + $this->showBots = $context->getRequest()->getBool( 'showbots', 0 ); + $this->hidePatrolled = $context->getRequest()->getBool( 'hidepatrolled', 0 ); + if ( is_numeric( $par ) ) { + $this->setLimit( $par ); + } + + parent::__construct( $context ); + } + + function getQueryInfo() { + $conds = $jconds = []; + $tables = [ 'image' ]; + $fields = [ 'img_name', 'img_user', 'img_timestamp' ]; + $options = []; + + if ( !$this->showBots ) { + $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); + + if ( count( $groupsWithBotPermission ) ) { + $tables[] = 'user_groups'; + $conds[] = 'ug_group IS NULL'; + $jconds['user_groups'] = [ + 'LEFT JOIN', + [ + 'ug_group' => $groupsWithBotPermission, + 'ug_user = img_user' + ] + ]; + } + } + + if ( $this->hidePatrolled ) { + $tables[] = 'recentchanges'; + $conds['rc_type'] = RC_LOG; + $conds['rc_log_type'] = 'upload'; + $conds['rc_patrolled'] = 0; + $conds['rc_namespace'] = NS_FILE; + $jconds['recentchanges'] = [ + 'INNER JOIN', + [ + 'rc_title = img_name', + 'rc_user = img_user', + 'rc_timestamp = img_timestamp' + ] + ]; + // We're ordering by img_timestamp, so we have to make sure MariaDB queries `image` first. + // It sometimes decides to query `recentchanges` first and filesort the result set later + // to get the right ordering. T124205 / https://mariadb.atlassian.net/browse/MDEV-8880 + $options[] = 'STRAIGHT_JOIN'; + } + + if ( !$this->getConfig()->get( 'MiserMode' ) && $this->like !== null ) { + $dbr = wfGetDB( DB_SLAVE ); + $likeObj = Title::newFromText( $this->like ); + if ( $likeObj instanceof Title ) { + $like = $dbr->buildLike( + $dbr->anyString(), + strtolower( $likeObj->getDBkey() ), + $dbr->anyString() + ); + $conds[] = "LOWER(img_name) $like"; + } + } + + $query = [ + 'tables' => $tables, + 'fields' => $fields, + 'join_conds' => $jconds, + 'conds' => $conds, + 'options' => $options, + ]; + + return $query; + } + + function getIndexField() { + return 'img_timestamp'; + } + + function getStartBody() { + if ( !$this->gallery ) { + // Note that null for mode is taken to mean use default. + $mode = $this->getRequest()->getVal( 'gallerymode', null ); + try { + $this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() ); + } catch ( Exception $e ) { + // User specified something invalid, fallback to default. + $this->gallery = ImageGalleryBase::factory( false, $this->getContext() ); + } + } + + return ''; + } + + function getEndBody() { + return $this->gallery->toHTML(); + } + + function formatRow( $row ) { + $name = $row->img_name; + $user = User::newFromId( $row->img_user ); + + $title = Title::makeTitle( NS_FILE, $name ); + $ul = Linker::link( $user->getUserPage(), $user->getName() ); + $time = $this->getLanguage()->userTimeAndDate( $row->img_timestamp, $this->getUser() ); + + $this->gallery->add( + $title, + "$ul
\n" + . htmlspecialchars( $time ) + . "
\n" + ); + } + + function getForm() { + $fields = [ + 'like' => [ + 'type' => 'text', + 'label-message' => 'newimages-label', + 'name' => 'like', + ], + 'showbots' => [ + 'type' => 'check', + 'label-message' => 'newimages-showbots', + 'name' => 'showbots', + ], + 'hidepatrolled' => [ + 'type' => 'check', + 'label-message' => 'newimages-hidepatrolled', + 'name' => 'hidepatrolled', + ], + 'limit' => [ + 'type' => 'hidden', + 'default' => $this->mLimit, + 'name' => 'limit', + ], + 'offset' => [ + 'type' => 'hidden', + 'default' => $this->getRequest()->getText( 'offset' ), + 'name' => 'offset', + ], + ]; + + if ( $this->getConfig()->get( 'MiserMode' ) ) { + unset( $fields['like'] ); + } + + if ( !$this->getUser()->useFilePatrol() ) { + unset( $fields['hidepatrolled'] ); + } + + $context = new DerivativeContext( $this->getContext() ); + $context->setTitle( $this->getTitle() ); // Remove subpage + $form = new HTMLForm( $fields, $context ); + + $form->setSubmitTextMsg( 'ilsubmit' ); + $form->setSubmitProgressive(); + + $form->setMethod( 'get' ); + $form->setWrapperLegendMsg( 'newimages-legend' ); + + return $form; + } +} diff --git a/includes/specials/pagers/NewPagesPager.php b/includes/specials/pagers/NewPagesPager.php new file mode 100644 index 0000000000..2d39f99dc7 --- /dev/null +++ b/includes/specials/pagers/NewPagesPager.php @@ -0,0 +1,148 @@ +getContext() ); + $this->mForm = $form; + $this->opts = $opts; + } + + function getQueryInfo() { + $conds = []; + $conds['rc_new'] = 1; + + $namespace = $this->opts->getValue( 'namespace' ); + $namespace = ( $namespace === 'all' ) ? false : intval( $namespace ); + + $username = $this->opts->getValue( 'username' ); + $user = Title::makeTitleSafe( NS_USER, $username ); + + $rcIndexes = []; + + if ( $namespace !== false ) { + if ( $this->opts->getValue( 'invert' ) ) { + $conds[] = 'rc_namespace != ' . $this->mDb->addQuotes( $namespace ); + } else { + $conds['rc_namespace'] = $namespace; + } + } + + if ( $user ) { + $conds['rc_user_text'] = $user->getText(); + $rcIndexes = 'rc_user_text'; + } elseif ( User::groupHasPermission( '*', 'createpage' ) && + $this->opts->getValue( 'hideliu' ) + ) { + # If anons cannot make new pages, don't "exclude logged in users"! + $conds['rc_user'] = 0; + } + + # If this user cannot see patrolled edits or they are off, don't do dumb queries! + if ( $this->opts->getValue( 'hidepatrolled' ) && $this->getUser()->useNPPatrol() ) { + $conds['rc_patrolled'] = 0; + } + + if ( $this->opts->getValue( 'hidebots' ) ) { + $conds['rc_bot'] = 0; + } + + if ( $this->opts->getValue( 'hideredirs' ) ) { + $conds['page_is_redirect'] = 0; + } + + // Allow changes to the New Pages query + $tables = [ 'recentchanges', 'page' ]; + $fields = [ + 'rc_namespace', 'rc_title', 'rc_cur_id', 'rc_user', 'rc_user_text', + 'rc_comment', 'rc_timestamp', 'rc_patrolled', 'rc_id', 'rc_deleted', + 'length' => 'page_len', 'rev_id' => 'page_latest', 'rc_this_oldid', + 'page_namespace', 'page_title' + ]; + $join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ]; + + Hooks::run( 'SpecialNewpagesConditions', + [ &$this, $this->opts, &$conds, &$tables, &$fields, &$join_conds ] ); + + $options = []; + + if ( $rcIndexes ) { + $options = [ 'USE INDEX' => [ 'recentchanges' => $rcIndexes ] ]; + } + + $info = [ + 'tables' => $tables, + 'fields' => $fields, + 'conds' => $conds, + 'options' => $options, + 'join_conds' => $join_conds + ]; + + // Modify query for tags + ChangeTags::modifyDisplayQuery( + $info['tables'], + $info['fields'], + $info['conds'], + $info['join_conds'], + $info['options'], + $this->opts['tagfilter'] + ); + + return $info; + } + + function getIndexField() { + return 'rc_timestamp'; + } + + function formatRow( $row ) { + return $this->mForm->formatRow( $row ); + } + + function getStartBody() { + # Do a batch existence check on pages + $linkBatch = new LinkBatch(); + foreach ( $this->mResult as $row ) { + $linkBatch->add( NS_USER, $row->rc_user_text ); + $linkBatch->add( NS_USER_TALK, $row->rc_user_text ); + $linkBatch->add( $row->page_namespace, $row->page_title ); + } + $linkBatch->execute(); + + return ''; + } +} diff --git a/includes/specials/pagers/ProtectedTitlesPager.php b/includes/specials/pagers/ProtectedTitlesPager.php new file mode 100644 index 0000000000..8f172f8b55 --- /dev/null +++ b/includes/specials/pagers/ProtectedTitlesPager.php @@ -0,0 +1,91 @@ +mForm = $form; + $this->mConds = $conds; + $this->level = $level; + $this->namespace = $namespace; + $this->size = intval( $size ); + parent::__construct( $form->getContext() ); + } + + function getStartBody() { + # Do a link batch query + $this->mResult->seek( 0 ); + $lb = new LinkBatch; + + foreach ( $this->mResult as $row ) { + $lb->add( $row->pt_namespace, $row->pt_title ); + } + + $lb->execute(); + + return ''; + } + + /** + * @return Title + */ + function getTitle() { + return $this->mForm->getTitle(); + } + + function formatRow( $row ) { + return $this->mForm->formatRow( $row ); + } + + /** + * @return array + */ + function getQueryInfo() { + $conds = $this->mConds; + $conds[] = 'pt_expiry > ' . $this->mDb->addQuotes( $this->mDb->timestamp() ) . + ' OR pt_expiry IS NULL'; + if ( $this->level ) { + $conds['pt_create_perm'] = $this->level; + } + + if ( !is_null( $this->namespace ) ) { + $conds[] = 'pt_namespace=' . $this->mDb->addQuotes( $this->namespace ); + } + + return [ + 'tables' => 'protected_titles', + 'fields' => [ 'pt_namespace', 'pt_title', 'pt_create_perm', + 'pt_expiry', 'pt_timestamp' ], + 'conds' => $conds + ]; + } + + function getIndexField() { + return 'pt_timestamp'; + } +} diff --git a/includes/specials/pagers/UsersPager.php b/includes/specials/pagers/UsersPager.php new file mode 100644 index 0000000000..7b058c19eb --- /dev/null +++ b/includes/specials/pagers/UsersPager.php @@ -0,0 +1,395 @@ + + * + * 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 Pager + */ + +/** + * This class is used to get a list of user. The ones with specials + * rights (sysop, bureaucrat, developer) will have them displayed + * next to their names. + * + * @ingroup Pager + */ +class UsersPager extends AlphabeticPager { + + /** + * @var array A array with user ids as key and a array of groups as value + */ + protected $userGroupCache; + + /** + * @param IContextSource $context + * @param array $par (Default null) + * @param bool $including Whether this page is being transcluded in + * another page + */ + function __construct( IContextSource $context = null, $par = null, $including = null ) { + if ( $context ) { + $this->setContext( $context ); + } + + $request = $this->getRequest(); + $par = ( $par !== null ) ? $par : ''; + $parms = explode( '/', $par ); + $symsForAll = [ '*', 'user' ]; + + if ( $parms[0] != '' && + ( in_array( $par, User::getAllGroups() ) || in_array( $par, $symsForAll ) ) + ) { + $this->requestedGroup = $par; + $un = $request->getText( 'username' ); + } elseif ( count( $parms ) == 2 ) { + $this->requestedGroup = $parms[0]; + $un = $parms[1]; + } else { + $this->requestedGroup = $request->getVal( 'group' ); + $un = ( $par != '' ) ? $par : $request->getText( 'username' ); + } + + if ( in_array( $this->requestedGroup, $symsForAll ) ) { + $this->requestedGroup = ''; + } + $this->editsOnly = $request->getBool( 'editsOnly' ); + $this->creationSort = $request->getBool( 'creationSort' ); + $this->including = $including; + $this->mDefaultDirection = $request->getBool( 'desc' ) + ? IndexPager::DIR_DESCENDING + : IndexPager::DIR_ASCENDING; + + $this->requestedUser = ''; + + if ( $un != '' ) { + $username = Title::makeTitleSafe( NS_USER, $un ); + + if ( !is_null( $username ) ) { + $this->requestedUser = $username->getText(); + } + } + + parent::__construct(); + } + + /** + * @return string + */ + function getIndexField() { + return $this->creationSort ? 'user_id' : 'user_name'; + } + + /** + * @return array + */ + function getQueryInfo() { + $dbr = wfGetDB( DB_SLAVE ); + $conds = []; + + // Don't show hidden names + if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { + $conds[] = 'ipb_deleted IS NULL OR ipb_deleted = 0'; + } + + $options = []; + + if ( $this->requestedGroup != '' ) { + $conds['ug_group'] = $this->requestedGroup; + } + + if ( $this->requestedUser != '' ) { + # Sorted either by account creation or name + if ( $this->creationSort ) { + $conds[] = 'user_id >= ' . intval( User::idFromName( $this->requestedUser ) ); + } else { + $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser ); + } + } + + if ( $this->editsOnly ) { + $conds[] = 'user_editcount > 0'; + } + + $options['GROUP BY'] = $this->creationSort ? 'user_id' : 'user_name'; + + $query = [ + 'tables' => [ 'user', 'user_groups', 'ipblocks' ], + 'fields' => [ + 'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name', + 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)', + 'edits' => 'MAX(user_editcount)', + 'creation' => 'MIN(user_registration)', + 'ipb_deleted' => 'MAX(ipb_deleted)' // block/hide status + ], + 'options' => $options, + 'join_conds' => [ + 'user_groups' => [ 'LEFT JOIN', 'user_id=ug_user' ], + 'ipblocks' => [ + 'LEFT JOIN', [ + 'user_id=ipb_user', + 'ipb_auto' => 0 + ] + ], + ], + 'conds' => $conds + ]; + + Hooks::run( 'SpecialListusersQueryInfo', [ $this, &$query ] ); + + return $query; + } + + /** + * @param stdClass $row + * @return string + */ + function formatRow( $row ) { + if ( $row->user_id == 0 ) { # Bug 16487 + return ''; + } + + $userName = $row->user_name; + + $ulinks = Linker::userLink( $row->user_id, $userName ); + $ulinks .= Linker::userToolLinksRedContribs( + $row->user_id, + $userName, + (int)$row->edits + ); + + $lang = $this->getLanguage(); + + $groups = ''; + $groups_list = self::getGroups( intval( $row->user_id ), $this->userGroupCache ); + + if ( !$this->including && count( $groups_list ) > 0 ) { + $list = []; + foreach ( $groups_list as $group ) { + $list[] = self::buildGroupLink( $group, $userName ); + } + $groups = $lang->commaList( $list ); + } + + $item = $lang->specialList( $ulinks, $groups ); + + if ( $row->ipb_deleted ) { + $item = "$item"; + } + + $edits = ''; + if ( !$this->including && $this->getConfig()->get( 'Edititis' ) ) { + $count = $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped(); + $edits = $this->msg( 'word-separator' )->escaped() . $this->msg( 'brackets', $count )->escaped(); + } + + $created = ''; + # Some rows may be null + if ( !$this->including && $row->creation ) { + $user = $this->getUser(); + $d = $lang->userDate( $row->creation, $user ); + $t = $lang->userTime( $row->creation, $user ); + $created = $this->msg( 'usercreated', $d, $t, $row->user_name )->escaped(); + $created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped(); + } + $blocked = !is_null( $row->ipb_deleted ) ? + ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : + ''; + + Hooks::run( 'SpecialListusersFormatRow', [ &$item, $row ] ); + + return Html::rawElement( 'li', [], "{$item}{$edits}{$created}{$blocked}" ); + } + + function doBatchLookups() { + $batch = new LinkBatch(); + $userIds = []; + # Give some pointers to make user links + foreach ( $this->mResult as $row ) { + $batch->add( NS_USER, $row->user_name ); + $batch->add( NS_USER_TALK, $row->user_name ); + $userIds[] = $row->user_id; + } + + // Lookup groups for all the users + $dbr = wfGetDB( DB_SLAVE ); + $groupRes = $dbr->select( + 'user_groups', + [ 'ug_user', 'ug_group' ], + [ 'ug_user' => $userIds ], + __METHOD__ + ); + $cache = []; + $groups = []; + foreach ( $groupRes as $row ) { + $cache[intval( $row->ug_user )][] = $row->ug_group; + $groups[$row->ug_group] = true; + } + $this->userGroupCache = $cache; + + // Add page of groups to link batch + foreach ( $groups as $group => $unused ) { + $groupPage = User::getGroupPage( $group ); + if ( $groupPage ) { + $batch->addObj( $groupPage ); + } + } + + $batch->execute(); + $this->mResult->rewind(); + } + + /** + * @return string + */ + function getPageHeader() { + list( $self ) = explode( '/', $this->getTitle()->getPrefixedDBkey() ); + + $this->getOutput()->addModules( 'mediawiki.userSuggest' ); + + # Form tag + $out = Xml::openElement( + 'form', + [ 'method' => 'get', 'action' => wfScript(), 'id' => 'mw-listusers-form' ] + ) . + Xml::fieldset( $this->msg( 'listusers' )->text() ) . + Html::hidden( 'title', $self ); + + # Username field (with autocompletion support) + $out .= Xml::label( $this->msg( 'listusersfrom' )->text(), 'offset' ) . ' ' . + Html::input( + 'username', + $this->requestedUser, + 'text', + [ + 'class' => 'mw-autocomplete-user', + 'id' => 'offset', + 'size' => 20, + 'autofocus' => $this->requestedUser === '' + ] + ) . ' '; + + # Group drop-down list + $sel = new XmlSelect( 'group', 'group', $this->requestedGroup ); + $sel->addOption( $this->msg( 'group-all' )->text(), '' ); + foreach ( $this->getAllGroups() as $group => $groupText ) { + $sel->addOption( $groupText, $group ); + } + + $out .= Xml::label( $this->msg( 'group' )->text(), 'group' ) . ' '; + $out .= $sel->getHTML() . '
'; + $out .= Xml::checkLabel( + $this->msg( 'listusers-editsonly' )->text(), + 'editsOnly', + 'editsOnly', + $this->editsOnly + ); + $out .= ' '; + $out .= Xml::checkLabel( + $this->msg( 'listusers-creationsort' )->text(), + 'creationSort', + 'creationSort', + $this->creationSort + ); + $out .= ' '; + $out .= Xml::checkLabel( + $this->msg( 'listusers-desc' )->text(), + 'desc', + 'desc', + $this->mDefaultDirection + ); + $out .= '
'; + + Hooks::run( 'SpecialListusersHeaderForm', [ $this, &$out ] ); + + # Submit button and form bottom + $out .= Html::hidden( 'limit', $this->mLimit ); + $out .= Xml::submitButton( $this->msg( 'listusers-submit' )->text() ); + Hooks::run( 'SpecialListusersHeader', [ $this, &$out ] ); + $out .= Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ); + + return $out; + } + + /** + * Get a list of all explicit groups + * @return array + */ + function getAllGroups() { + $result = []; + foreach ( User::getAllGroups() as $group ) { + $result[$group] = User::getGroupName( $group ); + } + asort( $result ); + + return $result; + } + + /** + * Preserve group and username offset parameters when paging + * @return array + */ + function getDefaultQuery() { + $query = parent::getDefaultQuery(); + if ( $this->requestedGroup != '' ) { + $query['group'] = $this->requestedGroup; + } + if ( $this->requestedUser != '' ) { + $query['username'] = $this->requestedUser; + } + Hooks::run( 'SpecialListusersDefaultQuery', [ $this, &$query ] ); + + return $query; + } + + /** + * Get a list of groups the specified user belongs to + * + * @param int $uid User id + * @param array|null $cache + * @return array + */ + protected static function getGroups( $uid, $cache = null ) { + if ( $cache === null ) { + $user = User::newFromId( $uid ); + $effectiveGroups = $user->getEffectiveGroups(); + } else { + $effectiveGroups = isset( $cache[$uid] ) ? $cache[$uid] : []; + } + $groups = array_diff( $effectiveGroups, User::getImplicitGroups() ); + + return $groups; + } + + /** + * Format a link to a group description page + * + * @param string $group Group name + * @param string $username Username + * @return string + */ + protected static function buildGroupLink( $group, $username ) { + return User::makeGroupLinkHTML( + $group, + User::getGroupMember( $group, $username ) + ); + } + +} -- 2.20.1