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