diff options
author | Thalia <thalia.e.chan@googlemail.com> | 2024-05-13 15:31:40 +0100 |
---|---|---|
committer | Thalia <thalia.e.chan@googlemail.com> | 2024-05-13 15:38:34 +0100 |
commit | 5362096f27df9d280e8e035891387b7161d4ec22 (patch) | |
tree | 9f71c1c0e1f303e956d07dca0ffccd30e4ed6449 /includes/pager | |
parent | 7020c8c9e3a78609c4ac42e4c08672542a66b5a6 (diff) | |
download | mediawikicore-5362096f27df9d280e8e035891387b7161d4ec22.tar.gz mediawikicore-5362096f27df9d280e8e035891387b7161d4ec22.zip |
Revert "Revert "Add ContributionsPager, an abstract parent for ContribsPager""
This reverts commit e6fb3df2a639ca95afdec2255f1da63187ba71c6.
This re-instates I08a5d39036047484e3b44fcd83989072006b88e2.
Bug: T363358
Change-Id: I847c60a493d9973554ceb1232f3799c42321ee2b
Diffstat (limited to 'includes/pager')
-rw-r--r-- | includes/pager/ContributionsPager.php | 789 |
1 files changed, 789 insertions, 0 deletions
diff --git a/includes/pager/ContributionsPager.php b/includes/pager/ContributionsPager.php new file mode 100644 index 000000000000..2b7192dae6ce --- /dev/null +++ b/includes/pager/ContributionsPager.php @@ -0,0 +1,789 @@ +<?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 + */ + +namespace MediaWiki\Pager; + +use ChangesList; +use ChangeTags; +use HtmlArmor; +use InvalidArgumentException; +use MapCacheLRU; +use MediaWiki\Cache\LinkBatchFactory; +use MediaWiki\CommentFormatter\CommentFormatter; +use MediaWiki\Context\IContextSource; +use MediaWiki\HookContainer\HookContainer; +use MediaWiki\HookContainer\HookRunner; +use MediaWiki\Html\Html; +use MediaWiki\Html\TemplateParser; +use MediaWiki\Linker\Linker; +use MediaWiki\Linker\LinkRenderer; +use MediaWiki\MainConfigNames; +use MediaWiki\Parser\Sanitizer; +use MediaWiki\Revision\RevisionRecord; +use MediaWiki\Revision\RevisionStore; +use MediaWiki\Title\NamespaceInfo; +use MediaWiki\Title\Title; +use MediaWiki\User\UserFactory; +use MediaWiki\User\UserIdentity; +use MediaWiki\User\UserRigorOptions; +use stdClass; +use Wikimedia\Rdbms\FakeResultWrapper; +use Wikimedia\Rdbms\IResultWrapper; + +/** + * Pager for Special:Contributions + * @ingroup Pager + */ +abstract class ContributionsPager extends RangeChronologicalPager { + + public $mGroupByDate = true; + + /** + * @var string[] Local cache for escaped messages + */ + private $messages; + + /** + * @var string User name, or a string describing an IP address range + */ + protected $target; + + /** + * @var string|int A single namespace number, or an empty string for all namespaces + */ + private $namespace; + + /** + * @var string[]|false Name of tag to filter, or false to ignore tags + */ + private $tagFilter; + + /** + * @var bool Set to true to invert the tag selection + */ + private $tagInvert; + + /** + * @var bool Set to true to invert the namespace selection + */ + private $nsInvert; + + /** + * @var bool Set to true to show both the subject and talk namespace, no matter which got + * selected + */ + private $associated; + + /** + * @var bool Set to true to show only deleted revisions + */ + private $deletedOnly; + + /** + * @var bool Set to true to show only latest (a.k.a. current) revisions + */ + private $topOnly; + + /** + * @var bool Set to true to show only new pages + */ + private $newOnly; + + /** + * @var bool Set to true to hide edits marked as minor by the user + */ + private $hideMinor; + + /** + * @var bool Set to true to only include mediawiki revisions. + * (restricts extensions from executing additional queries to include their own contributions) + */ + private $revisionsOnly; + + private $preventClickjacking = false; + + /** + * @var array + */ + private $mParentLens; + + /** @var UserIdentity */ + protected $targetUser; + + private TemplateParser $templateParser; + private CommentFormatter $commentFormatter; + private HookRunner $hookRunner; + private LinkBatchFactory $linkBatchFactory; + private NamespaceInfo $namespaceInfo; + protected RevisionStore $revisionStore; + + /** @var string[] */ + private $formattedComments = []; + + /** @var RevisionRecord[] Cached revisions by ID */ + private $revisions = []; + + /** @var MapCacheLRU */ + private $tagsCache; + + /** + * @param LinkRenderer $linkRenderer + * @param LinkBatchFactory $linkBatchFactory + * @param HookContainer $hookContainer + * @param RevisionStore $revisionStore + * @param NamespaceInfo $namespaceInfo + * @param CommentFormatter $commentFormatter + * @param UserFactory $userFactory + * @param IContextSource $context + * @param array $options + * @param UserIdentity|null $targetUser + */ + public function __construct( + LinkRenderer $linkRenderer, + LinkBatchFactory $linkBatchFactory, + HookContainer $hookContainer, + RevisionStore $revisionStore, + NamespaceInfo $namespaceInfo, + CommentFormatter $commentFormatter, + UserFactory $userFactory, + IContextSource $context, + array $options, + ?UserIdentity $targetUser + ) { + // Set ->target before calling parent::__construct() so + // parent can call $this->getIndexField() and get the right result. Set + // the rest too just to keep things simple. + if ( $targetUser ) { + $this->target = $options['target'] ?? $targetUser->getName(); + $this->targetUser = $targetUser; + } else { + // Use target option + // It's possible for the target to be empty. This is used by + // ContribsPagerTest and does not cause newFromName() to return + // false. It's probably not used by any production code. + $this->target = $options['target'] ?? ''; + // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null + $this->targetUser = $userFactory->newFromName( + $this->target, UserRigorOptions::RIGOR_NONE + ); + if ( !$this->targetUser ) { + // This can happen if the target contained "#". Callers + // typically pass user input through title normalization to + // avoid it. + throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' . + 'broken to use even with validation disabled.' ); + } + } + + $this->namespace = $options['namespace'] ?? ''; + $this->tagFilter = $options['tagfilter'] ?? false; + $this->tagInvert = $options['tagInvert'] ?? false; + $this->nsInvert = $options['nsInvert'] ?? false; + $this->associated = $options['associated'] ?? false; + + $this->deletedOnly = !empty( $options['deletedOnly'] ); + $this->topOnly = !empty( $options['topOnly'] ); + $this->newOnly = !empty( $options['newOnly'] ); + $this->hideMinor = !empty( $options['hideMinor'] ); + $this->revisionsOnly = !empty( $options['revisionsOnly'] ); + + parent::__construct( $context, $linkRenderer ); + + $msgs = [ + 'diff', + 'hist', + 'pipe-separator', + 'uctop', + 'changeslist-nocomment', + ]; + + foreach ( $msgs as $msg ) { + $this->messages[$msg] = $this->msg( $msg )->escaped(); + } + + // Date filtering: use timestamp if available + $startTimestamp = ''; + $endTimestamp = ''; + if ( isset( $options['start'] ) && $options['start'] ) { + $startTimestamp = $options['start'] . ' 00:00:00'; + } + if ( isset( $options['end'] ) && $options['end'] ) { + $endTimestamp = $options['end'] . ' 23:59:59'; + } + $this->getDateRangeCond( $startTimestamp, $endTimestamp ); + + $this->templateParser = new TemplateParser(); + $this->linkBatchFactory = $linkBatchFactory; + $this->hookRunner = new HookRunner( $hookContainer ); + $this->revisionStore = $revisionStore; + $this->namespaceInfo = $namespaceInfo; + $this->commentFormatter = $commentFormatter; + $this->tagsCache = new MapCacheLRU( 50 ); + } + + public 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 $order IndexPager::QUERY_ASCENDING or IndexPager::QUERY_DESCENDING + * @return IResultWrapper + */ + public function reallyDoQuery( $offset, $limit, $order ) { + [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->buildQueryInfo( + $offset, + $limit, + $order + ); + + $options['MAX_EXECUTION_TIME'] = + $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ); + /* + * 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 + */ + $dbr = $this->getDatabase(); + $data = [ $dbr->newSelectQueryBuilder() + ->tables( is_array( $tables ) ? $tables : [ $tables ] ) + ->fields( $fields ) + ->conds( $conds ) + ->caller( $fname ) + ->options( $options ) + ->joinConds( $join_conds ) + ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) ) + ->fetchResultSet() ]; + if ( !$this->revisionsOnly ) { + // TODO: Range offsets are fairly important and all handlers should take care of it. + // If this hook will be replaced (e.g. unified with the DeletedContribsPager one), + // please consider passing [ $this->endOffset, $this->startOffset ] to it (T167577). + $this->hookRunner->onContribsPager__reallyDoQuery( + $data, $this, $offset, $limit, $order ); + } + + $result = []; + + // loop all results and collect them in an array + foreach ( $data as $query ) { + foreach ( $query as $i => $row ) { + // If the query results are in descending order, the indexes must also be in descending order + $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i; + // Left-pad with zeroes, because these values will be sorted as strings + $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT ); + // use index column as key, allowing us to easily sort in PHP + $result[$row->{$this->getIndexField()} . "-$index"] = $row; + } + } + + // sort results + if ( $order === self::QUERY_ASCENDING ) { + 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 ); + } + + /** + * Get queryInfo for the main query selecting revisions, not including + * filtering on namespace, date, etc. + * + * @return array + */ + abstract protected function getRevisionQuery(); + + public function getQueryInfo() { + $queryInfo = $this->getRevisionQuery(); + + if ( $this->deletedOnly ) { + $queryInfo['conds'][] = 'rev_deleted != 0'; + } + + if ( $this->topOnly ) { + $queryInfo['conds'][] = 'rev_id = page_latest'; + } + + if ( $this->newOnly ) { + $queryInfo['conds'][] = 'rev_parent_id = 0'; + } + + if ( $this->hideMinor ) { + $queryInfo['conds'][] = 'rev_minor_edit = 0'; + } + + $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() ); + + // Paranoia: avoid brute force searches (T19342) + $dbr = $this->getDatabase(); + if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) { + $queryInfo['conds'][] = $dbr->bitAnd( + 'rev_deleted', RevisionRecord::DELETED_USER + ) . ' = 0'; + } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $queryInfo['conds'][] = $dbr->bitAnd( + 'rev_deleted', RevisionRecord::SUPPRESSED_USER + ) . ' != ' . RevisionRecord::SUPPRESSED_USER; + } + + // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it. + $indexField = $this->getIndexField(); + if ( $indexField !== 'rev_timestamp' ) { + $queryInfo['fields'][] = $indexField; + } + + ChangeTags::modifyDisplayQuery( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + $queryInfo['join_conds'], + $queryInfo['options'], + $this->tagFilter, + $this->tagInvert, + ); + + $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo ); + + return $queryInfo; + } + + protected function getNamespaceCond() { + if ( $this->namespace !== '' ) { + $dbr = $this->getDatabase(); + $selectedNS = $dbr->addQuotes( $this->namespace ); + $eq_op = $this->nsInvert ? '!=' : '='; + $bool_op = $this->nsInvert ? 'AND' : 'OR'; + + if ( !$this->associated ) { + return [ "page_namespace $eq_op $selectedNS" ]; + } + + $associatedNS = $dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) ); + + return [ + "page_namespace $eq_op $selectedNS " . + $bool_op . + " page_namespace $eq_op $associatedNS" + ]; + } + + return []; + } + + /** + * @return false|string[] + */ + public function getTagFilter() { + return $this->tagFilter; + } + + /** + * @return bool + */ + public function getTagInvert() { + return $this->tagInvert; + } + + /** + * @return string + */ + public function getTarget() { + return $this->target; + } + + /** + * @return bool + */ + public function isNewOnly() { + return $this->newOnly; + } + + /** + * @return int|string + */ + public function getNamespace() { + return $this->namespace; + } + + protected function doBatchLookups() { + # Do a link batch query + $this->mResult->seek( 0 ); + $parentRevIds = []; + $this->mParentLens = []; + $revisions = []; + $linkBatch = $this->linkBatchFactory->newLinkBatch(); + # Give some pointers to make (last) links + foreach ( $this->mResult as $row ) { + if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) { + $parentRevIds[] = (int)$row->rev_parent_id; + } + if ( $this->revisionStore->isRevisionRow( $row ) ) { + $this->mParentLens[(int)$row->rev_id] = $row->rev_len; + if ( $this->target !== $row->rev_user_text ) { + // If the target does not match the author, batch the author's talk page + $linkBatch->add( NS_USER_TALK, $row->rev_user_text ); + } + $linkBatch->add( $row->page_namespace, $row->page_title ); + $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row ); + } + } + # Fetch rev_len for revisions not already scanned above + $this->mParentLens += $this->revisionStore->getRevisionSizes( + array_diff( $parentRevIds, array_keys( $this->mParentLens ) ) + ); + $linkBatch->execute(); + + $this->formattedComments = $this->commentFormatter->createRevisionBatch() + ->authority( $this->getAuthority() ) + ->revisions( $revisions ) + ->hideIfDeleted() + ->execute(); + + # For performance, save the revision objects for later. + # The array is indexed by rev_id. doBatchLookups() may be called + # multiple times with different results, so merge the revisions array, + # ignoring any duplicates. + $this->revisions += $revisions; + } + + /** + * @inheritDoc + */ + protected function getStartBody() { + return "<section class='mw-pager-body'>\n"; + } + + /** + * @inheritDoc + */ + protected function getEndBody() { + return "</section>\n"; + } + + /** + * If the object looks like a revision row, or corresponds to a previously + * cached revision, return the RevisionRecord. Otherwise, return null. + * + * @since 1.35 + * + * @param mixed $row + * @param Title|null $title + * @return RevisionRecord|null + */ + public function tryCreatingRevisionRecord( $row, $title = null ) { + if ( $row instanceof stdClass && isset( $row->rev_id ) + && isset( $this->revisions[$row->rev_id] ) + ) { + return $this->revisions[$row->rev_id]; + } elseif ( $this->revisionStore->isRevisionRow( $row ) ) { + return $this->revisionStore->newRevisionFromRow( $row, 0, $title ); + } else { + return null; + } + } + + /** + * 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 stdClass|mixed $row + * @return string + */ + public function formatRow( $row ) { + $ret = ''; + $classes = []; + $attribs = []; + + $linkRenderer = $this->getLinkRenderer(); + + $page = null; + // Create a title for the revision if possible + // Rows from the hook may not include title information + if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) { + $page = Title::newFromRow( $row ); + } + // Flow overrides the ContribsPager::reallyDoQuery hook, causing this + // function to be called with a special object for $row. It expects us + // skip formatting so that the row can be formatted by the + // ContributionsLineEnding hook below. + // FIXME: have some better way for extensions to provide formatted rows. + $revRecord = $this->tryCreatingRevisionRecord( $row, $page ); + if ( $revRecord && $page ) { + $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page ); + $attribs['data-mw-revid'] = $revRecord->getId(); + + $link = $linkRenderer->makeLink( + $page, + $page->getPrefixedText(), + [ 'class' => 'mw-contributions-title' ], + $page->isRedirect() ? [ 'redirect' => 'no' ] : [] + ); + # Mark current revisions + $topmarktext = ''; + + $pagerTools = new PagerTools( + $revRecord, + null, + $row->rev_id === $row->page_latest && !$row->page_is_new, + $this->hookRunner, + $page, + $this->getContext(), + $this->getLinkRenderer() + ); + if ( $row->rev_id === $row->page_latest ) { + $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; + $classes[] = 'mw-contributions-current'; + } + if ( $pagerTools->shouldPreventClickjacking() ) { + $this->setPreventClickjacking( true ); + } + $topmarktext .= $pagerTools->toHTML(); + # Is there a visible previous revision? + if ( $revRecord->getParentId() !== 0 && + $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) + ) { + $difftext = $linkRenderer->makeKnownLink( + $page, + new HtmlArmor( $this->messages['diff'] ), + [ 'class' => 'mw-changeslist-diff' ], + [ + 'diff' => 'prev', + 'oldid' => $row->rev_id + ] + ); + } else { + $difftext = $this->messages['diff']; + } + $histlink = $linkRenderer->makeKnownLink( + $page, + new HtmlArmor( $this->messages['hist'] ), + [ 'class' => 'mw-changeslist-history' ], + [ '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 (T36922). + // 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 = $this->formattedComments[$row->rev_id]; + + if ( $comment === '' ) { + $defaultComment = $this->messages['changeslist-nocomment']; + $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>"; + } + + $comment = $lang->getDirMark() . $comment; + + $authority = $this->getAuthority(); + $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page ); + + // When the author is different from the target, always show user and user talk links + $userlink = ''; + $revUser = $revRecord->getUser(); + $revUserId = $revUser ? $revUser->getId() : 0; + $revUserText = $revUser ? $revUser->getName() : ''; + if ( $this->target !== $revUserText ) { + $userlink = ' <span class="mw-changeslist-separator"></span> ' + . $lang->getDirMark() + . Linker::userLink( $revUserId, $revUserText ); + $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( + Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' '; + } + + $flags = []; + if ( $revRecord->getParentId() === 0 ) { + $flags[] = ChangesList::flag( 'newpage' ); + } + + if ( $revRecord->isMinor() ) { + $flags[] = ChangesList::flag( 'minor' ); + } + + $del = Linker::getRevDeleteLink( $authority, $revRecord, $page ); + if ( $del !== '' ) { + $del .= ' '; + } + + // While it might be tempting to use a list here + // this would result in clutter and slows down navigating the content + // in assistive technology. + // See https://phabricator.wikimedia.org/T205581#4734812 + $diffHistLinks = Html::rawElement( 'span', + [ 'class' => 'mw-changeslist-links' ], + // The spans are needed to ensure the dividing '|' elements are not + // themselves styled as links. + Html::rawElement( 'span', [], $difftext ) . + ' ' . // Space needed for separating two words. + Html::rawElement( 'span', [], $histlink ) + ); + + # Tags, if any. Save some time using a cache. + [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback( + $this->tagsCache->makeKey( + $row->ts_tags ?? '', + $this->getUser()->getName(), + $lang->getCode() + ), + fn () => ChangeTags::formatSummaryRow( + $row->ts_tags, + null, + $this->getContext() + ) + ); + $classes = array_merge( $classes, $newClasses ); + + $this->hookRunner->onSpecialContributions__formatRow__flags( + $this->getContext(), $row, $flags ); + + $templateParams = [ + 'del' => $del, + 'timestamp' => $d, + 'diffHistLinks' => $diffHistLinks, + 'charDifference' => $chardiff, + 'flags' => $flags, + 'articleLink' => $link, + 'userlink' => $userlink, + 'logText' => $comment, + 'topmarktext' => $topmarktext, + 'tagSummary' => $tagSummary, + ]; + + # Denote if username is redacted for this edit + if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) { + $templateParams['rev-deleted-user-contribs'] = + $this->msg( 'rev-deleted-user-contribs' )->escaped(); + } + + $ret = $this->templateParser->processTemplate( + 'SpecialContributionsLine', + $templateParams + ); + } + + // Let extensions add data + $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs ); + $attribs = array_filter( $attribs, + [ Sanitizer::class, 'isReservedDataAttribute' ], + ARRAY_FILTER_USE_KEY + ); + + // TODO: Handle exceptions in the catch block above. Do any extensions rely on + // receiving empty rows? + + if ( $classes === [] && $attribs === [] && $ret === '' ) { + wfDebug( "Dropping Special:Contribution row that could not be formatted" ); + return "<!-- Could not format Special:Contribution row. -->\n"; + } + $attribs['class'] = $classes; + + // FIXME: The signature of the ContributionsLineEnding hook makes it + // very awkward to move this LI wrapper into the template. + return Html::rawElement( 'li', $attribs, $ret ) . "\n"; + } + + /** + * Overwrite Pager function and return a helpful comment + * @return string + */ + protected 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'; + } + } + + /** + * @deprecated since 1.38, use ::setPreventClickjacking() instead + */ + protected function preventClickjacking() { + $this->setPreventClickjacking( true ); + } + + /** + * @param bool $enable + * @since 1.38 + */ + protected function setPreventClickjacking( bool $enable ) { + $this->preventClickjacking = $enable; + } + + /** + * @return bool + */ + public function getPreventClickjacking() { + return $this->preventClickjacking; + } + +} |