Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.32% covered (danger)
3.32%
22 / 662
3.57% covered (danger)
3.57%
1 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImagePage
3.32% covered (danger)
3.32%
22 / 662
3.57% covered (danger)
3.57%
1 / 28
18620.28
0.00% covered (danger)
0.00%
0 / 1
 newPage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setFile
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 loadFile
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 view
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 1
462
 getDisplayedFile
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 showTOC
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
6
 makeMetadataTable
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 getLanguageForRendering
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
4.06
 openShowImage
0.00% covered (danger)
0.00%
0 / 222
0.00% covered (danger)
0.00%
0 / 1
1722
 getThumbPrevText
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 makeSizeLink
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 printSharedImageText
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
90
 getUploadUrl
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 uploadLinksBox
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 closeShowImage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 imageHistory
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 queryImageLinks
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 imageLinks
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 1
272
 imageDupes
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 showError
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 compare
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 doRenderLangOpt
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 createXmlOptionStringForLanguage
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getThumbSizes
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isLocal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDuplicates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getForeignCategories
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\Html\Html;
22use MediaWiki\Language\LanguageCode;
23use MediaWiki\Linker\Linker;
24use MediaWiki\MainConfigNames;
25use MediaWiki\MediaWikiServices;
26use MediaWiki\Request\WebRequest;
27use MediaWiki\SpecialPage\SpecialPage;
28use MediaWiki\Title\Title;
29use MediaWiki\Title\TitleArrayFromResult;
30use MediaWiki\Xml\Xml;
31use Wikimedia\Rdbms\IResultWrapper;
32
33/**
34 * Rendering of file description pages.
35 *
36 * @ingroup Media
37 * @method WikiFilePage getPage()
38 */
39class ImagePage extends Article {
40    use MediaFileTrait;
41
42    /** @var File|false Only temporary false, most code can assume this is a File */
43    private $displayImg;
44
45    /** @var FileRepo */
46    private $repo;
47
48    /** @var bool */
49    private $fileLoaded;
50
51    /** @var string|false Guaranteed to be HTML, {@see File::getDescriptionText} */
52    protected $mExtraDescription = false;
53
54    /**
55     * @param Title $title
56     * @return WikiFilePage
57     */
58    protected function newPage( Title $title ) {
59        // Overload mPage with a file-specific page
60        return new WikiFilePage( $title );
61    }
62
63    /**
64     * @param File $file
65     * @return void
66     */
67    public function setFile( $file ) {
68        $this->getPage()->setFile( $file );
69        $this->displayImg = $file;
70        $this->fileLoaded = true;
71    }
72
73    protected function loadFile() {
74        if ( $this->fileLoaded ) {
75            return;
76        }
77        $this->fileLoaded = true;
78
79        $this->displayImg = $img = false;
80
81        $this->getHookRunner()->onImagePageFindFile( $this, $img, $this->displayImg );
82        if ( !$img ) { // not set by hook?
83            $services = MediaWikiServices::getInstance();
84            $img = $services->getRepoGroup()->findFile( $this->getTitle() );
85            if ( !$img ) {
86                $img = $services->getRepoGroup()->getLocalRepo()->newFile( $this->getTitle() );
87            }
88        }
89        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable should be set
90        $this->getPage()->setFile( $img );
91        if ( !$this->displayImg ) { // not set by hook?
92            // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty should be set
93            $this->displayImg = $img;
94        }
95        $this->repo = $img->getRepo();
96    }
97
98    public function view() {
99        $context = $this->getContext();
100        $showEXIF = $context->getConfig()->get( MainConfigNames::ShowEXIF );
101
102        // For action=render, include body text only; none of the image extras
103        if ( $this->viewIsRenderAction ) {
104            parent::view();
105            return;
106        }
107
108        $out = $context->getOutput();
109        $request = $context->getRequest();
110        $diff = $request->getVal( 'diff' );
111
112        if ( $this->getTitle()->getNamespace() !== NS_FILE || ( $diff !== null && $this->isDiffOnlyView() ) ) {
113            parent::view();
114            return;
115        }
116
117        $this->loadFile();
118
119        if (
120            $this->getTitle()->getNamespace() === NS_FILE
121            && $this->getFile()->getRedirected()
122        ) {
123            if (
124                $this->getTitle()->getDBkey() == $this->getFile()->getName()
125                || $diff !== null
126            ) {
127                $request->setVal( 'diffonly', 'true' );
128            }
129
130            parent::view();
131            return;
132        }
133
134        if ( $showEXIF && $this->displayImg->exists() ) {
135            // @todo FIXME: Bad interface, see note on MediaHandler::formatMetadata().
136            $formattedMetadata = $this->displayImg->formatMetadata( $this->getContext() );
137        } else {
138            $formattedMetadata = false;
139        }
140
141        if ( !$diff && $this->displayImg->exists() ) {
142            $out->addHTML( $this->showTOC( (bool)$formattedMetadata ) );
143        }
144
145        if ( !$diff ) {
146            $this->openShowImage();
147        }
148
149        # No need to display noarticletext, we use our own message, output in openShowImage()
150        if ( $this->getPage()->getId() ) {
151            $out->addHTML( Html::openElement( 'div', [ 'id' => 'mw-imagepage-content' ] ) );
152            // NS_FILE pages render mostly in the user language (like special pages),
153            // except the editable wikitext content, which is rendered in the page content
154            // language by the parent class.
155            parent::view();
156            $out->addHTML( Html::closeElement( 'div' ) );
157        } else {
158            # Just need to set the right headers
159            $out->setArticleFlag( true );
160            $out->setPageTitle( $this->getTitle()->getPrefixedText() );
161            $this->getPage()->doViewUpdates(
162                $context->getAuthority(),
163                $this->getOldID()
164            );
165        }
166
167        # Show shared description, if needed
168        if ( $this->mExtraDescription ) {
169            $fol = $context->msg( 'shareddescriptionfollows' );
170            if ( !$fol->isDisabled() ) {
171                $out->addWikiTextAsInterface( $fol->plain() );
172            }
173            $out->addHTML(
174                Html::rawElement(
175                    'div',
176                    [ 'id' => 'shared-image-desc' ],
177                    $this->mExtraDescription
178                ) . "\n"
179            );
180        }
181
182        $this->closeShowImage();
183        $this->imageHistory();
184        // TODO: Cleanup the following
185
186        $out->addHTML( Xml::element( 'h2',
187            [ 'id' => 'filelinks' ],
188                $context->msg( 'imagelinks' )->text() ) . "\n" );
189        $this->imageDupes();
190        # @todo FIXME: For some freaky reason, we can't redirect to foreign images.
191        # Yet we return metadata about the target. Definitely an issue in the FileRepo
192        $this->imageLinks();
193
194        # Allow extensions to add something after the image links
195        $html = '';
196        $this->getHookRunner()->onImagePageAfterImageLinks( $this, $html );
197        if ( $html ) {
198            $out->addHTML( $html );
199        }
200
201        if ( $formattedMetadata ) {
202            $out->addHTML( Xml::element(
203                'h2',
204                [ 'id' => 'metadata' ],
205                    $context->msg( 'metadata' )->text() ) . "\n" );
206            $out->wrapWikiTextAsInterface(
207                'mw-imagepage-section-metadata',
208                $this->makeMetadataTable( $formattedMetadata )
209            );
210            $out->addModules( [ 'mediawiki.action.view.metadata' ] );
211        }
212
213        // Add remote Filepage.css
214        if ( !$this->repo->isLocal() ) {
215            $css = $this->repo->getDescriptionStylesheetUrl();
216            if ( $css ) {
217                $out->addStyle( $css );
218            }
219        }
220
221        $out->addModuleStyles( [
222            'mediawiki.action.view.filepage', // Add MediaWiki styles for a file page
223        ] );
224    }
225
226    /**
227     * @return File
228     */
229    public function getDisplayedFile() {
230        $this->loadFile();
231        return $this->displayImg;
232    }
233
234    /**
235     * Create the TOC
236     *
237     * @param bool $metadata Whether or not to show the metadata link
238     * @return string
239     */
240    protected function showTOC( $metadata ) {
241        $r = [
242            Html::rawElement(
243                'li',
244                [],
245                Html::rawElement(
246                    'a',
247                    [ 'href' => '#file' ],
248                    $this->getContext()->msg( 'file-anchor-link' )->escaped()
249                )
250            ),
251            Html::rawElement(
252                'li',
253                [],
254                Html::rawElement(
255                    'a',
256                    [ 'href' => '#filehistory' ],
257                    $this->getContext()->msg( 'filehist' )->escaped()
258                )
259            ),
260            Html::rawElement(
261                'li',
262                [],
263                Html::rawElement(
264                    'a',
265                    [ 'href' => '#filelinks' ],
266                    $this->getContext()->msg( 'imagelinks' )->escaped()
267                )
268            ),
269        ];
270
271        $this->getHookRunner()->onImagePageShowTOC( $this, $r );
272
273        if ( $metadata ) {
274            $r[] = Html::rawElement(
275                'li',
276                [],
277                Html::rawElement(
278                    'a',
279                    [ 'href' => '#metadata' ],
280                    $this->getContext()->msg( 'metadata' )->escaped()
281                )
282            );
283        }
284
285        return Html::rawElement( 'ul', [
286            'id' => 'filetoc',
287            'role' => 'navigation'
288        ], implode( "\n", $r ) );
289    }
290
291    /**
292     * Make a table with metadata to be shown in the output page.
293     *
294     * @todo FIXME: Bad interface, see note on MediaHandler::formatMetadata().
295     *
296     * @param array $metadata The array containing the Exif data
297     * @return string The metadata table. This is treated as Wikitext (!)
298     */
299    protected function makeMetadataTable( $metadata ) {
300        $r = $this->getContext()->msg( 'metadata-help' )->plain();
301        // Initial state of collapsible rows is collapsed
302        // see mediawiki.action.view.filepage.less and mediawiki.action.view.metadata module.
303        $r .= "<table id=\"mw_metadata\" class=\"mw_metadata collapsed\">\n";
304        foreach ( $metadata as $type => $stuff ) {
305            foreach ( $stuff as $v ) {
306                $class = str_replace( ' ', '_', $v['id'] );
307                if ( $type === 'collapsed' ) {
308                    $class .= ' mw-metadata-collapsible';
309                }
310                $r .= Html::rawElement( 'tr',
311                    [ 'class' => $class ],
312                    Html::rawElement( 'th', [], $v['name'] )
313                        . Html::rawElement( 'td', [], $v['value'] )
314                );
315            }
316        }
317        $r .= "</table>\n";
318        return $r;
319    }
320
321    /**
322     * Returns language code to be used for displaying the image, based on request context and
323     * languages available in the file.
324     *
325     * @param WebRequest $request
326     * @param File $file
327     * @return string|null a valid IETF language tag
328     */
329    private function getLanguageForRendering( WebRequest $request, File $file ) {
330        $handler = $file->getHandler();
331        if ( !$handler ) {
332            return null;
333        }
334
335        $requestLanguage = $request->getVal( 'lang' );
336        if ( $requestLanguage === null ) {
337            // For on File pages about a translatable SVG, decide which
338            // language to render the large thumbnail in (T310445)
339            $services = MediaWikiServices::getInstance();
340            $variantLangCode = $services->getLanguageConverterFactory()
341                ->getLanguageConverter( $services->getContentLanguage() )
342                ->getPreferredVariant();
343            $requestLanguage = LanguageCode::bcp47( $variantLangCode );
344        }
345        if ( $handler->validateParam( 'lang', $requestLanguage ) ) {
346            return $file->getMatchedLanguage( $requestLanguage );
347        }
348
349        return $handler->getDefaultRenderLanguage( $file );
350    }
351
352    protected function openShowImage() {
353        $context = $this->getContext();
354        $mainConfig = $context->getConfig();
355        $enableUploads = $mainConfig->get( MainConfigNames::EnableUploads );
356        $send404Code = $mainConfig->get( MainConfigNames::Send404Code );
357        $svgMaxSize = $mainConfig->get( MainConfigNames::SVGMaxSize );
358        $enableLegacyMediaDOM = $mainConfig->get( MainConfigNames::ParserEnableLegacyMediaDOM );
359        $this->loadFile();
360        $out = $context->getOutput();
361        $user = $context->getUser();
362        $lang = $context->getLanguage();
363        $sitedir = MediaWikiServices::getInstance()->getContentLanguage()->getDir();
364        $request = $context->getRequest();
365
366        if ( $this->displayImg->exists() ) {
367            [ $maxWidth, $maxHeight ] = $this->getImageLimitsFromOption( $user, 'imagesize' );
368
369            # image
370            $page = $request->getIntOrNull( 'page' );
371            if ( $page === null ) {
372                $params = [];
373                $page = 1;
374            } else {
375                $params = [ 'page' => $page ];
376            }
377
378            $renderLang = $this->getLanguageForRendering( $request, $this->displayImg );
379            if ( $renderLang !== null ) {
380                $params['lang'] = $renderLang;
381            }
382
383            $width_orig = $this->displayImg->getWidth( $page );
384            $width = $width_orig;
385            $height_orig = $this->displayImg->getHeight( $page );
386            $height = $height_orig;
387
388            $filename = wfEscapeWikiText( $this->displayImg->getName() );
389            $linktext = $filename;
390
391            $this->getHookRunner()->onImageOpenShowImageInlineBefore( $this, $out );
392
393            if ( $this->displayImg->allowInlineDisplay() ) {
394                # image
395                # "Download high res version" link below the image
396                # $msgsize = $this->getContext()->msg( 'file-info-size', $width_orig, $height_orig,
397                #   Language::formatSize( $this->displayImg->getSize() ), $mime )->escaped();
398                # We'll show a thumbnail of this image
399                if ( $width > $maxWidth ||
400                    $height > $maxHeight ||
401                    $this->displayImg->isVectorized()
402                ) {
403                    [ $width, $height ] = $this->displayImg->getDisplayWidthHeight(
404                        $maxWidth, $maxHeight, $page
405                    );
406                    $linktext = $context->msg( 'show-big-image' )->escaped();
407
408                    $thumbSizes = $this->getThumbSizes( $width_orig, $height_orig );
409                    # Generate thumbnails or thumbnail links as needed...
410                    $otherSizes = [];
411                    foreach ( $thumbSizes as $size ) {
412                        // We include a thumbnail size in the list, if it is
413                        // less than or equal to the original size of the image
414                        // asset ($width_orig/$height_orig). We also exclude
415                        // the current thumbnail's size ($width/$height)
416                        // since that is added to the message separately, so
417                        // it can be denoted as the current size being shown.
418                        // Vectorized images are limited by $wgSVGMaxSize big,
419                        // so all thumbs less than or equal that are shown.
420                        if ( ( ( $size[0] <= $width_orig && $size[1] <= $height_orig )
421                                || ( $this->displayImg->isVectorized()
422                                    && max( $size[0], $size[1] ) <= $svgMaxSize )
423                            )
424                            && $size[0] != $width && $size[1] != $height
425                            && $size[0] != $maxWidth && $size[1] != $maxHeight
426                        ) {
427                            $sizeLink = $this->makeSizeLink( $params, $size[0], $size[1] );
428                            if ( $sizeLink ) {
429                                $otherSizes[] = $sizeLink;
430                            }
431                        }
432                    }
433                    $otherSizes = array_unique( $otherSizes );
434
435                    $sizeLinkBigImagePreview = $this->makeSizeLink( $params, $width, $height );
436                    $msgsmall = $this->getThumbPrevText( $params, $sizeLinkBigImagePreview );
437                    if ( count( $otherSizes ) ) {
438                        $msgsmall .= ' ' .
439                        Html::rawElement(
440                            'span',
441                            [ 'class' => 'mw-filepage-other-resolutions' ],
442                            $context->msg( 'show-big-image-other' )
443                                ->rawParams( $lang->pipeList( $otherSizes ) )
444                                ->params( count( $otherSizes ) )
445                                ->parse()
446                        );
447                    }
448                } elseif ( $width == 0 && $height == 0 ) {
449                    # Some sort of audio file that doesn't have dimensions
450                    # Don't output a no hi res message for such a file
451                    $msgsmall = '';
452                } else {
453                    # Image is small enough to show full size on image page
454                    $msgsmall = $this->getContext()->msg( 'file-nohires' )->parse();
455                }
456
457                $params['width'] = $width;
458                $params['height'] = $height;
459                $params['isFilePageThumb'] = true;
460                // Allow the MediaHandler to handle query string parameters on the file page,
461                // e.g. start time for videos (T203994)
462                $params['imagePageParams'] = $request->getQueryValuesOnly();
463                $thumbnail = $this->displayImg->transform( $params );
464                Linker::processResponsiveImages( $this->displayImg, $thumbnail, $params );
465
466                $anchorclose = Html::rawElement(
467                    'div',
468                    [ 'class' => 'mw-filepage-resolutioninfo' ],
469                    $msgsmall
470                );
471
472                $isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1;
473                if ( $isMulti ) {
474                    $out->addModules( 'mediawiki.page.image.pagination' );
475                    /* TODO: multipageimage class is deprecated since Jan 2023 */
476                    $out->addHTML( '<div class="mw-filepage-multipage multipageimage">' );
477                }
478
479                if ( $thumbnail ) {
480                    $options = [
481                        'alt' => $this->displayImg->getTitle()->getPrefixedText(),
482                        'file-link' => true,
483                    ];
484                    $out->addHTML(
485                        Html::rawElement(
486                            'div',
487                            [ 'class' => 'fullImageLink', 'id' => 'file' ],
488                            $thumbnail->toHtml( $options ) . $anchorclose
489                        ) . "\n"
490                    );
491                }
492
493                if ( $isMulti ) {
494                    $linkPrev = $linkNext = '';
495                    $count = $this->displayImg->pageCount();
496                    if ( !$enableLegacyMediaDOM ) {
497                        $out->addModules( 'mediawiki.page.media' );
498                    }
499
500                    if ( $page > 1 ) {
501                        $label = $context->msg( 'imgmultipageprev' )->text();
502                        // on the client side, this link is generated in ajaxifyPageNavigation()
503                        // in the mediawiki.page.image.pagination module
504                        $linkPrev = $this->linkRenderer->makeKnownLink(
505                            $this->getTitle(),
506                            $label,
507                            [],
508                            [ 'page' => $page - 1 ]
509                        );
510                        $thumbPrevPage = Linker::makeThumbLinkObj(
511                            $this->getTitle(),
512                            $this->displayImg,
513                            $linkPrev,
514                            $label,
515                            'none',
516                            [ 'page' => $page - 1, 'isFilePageThumb' => true ]
517                        );
518                    } else {
519                        $thumbPrevPage = '';
520                    }
521
522                    if ( $page < $count ) {
523                        $label = $context->msg( 'imgmultipagenext' )->text();
524                        $linkNext = $this->linkRenderer->makeKnownLink(
525                            $this->getTitle(),
526                            $label,
527                            [],
528                            [ 'page' => $page + 1 ]
529                        );
530                        $thumbNextPage = Linker::makeThumbLinkObj(
531                            $this->getTitle(),
532                            $this->displayImg,
533                            $linkNext,
534                            $label,
535                            'none',
536                            [ 'page' => $page + 1, 'isFilePageThumb' => true ]
537                        );
538                    } else {
539                        $thumbNextPage = '';
540                    }
541
542                    $script = $mainConfig->get( MainConfigNames::Script );
543
544                    $formParams = [
545                        'name' => 'pageselector',
546                        'action' => $script,
547                    ];
548                    $options = [];
549                    for ( $i = 1; $i <= $count; $i++ ) {
550                        $options[] = Xml::option( $lang->formatNum( $i ), (string)$i, $i == $page );
551                    }
552                    $select = Xml::tags( 'select',
553                        [ 'id' => 'pageselector', 'name' => 'page' ],
554                        implode( "\n", $options ) );
555
556                    /* TODO: multipageimagenavbox class is deprecated since Jan 2023 */
557                    $out->addHTML(
558                        '<div class="mw-filepage-multipage-navigation multipageimagenavbox">' .
559                        $linkPrev .
560                        Html::rawElement( 'form', $formParams,
561                            Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) .
562                            $context->msg( 'imgmultigoto' )->rawParams( $select )->parse() .
563                            $context->msg( 'word-separator' )->escaped() .
564                            Xml::submitButton( $context->msg( 'imgmultigo' )->text() )
565                        ) .
566                        "$thumbPrevPage\n$thumbNextPage\n$linkNext</div></div>"
567                    );
568                }
569            } elseif ( $this->displayImg->isSafeFile() ) {
570                # if direct link is allowed but it's not a renderable image, show an icon.
571                $icon = $this->displayImg->iconThumb();
572
573                $out->addHTML(
574                    Html::rawElement(
575                        'div',
576                        [ 'class' => 'fullImageLink', 'id' => 'file' ],
577                        $icon->toHtml( [ 'file-link' => true ] )
578                    ) . "\n"
579                );
580            }
581
582            $longDesc = $context->msg( 'parentheses', $this->displayImg->getLongDesc() )->text();
583
584            $handler = $this->displayImg->getHandler();
585
586            // If this is a filetype with potential issues, warn the user.
587            if ( $handler ) {
588                $warningConfig = $handler->getWarningConfig( $this->displayImg );
589
590                if ( $warningConfig !== null ) {
591                    // The warning will be displayed via CSS and JavaScript.
592                    // We just need to tell the client side what message to use.
593                    $output = $context->getOutput();
594                    $output->addJsConfigVars( 'wgFileWarning', $warningConfig );
595                    $output->addModules( $warningConfig['module'] );
596                    $output->addModules( 'mediawiki.filewarning' );
597                }
598            }
599
600            $medialink = "[[Media:$filename|$linktext]]";
601
602            if ( !$this->displayImg->isSafeFile() ) {
603                $warning = $context->msg( 'mediawarning' )->plain();
604                // <bdi> is needed here to separate the file name, which
605                // most likely ends in Latin characters, from the description,
606                // which may begin with the file type. In RTL environment
607                // this will get messy.
608                $out->wrapWikiTextAsInterface( 'fullMedia', <<<EOT
609<bdi dir="$sitedir"><span class="dangerousLink">$medialink</span></bdi> <span class="fileInfo">$longDesc</span>
610EOT
611                );
612                // phpcs:enable
613                $out->wrapWikiTextAsInterface( 'mediaWarning', $warning );
614            } else {
615                $out->wrapWikiTextAsInterface( 'fullMedia', <<<EOT
616<bdi dir="$sitedir">$medialink</bdi> <span class="fileInfo">$longDesc</span>
617EOT
618                );
619            }
620
621            $renderLangOptions = $this->displayImg->getAvailableLanguages();
622            if ( count( $renderLangOptions ) >= 1 ) {
623                $out->addHTML( $this->doRenderLangOpt( $renderLangOptions, $renderLang ) );
624            }
625
626            // Add cannot animate thumbnail warning
627            if ( !$this->displayImg->canAnimateThumbIfAppropriate() ) {
628                // Include the extension so wiki admins can
629                // customize it on a per file-type basis
630                // (aka say things like use format X instead).
631                // additionally have a specific message for
632                // file-no-thumb-animation-gif
633                $ext = $this->displayImg->getExtension();
634                $noAnimMesg = wfMessageFallback(
635                    'file-no-thumb-animation-' . $ext,
636                    'file-no-thumb-animation'
637                )->setContext( $context )->plain();
638
639                $out->wrapWikiTextAsInterface( 'mw-noanimatethumb', $noAnimMesg );
640            }
641
642            if ( !$this->displayImg->isLocal() ) {
643                $this->printSharedImageText();
644            }
645        } else {
646            # Image does not exist
647            if ( !$this->getPage()->getId() ) {
648                $dbr = $this->dbProvider->getReplicaDatabase();
649
650                # No article exists either
651                # Show deletion log to be consistent with normal articles
652                LogEventsList::showLogExtract(
653                    $out,
654                    [ 'delete', 'move', 'protect', 'merge' ],
655                    $this->getTitle()->getPrefixedText(),
656                    '',
657                    [ 'lim' => 10,
658                        'conds' => [ $dbr->expr( 'log_action', '!=', 'revision' ) ],
659                        'showIfEmpty' => false,
660                        'msgKey' => [ 'moveddeleted-notice' ]
661                    ]
662                );
663            }
664
665            if ( $enableUploads &&
666                $context->getAuthority()->isAllowed( 'upload' )
667            ) {
668                // Only show an upload link if the user can upload
669                $uploadTitle = SpecialPage::getTitleFor( 'Upload' );
670                $nofile = [
671                    'filepage-nofile-link',
672                    $uploadTitle->getFullURL( [
673                        'wpDestFile' => $this->getFile()->getName()
674                    ] )
675                ];
676            } else {
677                $nofile = 'filepage-nofile';
678            }
679            // Note, if there is an image description page, but
680            // no image, then this setRobotPolicy is overridden
681            // by Article::View().
682            $out->setRobotPolicy( 'noindex,nofollow' );
683            $out->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile );
684            if ( !$this->getPage()->getId() && $send404Code ) {
685                // If there is no image, no shared image, and no description page,
686                // output a 404, to be consistent with Article::showMissingArticle.
687                $request->response()->statusHeader( 404 );
688            }
689        }
690        $out->setFileVersion( $this->displayImg );
691    }
692
693    /**
694     * Make the text under the image to say what size preview
695     *
696     * @param array $params parameters for thumbnail
697     * @param string $sizeLinkBigImagePreview HTML for the current size
698     * @return string HTML output
699     */
700    protected function getThumbPrevText( $params, $sizeLinkBigImagePreview ) {
701        if ( $sizeLinkBigImagePreview ) {
702            // Show a different message of preview is different format from original.
703            $previewTypeDiffers = false;
704            $origExt = $thumbExt = $this->displayImg->getExtension();
705            if ( $this->displayImg->getHandler() ) {
706                $origMime = $this->displayImg->getMimeType();
707                $typeParams = $params;
708                $this->displayImg->getHandler()->normaliseParams( $this->displayImg, $typeParams );
709                [ $thumbExt, $thumbMime ] = $this->displayImg->getHandler()->getThumbType(
710                    $origExt, $origMime, $typeParams );
711                if ( $thumbMime !== $origMime ) {
712                    $previewTypeDiffers = true;
713                }
714            }
715            if ( $previewTypeDiffers ) {
716                return $this->getContext()->msg( 'show-big-image-preview-differ' )->
717                    rawParams( $sizeLinkBigImagePreview )->
718                    params( strtoupper( $origExt ) )->
719                    params( strtoupper( $thumbExt ) )->
720                    parse();
721            } else {
722                return $this->getContext()->msg( 'show-big-image-preview' )->
723                    rawParams( $sizeLinkBigImagePreview )->
724                parse();
725            }
726        } else {
727            return '';
728        }
729    }
730
731    /**
732     * Creates a thumbnail of specified size and returns an HTML link to it
733     * @param array $params Scaler parameters
734     * @param int $width
735     * @param int $height
736     * @return string
737     */
738    protected function makeSizeLink( $params, $width, $height ) {
739        $params['width'] = $width;
740        $params['height'] = $height;
741        $thumbnail = $this->displayImg->transform( $params );
742        if ( $thumbnail && !$thumbnail->isError() ) {
743            return Html::rawElement( 'a', [
744                'href' => $thumbnail->getUrl(),
745                'class' => 'mw-thumbnail-link'
746                ], $this->getContext()->msg( 'show-big-image-size' )->numParams(
747                    $thumbnail->getWidth(), $thumbnail->getHeight()
748                )->parse() );
749        } else {
750            return '';
751        }
752    }
753
754    /**
755     * Show a notice that the file is from a shared repository
756     */
757    protected function printSharedImageText() {
758        $out = $this->getContext()->getOutput();
759        $this->loadFile();
760
761        $descUrl = $this->getFile()->getDescriptionUrl();
762        $descText = $this->getFile()->getDescriptionText( $this->getContext()->getLanguage() );
763
764        /* Add canonical to head if there is no local page for this shared file */
765        if ( $descUrl && !$this->getPage()->getId() ) {
766            $out->setCanonicalUrl( $descUrl );
767        }
768
769        $wrap = "<div class=\"sharedUploadNotice\">\n$1\n</div>\n";
770        $repo = $this->getFile()->getRepo()->getDisplayName();
771
772        if ( $descUrl &&
773            $descText &&
774            !$this->getContext()->msg( 'sharedupload-desc-here' )->isDisabled()
775        ) {
776            $out->wrapWikiMsg( $wrap, [ 'sharedupload-desc-here', $repo, $descUrl ] );
777        } elseif ( $descUrl &&
778            !$this->getContext()->msg( 'sharedupload-desc-there' )->isDisabled()
779        ) {
780            $out->wrapWikiMsg( $wrap, [ 'sharedupload-desc-there', $repo, $descUrl ] );
781        } else {
782            $out->wrapWikiMsg( $wrap, [ 'sharedupload', $repo ], ''/*BACKCOMPAT*/ );
783        }
784
785        if ( $descText ) {
786            $this->mExtraDescription = $descText;
787        }
788    }
789
790    public function getUploadUrl() {
791        $this->loadFile();
792        $uploadTitle = SpecialPage::getTitleFor( 'Upload' );
793        return $uploadTitle->getFullURL( [
794            'wpDestFile' => $this->getFile()->getName(),
795            'wpForReUpload' => 1
796        ] );
797    }
798
799    /**
800     * Add the re-upload link (or message about not being able to re-upload) to the output.
801     */
802    protected function uploadLinksBox() {
803        if ( !$this->getContext()->getConfig()->get( MainConfigNames::EnableUploads ) ) {
804            return;
805        }
806
807        $this->loadFile();
808        if ( !$this->getFile()->isLocal() ) {
809            return;
810        }
811
812        $canUpload = $this->getContext()->getAuthority()
813            ->probablyCan( 'upload', $this->getTitle() );
814        if ( $canUpload && UploadBase::userCanReUpload(
815                $this->getContext()->getAuthority(),
816                $this->getFile() )
817        ) {
818            // "Upload a new version of this file" link
819            $ulink = $this->linkRenderer->makeExternalLink(
820                $this->getUploadUrl(),
821                $this->getContext()->msg( 'uploadnewversion-linktext' ),
822                $this->getTitle()
823            );
824            $attrs = [ 'class' => 'plainlinks', 'id' => 'mw-imagepage-reupload-link' ];
825            $linkPara = Html::rawElement( 'p', $attrs, $ulink );
826        } else {
827            // "You cannot overwrite this file." message
828            $attrs = [ 'id' => 'mw-imagepage-upload-disallowed' ];
829            $msg = $this->getContext()->msg( 'upload-disallowed-here' )->text();
830            $linkPara = Html::element( 'p', $attrs, $msg );
831        }
832
833        $uploadLinks = Html::rawElement( 'div', [ 'class' => 'mw-imagepage-upload-links' ], $linkPara );
834        $this->getContext()->getOutput()->addHTML( $uploadLinks );
835    }
836
837    /**
838     * For overloading
839     */
840    protected function closeShowImage() {
841    }
842
843    /**
844     * If the page we've just displayed is in the "Image" namespace,
845     * we follow it with an upload history of the image and its usage.
846     */
847    protected function imageHistory() {
848        $this->loadFile();
849        $out = $this->getContext()->getOutput();
850        $pager = new ImageHistoryPseudoPager(
851            $this,
852            MediaWikiServices::getInstance()->getLinkBatchFactory()
853        );
854        $out->addHTML( $pager->getBody() );
855        $out->getMetadata()->setPreventClickjacking( $pager->getPreventClickjacking() );
856
857        $this->getFile()->resetHistory(); // free db resources
858
859        # Exist check because we don't want to show this on pages where an image
860        # doesn't exist along with the noimage message, that would suck. -ævar
861        if ( $this->getFile()->exists() ) {
862            $this->uploadLinksBox();
863        }
864    }
865
866    /**
867     * @param string|string[] $target
868     * @param int $limit
869     * @return IResultWrapper
870     */
871    protected function queryImageLinks( $target, $limit ) {
872        return $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
873            ->select( [ 'page_namespace', 'page_title', 'il_to' ] )
874            ->from( 'imagelinks' )
875            ->join( 'page', null, 'il_from = page_id' )
876            ->where( [ 'il_to' => $target ] )
877            ->orderBy( 'il_from' )
878            ->limit( $limit + 1 )
879            ->caller( __METHOD__ )->fetchResultSet();
880    }
881
882    protected function imageLinks() {
883        $limit = 100;
884
885        $out = $this->getContext()->getOutput();
886
887        $rows = [];
888        $redirects = [];
889        foreach ( $this->getTitle()->getRedirectsHere( NS_FILE ) as $redir ) {
890            $redirects[$redir->getDBkey()] = [];
891            $rows[] = (object)[
892                'page_namespace' => NS_FILE,
893                'page_title' => $redir->getDBkey(),
894            ];
895        }
896
897        $res = $this->queryImageLinks( $this->getTitle()->getDBkey(), $limit + 1 );
898        foreach ( $res as $row ) {
899            $rows[] = $row;
900        }
901        $count = count( $rows );
902
903        $hasMore = $count > $limit;
904        if ( !$hasMore && count( $redirects ) ) {
905            $res = $this->queryImageLinks( array_keys( $redirects ),
906                $limit - count( $rows ) + 1 );
907            foreach ( $res as $row ) {
908                $redirects[$row->il_to][] = $row;
909                $count++;
910            }
911            $hasMore = ( $res->numRows() + count( $rows ) ) > $limit;
912        }
913
914        if ( $count == 0 ) {
915            $out->wrapWikiMsg(
916                Html::rawElement( 'div',
917                    [ 'id' => 'mw-imagepage-nolinkstoimage' ], "\n$1\n" ),
918                'nolinkstoimage'
919            );
920            return;
921        }
922
923        $out->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" );
924        if ( !$hasMore ) {
925            $out->addWikiMsg( 'linkstoimage', $count );
926        } else {
927            // More links than the limit. Add a link to [[Special:Whatlinkshere]]
928            $out->addWikiMsg( 'linkstoimage-more',
929                $this->getContext()->getLanguage()->formatNum( $limit ),
930                $this->getTitle()->getPrefixedDBkey()
931            );
932        }
933
934        $out->addHTML(
935            Html::openElement( 'ul',
936                [ 'class' => 'mw-imagepage-linkstoimage' ] ) . "\n"
937        );
938        // Sort the list by namespace:title
939        usort( $rows, [ $this, 'compare' ] );
940
941        // Create links for every element
942        $currentCount = 0;
943        foreach ( $rows as $element ) {
944            $currentCount++;
945            if ( $currentCount > $limit ) {
946                break;
947            }
948
949            $link = $this->linkRenderer->makeKnownLink(
950                Title::makeTitle( $element->page_namespace, $element->page_title ),
951                null,
952                [],
953                // Add a redirect=no to make redirect pages reachable
954                [ 'redirect' => isset( $redirects[$element->page_title] ) ? 'no' : null ]
955            );
956            if ( !isset( $redirects[$element->page_title] ) ) {
957                # No redirects
958                $liContents = $link;
959            } elseif ( count( $redirects[$element->page_title] ) === 0 ) {
960                # Redirect without usages
961                $liContents = $this->getContext()->msg( 'linkstoimage-redirect' )
962                    ->rawParams( $link, '' )
963                    ->parse();
964            } else {
965                # Redirect with usages
966                $li = '';
967                foreach ( $redirects[$element->page_title] as $row ) {
968                    $currentCount++;
969                    if ( $currentCount > $limit ) {
970                        break;
971                    }
972
973                    $link2 = $this->linkRenderer->makeKnownLink(
974                        Title::makeTitle( $row->page_namespace, $row->page_title ) );
975                    $li .= Html::rawElement(
976                        'li',
977                        [ 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ],
978                        $link2
979                        ) . "\n";
980                }
981
982                $ul = Html::rawElement(
983                    'ul',
984                    [ 'class' => 'mw-imagepage-redirectstofile' ],
985                    $li
986                    ) . "\n";
987                $liContents = $this->getContext()->msg( 'linkstoimage-redirect' )->rawParams(
988                    $link, $ul )->parse();
989            }
990            $out->addHTML( Html::rawElement(
991                    'li',
992                    [ 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ],
993                    $liContents
994                ) . "\n"
995            );
996
997        }
998        $out->addHTML( Html::closeElement( 'ul' ) . "\n" );
999        $res->free();
1000
1001        // Add a links to [[Special:Whatlinkshere]]
1002        if ( $currentCount > $limit ) {
1003            $out->addWikiMsg( 'morelinkstoimage', $this->getTitle()->getPrefixedDBkey() );
1004        }
1005        $out->addHTML( Html::closeElement( 'div' ) . "\n" );
1006    }
1007
1008    protected function imageDupes() {
1009        $this->loadFile();
1010        $out = $this->getContext()->getOutput();
1011
1012        $dupes = $this->getPage()->getDuplicates();
1013        if ( count( $dupes ) == 0 ) {
1014            return;
1015        }
1016
1017        $out->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" );
1018        $out->addWikiMsg( 'duplicatesoffile',
1019            $this->getContext()->getLanguage()->formatNum( count( $dupes ) ), $this->getTitle()->getDBkey()
1020        );
1021        $out->addHTML( "<ul class='mw-imagepage-duplicates'>\n" );
1022
1023        /**
1024         * @var File $file
1025         */
1026        foreach ( $dupes as $file ) {
1027            $fromSrc = '';
1028            if ( $file->isLocal() ) {
1029                $link = $this->linkRenderer->makeKnownLink( $file->getTitle() );
1030            } else {
1031                $link = $this->linkRenderer->makeExternalLink(
1032                    $file->getDescriptionUrl(),
1033                    $file->getTitle()->getPrefixedText(),
1034                    $this->getTitle()
1035                );
1036                $fromSrc = $this->getContext()->msg(
1037                    'shared-repo-from',
1038                    $file->getRepo()->getDisplayName()
1039                )->escaped();
1040            }
1041            $out->addHTML( "<li>{$link} {$fromSrc}</li>\n" );
1042        }
1043        $out->addHTML( "</ul></div>\n" );
1044    }
1045
1046    /**
1047     * Display an error with a wikitext description
1048     *
1049     * @param string $description
1050     */
1051    public function showError( $description ) {
1052        $out = $this->getContext()->getOutput();
1053        $out->setPageTitleMsg( $this->getContext()->msg( 'internalerror' ) );
1054        $out->setRobotPolicy( 'noindex,nofollow' );
1055        $out->setArticleRelated( false );
1056        $out->disableClientCache();
1057        $out->addWikiTextAsInterface( $description );
1058    }
1059
1060    /**
1061     * Callback for usort() to do link sorts by (namespace, title)
1062     * Function copied from Title::compare()
1063     *
1064     * @param stdClass $a Object page to compare with
1065     * @param stdClass $b Object page to compare with
1066     * @return int Result of string comparison, or namespace comparison
1067     */
1068    protected function compare( $a, $b ) {
1069        return $a->page_namespace <=> $b->page_namespace
1070            ?: strcmp( $a->page_title, $b->page_title );
1071    }
1072
1073    /**
1074     * Output a drop-down box for language options for the file
1075     *
1076     * @param array $langChoices Array of string language codes
1077     * @param string|null $renderLang Language code for the language we want the file to rendered in,
1078     *  it is pre-selected in the drop down box, use null to select the default case in the option list
1079     * @return string HTML to insert underneath image.
1080     */
1081    protected function doRenderLangOpt( array $langChoices, $renderLang ) {
1082        $context = $this->getContext();
1083        $script = $context->getConfig()->get( MainConfigNames::Script );
1084        $opts = '';
1085
1086        $matchedRenderLang = $renderLang === null ? null : $this->displayImg->getMatchedLanguage( $renderLang );
1087
1088        foreach ( $langChoices as $lang ) {
1089            $opts .= $this->createXmlOptionStringForLanguage(
1090                $lang,
1091                $matchedRenderLang === $lang
1092            );
1093        }
1094
1095        // Allow for the default case in an svg <switch> that is displayed if no
1096        // systemLanguage attribute matches
1097        $opts .= "\n" .
1098            Xml::option(
1099                $context->msg( 'img-lang-default' )->text(),
1100                'und',
1101                $matchedRenderLang === null || $matchedRenderLang === 'und'
1102            );
1103
1104        $select = Html::rawElement(
1105            'select',
1106            [ 'id' => 'mw-imglangselector', 'name' => 'lang' ],
1107            $opts
1108        );
1109        $submit = Xml::submitButton( $context->msg( 'img-lang-go' )->text() );
1110
1111        $formContents = $context->msg( 'img-lang-info' )
1112            ->rawParams( $select, $submit )
1113            ->parse();
1114        $formContents .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() );
1115
1116        $langSelectLine = Html::rawElement( 'div', [ 'id' => 'mw-imglangselector-line' ],
1117            Html::rawElement( 'form', [ 'action' => $script ], $formContents )
1118        );
1119        return $langSelectLine;
1120    }
1121
1122    /**
1123     * @param string $lang
1124     * @param bool $selected
1125     * @return string
1126     */
1127    private function createXmlOptionStringForLanguage( $lang, $selected ) {
1128        // TODO: There is no good way to get the language name of a BCP code,
1129        // as MW language codes take precedence
1130        $name = MediaWikiServices::getInstance()
1131            ->getLanguageNameUtils()
1132            ->getLanguageName( $lang, $this->getContext()->getLanguage()->getCode() );
1133        if ( $name !== '' ) {
1134            $display = $this->getContext()->msg( 'img-lang-opt', $lang, $name )->text();
1135        } else {
1136            $display = $lang;
1137        }
1138        return "\n" .
1139            Xml::option(
1140                $display,
1141                $lang,
1142                $selected
1143            );
1144    }
1145
1146    /**
1147     * Get alternative thumbnail sizes.
1148     *
1149     * @note This will only list several alternatives if thumbnails are rendered on 404
1150     * @param int $origWidth Actual width of image
1151     * @param int $origHeight Actual height of image
1152     * @return int[][] An array of [width, height] pairs.
1153     * @phan-return array<int,array{0:int,1:int}>
1154     */
1155    protected function getThumbSizes( $origWidth, $origHeight ) {
1156        $context = $this->getContext();
1157        $imageLimits = $context->getConfig()->get( MainConfigNames::ImageLimits );
1158        if ( $this->displayImg->getRepo()->canTransformVia404() ) {
1159            $thumbSizes = $imageLimits;
1160            // Also include the full sized resolution in the list, so
1161            // that users know they can get it. This will link to the
1162            // original file asset if mustRender() === false. In the case
1163            // that we mustRender, some users have indicated that they would
1164            // find it useful to have the full size image in the rendered
1165            // image format.
1166            $thumbSizes[] = [ $origWidth, $origHeight ];
1167        } else {
1168            # Creating thumb links triggers thumbnail generation.
1169            # Just generate the thumb for the current users prefs.
1170            $thumbSizes = [
1171                $this->getImageLimitsFromOption( $context->getUser(), 'thumbsize' )
1172            ];
1173            if ( !$this->displayImg->mustRender() ) {
1174                // We can safely include a link to the "full-size" preview,
1175                // without actually rendering.
1176                $thumbSizes[] = [ $origWidth, $origHeight ];
1177            }
1178        }
1179        return $thumbSizes;
1180    }
1181
1182    /**
1183     * @see WikiFilePage::getFile
1184     * @return File
1185     */
1186    public function getFile(): File {
1187        return $this->getPage()->getFile();
1188    }
1189
1190    /**
1191     * @see WikiFilePage::isLocal
1192     * @return bool
1193     */
1194    public function isLocal() {
1195        return $this->getPage()->isLocal();
1196    }
1197
1198    /**
1199     * @see WikiFilePage::getDuplicates
1200     * @return File[]|null
1201     */
1202    public function getDuplicates() {
1203        return $this->getPage()->getDuplicates();
1204    }
1205
1206    /**
1207     * @see WikiFilePage::getForeignCategories
1208     * @return TitleArrayFromResult
1209     */
1210    public function getForeignCategories() {
1211        return $this->getPage()->getForeignCategories();
1212    }
1213
1214}