Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 314 |
|
0.00% |
0 / 18 |
CRAP | |
0.00% |
0 / 1 |
HistoryPager | |
0.00% |
0 / 313 |
|
0.00% |
0 / 18 |
4160 | |
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 / 107 |
|
0.00% |
0 / 1 |
506 | |||
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 MediaWiki\Actions\HistoryAction; |
27 | use MediaWiki\Cache\LinkBatchFactory; |
28 | use MediaWiki\ChangeTags\ChangeTags; |
29 | use MediaWiki\ChangeTags\ChangeTagsStore; |
30 | use MediaWiki\CommentFormatter\CommentFormatter; |
31 | use MediaWiki\HookContainer\HookContainer; |
32 | use MediaWiki\HookContainer\HookRunner; |
33 | use MediaWiki\Html\Html; |
34 | use MediaWiki\Html\ListToggle; |
35 | use MediaWiki\Linker\Linker; |
36 | use MediaWiki\MainConfigNames; |
37 | use MediaWiki\MediaWikiServices; |
38 | use MediaWiki\Page\Article; |
39 | use MediaWiki\Parser\Sanitizer; |
40 | use MediaWiki\RecentChanges\ChangesList; |
41 | use MediaWiki\Revision\RevisionRecord; |
42 | use MediaWiki\Revision\RevisionStore; |
43 | use MediaWiki\SpecialPage\SpecialPage; |
44 | use MediaWiki\User\UserIdentityValue; |
45 | use MediaWiki\Watchlist\WatchlistManager; |
46 | use stdClass; |
47 | use Wikimedia\HtmlArmor\HtmlArmor; |
48 | use Wikimedia\MapCacheLRU\MapCacheLRU; |
49 | use Wikimedia\Rdbms\IDBAccessObject; |
50 | |
51 | /** |
52 | * @ingroup Pager |
53 | * @ingroup Actions |
54 | */ |
55 | #[\AllowDynamicProperties] |
56 | class 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 */ |
685 | class_alias( HistoryPager::class, 'HistoryPager' ); |