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