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