MediaWiki  master
DeletedContribsPager.php
Go to the documentation of this file.
1 <?php
22 namespace MediaWiki\Pager;
23 
24 use ChangesList;
25 use ChangeTags;
26 use IContextSource;
39 use stdClass;
43 
48 
49  public $mGroupByDate = true;
50 
54  public $messages;
55 
59  public $target;
60 
64  public $namespace = '';
65 
67  private $formattedComments = [];
68 
70  private $revisions = [];
71 
72  private HookRunner $hookRunner;
73  private RevisionFactory $revisionFactory;
74  private CommentFormatter $commentFormatter;
75  private LinkBatchFactory $linkBatchFactory;
76 
88  public function __construct(
89  IContextSource $context,
90  HookContainer $hookContainer,
91  LinkRenderer $linkRenderer,
92  IConnectionProvider $dbProvider,
93  RevisionFactory $revisionFactory,
94  CommentFormatter $commentFormatter,
95  LinkBatchFactory $linkBatchFactory,
96  $target,
98  ) {
99  parent::__construct( $context, $linkRenderer );
100 
101  $msgs = [ 'deletionlog', 'undeleteviewlink', 'diff' ];
102  foreach ( $msgs as $msg ) {
103  $this->messages[$msg] = $this->msg( $msg )->text();
104  }
105  $this->target = $target;
106  $this->namespace = $namespace;
107  $this->hookRunner = new HookRunner( $hookContainer );
108  $this->revisionFactory = $revisionFactory;
109  $this->commentFormatter = $commentFormatter;
110  $this->linkBatchFactory = $linkBatchFactory;
111  }
112 
113  public function getDefaultQuery() {
114  $query = parent::getDefaultQuery();
115  $query['target'] = $this->target;
116 
117  return $query;
118  }
119 
120  public function getQueryInfo() {
121  $dbr = $this->getDatabase();
122  $queryBuilder = $this->revisionFactory->newArchiveSelectQueryBuilder( $dbr )
123  ->joinComment()
124  ->where( [ 'actor_name' => $this->target ] )
125  ->andWhere( $this->getNamespaceCond() );
126  // Paranoia: avoid brute force searches (T19792)
127  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
128  $queryBuilder->andWhere(
129  $dbr->bitAnd( 'ar_deleted', RevisionRecord::DELETED_USER ) . ' = 0'
130  );
131  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
132  $queryBuilder->andWhere(
133  $dbr->bitAnd( 'ar_deleted', RevisionRecord::SUPPRESSED_USER ) .
135  );
136  }
137 
138  $queryInfo = $queryBuilder->getQueryInfo( 'join_conds' );
140  $queryInfo['tables'],
141  $queryInfo['fields'],
142  $queryInfo['conds'],
143  $queryInfo['join_conds'],
144  $queryInfo['options'],
145  ''
146  );
147 
148  return $queryInfo;
149  }
150 
151  protected function doBatchLookups() {
152  // Do a link batch query
153  $this->mResult->seek( 0 );
154  $revisions = [];
155  $linkBatch = $this->linkBatchFactory->newLinkBatch();
156  // Give some pointers to make (last) links
157  $revisionRows = [];
158  foreach ( $this->mResult as $row ) {
159  if ( $this->revisionFactory->isRevisionRow( $row, 'archive' ) ) {
160  $revisionRows[] = $row;
161  $linkBatch->add( $row->ar_namespace, $row->ar_title );
162  }
163  }
164  // Cannot combine both loops, because RevisionFactory::newRevisionFromArchiveRow needs
165  // the title information in LinkCache to avoid extra db queries
166  $linkBatch->execute();
167 
168  foreach ( $revisionRows as $row ) {
169  $revisions[$row->ar_rev_id] = $this->revisionFactory->newRevisionFromArchiveRow(
170  $row,
171  RevisionFactory::READ_NORMAL,
172  Title::makeTitle( $row->ar_namespace, $row->ar_title )
173  );
174  }
175 
176  $this->formattedComments = $this->commentFormatter->createRevisionBatch()
177  ->authority( $this->getAuthority() )
178  ->revisions( $revisions )
179  ->execute();
180 
181  // For performance, save the revision objects for later.
182  // The array is indexed by rev_id. doBatchLookups() may be called
183  // multiple times with different results, so merge the revisions array,
184  // ignoring any duplicates.
185  $this->revisions += $revisions;
186  }
187 
197  public function reallyDoQuery( $offset, $limit, $order ) {
198  $data = [ parent::reallyDoQuery( $offset, $limit, $order ) ];
199 
200  // This hook will allow extensions to add in additional queries, nearly
201  // identical to ContribsPager::reallyDoQuery.
202  $this->hookRunner->onDeletedContribsPager__reallyDoQuery(
203  $data, $this, $offset, $limit, $order );
204 
205  $result = [];
206 
207  // loop all results and collect them in an array
208  foreach ( $data as $query ) {
209  foreach ( $query as $i => $row ) {
210  // use index column as key, allowing us to easily sort in PHP
211  $result[$row->{$this->getIndexField()} . "-$i"] = $row;
212  }
213  }
214 
215  // sort results
216  if ( $order === self::QUERY_ASCENDING ) {
217  ksort( $result );
218  } else {
219  krsort( $result );
220  }
221 
222  // enforce limit
223  $result = array_slice( $result, 0, $limit );
224 
225  // get rid of array keys
226  $result = array_values( $result );
227 
228  return new FakeResultWrapper( $result );
229  }
230 
234  protected function getExtraSortFields() {
235  return [ 'ar_id' ];
236  }
237 
238  public function getIndexField() {
239  return 'ar_timestamp';
240  }
241 
245  public function getTarget() {
246  return $this->target;
247  }
248 
252  public function getNamespace() {
253  return $this->namespace;
254  }
255 
259  protected function getStartBody() {
260  return "<section class='mw-pager-body'>\n";
261  }
262 
266  protected function getEndBody() {
267  return "</section>\n";
268  }
269 
270  private function getNamespaceCond() {
271  if ( $this->namespace !== '' ) {
272  return [ 'ar_namespace' => (int)$this->namespace ];
273  } else {
274  return [];
275  }
276  }
277 
285  public function formatRow( $row ) {
286  $ret = '';
287  $classes = [];
288  $attribs = [];
289 
290  if ( $this->revisionFactory->isRevisionRow( $row, 'archive' ) ) {
291  $attribs['data-mw-revid'] = $row->ar_rev_id;
292  [ $ret, $classes ] = $this->formatRevisionRow( $row );
293  }
294 
295  // Let extensions add data
296  $this->hookRunner->onDeletedContributionsLineEnding(
297  $this, $ret, $row, $classes, $attribs );
298  $attribs = array_filter( $attribs,
299  [ Sanitizer::class, 'isReservedDataAttribute' ],
300  ARRAY_FILTER_USE_KEY
301  );
302 
303  if ( $classes === [] && $attribs === [] && $ret === '' ) {
304  wfDebug( "Dropping Special:DeletedContribution row that could not be formatted" );
305  $ret = "<!-- Could not format Special:DeletedContribution row. -->\n";
306  } else {
307  $attribs['class'] = $classes;
308  $ret = Html::rawElement( 'li', $attribs, $ret ) . "\n";
309  }
310 
311  return $ret;
312  }
313 
326  private function formatRevisionRow( $row ) {
327  $page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
328 
329  $linkRenderer = $this->getLinkRenderer();
330 
331  $revRecord = $this->revisions[$row->ar_rev_id] ?? $this->revisionFactory->newRevisionFromArchiveRow(
332  $row,
333  RevisionFactory::READ_NORMAL,
334  $page
335  );
336 
337  $undelete = SpecialPage::getTitleFor( 'Undelete' );
338 
339  $logs = SpecialPage::getTitleFor( 'Log' );
340  $dellog = $linkRenderer->makeKnownLink(
341  $logs,
342  $this->messages['deletionlog'],
343  [],
344  [
345  'type' => 'delete',
346  'page' => $page->getPrefixedText()
347  ]
348  );
349 
350  $reviewlink = $linkRenderer->makeKnownLink(
351  SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ),
352  $this->messages['undeleteviewlink']
353  );
354 
355  $user = $this->getUser();
356 
357  if ( $this->getAuthority()->isAllowed( 'deletedtext' ) ) {
358  $last = $linkRenderer->makeKnownLink(
359  $undelete,
360  $this->messages['diff'],
361  [],
362  [
363  'target' => $page->getPrefixedText(),
364  'timestamp' => $revRecord->getTimestamp(),
365  'diff' => 'prev'
366  ]
367  );
368  } else {
369  $last = htmlspecialchars( $this->messages['diff'] );
370  }
371 
372  $comment = $row->ar_rev_id
373  ? $this->formattedComments[$row->ar_rev_id]
374  : $this->commentFormatter->formatRevision( $revRecord, $user );
375  $date = $this->getLanguage()->userTimeAndDate( $revRecord->getTimestamp(), $user );
376 
377  if ( !$this->getAuthority()->isAllowed( 'undelete' ) ||
378  !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
379  ) {
380  $link = htmlspecialchars( $date ); // unusable link
381  } else {
382  $link = $linkRenderer->makeKnownLink(
383  $undelete,
384  $date,
385  [ 'class' => 'mw-changeslist-date' ],
386  [
387  'target' => $page->getPrefixedText(),
388  'timestamp' => $revRecord->getTimestamp()
389  ]
390  );
391  }
392  // Style deleted items
393  if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
394  $class = Linker::getRevisionDeletedClass( $revRecord );
395  $link = '<span class="' . $class . '">' . $link . '</span>';
396  }
397 
398  $pagelink = $linkRenderer->makeLink(
399  $page,
400  null,
401  [ 'class' => 'mw-changeslist-title' ]
402  );
403 
404  if ( $revRecord->isMinor() ) {
405  $mflag = ChangesList::flag( 'minor' );
406  } else {
407  $mflag = '';
408  }
409 
410  // Revision delete link
411  $del = Linker::getRevDeleteLink( $user, $revRecord, $page );
412  if ( $del ) {
413  $del .= ' ';
414  }
415 
416  $tools = Html::rawElement(
417  'span',
418  [ 'class' => 'mw-deletedcontribs-tools' ],
419  $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList(
420  [ $last, $dellog, $reviewlink ] ) )->escaped()
421  );
422 
423  // Tags, if any.
424  [ $tagSummary, $classes ] = ChangeTags::formatSummaryRow(
425  $row->ts_tags,
426  'deletedcontributions',
427  $this->getContext()
428  );
429 
430  $separator = '<span class="mw-changeslist-separator">. .</span>';
431  $ret = "{$del}{$link} {$tools} {$separator} {$mflag} {$pagelink} {$comment} {$tagSummary}";
432 
433  # Denote if username is redacted for this edit
434  if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
435  $ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>";
436  }
437 
438  return [ $ret, $classes ];
439  }
440 }
441 
446 class_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 modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
Definition: ChangeTags.php:631
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:147
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
This is the main service interface for converting single-line comments from various DB comment fields...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:568
This class is a collection of static functions that serve two purposes:
Definition: Html.php:57
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:235
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition: Linker.php:65
static getRevDeleteLink(Authority $performer, RevisionRecord $revRecord, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2271
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition: Linker.php:1435
__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.@stable 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.
Definition: IndexPager.php:256
IndexPager with a formatted navigation bar.
HTML sanitizer for MediaWiki.
Definition: Sanitizer.php:46
Page revision base class.
Parent class for all special pages.
Definition: SpecialPage.php:66
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:76
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:624
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
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.