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