MediaWiki REL1_36
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,
83 WatchlistManager $watchlistManager = 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 $queryInfo = [
114 'tables' => $revQuery['tables'],
115 'fields' => $revQuery['fields'],
116 'conds' => array_merge(
117 [ 'rev_page' => $this->getWikiPage()->getId() ],
118 $this->conds ),
119 'options' => [ 'USE INDEX' => [ 'revision' => 'page_timestamp' ] ],
120 'join_conds' => $revQuery['joins'],
121 ];
123 $queryInfo['tables'],
124 $queryInfo['fields'],
125 $queryInfo['conds'],
126 $queryInfo['join_conds'],
127 $queryInfo['options'],
128 $this->tagFilter
129 );
130
131 $this->getHookRunner()->onPageHistoryPager__getQueryInfo( $this, $queryInfo );
132
133 return $queryInfo;
134 }
135
136 public function getIndexField() {
137 return [ [ 'rev_timestamp', 'rev_id' ] ];
138 }
139
144 public function formatRow( $row ) {
145 if ( $this->lastRow ) {
146 $firstInList = $this->counter == 1;
147 $this->counter++;
148
149 $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
150 ? $this->watchlistManager
151 ->getTitleNotificationTimestamp( $this->getUser(), $this->getTitle() )
152 : false;
153
154 $s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, false, $firstInList );
155 } else {
156 $s = '';
157 }
158 $this->lastRow = $row;
159
160 return $s;
161 }
162
163 protected function doBatchLookups() {
164 if ( !$this->getHookRunner()->onPageHistoryPager__doBatchLookups( $this, $this->mResult ) ) {
165 return;
166 }
167
168 # Do a link batch query
169 $this->mResult->seek( 0 );
170 $batch = $this->linkBatchFactory->newLinkBatch();
171 $revIds = [];
172 foreach ( $this->mResult as $row ) {
173 if ( $row->rev_parent_id ) {
174 $revIds[] = $row->rev_parent_id;
175 }
176 if ( $row->user_name !== null ) {
177 $batch->add( NS_USER, $row->user_name );
178 $batch->add( NS_USER_TALK, $row->user_name );
179 } else { # for anons or usernames of imported revisions
180 $batch->add( NS_USER, $row->rev_user_text );
181 $batch->add( NS_USER_TALK, $row->rev_user_text );
182 }
183 }
184 $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
185 $batch->execute();
186 $this->mResult->seek( 0 );
187 }
188
193 protected function getEmptyBody() {
194 return $this->msg( 'history-empty' )->escaped();
195 }
196
202 protected function getStartBody() {
203 $this->lastRow = false;
204 $this->counter = 1;
205 $this->oldIdChecked = 0;
206 $s = '';
207 // Button container stored in $this->buttons for re-use in getEndBody()
208 $this->buttons = '';
209 if ( $this->getNumRows() > 0 ) {
210 $this->getOutput()->wrapWikiMsg( "<div class='mw-history-legend'>\n$1\n</div>", 'histlegend' );
211 $s = Html::openElement( 'form', [
212 'action' => wfScript(),
213 'id' => 'mw-history-compare'
214 ] ) . "\n";
215 $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n";
216 $s .= Html::hidden( 'action', 'historysubmit' ) . "\n";
217 $s .= Html::hidden( 'type', 'revision' ) . "\n";
218
219 $this->buttons .= Html::openElement(
220 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
221 $className = 'historysubmit mw-history-compareselectedversions-button mw-ui-button';
222 $attrs = [ 'class' => $className ]
223 + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
224 $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
225 $attrs
226 ) . "\n";
227
228 $actionButtons = '';
229 if ( $this->getAuthority()->isAllowed( 'deleterevision' ) ) {
230 $actionButtons .= $this->getRevisionButton(
231 'revisiondelete', 'showhideselectedversions' );
232 }
233 if ( $this->showTagEditUI ) {
234 $actionButtons .= $this->getRevisionButton(
235 'editchangetags', 'history-edit-tags' );
236 }
237 if ( $actionButtons ) {
238 $this->buttons .= Xml::tags( 'div', [ 'class' =>
239 'mw-history-revisionactions' ], $actionButtons );
240 }
241
242 if ( $this->getAuthority()->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
243 $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
244 }
245
246 $this->buttons .= '</div>';
247
248 $s .= $this->buttons;
249 $s .= '<ul id="pagehistory">' . "\n";
250 }
251
252 return $s;
253 }
254
255 private function getRevisionButton( $name, $msg ) {
256 $this->preventClickjacking();
257 $element = Html::element(
258 'button',
259 [
260 'type' => 'submit',
261 'name' => $name,
262 'value' => '1',
263 'class' => "historysubmit mw-history-$name-button mw-ui-button",
264 ],
265 $this->msg( $msg )->text()
266 ) . "\n";
267 return $element;
268 }
269
270 protected function getEndBody() {
271 if ( $this->getNumRows() == 0 ) {
272 return '';
273 }
274
275 if ( $this->lastRow ) {
276 $firstInList = $this->counter == 1;
277 if ( $this->mIsBackwards ) {
278 # Next row is unknown, but for UI reasons, probably exists if an offset has been specified
279 if ( $this->mOffset == '' ) {
280 $next = null;
281 } else {
282 $next = 'unknown';
283 }
284 } else {
285 # The next row is the past-the-end row
286 $next = $this->mPastTheEndRow;
287 }
288 $this->counter++;
289
290 $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
291 ? $this->watchlistManager
292 ->getTitleNotificationTimestamp( $this->getUser(), $this->getTitle() )
293 : false;
294
295 $s = $this->historyLine( $this->lastRow, $next, $notifTimestamp, false, $firstInList );
296 } else {
297 $s = '';
298 }
299 $s .= "</ul>\n";
300 # Add second buttons only if there is more than one rev
301 if ( $this->getNumRows() > 2 ) {
302 $s .= $this->buttons;
303 }
304 $s .= '</form>';
305 return $s;
306 }
307
315 private function submitButton( $message, $attributes = [] ) {
316 # Disable submit button if history has 1 revision only
317 if ( $this->getNumRows() > 1 ) {
318 return Html::submitButton( $message, $attributes );
319 } else {
320 return '';
321 }
322 }
323
338 private function historyLine( $row, $next, $notificationtimestamp = false,
339 $dummy = false, $firstInList = false ) {
340 $revRecord = $this->revisionStore->newRevisionFromRow(
341 $row,
342 RevisionStore::READ_NORMAL,
343 $this->getTitle()
344 );
345
346 if ( is_object( $next ) ) {
347 $previousRevRecord = $this->revisionStore->newRevisionFromRow(
348 $next,
349 RevisionStore::READ_NORMAL,
350 $this->getTitle()
351 );
352 } else {
353 $previousRevRecord = null;
354 }
355
356 $latest = $revRecord->getId() === $this->getWikiPage()->getLatest();
357 $curlink = $this->curLink( $revRecord );
358 $lastlink = $this->lastLink( $revRecord, $next );
359 $curLastlinks = Html::rawElement( 'span', [], $curlink ) .
360 Html::rawElement( 'span', [], $lastlink );
361 $histLinks = Html::rawElement(
362 'span',
363 [ 'class' => 'mw-history-histlinks mw-changeslist-links' ],
364 $curLastlinks
365 );
366
367 $diffButtons = $this->diffButtons( $revRecord, $firstInList );
368 $s = $histLinks . $diffButtons;
369
370 $link = $this->revLink( $revRecord );
371 $classes = [];
372
373 $del = '';
374 $user = $this->getUser();
375 $canRevDelete = $this->getAuthority()->isAllowed( 'deleterevision' );
376 // Show checkboxes for each revision, to allow for revision deletion and
377 // change tags
378 $visibility = $revRecord->getVisibility();
379 if ( $canRevDelete || $this->showTagEditUI ) {
380 $this->preventClickjacking();
381 // If revision was hidden from sysops and we don't need the checkbox
382 // for anything else, disable it
383 if ( !$this->showTagEditUI
384 && !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $this->getAuthority() )
385 ) {
386 $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
387 // Otherwise, enable the checkbox...
388 } else {
389 $del = Xml::check( 'showhiderevisions', false,
390 [ 'name' => 'ids[' . $revRecord->getId() . ']' ] );
391 }
392 // User can only view deleted revisions...
393 } elseif ( $revRecord->getVisibility() && $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
394 // If revision was hidden from sysops, disable the link
395 if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $this->getAuthority() ) ) {
396 $del = Linker::revDeleteLinkDisabled( false );
397 // Otherwise, show the link...
398 } else {
399 $query = [
400 'type' => 'revision',
401 'target' => $this->getTitle()->getPrefixedDBkey(),
402 'ids' => $revRecord->getId()
403 ];
404 $del .= Linker::revDeleteLink(
405 $query,
406 $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
407 false
408 );
409 }
410 }
411 if ( $del ) {
412 $s .= " $del ";
413 }
414
415 $lang = $this->getLanguage();
416 $dirmark = $lang->getDirMark();
417
418 $s .= " $link";
419 $s .= $dirmark;
420 $s .= " <span class='history-user'>" .
421 Linker::revUserTools( $revRecord, true, false ) . "</span>";
422 $s .= $dirmark;
423
424 if ( $revRecord->isMinor() ) {
425 $s .= ' ' . ChangesList::flag( 'minor', $this->getContext() );
426 }
427
428 # Sometimes rev_len isn't populated
429 if ( $revRecord->getSize() !== null ) {
430 # Size is always public data
431 $prevSize = $this->parentLens[$row->rev_parent_id] ?? 0;
432 $sDiff = ChangesList::showCharacterDifference( $prevSize, $revRecord->getSize() );
433 $fSize = Linker::formatRevisionSize( $revRecord->getSize() );
434 $s .= ' <span class="mw-changeslist-separator"></span> ' . "$fSize $sDiff";
435 }
436
437 # Text following the character difference is added just before running hooks
438 $s2 = Linker::revComment( $revRecord, false, true, false );
439
440 if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) {
441 $s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';
442 $classes[] = 'mw-history-line-updated';
443 }
444
445 $tools = [];
446
447 # Rollback and undo links
448
449 if ( $previousRevRecord && $this->getAuthority()->probablyCan( 'edit', $this->getTitle() ) ) {
450 if ( $latest && $this->getAuthority()->probablyCan( 'rollback', $this->getTitle() )
451 ) {
452 // Get a rollback link without the brackets
453 $rollbackLink = Linker::generateRollback(
454 $revRecord,
455 $this->getContext(),
456 [ 'verify', 'noBrackets' ]
457 );
458 if ( $rollbackLink ) {
459 $this->preventClickjacking();
460 $tools[] = $rollbackLink;
461 }
462 }
463
464 if ( !$revRecord->isDeleted( RevisionRecord::DELETED_TEXT )
465 && !$previousRevRecord->isDeleted( RevisionRecord::DELETED_TEXT )
466 ) {
467 # Create undo tooltip for the first (=latest) line only
468 $undoTooltip = $latest
469 ? [ 'title' => $this->msg( 'tooltip-undo' )->text() ]
470 : [];
471 $undolink = $this->getLinkRenderer()->makeKnownLink(
472 $this->getTitle(),
473 $this->msg( 'editundo' )->text(),
474 $undoTooltip,
475 [
476 'action' => 'edit',
477 'undoafter' => $previousRevRecord->getId(),
478 'undo' => $revRecord->getId()
479 ]
480 );
481 $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
482 }
483 }
484 // Allow extension to add their own links here
485 $this->getHookRunner()->onHistoryTools(
486 $revRecord,
487 $tools,
488 $previousRevRecord,
489 $user
490 );
491
492 // Hook is deprecated since 1.35
493 if ( $this->getHookContainer()->isRegistered( 'HistoryRevisionTools' ) ) {
494 // Only create the Revision objects if needed
495 $this->getHookRunner()->onHistoryRevisionTools(
496 new Revision( $revRecord ),
497 $tools,
498 $previousRevRecord ? new Revision( $previousRevRecord ) : null,
499 $user
500 );
501 }
502
503 if ( $tools ) {
504 $s2 .= ' ' . Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] );
505 foreach ( $tools as $tool ) {
506 $s2 .= Html::rawElement( 'span', [], $tool );
507 }
508 $s2 .= Html::closeElement( 'span' );
509 }
510
511 # Tags
512 list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
513 $row->ts_tags,
514 'history',
515 $this->getContext()
516 );
517 $classes = array_merge( $classes, $newClasses );
518 if ( $tagSummary !== '' ) {
519 $s2 .= " $tagSummary";
520 }
521
522 # Include separator between character difference and following text
523 if ( $s2 !== '' ) {
524 $s .= ' <span class="mw-changeslist-separator"></span> ' . $s2;
525 }
526
527 $attribs = [ 'data-mw-revid' => $revRecord->getId() ];
528
529 $this->getHookRunner()->onPageHistoryLineEnding( $this, $row, $s, $classes, $attribs );
530 $attribs = array_filter( $attribs,
531 [ Sanitizer::class, 'isReservedDataAttribute' ],
532 ARRAY_FILTER_USE_KEY
533 );
534
535 if ( $classes ) {
536 $attribs['class'] = implode( ' ', $classes );
537 }
538
539 return Xml::tags( 'li', $attribs, $s ) . "\n";
540 }
541
548 private function revLink( RevisionRecord $rev ) {
549 return ChangesList::revDateLink( $rev, $this->getUser(), $this->getLanguage(),
550 $this->getTitle() );
551 }
552
559 private function curLink( RevisionRecord $rev ) {
560 $cur = $this->historyPage->message['cur'];
561 $latest = $this->getWikiPage()->getLatest();
562 if ( $latest === $rev->getId()
563 || !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
564 ) {
565 return $cur;
566 } else {
567 return $this->getLinkRenderer()->makeKnownLink(
568 $this->getTitle(),
569 new HtmlArmor( $cur ),
570 [],
571 [
572 'diff' => $latest,
573 'oldid' => $rev->getId()
574 ]
575 );
576 }
577 }
578
588 private function lastLink( RevisionRecord $prevRev, $next ) {
589 $last = $this->historyPage->message['last'];
590
591 if ( $next === null ) {
592 # Probably no next row
593 return $last;
594 }
595
597 if ( $next === 'unknown' ) {
598 # Next row probably exists but is unknown, use an oldid=prev link
600 $this->getTitle(),
601 new HtmlArmor( $last ),
602 [],
603 [
604 'diff' => $prevRev->getId(),
605 'oldid' => 'prev'
606 ]
607 );
608 }
609
610 $nextRev = $this->revisionStore->newRevisionFromRow(
611 $next,
612 RevisionStore::READ_NORMAL,
613 $this->getTitle()
614 );
615
616 if ( !$prevRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ||
617 !$nextRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
618 ) {
619 return $last;
620 }
621
623 $this->getTitle(),
624 new HtmlArmor( $last ),
625 [],
626 [
627 'diff' => $prevRev->getId(),
628 'oldid' => $next->rev_id
629 ]
630 );
631 }
632
641 private function diffButtons( RevisionRecord $rev, $firstInList ) {
642 if ( $this->getNumRows() > 1 ) {
643 $id = $rev->getId();
644 $radio = [ 'type' => 'radio', 'value' => $id ];
646 if ( $firstInList ) {
647 $first = Xml::element( 'input',
648 array_merge( $radio, [
649 'style' => 'visibility:hidden',
650 'name' => 'oldid',
651 'id' => 'mw-oldid-null' ] )
652 );
653 $checkmark = [ 'checked' => 'checked' ];
654 } else {
655 # Check visibility of old revisions
656 if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
657 $radio['disabled'] = 'disabled';
658 $checkmark = []; // We will check the next possible one
659 } elseif ( !$this->oldIdChecked ) {
660 $checkmark = [ 'checked' => 'checked' ];
661 $this->oldIdChecked = $id;
662 } else {
663 $checkmark = [];
664 }
665 $first = Xml::element( 'input',
666 array_merge( $radio, $checkmark, [
667 'name' => 'oldid',
668 'id' => "mw-oldid-$id" ] ) );
669 $checkmark = [];
670 }
671 $second = Xml::element( 'input',
672 array_merge( $radio, $checkmark, [
673 'name' => 'diff',
674 'id' => "mw-diff-$id" ] ) );
675
676 return $first . $second;
677 } else {
678 return '';
679 }
680 }
681
687 protected function isNavigationBarShown() {
688 if ( $this->getNumRows() == 0 ) {
689 return false;
690 }
691 return parent::isNavigationBarShown();
692 }
693
697 public function getDefaultQuery() {
698 parent::getDefaultQuery();
699 unset( $this->mDefaultQuery['date-range-to'] );
700 return $this->mDefaultQuery;
701 }
702
707 private function preventClickjacking( $enable = true ) {
708 $this->preventClickjacking = $enable;
709 }
710
715 public function getPreventClickjacking() {
716 return $this->preventClickjacking;
717 }
718
719}
getAuthority()
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:204
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
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 generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition Linker.php:1862
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2284
static revComment( $rev, $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:1610
static formatRevisionSize( $size)
Definition Linker.php:1651
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition Linker.php:1147
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2303
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2260
Class for generating clickable toggle links for a list of checkboxes.
makeKnownLink(LinkTarget $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