Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 318
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
HistoryPager
0.00% covered (danger)
0.00%
0 / 317
0.00% covered (danger)
0.00%
0 / 18
4290
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 getArticle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSqlComment
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doBatchLookups
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
30
 getEmptyBody
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStartBody
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
56
 getRevisionButton
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 getEndBody
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 submitButton
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 formatRow
0.00% covered (danger)
0.00%
0 / 111
0.00% covered (danger)
0.00%
0 / 1
552
 revLink
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 curLink
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 lastLink
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 diffButtons
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
30
 isNavigationBarShown
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getPreventClickjacking
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Page history pager
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Actions
22 */
23
24namespace MediaWiki\Pager;
25
26use ChangesList;
27use ChangeTags;
28use HistoryAction;
29use HtmlArmor;
30use IDBAccessObject;
31use MapCacheLRU;
32use MediaWiki\Cache\LinkBatchFactory;
33use MediaWiki\ChangeTags\ChangeTagsStore;
34use MediaWiki\CommentFormatter\CommentFormatter;
35use MediaWiki\HookContainer\HookContainer;
36use MediaWiki\HookContainer\HookRunner;
37use MediaWiki\Html\Html;
38use MediaWiki\Html\ListToggle;
39use MediaWiki\Linker\Linker;
40use MediaWiki\MainConfigNames;
41use MediaWiki\MediaWikiServices;
42use MediaWiki\Parser\Sanitizer;
43use MediaWiki\Revision\RevisionRecord;
44use MediaWiki\Revision\RevisionStore;
45use MediaWiki\SpecialPage\SpecialPage;
46use MediaWiki\Watchlist\WatchlistManager;
47use stdClass;
48
49/**
50 * @ingroup Pager
51 * @ingroup Actions
52 */
53#[\AllowDynamicProperties]
54class HistoryPager extends ReverseChronologicalPager {
55
56    public $mGroupByDate = true;
57
58    public HistoryAction $historyPage;
59    public string $buttons;
60    public array $conds;
61
62    protected $oldIdChecked;
63
64    protected $preventClickjacking = false;
65    /**
66     * @var array
67     */
68    protected $parentLens;
69
70    /** @var bool Whether to show the tag editing UI */
71    protected $showTagEditUI;
72
73    protected MapCacheLRU $tagsCache;
74
75    /** @var string */
76    private $tagFilter;
77
78    /** @var bool */
79    private $tagInvert;
80
81    /** @var string|null|false */
82    private $notificationTimestamp;
83
84    private RevisionStore $revisionStore;
85    private WatchlistManager $watchlistManager;
86    private LinkBatchFactory $linkBatchFactory;
87    private CommentFormatter $commentFormatter;
88    private HookRunner $hookRunner;
89    private ChangeTagsStore $changeTagsStore;
90
91    /**
92     * @var RevisionRecord[] Revisions, with the key being their result offset
93     */
94    private $revisions = [];
95
96    /**
97     * @var string[] Formatted comments, with the key being their result offset as for $revisions
98     */
99    private $formattedComments = [];
100
101    /**
102     * @param HistoryAction $historyPage
103     * @param int $year
104     * @param int $month
105     * @param int $day
106     * @param string $tagFilter
107     * @param bool $tagInvert
108     * @param array $conds
109     * @param LinkBatchFactory|null $linkBatchFactory
110     * @param WatchlistManager|null $watchlistManager
111     * @param CommentFormatter|null $commentFormatter
112     * @param HookContainer|null $hookContainer
113     * @param ChangeTagsStore|null $changeTagsStore
114     */
115    public function __construct(
116        HistoryAction $historyPage,
117        $year = 0,
118        $month = 0,
119        $day = 0,
120        $tagFilter = '',
121        $tagInvert = false,
122        array $conds = [],
123        LinkBatchFactory $linkBatchFactory = null,
124        WatchlistManager $watchlistManager = null,
125        CommentFormatter $commentFormatter = null,
126        HookContainer $hookContainer = null,
127        ChangeTagsStore $changeTagsStore = null
128    ) {
129        parent::__construct( $historyPage->getContext() );
130        $this->historyPage = $historyPage;
131        $this->tagFilter = $tagFilter;
132        $this->tagInvert = $tagInvert;
133        $this->getDateCond( $year, $month, $day );
134        $this->conds = $conds;
135        $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getAuthority() );
136        $this->tagsCache = new MapCacheLRU( 50 );
137        $services = MediaWikiServices::getInstance();
138        $this->revisionStore = $services->getRevisionStore();
139        $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
140        $this->watchlistManager = $watchlistManager
141            ?? $services->getWatchlistManager();
142        $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
143        $this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );
144        $this->notificationTimestamp = $this->getConfig()->get( MainConfigNames::ShowUpdatedMarker )
145            ? $this->watchlistManager->getTitleNotificationTimestamp( $this->getUser(), $this->getTitle() )
146            : false;
147        $this->changeTagsStore = $changeTagsStore ?? $services->getChangeTagsStore();
148    }
149
150    // For hook compatibility…
151    public function getArticle() {
152        return $this->historyPage->getArticle();
153    }
154
155    protected function getSqlComment() {
156        if ( $this->conds ) {
157            return 'history page filtered'; // potentially slow, see CR r58153
158        } else {
159            return 'history page unfiltered';
160        }
161    }
162
163    public function getQueryInfo() {
164        $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $this->mDb )
165            ->joinComment()
166            ->joinUser()
167            ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
168            ->where( [ 'rev_page' => $this->getWikiPage()->getId() ] )
169            ->andWhere( $this->conds );
170
171        $queryInfo = $queryBuilder->getQueryInfo( 'join_conds' );
172        $this->changeTagsStore->modifyDisplayQuery(
173            $queryInfo['tables'],
174            $queryInfo['fields'],
175            $queryInfo['conds'],
176            $queryInfo['join_conds'],
177            $queryInfo['options'],
178            $this->tagFilter,
179            $this->tagInvert
180        );
181
182        $this->hookRunner->onPageHistoryPager__getQueryInfo( $this, $queryInfo );
183
184        return $queryInfo;
185    }
186
187    public function getIndexField() {
188        return [ [ 'rev_timestamp', 'rev_id' ] ];
189    }
190
191    protected function doBatchLookups() {
192        if ( !$this->hookRunner->onPageHistoryPager__doBatchLookups( $this, $this->mResult ) ) {
193            return;
194        }
195
196        # Do a link batch query
197        $batch = $this->linkBatchFactory->newLinkBatch();
198        $revIds = [];
199        $title = $this->getTitle();
200        foreach ( $this->mResult as $row ) {
201            if ( $row->rev_parent_id ) {
202                $revIds[] = (int)$row->rev_parent_id;
203            }
204            if ( $row->user_name !== null ) {
205                $batch->add( NS_USER, $row->user_name );
206                $batch->add( NS_USER_TALK, $row->user_name );
207            } else { # for anons or usernames of imported revisions
208                $batch->add( NS_USER, $row->rev_user_text );
209                $batch->add( NS_USER_TALK, $row->rev_user_text );
210            }
211            $this->revisions[] = $this->revisionStore->newRevisionFromRow(
212                $row,
213                IDBAccessObject::READ_NORMAL,
214                $title
215            );
216        }
217        $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
218        $batch->execute();
219
220        # The keys of $this->formattedComments will be the same as the keys of $this->revisions
221        $this->formattedComments = $this->commentFormatter->createRevisionBatch()
222            ->revisions( $this->revisions )
223            ->authority( $this->getAuthority() )
224            ->samePage( false )
225            ->hideIfDeleted( true )
226            ->useParentheses( false )
227            ->execute();
228
229        $this->mResult->seek( 0 );
230    }
231
232    /**
233     * Returns message when query returns no revisions
234     * @return string escaped message
235     */
236    protected function getEmptyBody() {
237        return $this->msg( 'history-empty' )->escaped();
238    }
239
240    /**
241     * Creates begin of history list with a submit button
242     *
243     * @return string HTML output
244     */
245    protected function getStartBody() {
246        $this->oldIdChecked = 0;
247        $s = '';
248        // Button container stored in $this->buttons for re-use in getEndBody()
249        $this->buttons = '';
250        if ( $this->getNumRows() > 0 ) {
251            $this->getOutput()->wrapWikiMsg( "<div class='mw-history-legend'>\n$1\n</div>", 'histlegend' );
252            // Main form for comparing revisions
253            $s = Html::openElement( 'form', [
254                'action' => wfScript(),
255                'id' => 'mw-history-compare'
256            ] ) . "\n";
257            $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n";
258
259            $this->buttons .= Html::openElement(
260                'div', [ 'class' => 'mw-history-compareselectedversions' ] );
261            $className = 'historysubmit mw-history-compareselectedversions-button cdx-button';
262            $attrs = [ 'class' => $className ]
263                + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
264            $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
265                $attrs
266            ) . "\n";
267
268            $actionButtons = '';
269            if ( $this->getAuthority()->isAllowed( 'deleterevision' ) ) {
270                $actionButtons .= $this->getRevisionButton(
271                    'Revisiondelete', 'showhideselectedversions', 'mw-history-revisiondelete-button' );
272            }
273            if ( $this->showTagEditUI ) {
274                $actionButtons .= $this->getRevisionButton(
275                    'EditTags', 'history-edit-tags', 'mw-history-editchangetags-button' );
276            }
277            if ( $actionButtons ) {
278                // Prepend a mini-form for changing visibility and editing tags.
279                // Checkboxes and buttons are associated with it using the <input form="…"> attribute.
280                //
281                // This makes the submitted parameters cleaner (on supporting browsers - all except IE 11):
282                // the 'mw-history-compare' form submission will omit the `ids[…]` parameters, and the
283                // 'mw-history-revisionactions' form submission will omit the `diff` and `oldid` parameters.
284                $s = Html::rawElement( 'form', [
285                    'action' => wfScript(),
286                    'id' => 'mw-history-revisionactions',
287                ] ) . "\n" . $s;
288                $s .= Html::hidden( 'type', 'revision', [ 'form' => 'mw-history-revisionactions' ] ) . "\n";
289
290                $this->buttons .= Html::rawElement( 'div', [ 'class' =>
291                    'mw-history-revisionactions' ], $actionButtons );
292            }
293
294            if ( $this->getAuthority()->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
295                $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
296            }
297
298            $this->buttons .= '</div>';
299
300            $s .= $this->buttons;
301        }
302
303        $s .= '<section id="pagehistory" class="mw-pager-body">';
304
305        return $s;
306    }
307
308    private function getRevisionButton( $name, $msg, $class ) {
309        $this->preventClickjacking = true;
310        $element = Html::element(
311            'button',
312            [
313                'type' => 'submit',
314                'name' => 'title',
315                'value' => SpecialPage::getTitleFor( $name )->getPrefixedDBkey(),
316                'class' => [ 'cdx-button', $class, 'historysubmit' ],
317                'form' => 'mw-history-revisionactions',
318            ],
319            $this->msg( $msg )->text()
320        ) . "\n";
321        return $element;
322    }
323
324    protected function getEndBody() {
325        if ( $this->getNumRows() == 0 ) {
326            return '';
327        }
328        $s = '';
329        if ( $this->getNumRows() > 2 ) {
330            $s .= $this->buttons;
331        }
332        $s .= '</section>'; // closes section#pagehistory
333        $s .= '</form>';
334        return $s;
335    }
336
337    /**
338     * Creates a submit button
339     *
340     * @param string $message Text of the submit button, will be escaped
341     * @param array $attributes
342     * @return string HTML output for the submit button
343     */
344    private function submitButton( $message, $attributes = [] ) {
345        # Disable submit button if history has 1 revision only
346        if ( $this->getNumRows() > 1 ) {
347            return Html::submitButton( $message, $attributes );
348        } else {
349            return '';
350        }
351    }
352
353    /**
354     * Returns a row from the history printout.
355     *
356     * @param stdClass $row The database row corresponding to the current line.
357     * @return string HTML output for the row
358     */
359    public function formatRow( $row ) {
360        $resultOffset = $this->getResultOffset();
361        $numRows = min( $this->mResult->numRows(), $this->mLimit );
362
363        $firstInList = $resultOffset === ( $this->mIsBackwards ? $numRows - 1 : 0 );
364        // Next in the list, previous in chronological order.
365        $nextResultOffset = $resultOffset + ( $this->mIsBackwards ? -1 : 1 );
366
367        $revRecord = $this->revisions[$resultOffset];
368        // This may only be null if the current line is the last one in the list.
369        $previousRevRecord = $this->revisions[$nextResultOffset] ?? null;
370
371        $latest = $revRecord->getId() === $this->getWikiPage()->getLatest();
372        $curlink = $this->curLink( $revRecord );
373        if ( $previousRevRecord ) {
374            // Display a link to compare to the previous revision
375            $lastlink = $this->lastLink( $revRecord, $previousRevRecord );
376        } elseif ( $this->mIsBackwards && $this->mOffset !== '' ) {
377            // When paging "backwards", we don't have the extra result for the next revision that would
378            // appear in the list, and we don't know whether this is the oldest revision or not.
379            // However, if an offset has been specified, then the user probably reached this page by
380            // navigating from the "next" page, therefore the next revision probably exists.
381            // Display a link using &oldid=prev (this skips some checks but that's fine).
382            $lastlink = $this->lastLink( $revRecord, null );
383        } else {
384            // Do not display a link, because this is the oldest revision of the page
385            $lastlink = Html::element( 'span', [
386                'class' => 'mw-history-histlinks-previous',
387            ], $this->historyPage->message['last'] );
388        }
389        $curLastlinks = Html::rawElement( 'span', [], $curlink ) .
390            Html::rawElement( 'span', [], $lastlink );
391        $histLinks = Html::rawElement(
392            'span',
393            [ 'class' => 'mw-history-histlinks mw-changeslist-links' ],
394            $curLastlinks
395        );
396
397        $diffButtons = $this->diffButtons( $revRecord, $firstInList );
398        $s = $histLinks . $diffButtons;
399
400        $link = $this->revLink( $revRecord );
401        $classes = [];
402
403        $del = '';
404        $canRevDelete = $this->getAuthority()->isAllowed( 'deleterevision' );
405        // Show checkboxes for each revision, to allow for revision deletion and
406        // change tags
407        if ( $canRevDelete || $this->showTagEditUI ) {
408            $this->preventClickjacking = true;
409            // If revision was hidden from sysops and we don't need the checkbox
410            // for anything else, disable it
411            if ( !$this->showTagEditUI
412                && !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $this->getAuthority() )
413            ) {
414                $del = Html::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
415            // Otherwise, enable the checkbox…
416            } else {
417                $del = Html::check(
418                    'ids[' . $revRecord->getId() . ']', false,
419                    [ 'form' => 'mw-history-revisionactions' ]
420                );
421            }
422        // User can only view deleted revisions…
423        } elseif ( $revRecord->getVisibility() && $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
424            // If revision was hidden from sysops, disable the link
425            if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $this->getAuthority() ) ) {
426                $del = Linker::revDeleteLinkDisabled( false );
427            // Otherwise, show the link…
428            } else {
429                $query = [
430                    'type' => 'revision',
431                    'target' => $this->getTitle()->getPrefixedDBkey(),
432                    'ids' => $revRecord->getId()
433                ];
434                $del .= Linker::revDeleteLink(
435                    $query,
436                    $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
437                    false
438                );
439            }
440        }
441        if ( $del ) {
442            $s .= " $del ";
443        }
444
445        $lang = $this->getLanguage();
446        $dirmark = $lang->getDirMark();
447
448        $s .= " $link";
449        $s .= $dirmark;
450        $s .= " <span class='history-user'>" .
451            Linker::revUserTools( $revRecord, true, false ) . "</span>";
452        $s .= $dirmark;
453
454        if ( $revRecord->isMinor() ) {
455            $s .= ' ' . ChangesList::flag( 'minor', $this->getContext() );
456        }
457
458        # Sometimes rev_len isn't populated
459        if ( $revRecord->getSize() !== null ) {
460            # Size is always public data
461            $prevSize = $this->parentLens[$row->rev_parent_id] ?? 0;
462            $sDiff = ChangesList::showCharacterDifference( $prevSize, $revRecord->getSize() );
463            $fSize = Linker::formatRevisionSize( $revRecord->getSize() );
464            $s .= ' <span class="mw-changeslist-separator"></span> ' . "$fSize $sDiff";
465        }
466
467        # Include separator between character difference and following text
468        $s .= ' <span class="mw-changeslist-separator"></span> ';
469
470        # Text following the character difference is added just before running hooks
471        $comment = $this->formattedComments[$resultOffset];
472
473        if ( $comment === '' ) {
474            $defaultComment = $this->historyPage->message['changeslist-nocomment'];
475            $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
476        }
477        $s .= $comment;
478
479        if ( $this->notificationTimestamp && $row->rev_timestamp >= $this->notificationTimestamp ) {
480            $s .= ' <span class="updatedmarker">' . $this->historyPage->message['updatedmarker'] . '</span>';
481            $classes[] = 'mw-history-line-updated';
482        }
483
484        $pagerTools = new PagerTools(
485            $revRecord,
486            $previousRevRecord,
487            $latest && $previousRevRecord,
488            $this->hookRunner,
489            $this->getTitle(),
490            $this->getContext(),
491            $this->getLinkRenderer()
492        );
493        if ( $pagerTools->shouldPreventClickjacking() ) {
494            $this->preventClickjacking = true;
495        }
496        $s .= $pagerTools->toHTML();
497
498        # Tags
499        [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
500            $this->tagsCache->makeKey(
501                $row->ts_tags ?? '',
502                $this->getUser()->getName(),
503                $lang->getCode()
504            ),
505            fn () => ChangeTags::formatSummaryRow(
506                $row->ts_tags,
507                'history',
508                $this->getContext()
509            )
510        );
511        $classes = array_merge( $classes, $newClasses );
512        if ( $tagSummary !== '' ) {
513            $s .= " $tagSummary";
514        }
515
516        $attribs = [ 'data-mw-revid' => $revRecord->getId() ];
517
518        $this->hookRunner->onPageHistoryLineEnding( $this, $row, $s, $classes, $attribs );
519        $attribs = array_filter( $attribs,
520            [ Sanitizer::class, 'isReservedDataAttribute' ],
521            ARRAY_FILTER_USE_KEY
522        );
523
524        if ( $classes ) {
525            $attribs['class'] = implode( ' ', $classes );
526        }
527
528        return Html::rawElement( 'li', $attribs, $s ) . "\n";
529    }
530
531    /**
532     * Create a link to view this revision of the page
533     *
534     * @param RevisionRecord $rev
535     * @return string
536     */
537    private function revLink( RevisionRecord $rev ) {
538        return ChangesList::revDateLink( $rev, $this->getAuthority(), $this->getLanguage(),
539            $this->getTitle() );
540    }
541
542    /**
543     * Create a diff-to-current link for this revision for this page
544     *
545     * @param RevisionRecord $rev
546     * @return string
547     */
548    private function curLink( RevisionRecord $rev ) {
549        $cur = $this->historyPage->message['cur'];
550        $latest = $this->getWikiPage()->getLatest();
551        if ( $latest === $rev->getId()
552            || !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
553        ) {
554            return Html::element( 'span', [
555                'class' => 'mw-history-histlinks-current',
556            ], $cur );
557        } else {
558            return $this->getLinkRenderer()->makeKnownLink(
559                $this->getTitle(),
560                new HtmlArmor( $cur ),
561                [
562                    'class' => 'mw-history-histlinks-current',
563                    'title' => $this->historyPage->message['tooltip-cur']
564                ],
565                [
566                    'diff' => $latest,
567                    'oldid' => $rev->getId()
568                ]
569            );
570        }
571    }
572
573    /**
574     * Create a diff-to-previous link for this revision for this page.
575     *
576     * @param RevisionRecord $prevRev The revision being displayed
577     * @param RevisionRecord|null $nextRev The next revision in list (that is the previous one in
578     *        chronological order) or null if it is unknown, but a link should be created anyway.
579     * @return string
580     */
581    private function lastLink( RevisionRecord $prevRev, ?RevisionRecord $nextRev ) {
582        $last = $this->historyPage->message['last'];
583
584        if ( !$prevRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ||
585            ( $nextRev && !$nextRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) )
586        ) {
587            return Html::element( 'span', [
588                'class' => 'mw-history-histlinks-previous',
589            ], $last );
590        }
591
592        return $this->getLinkRenderer()->makeKnownLink(
593            $this->getTitle(),
594            new HtmlArmor( $last ),
595            [
596                'class' => 'mw-history-histlinks-previous',
597                'title' => $this->historyPage->message['tooltip-last']
598            ],
599            [
600                'diff' => 'prev', // T243569
601                'oldid' => $prevRev->getId()
602            ]
603        );
604    }
605
606    /**
607     * Create radio buttons for page history
608     *
609     * @param RevisionRecord $rev
610     * @param bool $firstInList Is this version the first one?
611     *
612     * @return string HTML output for the radio buttons
613     */
614    private function diffButtons( RevisionRecord $rev, $firstInList ) {
615        if ( $this->getNumRows() > 1 ) {
616            $id = $rev->getId();
617            $radio = [ 'type' => 'radio', 'value' => $id ];
618            /** @todo Move title texts to javascript */
619            if ( $firstInList ) {
620                $first = Html::element( 'input',
621                    array_merge( $radio, [
622                        // Disable the hidden radio because it can still
623                        // be selected with arrow keys on Firefox
624                        'disabled' => '',
625                        'name' => 'oldid',
626                        'id' => 'mw-oldid-null' ] )
627                );
628                $checkmark = [ 'checked' => 'checked' ];
629            } else {
630                # Check visibility of old revisions
631                if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
632                    $radio['disabled'] = 'disabled';
633                    $checkmark = []; // We will check the next possible one
634                } elseif ( !$this->oldIdChecked ) {
635                    $checkmark = [ 'checked' => 'checked' ];
636                    $this->oldIdChecked = $id;
637                } else {
638                    $checkmark = [];
639                }
640                $first = Html::element( 'input',
641                    array_merge( $radio, $checkmark, [
642                        'name' => 'oldid',
643                        'id' => "mw-oldid-$id" ] ) );
644                $checkmark = [];
645            }
646            $second = Html::element( 'input',
647                array_merge( $radio, $checkmark, [
648                    'name' => 'diff',
649                    'id' => "mw-diff-$id" ] ) );
650
651            return $first . $second;
652        } else {
653            return '';
654        }
655    }
656
657    /**
658     * Returns whether to show the "navigation bar"
659     *
660     * @return bool
661     */
662    protected function isNavigationBarShown() {
663        if ( $this->getNumRows() == 0 ) {
664            return false;
665        }
666        return parent::isNavigationBarShown();
667    }
668
669    /**
670     * Get the "prevent clickjacking" flag
671     * @return bool
672     */
673    public function getPreventClickjacking() {
674        return $this->preventClickjacking;
675    }
676
677}
678
679/** @deprecated class alias since 1.41 */
680class_alias( HistoryPager::class, 'HistoryPager' );