MediaWiki  master
HistoryPager.php
Go to the documentation of this file.
1 <?php
36 
42 
43  public $mGroupByDate = true;
44 
46 
47  protected $oldIdChecked;
48 
49  protected $preventClickjacking = false;
53  protected $parentLens;
54 
56  protected $showTagEditUI;
57 
59  private $tagFilter;
60 
62  private $notificationTimestamp;
63 
65  private $revisionStore;
66 
68  private $watchlistManager;
69 
71  private $linkBatchFactory;
72 
74  private $commentFormatter;
75 
77  private $hookRunner;
78 
82  private $revisions = [];
83 
87  private $formattedComments = [];
88 
101  public function __construct(
103  $year = 0,
104  $month = 0,
105  $tagFilter = '',
106  array $conds = [],
107  $day = 0,
108  LinkBatchFactory $linkBatchFactory = null,
109  WatchlistManager $watchlistManager = null,
110  CommentFormatter $commentFormatter = null,
111  HookContainer $hookContainer = null
112  ) {
113  parent::__construct( $historyPage->getContext() );
114  $this->historyPage = $historyPage;
115  $this->tagFilter = $tagFilter;
116  $this->getDateCond( $year, $month, $day );
117  $this->conds = $conds;
118  $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getAuthority() );
119  $services = MediaWikiServices::getInstance();
120  $this->revisionStore = $services->getRevisionStore();
121  $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
122  $this->watchlistManager = $watchlistManager
123  ?? $services->getWatchlistManager();
124  $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
125  $this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );
126  $this->notificationTimestamp = $this->getConfig()->get( MainConfigNames::ShowUpdatedMarker )
127  ? $this->watchlistManager->getTitleNotificationTimestamp( $this->getUser(), $this->getTitle() )
128  : false;
129  }
130 
131  // For hook compatibility...
132  public function getArticle() {
133  return $this->historyPage->getArticle();
134  }
135 
136  protected function getSqlComment() {
137  if ( $this->conds ) {
138  return 'history page filtered'; // potentially slow, see CR r58153
139  } else {
140  return 'history page unfiltered';
141  }
142  }
143 
144  public function getQueryInfo() {
145  $revQuery = $this->revisionStore->getQueryInfo( [ 'user' ] );
146 
147  $queryInfo = [
148  'tables' => $revQuery['tables'],
149  'fields' => $revQuery['fields'],
150  'conds' => array_merge(
151  [ 'rev_page' => $this->getWikiPage()->getId() ],
152  $this->conds ),
153  'options' => [ 'USE INDEX' => [ 'revision' => 'rev_page_timestamp' ] ],
154  'join_conds' => $revQuery['joins'],
155  ];
157  $queryInfo['tables'],
158  $queryInfo['fields'],
159  $queryInfo['conds'],
160  $queryInfo['join_conds'],
161  $queryInfo['options'],
162  $this->tagFilter
163  );
164 
165  $this->hookRunner->onPageHistoryPager__getQueryInfo( $this, $queryInfo );
166 
167  return $queryInfo;
168  }
169 
170  public function getIndexField() {
171  return [ [ 'rev_timestamp', 'rev_id' ] ];
172  }
173 
174  protected function doBatchLookups() {
175  if ( !$this->hookRunner->onPageHistoryPager__doBatchLookups( $this, $this->mResult ) ) {
176  return;
177  }
178 
179  # Do a link batch query
180  $batch = $this->linkBatchFactory->newLinkBatch();
181  $revIds = [];
182  $title = $this->getTitle();
183  foreach ( $this->mResult as $row ) {
184  if ( $row->rev_parent_id ) {
185  $revIds[] = (int)$row->rev_parent_id;
186  }
187  if ( $row->user_name !== null ) {
188  $batch->add( NS_USER, $row->user_name );
189  $batch->add( NS_USER_TALK, $row->user_name );
190  } else { # for anons or usernames of imported revisions
191  $batch->add( NS_USER, $row->rev_user_text );
192  $batch->add( NS_USER_TALK, $row->rev_user_text );
193  }
194  $this->revisions[] = $this->revisionStore->newRevisionFromRow(
195  $row,
196  RevisionStore::READ_NORMAL,
197  $title
198  );
199  }
200  $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
201  $batch->execute();
202 
203  # The keys of $this->formattedComments will be the same as the keys of $this->revisions
204  $this->formattedComments = $this->commentFormatter->createRevisionBatch()
205  ->revisions( $this->revisions )
206  ->authority( $this->getAuthority() )
207  ->samePage( false )
208  ->hideIfDeleted( true )
209  ->useParentheses( false )
210  ->execute();
211 
212  $this->mResult->seek( 0 );
213  }
214 
219  protected function getEmptyBody() {
220  return $this->msg( 'history-empty' )->escaped();
221  }
222 
228  protected function getStartBody() {
229  $this->oldIdChecked = 0;
230  $s = '';
231  // Button container stored in $this->buttons for re-use in getEndBody()
232  $this->buttons = '';
233  if ( $this->getNumRows() > 0 ) {
234  $this->getOutput()->wrapWikiMsg( "<div class='mw-history-legend'>\n$1\n</div>", 'histlegend' );
235  // Main form for comparing revisions
236  $s = Html::openElement( 'form', [
237  'action' => wfScript(),
238  'id' => 'mw-history-compare'
239  ] ) . "\n";
240  $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n";
241  $s .= Html::hidden( 'type', 'revision', [ 'form' => 'mw-history-revisionactions' ] ) . "\n";
242 
243  $this->buttons .= Html::openElement(
244  'div', [ 'class' => 'mw-history-compareselectedversions' ] );
245  $className = 'historysubmit mw-history-compareselectedversions-button mw-ui-button';
246  $attrs = [ 'class' => $className ]
247  + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
248  $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
249  $attrs
250  ) . "\n";
251 
252  $actionButtons = '';
253  if ( $this->getAuthority()->isAllowed( 'deleterevision' ) ) {
254  $actionButtons .= $this->getRevisionButton(
255  'revisiondelete', 'showhideselectedversions' );
256  }
257  if ( $this->showTagEditUI ) {
258  $actionButtons .= $this->getRevisionButton(
259  'editchangetags', 'history-edit-tags' );
260  }
261  if ( $actionButtons ) {
262  // Prepend a mini-form for changing visibility and editing tags.
263  // Checkboxes and buttons are associated with it using the <input form="…"> attribute.
264  //
265  // This makes the submitted parameters cleaner (on supporting browsers - all except IE 11):
266  // the 'mw-history-compare' form submission will omit the `ids[…]` parameters, and the
267  // 'mw-history-revisionactions' form submission will omit the `diff` and `oldid` parameters.
268  $s = Html::rawElement( 'form', [
269  'action' => wfScript(),
270  'id' => 'mw-history-revisionactions',
271  ], Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) ) . "\n" . $s;
272 
273  $this->buttons .= Xml::tags( 'div', [ 'class' =>
274  'mw-history-revisionactions' ], $actionButtons );
275  }
276 
277  if ( $this->getAuthority()->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
278  $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
279  }
280 
281  $this->buttons .= '</div>';
282 
283  $s .= $this->buttons;
284  }
285 
286  $s .= '<section id="pagehistory" class="mw-pager-body">';
287 
288  return $s;
289  }
290 
291  private function getRevisionButton( $name, $msg ) {
292  $this->preventClickjacking = true;
293  $element = Html::element(
294  'button',
295  [
296  'type' => 'submit',
297  'name' => 'action',
298  'value' => $name,
299  'class' => "historysubmit mw-history-$name-button mw-ui-button",
300  'form' => 'mw-history-revisionactions',
301  ],
302  $this->msg( $msg )->text()
303  ) . "\n";
304  return $element;
305  }
306 
307  protected function getEndBody() {
308  if ( $this->getNumRows() == 0 ) {
309  return '';
310  }
311  $s = '';
312  # Add second buttons only if there is more than one rev
313  if ( $this->getNumRows() > 2 ) {
314  $s .= $this->buttons;
315  }
316  $s .= '</section>'; // closes section#pagehistory
317  $s .= '</form>';
318  return $s;
319  }
320 
328  private function submitButton( $message, $attributes = [] ) {
329  # Disable submit button if history has 1 revision only
330  if ( $this->getNumRows() > 1 ) {
331  return Html::submitButton( $message, $attributes );
332  } else {
333  return '';
334  }
335  }
336 
343  public function formatRow( $row ) {
344  $resultOffset = $this->getResultOffset();
345  $numRows = min( $this->mResult->numRows(), $this->mLimit );
346 
347  $firstInList = $resultOffset === ( $this->mIsBackwards ? $numRows - 1 : 0 );
348  // Next in the list, previous in chronological order.
349  $nextResultOffset = $resultOffset + ( $this->mIsBackwards ? -1 : 1 );
350 
351  $revRecord = $this->revisions[$resultOffset];
352  // This may only be null if the current line is the last one in the list.
353  $previousRevRecord = $this->revisions[$nextResultOffset] ?? null;
354 
355  $latest = $revRecord->getId() === $this->getWikiPage()->getLatest();
356  $curlink = $this->curLink( $revRecord );
357  if ( $previousRevRecord ) {
358  // Display a link to compare to the previous revision
359  $lastlink = $this->lastLink( $revRecord, $previousRevRecord );
360  } elseif ( $this->mIsBackwards && $this->mOffset !== '' ) {
361  // When paging "backwards", we don't have the extra result for the next revision that would
362  // appear in the list, and we don't know whether this is the oldest revision or not.
363  // However, if an offset has been specified, then the user probably reached this page by
364  // navigating from the "next" page, therefore the next revision probably exists.
365  // Display a link using &oldid=prev (this skips some checks but that's fine).
366  $lastlink = $this->lastLink( $revRecord, null );
367  } else {
368  // Do not display a link, because this is the oldest revision of the page
369  $lastlink = $this->historyPage->message['last'];
370  }
371  $curLastlinks = Html::rawElement( 'span', [], $curlink ) .
372  Html::rawElement( 'span', [], $lastlink );
373  $histLinks = Html::rawElement(
374  'span',
375  [ 'class' => 'mw-history-histlinks mw-changeslist-links' ],
376  $curLastlinks
377  );
378 
379  $diffButtons = $this->diffButtons( $revRecord, $firstInList );
380  $s = $histLinks . $diffButtons;
381 
382  $link = $this->revLink( $revRecord );
383  $classes = [];
384 
385  $del = '';
386  $canRevDelete = $this->getAuthority()->isAllowed( 'deleterevision' );
387  // Show checkboxes for each revision, to allow for revision deletion and
388  // change tags
389  if ( $canRevDelete || $this->showTagEditUI ) {
390  $this->preventClickjacking = true;
391  // If revision was hidden from sysops and we don't need the checkbox
392  // for anything else, disable it
393  if ( !$this->showTagEditUI
394  && !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $this->getAuthority() )
395  ) {
396  $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
397  // Otherwise, enable the checkbox...
398  } else {
399  $del = Xml::check( 'showhiderevisions', false,
400  [ 'name' => 'ids[' . $revRecord->getId() . ']', 'form' => 'mw-history-revisionactions' ] );
401  }
402  // User can only view deleted revisions...
403  } elseif ( $revRecord->getVisibility() && $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
404  // If revision was hidden from sysops, disable the link
405  if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $this->getAuthority() ) ) {
406  $del = Linker::revDeleteLinkDisabled( false );
407  // Otherwise, show the link...
408  } else {
409  $query = [
410  'type' => 'revision',
411  'target' => $this->getTitle()->getPrefixedDBkey(),
412  'ids' => $revRecord->getId()
413  ];
414  $del .= Linker::revDeleteLink(
415  $query,
416  $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
417  false
418  );
419  }
420  }
421  if ( $del ) {
422  $s .= " $del ";
423  }
424 
425  $lang = $this->getLanguage();
426  $dirmark = $lang->getDirMark();
427 
428  $s .= " $link";
429  $s .= $dirmark;
430  $s .= " <span class='history-user'>" .
431  Linker::revUserTools( $revRecord, true, false ) . "</span>";
432  $s .= $dirmark;
433 
434  if ( $revRecord->isMinor() ) {
435  $s .= ' ' . ChangesList::flag( 'minor', $this->getContext() );
436  }
437 
438  # Sometimes rev_len isn't populated
439  if ( $revRecord->getSize() !== null ) {
440  # Size is always public data
441  $prevSize = $this->parentLens[$row->rev_parent_id] ?? 0;
442  $sDiff = ChangesList::showCharacterDifference( $prevSize, $revRecord->getSize() );
443  $fSize = Linker::formatRevisionSize( $revRecord->getSize() );
444  $s .= ' <span class="mw-changeslist-separator"></span> ' . "$fSize $sDiff";
445  }
446 
447  # Text following the character difference is added just before running hooks
448  $s2 = $this->formattedComments[$resultOffset];
449 
450  if ( $s2 === '' ) {
451  $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
452  $s2 = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
453  }
454 
455  if ( $this->notificationTimestamp && $row->rev_timestamp >= $this->notificationTimestamp ) {
456  $s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';
457  $classes[] = 'mw-history-line-updated';
458  }
459 
460  $pagerTools = new PagerTools(
461  $revRecord,
462  $previousRevRecord,
463  $latest && $previousRevRecord,
464  $this->hookRunner,
465  $this->getTitle(),
466  $this->getContext(),
467  $this->getLinkRenderer()
468  );
469  if ( $pagerTools->shouldPreventClickjacking() ) {
470  $this->preventClickjacking = true;
471  }
472  $s2 .= $pagerTools->toHTML();
473 
474  # Tags
475  [ $tagSummary, $newClasses ] = ChangeTags::formatSummaryRow(
476  $row->ts_tags,
477  'history',
478  $this->getContext()
479  );
480  $classes = array_merge( $classes, $newClasses );
481  if ( $tagSummary !== '' ) {
482  $s2 .= " $tagSummary";
483  }
484 
485  # Include separator between character difference and following text
486  $s .= ' <span class="mw-changeslist-separator"></span> ' . $s2;
487 
488  $attribs = [ 'data-mw-revid' => $revRecord->getId() ];
489 
490  $this->hookRunner->onPageHistoryLineEnding( $this, $row, $s, $classes, $attribs );
491  $attribs = array_filter( $attribs,
492  [ Sanitizer::class, 'isReservedDataAttribute' ],
493  ARRAY_FILTER_USE_KEY
494  );
495 
496  if ( $classes ) {
497  $attribs['class'] = implode( ' ', $classes );
498  }
499 
500  return Xml::tags( 'li', $attribs, $s ) . "\n";
501  }
502 
509  private function revLink( RevisionRecord $rev ) {
510  return ChangesList::revDateLink( $rev, $this->getAuthority(), $this->getLanguage(),
511  $this->getTitle() );
512  }
513 
520  private function curLink( RevisionRecord $rev ) {
521  $cur = $this->historyPage->message['cur'];
522  $latest = $this->getWikiPage()->getLatest();
523  if ( $latest === $rev->getId()
524  || !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
525  ) {
526  return $cur;
527  } else {
528  return $this->getLinkRenderer()->makeKnownLink(
529  $this->getTitle(),
530  new HtmlArmor( $cur ),
531  [
532  'title' => $this->historyPage->message['tooltip-cur']
533  ],
534  [
535  'diff' => $latest,
536  'oldid' => $rev->getId()
537  ]
538  );
539  }
540  }
541 
550  private function lastLink( RevisionRecord $prevRev, ?RevisionRecord $nextRev ) {
551  $last = $this->historyPage->message['last'];
552 
553  if ( !$prevRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ||
554  ( $nextRev && !$nextRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) )
555  ) {
556  return $last;
557  }
558 
559  return $this->getLinkRenderer()->makeKnownLink(
560  $this->getTitle(),
561  new HtmlArmor( $last ),
562  [
563  'title' => $this->historyPage->message['tooltip-last']
564  ],
565  [
566  'diff' => 'prev', // T243569
567  'oldid' => $prevRev->getId()
568  ]
569  );
570  }
571 
580  private function diffButtons( RevisionRecord $rev, $firstInList ) {
581  if ( $this->getNumRows() > 1 ) {
582  $id = $rev->getId();
583  $radio = [ 'type' => 'radio', 'value' => $id ];
585  if ( $firstInList ) {
586  $first = Xml::element( 'input',
587  array_merge( $radio, [
588  // Disable the hidden radio because it can still
589  // be selected with arrow keys on Firefox
590  'disabled' => '',
591  'name' => 'oldid',
592  'id' => 'mw-oldid-null' ] )
593  );
594  $checkmark = [ 'checked' => 'checked' ];
595  } else {
596  # Check visibility of old revisions
597  if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
598  $radio['disabled'] = 'disabled';
599  $checkmark = []; // We will check the next possible one
600  } elseif ( !$this->oldIdChecked ) {
601  $checkmark = [ 'checked' => 'checked' ];
602  $this->oldIdChecked = $id;
603  } else {
604  $checkmark = [];
605  }
606  $first = Xml::element( 'input',
607  array_merge( $radio, $checkmark, [
608  'name' => 'oldid',
609  'id' => "mw-oldid-$id" ] ) );
610  $checkmark = [];
611  }
612  $second = Xml::element( 'input',
613  array_merge( $radio, $checkmark, [
614  'name' => 'diff',
615  'id' => "mw-diff-$id" ] ) );
616 
617  return $first . $second;
618  } else {
619  return '';
620  }
621  }
622 
628  protected function isNavigationBarShown() {
629  if ( $this->getNumRows() == 0 ) {
630  return false;
631  }
632  return parent::isNavigationBarShown();
633  }
634 
639  public function getPreventClickjacking() {
641  }
642 
643 }
const NS_USER
Definition: Defines.php:66
const NS_USER_TALK
Definition: Defines.php:67
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
Definition: ChangeTags.php:904
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:198
static revDateLink(RevisionRecord $rev, Authority $performer, Language $lang, $title=null)
Render the date and time of a revision in the current user language based on whether the user is able...
static showCharacterDifference( $old, $new, IContextSource $context=null)
Show formatted char difference.
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()
getWikiPage()
Get the WikiPage object.
getContext()
Get the base IContextSource object.
This class handles printing the history page for an article.
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
formatRow( $row)
Returns a row from the history printout.
isNavigationBarShown()
Returns whether to show the "navigation bar".
getEndBody()
Hook into getBody() for the end of the list.
__construct(HistoryAction $historyPage, $year=0, $month=0, $tagFilter='', array $conds=[], $day=0, LinkBatchFactory $linkBatchFactory=null, WatchlistManager $watchlistManager=null, CommentFormatter $commentFormatter=null, HookContainer $hookContainer=null)
array $parentLens
bool $showTagEditUI
Whether to show the tag editing UI.
getSqlComment()
Get some text to go in brackets in the "function name" part of the SQL comment.
getPreventClickjacking()
Get the "prevent clickjacking" flag.
getIndexField()
Returns the name of the index field.
getQueryInfo()
Provides all parameters needed for the main paged query.
getStartBody()
Creates begin of history list with a submit button.
getEmptyBody()
Returns message when query returns no revisions.
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
getNumRows()
Get the number of rows in the result set.
Definition: IndexPager.php:729
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:55
Class for generating clickable toggle links for a list of checkboxes.
Definition: ListToggle.php:35
Some internal bits split of from Skin.php.
Definition: Linker.php:65
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Page revision base class.
userCan( $field, Authority $performer)
Determine if the give authority is allowed to view a particular field of this revision,...
getId( $wikiId=self::LOCAL)
Get revision ID.
Service for looking up page revisions.
Generate a set of tools for a revision.
Definition: PagerTools.php:14
IndexPager with a formatted navigation bar.
getDateCond( $year, $month, $day=-1)
Set and return the offset timestamp such that we can get all revisions with a timestamp up to the spe...
static check( $name, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox.
Definition: Xml.php:330
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:135
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:44
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
if(!isset( $args[0])) $lang
$revQuery