MediaWiki REL1_37
HistoryPager.php
Go to the documentation of this file.
1<?php
29
38 public $lastRow = false;
39
41
42 protected $oldIdChecked;
43
44 protected $preventClickjacking = false;
48 protected $parentLens;
49
51 protected $showTagEditUI;
52
54 private $tagFilter;
55
58
61
64
75 public function __construct(
76 HistoryAction $historyPage,
77 $year = '',
78 $month = '',
79 $tagFilter = '',
80 array $conds = [],
81 $day = '',
82 LinkBatchFactory $linkBatchFactory = null,
84 ) {
85 parent::__construct( $historyPage->getContext() );
86 $this->historyPage = $historyPage;
87 $this->tagFilter = $tagFilter;
88 $this->getDateCond( $year, $month, $day );
89 $this->conds = $conds;
90 $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getAuthority() );
91 $services = MediaWikiServices::getInstance();
92 $this->revisionStore = $services->getRevisionStore();
93 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
94 $this->watchlistManager = $watchlistManager
95 ?? $services->getWatchlistManager();
96 }
97
98 // For hook compatibility...
99 public function getArticle() {
100 return $this->historyPage->getArticle();
101 }
102
103 protected function getSqlComment() {
104 if ( $this->conds ) {
105 return 'history page filtered'; // potentially slow, see CR r58153
106 } else {
107 return 'history page unfiltered';
108 }
109 }
110
111 public function getQueryInfo() {
112 $revQuery = $this->revisionStore->getQueryInfo( [ 'user' ] );
113 // T270033 Index renaming
114 $revIndex = $this->mDb->indexExists( 'revision', 'page_timestamp', __METHOD__ )
115 ? 'page_timestamp'
116 : 'rev_page_timestamp';
117
118 $queryInfo = [
119 'tables' => $revQuery['tables'],
120 'fields' => $revQuery['fields'],
121 'conds' => array_merge(
122 [ 'rev_page' => $this->getWikiPage()->getId() ],
123 $this->conds ),
124 'options' => [ 'USE INDEX' => [ 'revision' => $revIndex ] ],
125 'join_conds' => $revQuery['joins'],
126 ];
128 $queryInfo['tables'],
129 $queryInfo['fields'],
130 $queryInfo['conds'],
131 $queryInfo['join_conds'],
132 $queryInfo['options'],
133 $this->tagFilter
134 );
135
136 $this->getHookRunner()->onPageHistoryPager__getQueryInfo( $this, $queryInfo );
137
138 return $queryInfo;
139 }
140
141 public function getIndexField() {
142 return [ [ 'rev_timestamp', 'rev_id' ] ];
143 }
144
149 public function formatRow( $row ) {
150 if ( $this->lastRow ) {
151 $firstInList = $this->counter == 1;
152 $this->counter++;
153
154 $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
155 ? $this->watchlistManager
156 ->getTitleNotificationTimestamp( $this->getUser(), $this->getTitle() )
157 : false;
158
159 $s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, false, $firstInList );
160 } else {
161 $s = '';
162 }
163 $this->lastRow = $row;
164
165 return $s;
166 }
167
168 protected function doBatchLookups() {
169 if ( !$this->getHookRunner()->onPageHistoryPager__doBatchLookups( $this, $this->mResult ) ) {
170 return;
171 }
172
173 # Do a link batch query
174 $this->mResult->seek( 0 );
175 $batch = $this->linkBatchFactory->newLinkBatch();
176 $revIds = [];
177 foreach ( $this->mResult as $row ) {
178 if ( $row->rev_parent_id ) {
179 $revIds[] = $row->rev_parent_id;
180 }
181 if ( $row->user_name !== null ) {
182 $batch->add( NS_USER, $row->user_name );
183 $batch->add( NS_USER_TALK, $row->user_name );
184 } else { # for anons or usernames of imported revisions
185 $batch->add( NS_USER, $row->rev_user_text );
186 $batch->add( NS_USER_TALK, $row->rev_user_text );
187 }
188 }
189 $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
190 $batch->execute();
191 $this->mResult->seek( 0 );
192 }
193
198 protected function getEmptyBody() {
199 return $this->msg( 'history-empty' )->escaped();
200 }
201
207 protected function getStartBody() {
208 $this->lastRow = false;
209 $this->counter = 1;
210 $this->oldIdChecked = 0;
211 $s = '';
212 // Button container stored in $this->buttons for re-use in getEndBody()
213 $this->buttons = '';
214 if ( $this->getNumRows() > 0 ) {
215 $this->getOutput()->wrapWikiMsg( "<div class='mw-history-legend'>\n$1\n</div>", 'histlegend' );
216 $s = Html::openElement( 'form', [
217 'action' => wfScript(),
218 'id' => 'mw-history-compare'
219 ] ) . "\n";
220 $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n";
221 $s .= Html::hidden( 'action', 'historysubmit' ) . "\n";
222 $s .= Html::hidden( 'type', 'revision' ) . "\n";
223
224 $this->buttons .= Html::openElement(
225 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
226 $className = 'historysubmit mw-history-compareselectedversions-button mw-ui-button';
227 $attrs = [ 'class' => $className ]
228 + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
229 $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
230 $attrs
231 ) . "\n";
232
233 $actionButtons = '';
234 if ( $this->getAuthority()->isAllowed( 'deleterevision' ) ) {
235 $actionButtons .= $this->getRevisionButton(
236 'revisiondelete', 'showhideselectedversions' );
237 }
238 if ( $this->showTagEditUI ) {
239 $actionButtons .= $this->getRevisionButton(
240 'editchangetags', 'history-edit-tags' );
241 }
242 if ( $actionButtons ) {
243 $this->buttons .= Xml::tags( 'div', [ 'class' =>
244 'mw-history-revisionactions' ], $actionButtons );
245 }
246
247 if ( $this->getAuthority()->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
248 $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
249 }
250
251 $this->buttons .= '</div>';
252
253 $s .= $this->buttons;
254 $s .= '<ul id="pagehistory">' . "\n";
255 }
256
257 return $s;
258 }
259
260 private function getRevisionButton( $name, $msg ) {
261 $this->preventClickjacking();
262 $element = Html::element(
263 'button',
264 [
265 'type' => 'submit',
266 'name' => $name,
267 'value' => '1',
268 'class' => "historysubmit mw-history-$name-button mw-ui-button",
269 ],
270 $this->msg( $msg )->text()
271 ) . "\n";
272 return $element;
273 }
274
275 protected function getEndBody() {
276 if ( $this->getNumRows() == 0 ) {
277 return '';
278 }
279
280 if ( $this->lastRow ) {
281 $firstInList = $this->counter == 1;
282 if ( $this->mIsBackwards ) {
283 # Next row is unknown, but for UI reasons, probably exists if an offset has been specified
284 if ( $this->mOffset == '' ) {
285 $next = null;
286 } else {
287 $next = 'unknown';
288 }
289 } else {
290 # The next row is the past-the-end row
291 $next = $this->mPastTheEndRow;
292 }
293 $this->counter++;
294
295 $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
296 ? $this->watchlistManager
297 ->getTitleNotificationTimestamp( $this->getUser(), $this->getTitle() )
298 : false;
299
300 $s = $this->historyLine( $this->lastRow, $next, $notifTimestamp, false, $firstInList );
301 } else {
302 $s = '';
303 }
304 $s .= "</ul>\n";
305 # Add second buttons only if there is more than one rev
306 if ( $this->getNumRows() > 2 ) {
307 $s .= $this->buttons;
308 }
309 $s .= '</form>';
310 return $s;
311 }
312
320 private function submitButton( $message, $attributes = [] ) {
321 # Disable submit button if history has 1 revision only
322 if ( $this->getNumRows() > 1 ) {
323 return Html::submitButton( $message, $attributes );
324 } else {
325 return '';
326 }
327 }
328
343 private function historyLine( $row, $next, $notificationtimestamp = false,
344 $dummy = false, $firstInList = false ) {
345 $revRecord = $this->revisionStore->newRevisionFromRow(
346 $row,
347 RevisionStore::READ_NORMAL,
348 $this->getTitle()
349 );
350
351 if ( is_object( $next ) ) {
352 $previousRevRecord = $this->revisionStore->newRevisionFromRow(
353 $next,
354 RevisionStore::READ_NORMAL,
355 $this->getTitle()
356 );
357 } else {
358 $previousRevRecord = null;
359 }
360
361 $latest = $revRecord->getId() === $this->getWikiPage()->getLatest();
362 $curlink = $this->curLink( $revRecord );
363 $lastlink = $this->lastLink( $revRecord, $next );
364 $curLastlinks = Html::rawElement( 'span', [], $curlink ) .
365 Html::rawElement( 'span', [], $lastlink );
366 $histLinks = Html::rawElement(
367 'span',
368 [ 'class' => 'mw-history-histlinks mw-changeslist-links' ],
369 $curLastlinks
370 );
371
372 $diffButtons = $this->diffButtons( $revRecord, $firstInList );
373 $s = $histLinks . $diffButtons;
374
375 $link = $this->revLink( $revRecord );
376 $classes = [];
377
378 $del = '';
379 $user = $this->getUser();
380 $canRevDelete = $this->getAuthority()->isAllowed( 'deleterevision' );
381 // Show checkboxes for each revision, to allow for revision deletion and
382 // change tags
383 $visibility = $revRecord->getVisibility();
384 if ( $canRevDelete || $this->showTagEditUI ) {
385 $this->preventClickjacking();
386 // If revision was hidden from sysops and we don't need the checkbox
387 // for anything else, disable it
388 if ( !$this->showTagEditUI
389 && !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $this->getAuthority() )
390 ) {
391 $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
392 // Otherwise, enable the checkbox...
393 } else {
394 $del = Xml::check( 'showhiderevisions', false,
395 [ 'name' => 'ids[' . $revRecord->getId() . ']' ] );
396 }
397 // User can only view deleted revisions...
398 } elseif ( $revRecord->getVisibility() && $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
399 // If revision was hidden from sysops, disable the link
400 if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $this->getAuthority() ) ) {
401 $del = Linker::revDeleteLinkDisabled( false );
402 // Otherwise, show the link...
403 } else {
404 $query = [
405 'type' => 'revision',
406 'target' => $this->getTitle()->getPrefixedDBkey(),
407 'ids' => $revRecord->getId()
408 ];
409 $del .= Linker::revDeleteLink(
410 $query,
411 $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
412 false
413 );
414 }
415 }
416 if ( $del ) {
417 $s .= " $del ";
418 }
419
420 $lang = $this->getLanguage();
421 $dirmark = $lang->getDirMark();
422
423 $s .= " $link";
424 $s .= $dirmark;
425 $s .= " <span class='history-user'>" .
426 Linker::revUserTools( $revRecord, true, false ) . "</span>";
427 $s .= $dirmark;
428
429 if ( $revRecord->isMinor() ) {
430 $s .= ' ' . ChangesList::flag( 'minor', $this->getContext() );
431 }
432
433 # Sometimes rev_len isn't populated
434 if ( $revRecord->getSize() !== null ) {
435 # Size is always public data
436 $prevSize = $this->parentLens[$row->rev_parent_id] ?? 0;
437 $sDiff = ChangesList::showCharacterDifference( $prevSize, $revRecord->getSize() );
438 $fSize = Linker::formatRevisionSize( $revRecord->getSize() );
439 $s .= ' <span class="mw-changeslist-separator"></span> ' . "$fSize $sDiff";
440 }
441
442 # Text following the character difference is added just before running hooks
443 $s2 = Linker::revComment( $revRecord, false, true, false );
444
445 if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) {
446 $s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';
447 $classes[] = 'mw-history-line-updated';
448 }
449
450 $tools = [];
451
452 # Rollback and undo links
453
454 if ( $previousRevRecord && $this->getAuthority()->probablyCan( 'edit', $this->getTitle() ) ) {
455 if ( $latest && $this->getAuthority()->probablyCan( 'rollback', $this->getTitle() )
456 ) {
457 // Get a rollback link without the brackets
458 $rollbackLink = Linker::generateRollback(
459 $revRecord,
460 $this->getContext(),
461 [ 'verify', 'noBrackets' ]
462 );
463 if ( $rollbackLink ) {
464 $this->preventClickjacking();
465 $tools[] = $rollbackLink;
466 }
467 }
468
469 if ( !$revRecord->isDeleted( RevisionRecord::DELETED_TEXT )
470 && !$previousRevRecord->isDeleted( RevisionRecord::DELETED_TEXT )
471 ) {
472 # Create undo tooltip for the first (=latest) line only
473 $undoTooltip = $latest
474 ? [ 'title' => $this->msg( 'tooltip-undo' )->text() ]
475 : [];
476 $undolink = $this->getLinkRenderer()->makeKnownLink(
477 $this->getTitle(),
478 $this->msg( 'editundo' )->text(),
479 $undoTooltip,
480 [
481 'action' => 'edit',
482 'undoafter' => $previousRevRecord->getId(),
483 'undo' => $revRecord->getId()
484 ]
485 );
486 $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
487 }
488 }
489 // Allow extension to add their own links here
490 $this->getHookRunner()->onHistoryTools(
491 $revRecord,
492 $tools,
493 $previousRevRecord,
494 $user
495 );
496
497 if ( $tools ) {
498 $s2 .= ' ' . Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] );
499 foreach ( $tools as $tool ) {
500 $s2 .= Html::rawElement( 'span', [], $tool );
501 }
502 $s2 .= Html::closeElement( 'span' );
503 }
504
505 # Tags
506 list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
507 $row->ts_tags,
508 'history',
509 $this->getContext()
510 );
511 $classes = array_merge( $classes, $newClasses );
512 if ( $tagSummary !== '' ) {
513 $s2 .= " $tagSummary";
514 }
515
516 # Include separator between character difference and following text
517 if ( $s2 !== '' ) {
518 $s .= ' <span class="mw-changeslist-separator"></span> ' . $s2;
519 }
520
521 $attribs = [ 'data-mw-revid' => $revRecord->getId() ];
522
523 $this->getHookRunner()->onPageHistoryLineEnding( $this, $row, $s, $classes, $attribs );
524 $attribs = array_filter( $attribs,
525 [ Sanitizer::class, 'isReservedDataAttribute' ],
526 ARRAY_FILTER_USE_KEY
527 );
528
529 if ( $classes ) {
530 $attribs['class'] = implode( ' ', $classes );
531 }
532
533 return Xml::tags( 'li', $attribs, $s ) . "\n";
534 }
535
542 private function revLink( RevisionRecord $rev ) {
543 return ChangesList::revDateLink( $rev, $this->getUser(), $this->getLanguage(),
544 $this->getTitle() );
545 }
546
553 private function curLink( RevisionRecord $rev ) {
554 $cur = $this->historyPage->message['cur'];
555 $latest = $this->getWikiPage()->getLatest();
556 if ( $latest === $rev->getId()
557 || !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
558 ) {
559 return $cur;
560 } else {
561 return $this->getLinkRenderer()->makeKnownLink(
562 $this->getTitle(),
563 new HtmlArmor( $cur ),
564 [],
565 [
566 'diff' => $latest,
567 'oldid' => $rev->getId()
568 ]
569 );
570 }
571 }
572
582 private function lastLink( RevisionRecord $prevRev, $next ) {
583 $last = $this->historyPage->message['last'];
584
585 if ( $next === null ) {
586 # Probably no next row
587 return $last;
588 }
589
591 if ( $next === 'unknown' ) {
592 # Next row probably exists but is unknown, use an oldid=prev link
594 $this->getTitle(),
595 new HtmlArmor( $last ),
596 [],
597 [
598 'diff' => $prevRev->getId(),
599 'oldid' => 'prev'
600 ]
601 );
602 }
603
604 $nextRev = $this->revisionStore->newRevisionFromRow(
605 $next,
606 RevisionStore::READ_NORMAL,
607 $this->getTitle()
608 );
609
610 if ( !$prevRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ||
611 !$nextRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
612 ) {
613 return $last;
614 }
615
617 $this->getTitle(),
618 new HtmlArmor( $last ),
619 [],
620 [
621 'diff' => $prevRev->getId(),
622 'oldid' => $next->rev_id
623 ]
624 );
625 }
626
635 private function diffButtons( RevisionRecord $rev, $firstInList ) {
636 if ( $this->getNumRows() > 1 ) {
637 $id = $rev->getId();
638 $radio = [ 'type' => 'radio', 'value' => $id ];
640 if ( $firstInList ) {
641 $first = Xml::element( 'input',
642 array_merge( $radio, [
643 'style' => 'visibility:hidden',
644 'name' => 'oldid',
645 'id' => 'mw-oldid-null' ] )
646 );
647 $checkmark = [ 'checked' => 'checked' ];
648 } else {
649 # Check visibility of old revisions
650 if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
651 $radio['disabled'] = 'disabled';
652 $checkmark = []; // We will check the next possible one
653 } elseif ( !$this->oldIdChecked ) {
654 $checkmark = [ 'checked' => 'checked' ];
655 $this->oldIdChecked = $id;
656 } else {
657 $checkmark = [];
658 }
659 $first = Xml::element( 'input',
660 array_merge( $radio, $checkmark, [
661 'name' => 'oldid',
662 'id' => "mw-oldid-$id" ] ) );
663 $checkmark = [];
664 }
665 $second = Xml::element( 'input',
666 array_merge( $radio, $checkmark, [
667 'name' => 'diff',
668 'id' => "mw-diff-$id" ] ) );
669
670 return $first . $second;
671 } else {
672 return '';
673 }
674 }
675
681 protected function isNavigationBarShown() {
682 if ( $this->getNumRows() == 0 ) {
683 return false;
684 }
685 return parent::isNavigationBarShown();
686 }
687
691 public function getDefaultQuery() {
692 parent::getDefaultQuery();
693 unset( $this->mDefaultQuery['date-range-to'] );
694 return $this->mDefaultQuery;
695 }
696
701 private function preventClickjacking( $enable = true ) {
702 $this->preventClickjacking = $enable;
703 }
704
709 public function getPreventClickjacking() {
710 return $this->preventClickjacking;
711 }
712
713}
getAuthority()
WatchlistManager $watchlistManager
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...
getContext()
getContext()
Get the IContextSource in use here.
Definition Action.php:132
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getWikiPage()
Get the WikiPage object.
This class handles printing the history page for an article.
preventClickjacking( $enable=true)
This is called if a write operation is possible from the generated HTML.
getRevisionButton( $name, $msg)
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
revLink(RevisionRecord $rev)
Create a link to view this revision of the page.
string $tagFilter
isNavigationBarShown()
Returns whether to show the "navigation bar".
getEndBody()
Hook into getBody() for the end of the list.
historyLine( $row, $next, $notificationtimestamp=false, $dummy=false, $firstInList=false)
Returns a row from the history printout.
lastLink(RevisionRecord $prevRev, $next)
Create a diff-to-previous link for this revision for this page.
getDefaultQuery()
Get an array of query parameters that should be put into self-links.By default, all parameters passed...
submitButton( $message, $attributes=[])
Creates a submit button.
__construct(HistoryAction $historyPage, $year='', $month='', $tagFilter='', array $conds=[], $day='', LinkBatchFactory $linkBatchFactory=null, WatchlistManager $watchlistManager=null)
diffButtons(RevisionRecord $rev, $firstInList)
Create radio buttons for page history.
LinkBatchFactory $linkBatchFactory
curLink(RevisionRecord $rev)
Create a diff-to-current link for this revision for this page.
WatchlistManager $watchlistManager
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.
bool stdClass $lastRow
RevisionStore $revisionStore
getEmptyBody()
Returns message when query returns no revisions.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
LinkRenderer $linkRenderer
getNumRows()
Get the number of rows in the result set.
static revComment(RevisionRecord $revRecord, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition Linker.php:1782
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2436
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition Linker.php:2031
static formatRevisionSize( $size)
Definition Linker.php:1820
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition Linker.php:1319
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2455
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2412
Class for generating clickable toggle links for a list of checkboxes.
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
MediaWikiServices is the service locator for the application scope of MediaWiki.
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.
Efficient paging for SQL queries.
getDateCond( $year, $month, $day=-1)
Set and return the mOffset timestamp such that we can get all revisions with a timestamp up to the sp...
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s
if(!isset( $args[0])) $lang