MediaWiki REL1_34
HistoryPager.php
Go to the documentation of this file.
1<?php
26
35 public $lastRow = false;
36
38
39 protected $oldIdChecked;
40
41 protected $preventClickjacking = false;
45 protected $parentLens;
46
48 protected $showTagEditUI;
49
51 private $tagFilter;
52
61 public function __construct(
63 $year = '',
64 $month = '',
65 $tagFilter = '',
66 array $conds = [],
67 $day = ''
68 ) {
69 parent::__construct( $historyPage->getContext() );
70 $this->historyPage = $historyPage;
71 $this->tagFilter = $tagFilter;
72 $this->getDateCond( $year, $month, $day );
73 $this->conds = $conds;
74 $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getUser() );
75 }
76
77 // For hook compatibility...
78 function getArticle() {
79 return $this->historyPage->getArticle();
80 }
81
82 function getSqlComment() {
83 if ( $this->conds ) {
84 return 'history page filtered'; // potentially slow, see CR r58153
85 } else {
86 return 'history page unfiltered';
87 }
88 }
89
90 function getQueryInfo() {
91 $revQuery = Revision::getQueryInfo( [ 'user' ] );
92 $queryInfo = [
93 'tables' => $revQuery['tables'],
94 'fields' => $revQuery['fields'],
95 'conds' => array_merge(
96 [ 'rev_page' => $this->getWikiPage()->getId() ],
97 $this->conds ),
98 'options' => [ 'USE INDEX' => [ 'revision' => 'page_timestamp' ] ],
99 'join_conds' => $revQuery['joins'],
100 ];
102 $queryInfo['tables'],
103 $queryInfo['fields'],
104 $queryInfo['conds'],
105 $queryInfo['join_conds'],
106 $queryInfo['options'],
107 $this->tagFilter
108 );
109
110 // Avoid PHP 7.1 warning of passing $this by reference
111 $historyPager = $this;
112 Hooks::run( 'PageHistoryPager::getQueryInfo', [ &$historyPager, &$queryInfo ] );
113
114 return $queryInfo;
115 }
116
117 function getIndexField() {
118 return 'rev_timestamp';
119 }
120
125 function formatRow( $row ) {
126 if ( $this->lastRow ) {
127 $firstInList = $this->counter == 1;
128 $this->counter++;
129
130 $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
131 ? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
132 : false;
133
134 $s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, false, $firstInList );
135 } else {
136 $s = '';
137 }
138 $this->lastRow = $row;
139
140 return $s;
141 }
142
143 protected function doBatchLookups() {
144 if ( !Hooks::run( 'PageHistoryPager::doBatchLookups', [ $this, $this->mResult ] ) ) {
145 return;
146 }
147
148 # Do a link batch query
149 $this->mResult->seek( 0 );
150 $batch = new LinkBatch();
151 $revIds = [];
152 foreach ( $this->mResult as $row ) {
153 if ( $row->rev_parent_id ) {
154 $revIds[] = $row->rev_parent_id;
155 }
156 if ( $row->user_name !== null ) {
157 $batch->add( NS_USER, $row->user_name );
158 $batch->add( NS_USER_TALK, $row->user_name );
159 } else { # for anons or usernames of imported revisions
160 $batch->add( NS_USER, $row->rev_user_text );
161 $batch->add( NS_USER_TALK, $row->rev_user_text );
162 }
163 }
164 $this->parentLens = Revision::getParentLengths( $this->mDb, $revIds );
165 $batch->execute();
166 $this->mResult->seek( 0 );
167 }
168
174 protected function getStartBody() {
175 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
176 $this->lastRow = false;
177 $this->counter = 1;
178 $this->oldIdChecked = 0;
179
180 $this->getOutput()->wrapWikiMsg( "<div class='mw-history-legend'>\n$1\n</div>", 'histlegend' );
181 $s = Html::openElement( 'form', [ 'action' => wfScript(),
182 'id' => 'mw-history-compare' ] ) . "\n";
183 $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n";
184 $s .= Html::hidden( 'action', 'historysubmit' ) . "\n";
185 $s .= Html::hidden( 'type', 'revision' ) . "\n";
186
187 // Button container stored in $this->buttons for re-use in getEndBody()
188 $this->buttons = '';
189 if ( $this->getNumRows() > 0 ) {
190 $this->buttons .= Html::openElement(
191 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
192 $className = 'historysubmit mw-history-compareselectedversions-button';
193 $attrs = [ 'class' => $className ]
194 + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
195 $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
196 $attrs
197 ) . "\n";
198
199 $user = $this->getUser();
200 $actionButtons = '';
201 if ( $permissionManager->userHasRight( $user, 'deleterevision' ) ) {
202 $actionButtons .= $this->getRevisionButton(
203 'revisiondelete', 'showhideselectedversions' );
204 }
205 if ( $this->showTagEditUI ) {
206 $actionButtons .= $this->getRevisionButton(
207 'editchangetags', 'history-edit-tags' );
208 }
209 if ( $actionButtons ) {
210 $this->buttons .= Xml::tags( 'div', [ 'class' =>
211 'mw-history-revisionactions' ], $actionButtons );
212 }
213
214 if ( $permissionManager->userHasRight( $user, 'deleterevision' ) || $this->showTagEditUI ) {
215 $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
216 }
217
218 $this->buttons .= '</div>';
219
221 }
222 $s .= '<ul id="pagehistory">' . "\n";
223
224 return $s;
225 }
226
227 private function getRevisionButton( $name, $msg ) {
228 $this->preventClickjacking();
229 # Note T22966, <button> is non-standard in IE<8
230 $element = Html::element(
231 'button',
232 [
233 'type' => 'submit',
234 'name' => $name,
235 'value' => '1',
236 'class' => "historysubmit mw-history-$name-button",
237 ],
238 $this->msg( $msg )->text()
239 ) . "\n";
240 return $element;
241 }
242
243 protected function getEndBody() {
244 if ( $this->lastRow ) {
245 $firstInList = $this->counter == 1;
246 if ( $this->mIsBackwards ) {
247 # Next row is unknown, but for UI reasons, probably exists if an offset has been specified
248 if ( $this->mOffset == '' ) {
249 $next = null;
250 } else {
251 $next = 'unknown';
252 }
253 } else {
254 # The next row is the past-the-end row
255 $next = $this->mPastTheEndRow;
256 }
257 $this->counter++;
258
259 $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
260 ? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
261 : false;
262
263 $s = $this->historyLine( $this->lastRow, $next, $notifTimestamp, false, $firstInList );
264 } else {
265 $s = '';
266 }
267 $s .= "</ul>\n";
268 # Add second buttons only if there is more than one rev
269 if ( $this->getNumRows() > 2 ) {
271 }
272 $s .= '</form>';
273
274 return $s;
275 }
276
284 function submitButton( $message, $attributes = [] ) {
285 # Disable submit button if history has 1 revision only
286 if ( $this->getNumRows() > 1 ) {
287 return Html::submitButton( $message, $attributes );
288 } else {
289 return '';
290 }
291 }
292
307 function historyLine( $row, $next, $notificationtimestamp = false,
308 $dummy = false, $firstInList = false ) {
309 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
310 $rev = new Revision( $row, 0, $this->getTitle() );
311
312 if ( is_object( $next ) ) {
313 $prevRev = new Revision( $next, 0, $this->getTitle() );
314 } else {
315 $prevRev = null;
316 }
317
318 $latest = $rev->getId() === $this->getWikiPage()->getLatest();
319 $curlink = $this->curLink( $rev );
320 $lastlink = $this->lastLink( $rev, $next );
321 $curLastlinks = Html::rawElement( 'span', [], $curlink ) .
322 Html::rawElement( 'span', [], $lastlink );
323 $histLinks = Html::rawElement(
324 'span',
325 [ 'class' => 'mw-history-histlinks mw-changeslist-links' ],
326 $curLastlinks
327 );
328
329 $diffButtons = $this->diffButtons( $rev, $firstInList );
330 $s = $histLinks . $diffButtons;
331
332 $link = $this->revLink( $rev );
333 $classes = [];
334
335 $del = '';
336 $user = $this->getUser();
337 $canRevDelete = $permissionManager->userHasRight( $user, 'deleterevision' );
338 // Show checkboxes for each revision, to allow for revision deletion and
339 // change tags
340 if ( $canRevDelete || $this->showTagEditUI ) {
341 $this->preventClickjacking();
342 // If revision was hidden from sysops and we don't need the checkbox
343 // for anything else, disable it
344 if ( !$this->showTagEditUI
345 && !$rev->userCan( RevisionRecord::DELETED_RESTRICTED, $user )
346 ) {
347 $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
348 // Otherwise, enable the checkbox...
349 } else {
350 $del = Xml::check( 'showhiderevisions', false,
351 [ 'name' => 'ids[' . $rev->getId() . ']' ] );
352 }
353 // User can only view deleted revisions...
354 } elseif ( $rev->getVisibility() &&
355 $permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
356 // If revision was hidden from sysops, disable the link
357 if ( !$rev->userCan( RevisionRecord::DELETED_RESTRICTED, $user ) ) {
358 $del = Linker::revDeleteLinkDisabled( false );
359 // Otherwise, show the link...
360 } else {
361 $query = [ 'type' => 'revision',
362 'target' => $this->getTitle()->getPrefixedDBkey(), 'ids' => $rev->getId() ];
363 $del .= Linker::revDeleteLink( $query,
364 $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ), false );
365 }
366 }
367 if ( $del ) {
368 $s .= " $del ";
369 }
370
371 $lang = $this->getLanguage();
372 $dirmark = $lang->getDirMark();
373
374 $s .= " $link";
375 $s .= $dirmark;
376 $s .= " <span class='history-user'>" .
377 Linker::revUserTools( $rev, true, false ) . "</span>";
378 $s .= $dirmark;
379
380 if ( $rev->isMinor() ) {
381 $s .= ' ' . ChangesList::flag( 'minor', $this->getContext() );
382 }
383
384 # Sometimes rev_len isn't populated
385 if ( $rev->getSize() !== null ) {
386 # Size is always public data
387 $prevSize = $this->parentLens[$row->rev_parent_id] ?? 0;
388 $sDiff = ChangesList::showCharacterDifference( $prevSize, $rev->getSize() );
389 $fSize = Linker::formatRevisionSize( $rev->getSize(), false );
390 $s .= ' <span class="mw-changeslist-separator"></span> ' . "$fSize $sDiff";
391 }
392
393 # Text following the character difference is added just before running hooks
394 $s2 = Linker::revComment( $rev, false, true, false );
395
396 if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) {
397 $s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';
398 $classes[] = 'mw-history-line-updated';
399 }
400
401 $tools = [];
402
403 # Rollback and undo links
404
405 if ( $prevRev && $permissionManager->quickUserCan( 'edit', $user, $this->getTitle() ) ) {
406 if ( $latest && $permissionManager->quickUserCan( 'rollback',
407 $user, $this->getTitle() )
408 ) {
409 // Get a rollback link without the brackets
410 $rollbackLink = Linker::generateRollback(
411 $rev,
412 $this->getContext(),
413 [ 'verify', 'noBrackets' ]
414 );
415 if ( $rollbackLink ) {
416 $this->preventClickjacking();
417 $tools[] = $rollbackLink;
418 }
419 }
420
421 if ( !$rev->isDeleted( RevisionRecord::DELETED_TEXT )
422 && !$prevRev->isDeleted( RevisionRecord::DELETED_TEXT )
423 ) {
424 # Create undo tooltip for the first (=latest) line only
425 $undoTooltip = $latest
426 ? [ 'title' => $this->msg( 'tooltip-undo' )->text() ]
427 : [];
428 $undolink = $this->getLinkRenderer()->makeKnownLink(
429 $this->getTitle(),
430 $this->msg( 'editundo' )->text(),
431 $undoTooltip,
432 [
433 'action' => 'edit',
434 'undoafter' => $prevRev->getId(),
435 'undo' => $rev->getId()
436 ]
437 );
438 $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
439 }
440 }
441 // Allow extension to add their own links here
442 Hooks::run( 'HistoryRevisionTools', [ $rev, &$tools, $prevRev, $user ] );
443
444 if ( $tools ) {
445 $s2 .= ' ' . Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] );
446 foreach ( $tools as $tool ) {
447 $s2 .= Html::rawElement( 'span', [], $tool );
448 }
449 $s2 .= Html::closeElement( 'span' );
450 }
451
452 # Tags
453 list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
454 $row->ts_tags,
455 'history',
456 $this->getContext()
457 );
458 $classes = array_merge( $classes, $newClasses );
459 if ( $tagSummary !== '' ) {
460 $s2 .= " $tagSummary";
461 }
462
463 # Include separator between character difference and following text
464 if ( $s2 !== '' ) {
465 $s .= ' <span class="mw-changeslist-separator"></span> ' . $s2;
466 }
467
468 $attribs = [ 'data-mw-revid' => $rev->getId() ];
469
470 Hooks::run( 'PageHistoryLineEnding', [ $this, &$row, &$s, &$classes, &$attribs ] );
471 $attribs = array_filter( $attribs,
472 [ Sanitizer::class, 'isReservedDataAttribute' ],
473 ARRAY_FILTER_USE_KEY
474 );
475
476 if ( $classes ) {
477 $attribs['class'] = implode( ' ', $classes );
478 }
479
480 return Xml::tags( 'li', $attribs, $s ) . "\n";
481 }
482
489 function revLink( $rev ) {
490 return ChangesList::revDateLink( $rev, $this->getUser(), $this->getLanguage(),
491 $this->getTitle() );
492 }
493
500 function curLink( $rev ) {
501 $cur = $this->historyPage->message['cur'];
502 $latest = $this->getWikiPage()->getLatest();
503 if ( $latest === $rev->getId()
504 || !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
505 ) {
506 return $cur;
507 } else {
508 return $this->getLinkRenderer()->makeKnownLink(
509 $this->getTitle(),
510 new HtmlArmor( $cur ),
511 [],
512 [
513 'diff' => $latest,
514 'oldid' => $rev->getId()
515 ]
516 );
517 }
518 }
519
529 function lastLink( $prevRev, $next ) {
530 $last = $this->historyPage->message['last'];
531
532 if ( $next === null ) {
533 # Probably no next row
534 return $last;
535 }
536
538 if ( $next === 'unknown' ) {
539 # Next row probably exists but is unknown, use an oldid=prev link
541 $this->getTitle(),
542 new HtmlArmor( $last ),
543 [],
544 [
545 'diff' => $prevRev->getId(),
546 'oldid' => 'prev'
547 ]
548 );
549 }
550
551 $nextRev = new Revision( $next, 0, $this->getTitle() );
552
553 if ( !$prevRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
554 || !$nextRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
555 ) {
556 return $last;
557 }
558
560 $this->getTitle(),
561 new HtmlArmor( $last ),
562 [],
563 [
564 'diff' => $prevRev->getId(),
565 'oldid' => $next->rev_id
566 ]
567 );
568 }
569
578 function diffButtons( $rev, $firstInList ) {
579 if ( $this->getNumRows() > 1 ) {
580 $id = $rev->getId();
581 $radio = [ 'type' => 'radio', 'value' => $id ];
583 if ( $firstInList ) {
584 $first = Xml::element( 'input',
585 array_merge( $radio, [
586 'style' => 'visibility:hidden',
587 'name' => 'oldid',
588 'id' => 'mw-oldid-null' ] )
589 );
590 $checkmark = [ 'checked' => 'checked' ];
591 } else {
592 # Check visibility of old revisions
593 if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() ) ) {
594 $radio['disabled'] = 'disabled';
595 $checkmark = []; // We will check the next possible one
596 } elseif ( !$this->oldIdChecked ) {
597 $checkmark = [ 'checked' => 'checked' ];
598 $this->oldIdChecked = $id;
599 } else {
600 $checkmark = [];
601 }
602 $first = Xml::element( 'input',
603 array_merge( $radio, $checkmark, [
604 'name' => 'oldid',
605 'id' => "mw-oldid-$id" ] ) );
606 $checkmark = [];
607 }
608 $second = Xml::element( 'input',
609 array_merge( $radio, $checkmark, [
610 'name' => 'diff',
611 'id' => "mw-diff-$id" ] ) );
612
613 return $first . $second;
614 } else {
615 return '';
616 }
617 }
618
622 function getDefaultQuery() {
623 parent::getDefaultQuery();
624 unset( $this->mDefaultQuery['date-range-to'] );
626 }
627
632 function preventClickjacking( $enable = true ) {
633 $this->preventClickjacking = $enable;
634 }
635
643
644}
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='')
Applies all tags-related changes to a query.
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
static showTagEditingUI(User $user)
Indicate whether change tag editing UI is relevant.
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.
static revDateLink(Revision $rev, User $user, Language $lang, $title=null)
Render the date and time of a revision in the current user language based on whether the user is able...
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.
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='')
string $tagFilter
lastLink( $prevRev, $next)
Create a diff-to-previous link for this revision for this page.
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.
getDefaultQuery()
Get an array of query parameters that should be put into self-links.By default, all parameters passed...
diffButtons( $rev, $firstInList)
Create radio buttons for page history.
submitButton( $message, $attributes=[])
Creates a submit button.
revLink( $rev)
Create a link to view this revision of the 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()
This function should be overridden to return the name of the index fi- eld.
curLink( $rev)
Create a diff-to-current link for this revision for this page.
getQueryInfo()
This function should be overridden to provide all parameters needed for the main paged query.
getStartBody()
Creates begin of history list with a submit button.
bool stdClass $lastRow
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:28
array $mDefaultQuery
LinkRenderer $linkRenderer
getNumRows()
Get the number of rows in the result set.
stdClass bool null $mPastTheEndRow
Extra row fetched at the end to see if the end was reached.
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:34
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition Linker.php:1811
static revComment(Revision $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:1577
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2176
static formatRevisionSize( $size)
Definition Linker.php:1602
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:1124
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2195
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2154
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.
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...
static getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object.
Definition Revision.php:315
static getParentLengths( $db, array $revIds)
Do a batched query to get the parent revision lengths.
Definition Revision.php:342
const NS_USER
Definition Defines.php:71
const NS_USER_TALK
Definition Defines.php:72
$last
if(!isset( $args[0])) $lang