Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 318 |
|
0.00% |
0 / 18 |
CRAP | |
0.00% |
0 / 1 |
HistoryPager | |
0.00% |
0 / 317 |
|
0.00% |
0 / 18 |
4290 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
6 | |||
getArticle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSqlComment | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getQueryInfo | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
2 | |||
getIndexField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doBatchLookups | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
30 | |||
getEmptyBody | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getStartBody | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
56 | |||
getRevisionButton | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
getEndBody | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
submitButton | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
formatRow | |
0.00% |
0 / 111 |
|
0.00% |
0 / 1 |
552 | |||
revLink | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
curLink | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
lastLink | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
20 | |||
diffButtons | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
30 | |||
isNavigationBarShown | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getPreventClickjacking | |
0.00% |
0 / 1 |
|
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 | |
24 | namespace MediaWiki\Pager; |
25 | |
26 | use ChangesList; |
27 | use ChangeTags; |
28 | use HistoryAction; |
29 | use HtmlArmor; |
30 | use IDBAccessObject; |
31 | use MapCacheLRU; |
32 | use MediaWiki\Cache\LinkBatchFactory; |
33 | use MediaWiki\ChangeTags\ChangeTagsStore; |
34 | use MediaWiki\CommentFormatter\CommentFormatter; |
35 | use MediaWiki\HookContainer\HookContainer; |
36 | use MediaWiki\HookContainer\HookRunner; |
37 | use MediaWiki\Html\Html; |
38 | use MediaWiki\Html\ListToggle; |
39 | use MediaWiki\Linker\Linker; |
40 | use MediaWiki\MainConfigNames; |
41 | use MediaWiki\MediaWikiServices; |
42 | use MediaWiki\Parser\Sanitizer; |
43 | use MediaWiki\Revision\RevisionRecord; |
44 | use MediaWiki\Revision\RevisionStore; |
45 | use MediaWiki\SpecialPage\SpecialPage; |
46 | use MediaWiki\Watchlist\WatchlistManager; |
47 | use stdClass; |
48 | |
49 | /** |
50 | * @ingroup Pager |
51 | * @ingroup Actions |
52 | */ |
53 | #[\AllowDynamicProperties] |
54 | class 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 */ |
678 | class_alias( HistoryPager::class, 'HistoryPager' ); |