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