MediaWiki REL1_35
HistoryPager.php
Go to the documentation of this file.
1<?php
27
36 public $lastRow = false;
37
39
40 protected $oldIdChecked;
41
42 protected $preventClickjacking = false;
46 protected $parentLens;
47
49 protected $showTagEditUI;
50
52 private $tagFilter;
53
56
65 public function __construct(
66 HistoryAction $historyPage,
67 $year = '',
68 $month = '',
69 $tagFilter = '',
70 array $conds = [],
71 $day = ''
72 ) {
73 parent::__construct( $historyPage->getContext() );
74 $this->historyPage = $historyPage;
75 $this->tagFilter = $tagFilter;
76 $this->getDateCond( $year, $month, $day );
77 $this->conds = $conds;
78 $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getUser() );
79 $this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
80 }
81
82 // For hook compatibility...
83 public function getArticle() {
84 return $this->historyPage->getArticle();
85 }
86
87 protected function getSqlComment() {
88 if ( $this->conds ) {
89 return 'history page filtered'; // potentially slow, see CR r58153
90 } else {
91 return 'history page unfiltered';
92 }
93 }
94
95 public function getQueryInfo() {
96 $revQuery = $this->revisionStore->getQueryInfo( [ 'user' ] );
97 $queryInfo = [
98 'tables' => $revQuery['tables'],
99 'fields' => $revQuery['fields'],
100 'conds' => array_merge(
101 [ 'rev_page' => $this->getWikiPage()->getId() ],
102 $this->conds ),
103 'options' => [ 'USE INDEX' => [ 'revision' => 'page_timestamp' ] ],
104 'join_conds' => $revQuery['joins'],
105 ];
107 $queryInfo['tables'],
108 $queryInfo['fields'],
109 $queryInfo['conds'],
110 $queryInfo['join_conds'],
111 $queryInfo['options'],
112 $this->tagFilter
113 );
114
115 $this->getHookRunner()->onPageHistoryPager__getQueryInfo( $this, $queryInfo );
116
117 return $queryInfo;
118 }
119
120 public function getIndexField() {
121 return [ [ 'rev_timestamp', 'rev_id' ] ];
122 }
123
128 public function formatRow( $row ) {
129 if ( $this->lastRow ) {
130 $firstInList = $this->counter == 1;
131 $this->counter++;
132
133 $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
134 ? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
135 : false;
136
137 $s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, false, $firstInList );
138 } else {
139 $s = '';
140 }
141 $this->lastRow = $row;
142
143 return $s;
144 }
145
146 protected function doBatchLookups() {
147 if ( !$this->getHookRunner()->onPageHistoryPager__doBatchLookups( $this, $this->mResult ) ) {
148 return;
149 }
150
151 # Do a link batch query
152 $this->mResult->seek( 0 );
153 $batch = new LinkBatch();
154 $revIds = [];
155 foreach ( $this->mResult as $row ) {
156 if ( $row->rev_parent_id ) {
157 $revIds[] = $row->rev_parent_id;
158 }
159 if ( $row->user_name !== null ) {
160 $batch->add( NS_USER, $row->user_name );
161 $batch->add( NS_USER_TALK, $row->user_name );
162 } else { # for anons or usernames of imported revisions
163 $batch->add( NS_USER, $row->rev_user_text );
164 $batch->add( NS_USER_TALK, $row->rev_user_text );
165 }
166 }
167 $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
168 $batch->execute();
169 $this->mResult->seek( 0 );
170 }
171
176 protected function getEmptyBody() {
177 return $this->msg( 'history-empty' )->escaped();
178 }
179
185 protected function getStartBody() {
186 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
187 $this->lastRow = false;
188 $this->counter = 1;
189 $this->oldIdChecked = 0;
190 $s = '';
191 // Button container stored in $this->buttons for re-use in getEndBody()
192 $this->buttons = '';
193 if ( $this->getNumRows() > 0 ) {
194 $this->getOutput()->wrapWikiMsg( "<div class='mw-history-legend'>\n$1\n</div>", 'histlegend' );
195 $s = Html::openElement( 'form', [
196 'action' => wfScript(),
197 'id' => 'mw-history-compare'
198 ] ) . "\n";
199 $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n";
200 $s .= Html::hidden( 'action', 'historysubmit' ) . "\n";
201 $s .= Html::hidden( 'type', 'revision' ) . "\n";
202
203 $this->buttons .= Html::openElement(
204 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
205 $className = 'historysubmit mw-history-compareselectedversions-button mw-ui-button';
206 $attrs = [ 'class' => $className ]
207 + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
208 $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
209 $attrs
210 ) . "\n";
211
212 $user = $this->getUser();
213 $actionButtons = '';
214 if ( $permissionManager->userHasRight( $user, 'deleterevision' ) ) {
215 $actionButtons .= $this->getRevisionButton(
216 'revisiondelete', 'showhideselectedversions' );
217 }
218 if ( $this->showTagEditUI ) {
219 $actionButtons .= $this->getRevisionButton(
220 'editchangetags', 'history-edit-tags' );
221 }
222 if ( $actionButtons ) {
223 $this->buttons .= Xml::tags( 'div', [ 'class' =>
224 'mw-history-revisionactions' ], $actionButtons );
225 }
226
227 if ( $permissionManager->userHasRight( $user, 'deleterevision' ) || $this->showTagEditUI ) {
228 $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
229 }
230
231 $this->buttons .= '</div>';
232
233 $s .= $this->buttons;
234 $s .= '<ul id="pagehistory">' . "\n";
235 }
236
237 return $s;
238 }
239
240 private function getRevisionButton( $name, $msg ) {
241 $this->preventClickjacking();
242 $element = Html::element(
243 'button',
244 [
245 'type' => 'submit',
246 'name' => $name,
247 'value' => '1',
248 'class' => "historysubmit mw-history-$name-button mw-ui-button",
249 ],
250 $this->msg( $msg )->text()
251 ) . "\n";
252 return $element;
253 }
254
255 protected function getEndBody() {
256 if ( $this->getNumRows() == 0 ) {
257 return '';
258 }
259
260 if ( $this->lastRow ) {
261 $firstInList = $this->counter == 1;
262 if ( $this->mIsBackwards ) {
263 # Next row is unknown, but for UI reasons, probably exists if an offset has been specified
264 if ( $this->mOffset == '' ) {
265 $next = null;
266 } else {
267 $next = 'unknown';
268 }
269 } else {
270 # The next row is the past-the-end row
271 $next = $this->mPastTheEndRow;
272 }
273 $this->counter++;
274
275 $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
276 ? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
277 : false;
278
279 $s = $this->historyLine( $this->lastRow, $next, $notifTimestamp, false, $firstInList );
280 } else {
281 $s = '';
282 }
283 $s .= "</ul>\n";
284 # Add second buttons only if there is more than one rev
285 if ( $this->getNumRows() > 2 ) {
286 $s .= $this->buttons;
287 }
288 $s .= '</form>';
289 return $s;
290 }
291
299 private function submitButton( $message, $attributes = [] ) {
300 # Disable submit button if history has 1 revision only
301 if ( $this->getNumRows() > 1 ) {
302 return Html::submitButton( $message, $attributes );
303 } else {
304 return '';
305 }
306 }
307
322 private function historyLine( $row, $next, $notificationtimestamp = false,
323 $dummy = false, $firstInList = false ) {
324 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
325
326 $revRecord = $this->revisionStore->newRevisionFromRow(
327 $row,
328 RevisionStore::READ_NORMAL,
329 $this->getTitle()
330 );
331
332 if ( is_object( $next ) ) {
333 $previousRevRecord = $this->revisionStore->newRevisionFromRow(
334 $next,
335 RevisionStore::READ_NORMAL,
336 $this->getTitle()
337 );
338 } else {
339 $previousRevRecord = null;
340 }
341
342 $latest = $revRecord->getId() === $this->getWikiPage()->getLatest();
343 $curlink = $this->curLink( $revRecord );
344 $lastlink = $this->lastLink( $revRecord, $next );
345 $curLastlinks = Html::rawElement( 'span', [], $curlink ) .
346 Html::rawElement( 'span', [], $lastlink );
347 $histLinks = Html::rawElement(
348 'span',
349 [ 'class' => 'mw-history-histlinks mw-changeslist-links' ],
350 $curLastlinks
351 );
352
353 $diffButtons = $this->diffButtons( $revRecord, $firstInList );
354 $s = $histLinks . $diffButtons;
355
356 $link = $this->revLink( $revRecord );
357 $classes = [];
358
359 $del = '';
360 $user = $this->getUser();
361 $canRevDelete = $permissionManager->userHasRight( $user, 'deleterevision' );
362 // Show checkboxes for each revision, to allow for revision deletion and
363 // change tags
364 $visibility = $revRecord->getVisibility();
365 if ( $canRevDelete || $this->showTagEditUI ) {
366 $this->preventClickjacking();
367 // If revision was hidden from sysops and we don't need the checkbox
368 // for anything else, disable it
369 if ( !$this->showTagEditUI
370 && !RevisionRecord::userCanBitfield(
371 $visibility,
372 RevisionRecord::DELETED_RESTRICTED,
373 $user
374 )
375 ) {
376 $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
377 // Otherwise, enable the checkbox...
378 } else {
379 $del = Xml::check( 'showhiderevisions', false,
380 [ 'name' => 'ids[' . $revRecord->getId() . ']' ] );
381 }
382 // User can only view deleted revisions...
383 } elseif ( $visibility && $permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
384 // If revision was hidden from sysops, disable the link
385 if ( !RevisionRecord::userCanBitfield(
386 $visibility,
387 RevisionRecord::DELETED_RESTRICTED,
388 $user
389 ) ) {
390 $del = Linker::revDeleteLinkDisabled( false );
391 // Otherwise, show the link...
392 } else {
393 $query = [
394 'type' => 'revision',
395 'target' => $this->getTitle()->getPrefixedDBkey(),
396 'ids' => $revRecord->getId()
397 ];
398 $del .= Linker::revDeleteLink(
399 $query,
400 $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
401 false
402 );
403 }
404 }
405 if ( $del ) {
406 $s .= " $del ";
407 }
408
409 $lang = $this->getLanguage();
410 $dirmark = $lang->getDirMark();
411
412 $s .= " $link";
413 $s .= $dirmark;
414 $s .= " <span class='history-user'>" .
415 Linker::revUserTools( $revRecord, true, false ) . "</span>";
416 $s .= $dirmark;
417
418 if ( $revRecord->isMinor() ) {
419 $s .= ' ' . ChangesList::flag( 'minor', $this->getContext() );
420 }
421
422 # Sometimes rev_len isn't populated
423 if ( $revRecord->getSize() !== null ) {
424 # Size is always public data
425 $prevSize = $this->parentLens[$row->rev_parent_id] ?? 0;
426 $sDiff = ChangesList::showCharacterDifference( $prevSize, $revRecord->getSize() );
427 $fSize = Linker::formatRevisionSize( $revRecord->getSize() );
428 $s .= ' <span class="mw-changeslist-separator"></span> ' . "$fSize $sDiff";
429 }
430
431 # Text following the character difference is added just before running hooks
432 $s2 = Linker::revComment( $revRecord, false, true, false );
433
434 if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) {
435 $s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';
436 $classes[] = 'mw-history-line-updated';
437 }
438
439 $tools = [];
440
441 # Rollback and undo links
442
443 if ( $previousRevRecord &&
444 $permissionManager->quickUserCan( 'edit', $user, $this->getTitle() )
445 ) {
446 if ( $latest && $permissionManager->quickUserCan( 'rollback',
447 $user, $this->getTitle() )
448 ) {
449 // Get a rollback link without the brackets
450 $rollbackLink = Linker::generateRollback(
451 $revRecord,
452 $this->getContext(),
453 [ 'verify', 'noBrackets' ]
454 );
455 if ( $rollbackLink ) {
456 $this->preventClickjacking();
457 $tools[] = $rollbackLink;
458 }
459 }
460
461 if ( !$revRecord->isDeleted( RevisionRecord::DELETED_TEXT )
462 && !$previousRevRecord->isDeleted( RevisionRecord::DELETED_TEXT )
463 ) {
464 # Create undo tooltip for the first (=latest) line only
465 $undoTooltip = $latest
466 ? [ 'title' => $this->msg( 'tooltip-undo' )->text() ]
467 : [];
468 $undolink = $this->getLinkRenderer()->makeKnownLink(
469 $this->getTitle(),
470 $this->msg( 'editundo' )->text(),
471 $undoTooltip,
472 [
473 'action' => 'edit',
474 'undoafter' => $previousRevRecord->getId(),
475 'undo' => $revRecord->getId()
476 ]
477 );
478 $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
479 }
480 }
481 // Allow extension to add their own links here
482 $this->getHookRunner()->onHistoryTools(
483 $revRecord,
484 $tools,
485 $previousRevRecord,
486 $user
487 );
488
489 // Hook is deprecated since 1.35
490 if ( $this->getHookContainer()->isRegistered( 'HistoryRevisionTools' ) ) {
491 // Only create the Revision objects if needed
492 $this->getHookRunner()->onHistoryRevisionTools(
493 new Revision( $revRecord ),
494 $tools,
495 $previousRevRecord ? new Revision( $previousRevRecord ) : null,
496 $user
497 );
498 }
499
500 if ( $tools ) {
501 $s2 .= ' ' . Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] );
502 foreach ( $tools as $tool ) {
503 $s2 .= Html::rawElement( 'span', [], $tool );
504 }
505 $s2 .= Html::closeElement( 'span' );
506 }
507
508 # Tags
509 list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
510 $row->ts_tags,
511 'history',
512 $this->getContext()
513 );
514 $classes = array_merge( $classes, $newClasses );
515 if ( $tagSummary !== '' ) {
516 $s2 .= " $tagSummary";
517 }
518
519 # Include separator between character difference and following text
520 if ( $s2 !== '' ) {
521 $s .= ' <span class="mw-changeslist-separator"></span> ' . $s2;
522 }
523
524 $attribs = [ 'data-mw-revid' => $revRecord->getId() ];
525
526 $this->getHookRunner()->onPageHistoryLineEnding( $this, $row, $s, $classes, $attribs );
527 $attribs = array_filter( $attribs,
528 [ Sanitizer::class, 'isReservedDataAttribute' ],
529 ARRAY_FILTER_USE_KEY
530 );
531
532 if ( $classes ) {
533 $attribs['class'] = implode( ' ', $classes );
534 }
535
536 return Xml::tags( 'li', $attribs, $s ) . "\n";
537 }
538
545 private function revLink( RevisionRecord $rev ) {
546 return ChangesList::revDateLink( $rev, $this->getUser(), $this->getLanguage(),
547 $this->getTitle() );
548 }
549
556 private function curLink( RevisionRecord $rev ) {
557 $cur = $this->historyPage->message['cur'];
558 $latest = $this->getWikiPage()->getLatest();
559 if ( $latest === $rev->getId()
560 || !RevisionRecord::userCanBitfield(
561 $rev->getVisibility(),
562 RevisionRecord::DELETED_TEXT,
563 $this->getUser()
564 )
565 ) {
566 return $cur;
567 } else {
568 return $this->getLinkRenderer()->makeKnownLink(
569 $this->getTitle(),
570 new HtmlArmor( $cur ),
571 [],
572 [
573 'diff' => $latest,
574 'oldid' => $rev->getId()
575 ]
576 );
577 }
578 }
579
589 private function lastLink( RevisionRecord $prevRev, $next ) {
590 $last = $this->historyPage->message['last'];
591
592 if ( $next === null ) {
593 # Probably no next row
594 return $last;
595 }
596
598 if ( $next === 'unknown' ) {
599 # Next row probably exists but is unknown, use an oldid=prev link
601 $this->getTitle(),
602 new HtmlArmor( $last ),
603 [],
604 [
605 'diff' => $prevRev->getId(),
606 'oldid' => 'prev'
607 ]
608 );
609 }
610
611 $nextRev = $this->revisionStore->newRevisionFromRow(
612 $next,
613 RevisionStore::READ_NORMAL,
614 $this->getTitle()
615 );
616
617 if ( !RevisionRecord::userCanBitfield(
618 $prevRev->getVisibility(),
619 RevisionRecord::DELETED_TEXT,
620 $this->getUser()
621 ) || !RevisionRecord::userCanBitfield(
622 $nextRev->getVisibility(),
623 RevisionRecord::DELETED_TEXT,
624 $this->getUser()
625 )
626 ) {
627 return $last;
628 }
629
631 $this->getTitle(),
632 new HtmlArmor( $last ),
633 [],
634 [
635 'diff' => $prevRev->getId(),
636 'oldid' => $next->rev_id
637 ]
638 );
639 }
640
649 private function diffButtons( RevisionRecord $rev, $firstInList ) {
650 if ( $this->getNumRows() > 1 ) {
651 $id = $rev->getId();
652 $radio = [ 'type' => 'radio', 'value' => $id ];
654 if ( $firstInList ) {
655 $first = Xml::element( 'input',
656 array_merge( $radio, [
657 'style' => 'visibility:hidden',
658 'name' => 'oldid',
659 'id' => 'mw-oldid-null' ] )
660 );
661 $checkmark = [ 'checked' => 'checked' ];
662 } else {
663 # Check visibility of old revisions
664 if ( !RevisionRecord::userCanBitfield(
665 $rev->getVisibility(),
666 RevisionRecord::DELETED_TEXT,
667 $this->getUser()
668 ) ) {
669 $radio['disabled'] = 'disabled';
670 $checkmark = []; // We will check the next possible one
671 } elseif ( !$this->oldIdChecked ) {
672 $checkmark = [ 'checked' => 'checked' ];
673 $this->oldIdChecked = $id;
674 } else {
675 $checkmark = [];
676 }
677 $first = Xml::element( 'input',
678 array_merge( $radio, $checkmark, [
679 'name' => 'oldid',
680 'id' => "mw-oldid-$id" ] ) );
681 $checkmark = [];
682 }
683 $second = Xml::element( 'input',
684 array_merge( $radio, $checkmark, [
685 'name' => 'diff',
686 'id' => "mw-diff-$id" ] ) );
687
688 return $first . $second;
689 } else {
690 return '';
691 }
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}
getUser()
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:215
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.
static showTagEditingUI(User $user)
Indicate whether change tag editing UI is relevant.
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.
__construct(HistoryAction $historyPage, $year='', $month='', $tagFilter='', array $conds=[], $day='')
revLink(RevisionRecord $rev)
Create a link to view this revision of the page.
string $tagFilter
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.
diffButtons(RevisionRecord $rev, $firstInList)
Create radio buttons for page history.
curLink(RevisionRecord $rev)
Create a diff-to-current link for this revision for this page.
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.
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:35
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition Linker.php:1871
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2285
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:1615
static formatRevisionSize( $size)
Definition Linker.php:1660
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:1152
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2304
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2263
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.
getVisibility()
Get the deletion bitfield of the revision.
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...
const NS_USER
Definition Defines.php:72
const NS_USER_TALK
Definition Defines.php:73
if(!isset( $args[0])) $lang