'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',
'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',
'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',
'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',
'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',
'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',
'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',
'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',
'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',
'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',
* @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 = "<span class=\"deleted\">$item</span>";
- }
- $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' => '' ] : []
- )
- ) . '<br />';
-
- $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 ]
- ) . '<br />';
-
- # 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
*/
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" .
- '<tr>
- <td class="mw-label">' .
- Xml::label( $this->msg( 'allmessages-prefix' )->text(), 'mw-allmessages-form-prefix' ) .
- "</td>\n
- <td class=\"mw-input\">" .
- Xml::input(
- 'prefix',
- 20,
- str_replace( '_', ' ', $this->displayPrefix ),
- [ 'id' => 'mw-allmessages-form-prefix' ]
- ) .
- "</td>\n
- </tr>
- <tr>\n
- <td class='mw-label'>" .
- $this->msg( 'allmessages-filter' )->escaped() .
- "</td>\n
- <td class='mw-input'>" .
- 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' )
- ) .
- "</td>\n
- </tr>
- <tr>\n
- <td class=\"mw-label\">" . $langSelect[0] . "</td>\n
- <td class=\"mw-input\">" . $langSelect[1] . "</td>\n
- </tr>" .
-
- '<tr>
- <td class="mw-label">' .
- Xml::label( $this->msg( 'table_pager_limit_label' )->text(), 'mw-table_pager_limit_label' ) .
- '</td>
- <td class="mw-input">' .
- $this->getLimitSelect( [ 'id' => 'mw-table_pager_limit_label' ] ) .
- '</td>
- <tr>
- <td></td>
- <td>' .
- Xml::submitButton( $this->msg( 'allmessages-filter-submit' )->text() ) .
- "</td>\n
- </tr>" .
-
- 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" .
- "<thead><tr>
- <th rowspan=\"2\">" .
- $this->msg( 'allmessagesname' )->escaped() . "
- </th>
- <th>" .
- $this->msg( 'allmessagesdefault' )->escaped() .
- "</th>
- </tr>\n
- <tr>
- <th>" .
- $this->msg( 'allmessagescurrent' )->escaped() .
- "</th>
- </tr></thead><tbody>\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 )
- . "</tr>\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 '';
- }
-}
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();
- }
-}
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' ]
- )
- )
- );
- }
-}
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 "<ul class=\"mw-contributions-list\">\n";
- }
-
- /**
- * @return string
- */
- function getEndBody() {
- return "</ul>\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 .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
- # 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 = ' <span class="mw-changeslist-separator">. .</span> ';
- $chardiff .= Linker::formatRevisionSize( $row->rev_len );
- $chardiff .= ' <span class="mw-changeslist-separator">. .</span> ';
- } else {
- $parentLen = 0;
- if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
- $parentLen = $this->mParentLens[$row->rev_parent_id];
- }
-
- $chardiff = ' <span class="mw-changeslist-separator">. .</span> ';
- $chardiff .= ChangesList::showCharacterDifference(
- $parentLen,
- $row->rev_len,
- $this->getContext()
- );
- $chardiff .= ' <span class="mw-changeslist-separator">. .</span> ';
- }
-
- $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 = '<span class="history-deleted">' . $d . '</span>';
- }
-
- # 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 .= " <strong>" .
- $this->msg( 'rev-deleted-user-contribs' )->escaped() .
- "</strong>";
- }
-
- # 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 = "<!-- Could not format Special:Contribution row. -->\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;
- }
-}
* 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 "<ul>\n";
- }
-
- function getEndBody() {
- return "</ul>\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 = "<!-- Could not format Special:DeletedContribution row. -->\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 = '<span class="history-deleted">' . $link . '</span>';
- }
-
- $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 = '<span class="mw-changeslist-separator">. .</span>';
- $ret = "{$del}{$link} {$tools} {$separator} {$mflag} {$pagelink} {$comment}";
-
- # Denote if username is redacted for this edit
- if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
- $ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>";
- }
-
- 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',
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(
- "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
- [
- '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' );
- }
-}
* @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 = "<span class=\"deleted\">$item</span>";
- }
-
- $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() . '<br />';
- $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 .= '<br />';
-
- 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
*/
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';
- }
-}
}
}
}
-
-/**
- * @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<br />\n<i>"
- . htmlspecialchars( $time )
- . "</i><br />\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;
- }
-}
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 '<ul>';
- }
-
- function getEndBody() {
- return '</ul>';
- }
-}
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';
- }
-}
--- /dev/null
+<?php
+/**
+ * Copyright © 2008 Aaron Schulz
+ *
+ * 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 active users. The ones with specials
+ * rights (sysop, bureaucrat, developer) will have them displayed
+ * next to their names.
+ *
+ * @ingroup Pager
+ */
+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 = "<span class=\"deleted\">$item</span>";
+ }
+ $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' => '' ] : []
+ )
+ ) . '<br />';
+
+ $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 ]
+ ) . '<br />';
+
+ # 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;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+
+/**
+ * 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.
+ *
+ * @ingroup Pager
+ */
+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" .
+ '<tr>
+ <td class="mw-label">' .
+ Xml::label( $this->msg( 'allmessages-prefix' )->text(), 'mw-allmessages-form-prefix' ) .
+ "</td>\n
+ <td class=\"mw-input\">" .
+ Xml::input(
+ 'prefix',
+ 20,
+ str_replace( '_', ' ', $this->displayPrefix ),
+ [ 'id' => 'mw-allmessages-form-prefix' ]
+ ) .
+ "</td>\n
+ </tr>
+ <tr>\n
+ <td class='mw-label'>" .
+ $this->msg( 'allmessages-filter' )->escaped() .
+ "</td>\n
+ <td class='mw-input'>" .
+ 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' )
+ ) .
+ "</td>\n
+ </tr>
+ <tr>\n
+ <td class=\"mw-label\">" . $langSelect[0] . "</td>\n
+ <td class=\"mw-input\">" . $langSelect[1] . "</td>\n
+ </tr>" .
+
+ '<tr>
+ <td class="mw-label">' .
+ Xml::label( $this->msg( 'table_pager_limit_label' )->text(), 'mw-table_pager_limit_label' ) .
+ '</td>
+ <td class="mw-input">' .
+ $this->getLimitSelect( [ 'id' => 'mw-table_pager_limit_label' ] ) .
+ '</td>
+ <tr>
+ <td></td>
+ <td>' .
+ Xml::submitButton( $this->msg( 'allmessages-filter-submit' )->text() ) .
+ "</td>\n
+ </tr>" .
+
+ 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" .
+ "<thead><tr>
+ <th rowspan=\"2\">" .
+ $this->msg( 'allmessagesname' )->escaped() . "
+ </th>
+ <th>" .
+ $this->msg( 'allmessagesdefault' )->escaped() .
+ "</th>
+ </tr>\n
+ <tr>
+ <th>" .
+ $this->msg( 'allmessagescurrent' )->escaped() .
+ "</th>
+ </tr></thead><tbody>\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 )
+ . "</tr>\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 '';
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+
+/**
+ * @ingroup Pager
+ */
+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();
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+
+/**
+ * TODO: Allow sorting by count. We need to have a unique index to do this
+ * properly.
+ *
+ * @ingroup 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' ]
+ )
+ )
+ );
+ }
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+
+/**
+ * Pager for Special:Contributions
+ * @ingroup 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 "<ul class=\"mw-contributions-list\">\n";
+ }
+
+ /**
+ * @return string
+ */
+ function getEndBody() {
+ return "</ul>\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 .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
+ # 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 = ' <span class="mw-changeslist-separator">. .</span> ';
+ $chardiff .= Linker::formatRevisionSize( $row->rev_len );
+ $chardiff .= ' <span class="mw-changeslist-separator">. .</span> ';
+ } else {
+ $parentLen = 0;
+ if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
+ $parentLen = $this->mParentLens[$row->rev_parent_id];
+ }
+
+ $chardiff = ' <span class="mw-changeslist-separator">. .</span> ';
+ $chardiff .= ChangesList::showCharacterDifference(
+ $parentLen,
+ $row->rev_len,
+ $this->getContext()
+ );
+ $chardiff .= ' <span class="mw-changeslist-separator">. .</span> ';
+ }
+
+ $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 = '<span class="history-deleted">' . $d . '</span>';
+ }
+
+ # 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 .= " <strong>" .
+ $this->msg( 'rev-deleted-user-contribs' )->escaped() .
+ "</strong>";
+ }
+
+ # 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 = "<!-- Could not format Special:Contribution row. -->\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;
+ }
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+
+/**
+ * @ingroup Pager
+ */
+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 "<ul>\n";
+ }
+
+ function getEndBody() {
+ return "</ul>\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 = "<!-- Could not format Special:DeletedContribution row. -->\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 = '<span class="history-deleted">' . $link . '</span>';
+ }
+
+ $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 = '<span class="mw-changeslist-separator">. .</span>';
+ $ret = "{$del}{$link} {$tools} {$separator} {$mflag} {$pagelink} {$comment}";
+
+ # Denote if username is redacted for this edit
+ if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
+ $ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>";
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Get the Database object in use
+ *
+ * @return IDatabase
+ */
+ public function getDatabase() {
+ return $this->mDb;
+ }
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+
+/**
+ * @ingroup 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(
+ "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
+ [
+ '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' );
+ }
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+
+/**
+ * @ingroup Pager
+ */
+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';
+ }
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+
+/**
+ * @ingroup 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<br />\n<i>"
+ . htmlspecialchars( $time )
+ . "</i><br />\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;
+ }
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+
+/**
+ * @ingroup 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 '<ul>';
+ }
+
+ function getEndBody() {
+ return '</ul>';
+ }
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+
+/**
+ * @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';
+ }
+}
--- /dev/null
+<?php
+/**
+ * Copyright © 2004 Brion Vibber, lcrocker, Tim Starling,
+ * Domas Mituzas, Antoine Musso, Jens Frank, Zhengzhu,
+ * 2006 Rob Church <robchur@gmail.com>
+ *
+ * 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 = "<span class=\"deleted\">$item</span>";
+ }
+
+ $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() . '<br />';
+ $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 .= '<br />';
+
+ 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 )
+ );
+ }
+
+}