MediaWiki master
DeletedContribsPager.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Pager;
23
24use ChangesList;
25use ChangeTags;
41use stdClass;
45
50
51 public $mGroupByDate = true;
52
56 public $messages;
57
61 public $target;
62
66 public $namespace = '';
67
69 private $formattedComments = [];
70
72 private $revisions = [];
73
74 private HookRunner $hookRunner;
75 private RevisionFactory $revisionFactory;
76 private CommentFormatter $commentFormatter;
77 private LinkBatchFactory $linkBatchFactory;
78
90 public function __construct(
91 IContextSource $context,
92 HookContainer $hookContainer,
93 LinkRenderer $linkRenderer,
94 IConnectionProvider $dbProvider,
95 RevisionFactory $revisionFactory,
96 CommentFormatter $commentFormatter,
97 LinkBatchFactory $linkBatchFactory,
98 $target,
100 ) {
101 parent::__construct( $context, $linkRenderer );
102
103 $msgs = [ 'deletionlog', 'undeleteviewlink', 'diff' ];
104 foreach ( $msgs as $msg ) {
105 $this->messages[$msg] = $this->msg( $msg )->text();
106 }
107 $this->target = $target;
108 $this->namespace = $namespace;
109 $this->hookRunner = new HookRunner( $hookContainer );
110 $this->revisionFactory = $revisionFactory;
111 $this->commentFormatter = $commentFormatter;
112 $this->linkBatchFactory = $linkBatchFactory;
113 }
114
115 public function getDefaultQuery() {
116 $query = parent::getDefaultQuery();
117 $query['target'] = $this->target;
118
119 return $query;
120 }
121
122 public function getQueryInfo() {
123 $dbr = $this->getDatabase();
124 $queryBuilder = $this->revisionFactory->newArchiveSelectQueryBuilder( $dbr )
125 ->joinComment()
126 ->where( [ 'actor_name' => $this->target ] )
127 ->andWhere( $this->getNamespaceCond() );
128 // Paranoia: avoid brute force searches (T19792)
129 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
130 $queryBuilder->andWhere(
131 $dbr->bitAnd( 'ar_deleted', RevisionRecord::DELETED_USER ) . ' = 0'
132 );
133 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
134 $queryBuilder->andWhere(
135 $dbr->bitAnd( 'ar_deleted', RevisionRecord::SUPPRESSED_USER ) .
136 ' != ' . RevisionRecord::SUPPRESSED_USER
137 );
138 }
139
140 MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQueryBuilder( $queryBuilder, 'archive' );
141
142 return $queryBuilder->getQueryInfo( 'join_conds' );
143 }
144
145 protected function doBatchLookups() {
146 // Do a link batch query
147 $this->mResult->seek( 0 );
148 $revisions = [];
149 $linkBatch = $this->linkBatchFactory->newLinkBatch();
150 // Give some pointers to make (last) links
151 $revisionRows = [];
152 foreach ( $this->mResult as $row ) {
153 if ( $this->revisionFactory->isRevisionRow( $row, 'archive' ) ) {
154 $revisionRows[] = $row;
155 $linkBatch->add( $row->ar_namespace, $row->ar_title );
156 }
157 }
158 // Cannot combine both loops, because RevisionFactory::newRevisionFromArchiveRow needs
159 // the title information in LinkCache to avoid extra db queries
160 $linkBatch->execute();
161
162 foreach ( $revisionRows as $row ) {
163 $revisions[$row->ar_rev_id] = $this->revisionFactory->newRevisionFromArchiveRow(
164 $row,
165 IDBAccessObject::READ_NORMAL,
166 Title::makeTitle( $row->ar_namespace, $row->ar_title )
167 );
168 }
169
170 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
171 ->authority( $this->getAuthority() )
172 ->revisions( $revisions )
173 ->execute();
174
175 // For performance, save the revision objects for later.
176 // The array is indexed by rev_id. doBatchLookups() may be called
177 // multiple times with different results, so merge the revisions array,
178 // ignoring any duplicates.
179 $this->revisions += $revisions;
180 }
181
191 public function reallyDoQuery( $offset, $limit, $order ) {
192 $data = [ parent::reallyDoQuery( $offset, $limit, $order ) ];
193
194 // This hook will allow extensions to add in additional queries, nearly
195 // identical to ContribsPager::reallyDoQuery.
196 $this->hookRunner->onDeletedContribsPager__reallyDoQuery(
197 $data, $this, $offset, $limit, $order );
198
199 $result = [];
200
201 // loop all results and collect them in an array
202 foreach ( $data as $query ) {
203 foreach ( $query as $i => $row ) {
204 // use index column as key, allowing us to easily sort in PHP
205 $result[$row->{$this->getIndexField()} . "-$i"] = $row;
206 }
207 }
208
209 // sort results
210 if ( $order === self::QUERY_ASCENDING ) {
211 ksort( $result );
212 } else {
213 krsort( $result );
214 }
215
216 // enforce limit
217 $result = array_slice( $result, 0, $limit );
218
219 // get rid of array keys
220 $result = array_values( $result );
221
222 return new FakeResultWrapper( $result );
223 }
224
228 protected function getExtraSortFields() {
229 return [ 'ar_id' ];
230 }
231
232 public function getIndexField() {
233 return 'ar_timestamp';
234 }
235
239 public function getTarget() {
240 return $this->target;
241 }
242
246 public function getNamespace() {
247 return $this->namespace;
248 }
249
253 protected function getStartBody() {
254 return "<section class='mw-pager-body'>\n";
255 }
256
260 protected function getEndBody() {
261 return "</section>\n";
262 }
263
264 private function getNamespaceCond() {
265 if ( $this->namespace !== '' ) {
266 return [ 'ar_namespace' => (int)$this->namespace ];
267 } else {
268 return [];
269 }
270 }
271
279 public function formatRow( $row ) {
280 $ret = '';
281 $classes = [];
282 $attribs = [];
283
284 if ( $this->revisionFactory->isRevisionRow( $row, 'archive' ) ) {
285 $attribs['data-mw-revid'] = $row->ar_rev_id;
286 [ $ret, $classes ] = $this->formatRevisionRow( $row );
287 }
288
289 // Let extensions add data
290 $this->hookRunner->onDeletedContributionsLineEnding(
291 $this, $ret, $row, $classes, $attribs );
292 $attribs = array_filter( $attribs,
293 [ Sanitizer::class, 'isReservedDataAttribute' ],
294 ARRAY_FILTER_USE_KEY
295 );
296
297 if ( $classes === [] && $attribs === [] && $ret === '' ) {
298 wfDebug( "Dropping Special:DeletedContribution row that could not be formatted" );
299 $ret = "<!-- Could not format Special:DeletedContribution row. -->\n";
300 } else {
301 $attribs['class'] = $classes;
302 $ret = Html::rawElement( 'li', $attribs, $ret ) . "\n";
303 }
304
305 return $ret;
306 }
307
320 private function formatRevisionRow( $row ) {
321 $page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
322
323 $linkRenderer = $this->getLinkRenderer();
324
325 $revRecord = $this->revisions[$row->ar_rev_id] ?? $this->revisionFactory->newRevisionFromArchiveRow(
326 $row,
327 IDBAccessObject::READ_NORMAL,
328 $page
329 );
330
331 $undelete = SpecialPage::getTitleFor( 'Undelete' );
332
333 $logs = SpecialPage::getTitleFor( 'Log' );
334 $dellog = $linkRenderer->makeKnownLink(
335 $logs,
336 $this->messages['deletionlog'],
337 [],
338 [
339 'type' => 'delete',
340 'page' => $page->getPrefixedText()
341 ]
342 );
343
344 $reviewlink = $linkRenderer->makeKnownLink(
345 SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ),
346 $this->messages['undeleteviewlink']
347 );
348
349 $user = $this->getUser();
350
351 if ( $this->getAuthority()->isAllowed( 'deletedtext' ) ) {
352 $last = $linkRenderer->makeKnownLink(
353 $undelete,
354 $this->messages['diff'],
355 [],
356 [
357 'target' => $page->getPrefixedText(),
358 'timestamp' => $revRecord->getTimestamp(),
359 'diff' => 'prev'
360 ]
361 );
362 } else {
363 $last = htmlspecialchars( $this->messages['diff'] );
364 }
365
366 $comment = $row->ar_rev_id
367 ? $this->formattedComments[$row->ar_rev_id]
368 : $this->commentFormatter->formatRevision( $revRecord, $user );
369 $date = $this->getLanguage()->userTimeAndDate( $revRecord->getTimestamp(), $user );
370
371 if ( !$this->getAuthority()->isAllowed( 'undelete' ) ||
372 !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
373 ) {
374 $link = htmlspecialchars( $date ); // unusable link
375 } else {
376 $link = $linkRenderer->makeKnownLink(
377 $undelete,
378 $date,
379 [ 'class' => 'mw-changeslist-date' ],
380 [
381 'target' => $page->getPrefixedText(),
382 'timestamp' => $revRecord->getTimestamp()
383 ]
384 );
385 }
386 // Style deleted items
387 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
388 $class = Linker::getRevisionDeletedClass( $revRecord );
389 $link = '<span class="' . $class . '">' . $link . '</span>';
390 }
391
392 $pagelink = $linkRenderer->makeLink(
393 $page,
394 null,
395 [ 'class' => 'mw-changeslist-title' ]
396 );
397
398 if ( $revRecord->isMinor() ) {
399 $mflag = ChangesList::flag( 'minor' );
400 } else {
401 $mflag = '';
402 }
403
404 // Revision delete link
405 $del = Linker::getRevDeleteLink( $user, $revRecord, $page );
406 if ( $del ) {
407 $del .= ' ';
408 }
409
410 $tools = Html::rawElement(
411 'span',
412 [ 'class' => 'mw-deletedcontribs-tools' ],
413 $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList(
414 [ $last, $dellog, $reviewlink ] ) )->escaped()
415 );
416
417 // Tags, if any.
418 [ $tagSummary, $classes ] = ChangeTags::formatSummaryRow(
419 $row->ts_tags,
420 'deletedcontributions',
421 $this->getContext()
422 );
423
424 $separator = '<span class="mw-changeslist-separator">. .</span>';
425 $ret = "{$del}{$link} {$tools} {$separator} {$mflag} {$pagelink} {$comment} {$tagSummary}";
426
427 # Denote if username is redacted for this edit
428 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
429 $ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>";
430 }
431
432 return [ $ret, $classes ];
433 }
434}
435
440class_alias( DeletedContribsPager::class, 'DeletedContribsPager' );
getUser()
getAuthority()
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
This is the main service interface for converting single-line comments from various DB comment fields...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:65
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
__construct(IContextSource $context, HookContainer $hookContainer, LinkRenderer $linkRenderer, IConnectionProvider $dbProvider, RevisionFactory $revisionFactory, CommentFormatter $commentFormatter, LinkBatchFactory $linkBatchFactory, $target, $namespace)
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
getQueryInfo()
Provides all parameters needed for the main paged query.
getEndBody()
Hook into getBody() for the end of the list.to overridestring
reallyDoQuery( $offset, $limit, $order)
This method basically executes the exact same code as the parent class, though with a hook added,...
formatRow( $row)
Generates each row in the contributions list.
string $target
User name, or a string describing an IP address range.
getStartBody()
Hook into getBody(), allows text to be inserted at the start.This will be called even if there are no...
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
getIndexField()
Returns the name of the index field.
string int $namespace
A single namespace number, or an empty string for all namespaces.
string[] $messages
Local cache for escaped messages.
getDatabase()
Get the Database object in use.
IndexPager with a formatted navigation bar.
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Page revision base class.
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Represents a title within MediaWiki.
Definition Title.php:78
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Interface for database access objects.
Interface for objects which can provide a MediaWiki context on request.
Service for constructing RevisionRecord objects.
Provide primary and replica IDatabase connections.
Result wrapper for grabbing data queried from an IDatabase object.