Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 167
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageHistoryList
0.00% covered (danger)
0.00%
0 / 166
0.00% covered (danger)
0.00%
0 / 10
2450
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getImagePage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 beginImageHistoryList
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 endImageHistoryList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 imageHistoryLine
0.00% covered (danger)
0.00%
0 / 112
0.00% covered (danger)
0.00%
0 / 1
870
 getThumbForLine
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 preventClickjacking
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPreventClickjacking
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPreventClickjacking
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Page;
8
9use MediaWiki\Context\ContextSource;
10use MediaWiki\FileRepo\File\File;
11use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
12use MediaWiki\Html\Html;
13use MediaWiki\Linker\Linker;
14use MediaWiki\MainConfigNames;
15use MediaWiki\MediaWikiServices;
16use MediaWiki\SpecialPage\SpecialPage;
17use MediaWiki\Title\Title;
18use Wikimedia\Timestamp\TimestampFormat as TS;
19
20/**
21 * Builds the image revision log shown on image pages
22 *
23 * @ingroup Media
24 */
25class 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 */
333class_alias( ImageHistoryList::class, 'ImageHistoryList' );