Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 167 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
| ImageHistoryList | |
0.00% |
0 / 166 |
|
0.00% |
0 / 10 |
2450 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| getImagePage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getFile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| beginImageHistoryList | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
56 | |||
| endImageHistoryList | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| imageHistoryLine | |
0.00% |
0 / 112 |
|
0.00% |
0 / 1 |
870 | |||
| getThumbForLine | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
30 | |||
| preventClickjacking | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| setPreventClickjacking | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getPreventClickjacking | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\Page; |
| 8 | |
| 9 | use MediaWiki\Context\ContextSource; |
| 10 | use MediaWiki\FileRepo\File\File; |
| 11 | use MediaWiki\HookContainer\ProtectedHookAccessorTrait; |
| 12 | use MediaWiki\Html\Html; |
| 13 | use MediaWiki\Linker\Linker; |
| 14 | use MediaWiki\MainConfigNames; |
| 15 | use MediaWiki\MediaWikiServices; |
| 16 | use MediaWiki\SpecialPage\SpecialPage; |
| 17 | use MediaWiki\Title\Title; |
| 18 | use Wikimedia\Timestamp\TimestampFormat as TS; |
| 19 | |
| 20 | /** |
| 21 | * Builds the image revision log shown on image pages |
| 22 | * |
| 23 | * @ingroup Media |
| 24 | */ |
| 25 | class ImageHistoryList extends ContextSource { |
| 26 | use ProtectedHookAccessorTrait; |
| 27 | |
| 28 | protected Title $title; |
| 29 | protected File $img; |
| 30 | protected ImagePage $imagePage; |
| 31 | protected File $current; |
| 32 | |
| 33 | protected bool $showThumb; |
| 34 | /** @var bool */ |
| 35 | protected $preventClickjacking = false; |
| 36 | |
| 37 | /** |
| 38 | * @param ImagePage $imagePage |
| 39 | */ |
| 40 | public function __construct( $imagePage ) { |
| 41 | $context = $imagePage->getContext(); |
| 42 | $this->current = $imagePage->getPage()->getFile(); |
| 43 | $this->img = $imagePage->getDisplayedFile(); |
| 44 | $this->title = $imagePage->getTitle(); |
| 45 | $this->imagePage = $imagePage; |
| 46 | $this->showThumb = $context->getConfig()->get( MainConfigNames::ShowArchiveThumbnails ) && |
| 47 | $this->img->canRender(); |
| 48 | $this->setContext( $context ); |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * @return ImagePage |
| 53 | */ |
| 54 | public function getImagePage() { |
| 55 | return $this->imagePage; |
| 56 | } |
| 57 | |
| 58 | /** |
| 59 | * @return File |
| 60 | */ |
| 61 | public function getFile() { |
| 62 | return $this->img; |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * @return string |
| 67 | */ |
| 68 | public function beginImageHistoryList() { |
| 69 | // Styles for class=history-deleted |
| 70 | $this->getOutput()->addModuleStyles( 'mediawiki.interface.helpers.styles' ); |
| 71 | |
| 72 | $html = ''; |
| 73 | $canDelete = $this->current->isLocal() && |
| 74 | $this->getAuthority()->isAllowedAny( 'delete', 'deletedhistory' ); |
| 75 | |
| 76 | foreach ( [ |
| 77 | '', |
| 78 | $canDelete ? '' : null, |
| 79 | 'filehist-datetime', |
| 80 | $this->showThumb ? 'filehist-thumb' : null, |
| 81 | 'filehist-dimensions', |
| 82 | 'filehist-user', |
| 83 | 'filehist-comment', |
| 84 | ] as $key ) { |
| 85 | if ( $key !== null ) { |
| 86 | $html .= Html::element( 'th', [], $key ? $this->msg( $key )->text() : '' ); |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | return Html::openElement( 'table', [ 'class' => 'wikitable filehistory' ] ) . "\n" |
| 91 | . Html::rawElement( 'tr', [], $html ) . "\n"; |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * @return string |
| 96 | */ |
| 97 | public function endImageHistoryList() { |
| 98 | return Html::closeElement( 'table' ) . "\n"; |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * @internal |
| 103 | * @param bool $iscur |
| 104 | * @param File $file |
| 105 | * @param string $formattedComment |
| 106 | * @return string |
| 107 | */ |
| 108 | public function imageHistoryLine( $iscur, $file, $formattedComment ) { |
| 109 | $user = $this->getUser(); |
| 110 | $lang = $this->getLanguage(); |
| 111 | $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); |
| 112 | $timestamp = wfTimestamp( TS::MW, $file->getTimestamp() ); |
| 113 | // @phan-suppress-next-line PhanUndeclaredMethod |
| 114 | $img = $iscur ? $file->getName() : $file->getArchiveName(); |
| 115 | $uploader = $file->getUploader( File::FOR_THIS_USER, $user ); |
| 116 | |
| 117 | $local = $this->current->isLocal(); |
| 118 | $row = ''; |
| 119 | |
| 120 | // Deletion link |
| 121 | if ( $local && ( $this->getAuthority()->isAllowedAny( 'delete', 'deletedhistory' ) ) ) { |
| 122 | $row .= Html::openElement( 'td' ); |
| 123 | # Link to hide content. Don't show useless link to people who cannot hide revisions. |
| 124 | if ( !$iscur && $this->getAuthority()->isAllowed( 'deleterevision' ) ) { |
| 125 | // If file is top revision, is missing or locked from this user, don't link |
| 126 | if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) || !$file->exists() ) { |
| 127 | $row .= Html::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] ); |
| 128 | } else { |
| 129 | $row .= Html::check( 'ids[' . explode( '!', $img, 2 )[0] . ']', false ); |
| 130 | } |
| 131 | if ( $this->getAuthority()->isAllowed( 'delete' ) ) { |
| 132 | $row .= ' '; |
| 133 | } |
| 134 | } |
| 135 | # Link to remove from history |
| 136 | if ( $this->getAuthority()->isAllowed( 'delete' ) ) { |
| 137 | if ( $file->exists() ) { |
| 138 | $row .= $linkRenderer->makeKnownLink( |
| 139 | $this->title, |
| 140 | $this->msg( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' )->text(), |
| 141 | [], |
| 142 | [ 'action' => 'delete', 'oldimage' => $iscur ? null : $img ] |
| 143 | ); |
| 144 | } else { |
| 145 | // T244567: Non-existing file can not be deleted. |
| 146 | $row .= $this->msg( 'filehist-missing' )->escaped(); |
| 147 | } |
| 148 | |
| 149 | } |
| 150 | $row .= Html::closeElement( 'td' ); |
| 151 | } |
| 152 | |
| 153 | // Reversion link/current indicator |
| 154 | $row .= Html::openElement( 'td' ); |
| 155 | if ( $iscur ) { |
| 156 | $row .= $this->msg( 'filehist-current' )->escaped(); |
| 157 | } elseif ( $local && $this->getAuthority()->probablyCan( 'edit', $this->title ) |
| 158 | && $this->getAuthority()->probablyCan( 'upload', $this->title ) |
| 159 | ) { |
| 160 | if ( $file->isDeleted( File::DELETED_FILE ) ) { |
| 161 | $row .= $this->msg( 'filehist-revert' )->escaped(); |
| 162 | } elseif ( !$file->exists() ) { |
| 163 | // T328112: Lost file, in this case there's no version to revert back to. |
| 164 | $row .= $this->msg( 'filehist-missing' )->escaped(); |
| 165 | } else { |
| 166 | $row .= $linkRenderer->makeKnownLink( |
| 167 | $this->title, |
| 168 | $this->msg( 'filehist-revert' )->text(), |
| 169 | [], |
| 170 | [ |
| 171 | 'action' => 'revert', |
| 172 | 'oldimage' => $img, |
| 173 | ] |
| 174 | ); |
| 175 | } |
| 176 | } |
| 177 | $row .= Html::closeElement( 'td' ); |
| 178 | |
| 179 | // Date/time and image link |
| 180 | $selected = $file->getTimestamp() === $this->img->getTimestamp(); |
| 181 | $row .= Html::openElement( 'td', [ |
| 182 | 'class' => $selected ? 'filehistory-selected' : null, |
| 183 | 'style' => 'white-space: nowrap;' |
| 184 | ] ); |
| 185 | if ( !$file->userCan( File::DELETED_FILE, $user ) ) { |
| 186 | # Don't link to unviewable files |
| 187 | $row .= Html::element( 'span', [ 'class' => 'history-deleted' ], |
| 188 | $lang->userTimeAndDate( $timestamp, $user ) |
| 189 | ); |
| 190 | } elseif ( $file->isDeleted( File::DELETED_FILE ) ) { |
| 191 | $timeAndDate = $lang->userTimeAndDate( $timestamp, $user ); |
| 192 | if ( $local ) { |
| 193 | $this->setPreventClickjacking( true ); |
| 194 | # Make a link to review the image |
| 195 | $url = $linkRenderer->makeKnownLink( |
| 196 | SpecialPage::getTitleFor( 'Revisiondelete' ), |
| 197 | $timeAndDate, |
| 198 | [], |
| 199 | [ |
| 200 | 'target' => $this->title->getPrefixedText(), |
| 201 | 'file' => $img, |
| 202 | 'token' => $user->getEditToken( $img ) |
| 203 | ] |
| 204 | ); |
| 205 | } else { |
| 206 | $url = htmlspecialchars( $timeAndDate ); |
| 207 | } |
| 208 | $row .= Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $url ); |
| 209 | } elseif ( !$file->exists() ) { |
| 210 | $row .= Html::element( 'span', [ 'class' => 'mw-file-missing' ], |
| 211 | $lang->userTimeAndDate( $timestamp, $user ) |
| 212 | ); |
| 213 | } else { |
| 214 | $url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img ); |
| 215 | $row .= Html::element( 'a', [ 'href' => $url ], |
| 216 | $lang->userTimeAndDate( $timestamp, $user ) |
| 217 | ); |
| 218 | } |
| 219 | $row .= Html::closeElement( 'td' ); |
| 220 | |
| 221 | // Thumbnail |
| 222 | if ( $this->showThumb ) { |
| 223 | $row .= Html::rawElement( 'td', [], |
| 224 | $this->getThumbForLine( $file, $iscur ) ?? $this->msg( 'filehist-nothumb' )->escaped() |
| 225 | ); |
| 226 | } |
| 227 | |
| 228 | // Image dimensions + size |
| 229 | $row .= Html::openElement( 'td' ); |
| 230 | $row .= htmlspecialchars( $file->getDimensionsString() ); |
| 231 | $row .= $this->msg( 'word-separator' )->escaped(); |
| 232 | $row .= Html::element( 'span', [ 'style' => 'white-space: nowrap;' ], |
| 233 | $this->msg( 'parentheses' )->sizeParams( $file->getSize() )->text() |
| 234 | ); |
| 235 | $row .= Html::closeElement( 'td' ); |
| 236 | |
| 237 | // Uploading user |
| 238 | $row .= Html::openElement( 'td' ); |
| 239 | // Hide deleted usernames |
| 240 | if ( $uploader ) { |
| 241 | $row .= Linker::userLink( $uploader->getId(), $uploader->getName() ); |
| 242 | if ( $local ) { |
| 243 | $row .= Html::rawElement( 'span', [ 'style' => 'white-space: nowrap;' ], |
| 244 | Linker::userToolLinks( $uploader->getId(), $uploader->getName() ) |
| 245 | ); |
| 246 | } |
| 247 | } else { |
| 248 | $row .= Html::element( 'span', [ 'class' => 'history-deleted' ], |
| 249 | $this->msg( 'rev-deleted-user' )->text() |
| 250 | ); |
| 251 | } |
| 252 | $row .= Html::closeElement( 'td' ); |
| 253 | |
| 254 | // Don't show deleted descriptions |
| 255 | if ( $file->isDeleted( File::DELETED_COMMENT ) ) { |
| 256 | $row .= Html::rawElement( 'td', [], |
| 257 | Html::element( 'span', [ 'class' => 'history-deleted' ], |
| 258 | $this->msg( 'rev-deleted-comment' )->text() |
| 259 | ) |
| 260 | ); |
| 261 | } else { |
| 262 | $contLang = MediaWikiServices::getInstance()->getContentLanguage(); |
| 263 | $row .= Html::rawElement( 'td', [ 'dir' => $contLang->getDir() ], $formattedComment ); |
| 264 | } |
| 265 | |
| 266 | $rowClass = null; |
| 267 | $this->getHookRunner()->onImagePageFileHistoryLine( $this, $file, $row, $rowClass ); |
| 268 | |
| 269 | return Html::rawElement( 'tr', [ 'class' => $rowClass ], $row ) . "\n"; |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * @param File $file |
| 274 | * @param bool $iscur |
| 275 | * @return string|null |
| 276 | */ |
| 277 | protected function getThumbForLine( $file, $iscur ) { |
| 278 | $user = $this->getUser(); |
| 279 | if ( !$file->allowInlineDisplay() || |
| 280 | $file->isDeleted( File::DELETED_FILE ) || |
| 281 | !$file->userCan( File::DELETED_FILE, $user ) |
| 282 | ) { |
| 283 | return null; |
| 284 | } |
| 285 | |
| 286 | $thumbnail = $file->transform( |
| 287 | [ |
| 288 | 'width' => '120', |
| 289 | 'height' => '120', |
| 290 | 'isFilePageThumb' => $iscur // old revisions are already versioned |
| 291 | ] |
| 292 | ); |
| 293 | if ( !$thumbnail ) { |
| 294 | return null; |
| 295 | } |
| 296 | |
| 297 | $lang = $this->getLanguage(); |
| 298 | $timestamp = wfTimestamp( TS::MW, $file->getTimestamp() ); |
| 299 | $alt = $this->msg( |
| 300 | 'filehist-thumbtext', |
| 301 | $lang->userTimeAndDate( $timestamp, $user ), |
| 302 | $lang->userDate( $timestamp, $user ), |
| 303 | $lang->userTime( $timestamp, $user ) |
| 304 | )->text(); |
| 305 | return $thumbnail->toHtml( [ 'alt' => $alt, 'file-link' => true, 'loading' => 'lazy' ] ); |
| 306 | } |
| 307 | |
| 308 | /** |
| 309 | * @param bool $enable |
| 310 | * @deprecated since 1.38, use ::setPreventClickjacking() instead |
| 311 | */ |
| 312 | protected function preventClickjacking( $enable = true ) { |
| 313 | $this->preventClickjacking = $enable; |
| 314 | } |
| 315 | |
| 316 | /** |
| 317 | * @param bool $enable |
| 318 | * @since 1.38 |
| 319 | */ |
| 320 | protected function setPreventClickjacking( bool $enable ) { |
| 321 | $this->preventClickjacking = $enable; |
| 322 | } |
| 323 | |
| 324 | /** |
| 325 | * @return bool |
| 326 | */ |
| 327 | public function getPreventClickjacking() { |
| 328 | return $this->preventClickjacking; |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | /** @deprecated class alias since 1.44 */ |
| 333 | class_alias( ImageHistoryList::class, 'ImageHistoryList' ); |