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