Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.23% covered (danger)
3.23%
22 / 681
3.57% covered (danger)
3.57%
1 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImagePage
3.24% covered (danger)
3.24%
22 / 680
3.57% covered (danger)
3.57%
1 / 28
19459.31
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 / 233
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 / 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 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 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->getOldID()
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 $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            $filename = wfEscapeWikiText( $this->displayImg->getName() );
385            $linktext = $filename;
386
387            $this->getHookRunner()->onImageOpenShowImageInlineBefore( $this, $out );
388
389            if ( $this->displayImg->allowInlineDisplay() ) {
390                if (
391                    $this->displayImg->isVectorized() &&
392                    $mainConfig->get( MainConfigNames::SVGNativeRendering ) === true
393                ) {
394                    // SVG that is rendered native doesn't need these links
395                    $msgsmall = '';
396                } elseif ( $width > $maxWidth ||
397                    $height > $maxHeight ||
398                    $this->displayImg->isVectorized()
399                ) {
400                    // "Download high res version" link below the image
401                    // $msgsize = $this->getContext()->msg( 'file-info-size', $width_orig, $height_orig,
402                    //   Language::formatSize( $this->displayImg->getSize() ), $mime )->escaped();
403                    // We'll show a thumbnail of this image
404                    [ $width, $height ] = $this->displayImg->getDisplayWidthHeight(
405                        $maxWidth, $maxHeight, $page
406                    );
407                    $linktext = $context->msg( 'show-big-image' )->escaped();
408
409                    $thumbSizes = $this->getThumbSizes( $width_orig, $height_orig );
410                    // Generate thumbnails or thumbnail links as needed...
411                    $otherSizes = [];
412                    foreach ( $thumbSizes as $size ) {
413                        // We include a thumbnail size in the list, if it is
414                        // less than or equal to the original size of the image
415                        // asset ($width_orig/$height_orig). We also exclude
416                        // the current thumbnail's size ($width/$height)
417                        // since that is added to the message separately, so
418                        // it can be denoted as the current size being shown.
419                        // Vectorized images are limited by $wgSVGMaxSize big,
420                        // so all thumbs less than or equal that are shown.
421                        if ( ( ( $size[0] <= $width_orig && $size[1] <= $height_orig )
422                                || ( $this->displayImg->isVectorized()
423                                    && max( $size[0], $size[1] ) <= $svgMaxSize )
424                            )
425                            && $size[0] != $width && $size[1] != $height
426                            && $size[0] != $maxWidth && $size[1] != $maxHeight
427                        ) {
428                            $sizeLink = $this->makeSizeLink( $params, $size[0], $size[1] );
429                            if ( $sizeLink ) {
430                                $otherSizes[] = $sizeLink;
431                            }
432                        }
433                    }
434                    $otherSizes = array_unique( $otherSizes );
435
436                    $sizeLinkBigImagePreview = $this->makeSizeLink( $params, $width, $height );
437                    $msgsmall = $this->getThumbPrevText( $params, $sizeLinkBigImagePreview );
438                    if ( count( $otherSizes ) ) {
439                        $msgsmall .= ' ' .
440                        Html::rawElement(
441                            'span',
442                            [ 'class' => 'mw-filepage-other-resolutions' ],
443                            $context->msg( 'show-big-image-other' )
444                                ->rawParams( $lang->pipeList( $otherSizes ) )
445                                ->params( count( $otherSizes ) )
446                                ->parse()
447                        );
448                    }
449                } elseif ( $width == 0 && $height == 0 ) {
450                    # Some sort of audio file that doesn't have dimensions
451                    # Don't output a no hi res message for such a file
452                    $msgsmall = '';
453                } else {
454                    # Image is small enough to show full size on image page
455                    $msgsmall = $this->getContext()->msg( 'file-nohires' )->parse();
456                }
457
458                $params['width'] = $width;
459                $params['height'] = $height;
460                $params['isFilePageThumb'] = true;
461                // Allow the MediaHandler to handle query string parameters on the file page,
462                // e.g. start time for videos (T203994)
463                $params['imagePageParams'] = $request->getQueryValuesOnly();
464                $thumbnail = $this->displayImg->transform( $params );
465                Linker::processResponsiveImages( $this->displayImg, $thumbnail, $params );
466
467                $anchorclose = Html::rawElement(
468                    'div',
469                    [ 'class' => 'mw-filepage-resolutioninfo' ],
470                    $msgsmall
471                );
472
473                $isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1;
474                if ( $isMulti ) {
475                    $out->addModules( 'mediawiki.page.image.pagination' );
476                    /* TODO: multipageimage class is deprecated since Jan 2023 */
477                    $out->addHTML( '<div class="mw-filepage-multipage multipageimage">' );
478                }
479
480                if ( $thumbnail ) {
481                    $options = [
482                        'alt' => $this->displayImg->getTitle()->getPrefixedText(),
483                        'file-link' => true,
484                    ];
485                    $out->addHTML(
486                        Html::rawElement(
487                            'div',
488                            [ 'class' => 'fullImageLink', 'id' => 'file' ],
489                            $thumbnail->toHtml( $options ) . $anchorclose
490                        ) . "\n"
491                    );
492                }
493
494                if ( $isMulti ) {
495                    $linkPrev = $linkNext = '';
496                    $count = $this->displayImg->pageCount();
497                    $out->addModules( 'mediawiki.page.media' );
498
499                    if ( $page > 1 ) {
500                        $label = $context->msg( 'imgmultipageprev' )->text();
501                        // on the client side, this link is generated in ajaxifyPageNavigation()
502                        // in the mediawiki.page.image.pagination module
503                        $linkPrev = $this->linkRenderer->makeKnownLink(
504                            $this->getTitle(),
505                            $label,
506                            [],
507                            [ 'page' => $page - 1 ]
508                        );
509                        $thumbPrevPage = Linker::makeThumbLinkObj(
510                            $this->getTitle(),
511                            $this->displayImg,
512                            $linkPrev,
513                            $label,
514                            'none',
515                            [ 'page' => $page - 1, 'isFilePageThumb' => true ]
516                        );
517                    } else {
518                        $thumbPrevPage = '';
519                    }
520
521                    if ( $page < $count ) {
522                        $label = $context->msg( 'imgmultipagenext' )->text();
523                        $linkNext = $this->linkRenderer->makeKnownLink(
524                            $this->getTitle(),
525                            $label,
526                            [],
527                            [ 'page' => $page + 1 ]
528                        );
529                        $thumbNextPage = Linker::makeThumbLinkObj(
530                            $this->getTitle(),
531                            $this->displayImg,
532                            $linkNext,
533                            $label,
534                            'none',
535                            [ 'page' => $page + 1, 'isFilePageThumb' => true ]
536                        );
537                    } else {
538                        $thumbNextPage = '';
539                    }
540
541                    $script = $mainConfig->get( MainConfigNames::Script );
542
543                    $formParams = [
544                        'name' => 'pageselector',
545                        'action' => $script,
546                    ];
547                    $options = [];
548                    for ( $i = 1; $i <= $count; $i++ ) {
549                        $options[] = Html::element(
550                            'option',
551                            [ 'value' => (string)$i, 'selected' => $i == $page ],
552                            $lang->formatNum( $i )
553                        );
554                    }
555                    $select = Html::rawElement( 'select',
556                        [ 'id' => 'pageselector', 'name' => 'page' ],
557                        implode( "\n", $options ) );
558
559                    /* TODO: multipageimagenavbox class is deprecated since Jan 2023 */
560                    $out->addHTML(
561                        '<div class="mw-filepage-multipage-navigation multipageimagenavbox">' .
562                        $linkPrev .
563                        Html::rawElement( 'form', $formParams,
564                            Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) .
565                            $context->msg( 'imgmultigoto' )->rawParams( $select )->parse() .
566                            $context->msg( 'word-separator' )->escaped() .
567                            Html::submitButton( $context->msg( 'imgmultigo' )->text() )
568                        ) .
569                        "$thumbPrevPage\n$thumbNextPage\n$linkNext</div></div>"
570                    );
571                }
572            } elseif ( $this->displayImg->isSafeFile() ) {
573                # if direct link is allowed but it's not a renderable image, show an icon.
574                $icon = $this->displayImg->iconThumb();
575
576                $out->addHTML(
577                    Html::rawElement(
578                        'div',
579                        [ 'class' => 'fullImageLink', 'id' => 'file' ],
580                        $icon->toHtml( [ 'file-link' => true ] )
581                    ) . "\n"
582                );
583            }
584
585            $handler = $this->displayImg->getHandler();
586
587            // If this is a filetype with potential issues, warn the user.
588            if ( $handler ) {
589                $warningConfig = $handler->getWarningConfig( $this->displayImg );
590
591                if ( $warningConfig !== null ) {
592                    // The warning will be displayed via CSS and JavaScript.
593                    // We just need to tell the client side what message to use.
594                    $output = $context->getOutput();
595                    $output->addJsConfigVars( 'wgFileWarning', $warningConfig );
596                    $output->addModules( $warningConfig['module'] );
597                    $output->addModules( 'mediawiki.filewarning' );
598                }
599            }
600
601            $medialink = $context->msg( new RawMessage( "[[Media:$filename|$linktext]]" ) )->parse();
602            if ( !$this->displayImg->isSafeFile() ) {
603                $medialink = Html::rawElement( 'span', [ 'class' => 'dangerousLink' ], $medialink );
604            }
605
606            // File::getLongDesc() is documented to return HTML, but many handlers used to incorrectly
607            // return plain text (T395834), so sanitize it in case the same bug is present in extensions.
608            $unsafeLongDesc = $this->displayImg->getLongDesc();
609            $longDesc = Sanitizer::removeSomeTags( $unsafeLongDesc );
610            $longDesc = $context->msg( 'parentheses' )->rawParams( $longDesc )->escaped();
611
612            $out->addHTML(
613                Html::rawElement( 'div', [ 'class' => 'fullMedia' ],
614                    // <bdi> is needed here to separate the file name, which
615                    // most likely ends in Latin characters, from the description,
616                    // which may begin with the file type. In RTL environment
617                    // this will get messy.
618                    Html::rawElement( 'bdi', [ 'dir' => $sitedir ], $medialink ) .
619                    ' ' .
620                    Html::rawElement( 'span', [ 'class' => 'fileInfo' ], $longDesc )
621                )
622            );
623
624            if ( !$this->displayImg->isSafeFile() ) {
625                $out->addHTML(
626                    Html::rawElement( 'div', [ 'class' => 'mediaWarning' ], $context->msg( 'mediawarning' )->parse() )
627                );
628            }
629
630            $renderLangOptions = $this->displayImg->getAvailableLanguages();
631            if ( count( $renderLangOptions ) >= 1 ) {
632                $out->addHTML( $this->doRenderLangOpt( $renderLangOptions, $renderLang ) );
633            }
634
635            // Add cannot animate thumbnail warning
636            if ( !$this->displayImg->canAnimateThumbIfAppropriate() ) {
637                // Include the extension so wiki admins can
638                // customize it on a per file-type basis
639                // (aka say things like use format X instead).
640                // additionally have a specific message for
641                // file-no-thumb-animation-gif
642                $ext = $this->displayImg->getExtension();
643                $out->wrapWikiMsg(
644                    Html::element( 'div', [ 'class' => 'mw-noanimatethumb' ], '$1' ),
645                    wfMessageFallback(
646                        'file-no-thumb-animation-' . $ext,
647                        'file-no-thumb-animation'
648                    )
649                );
650            }
651
652            if ( !$this->displayImg->isLocal() ) {
653                $this->printSharedImageText();
654            }
655        } else {
656            # Image does not exist
657            if ( !$this->getPage()->getId() ) {
658                $dbr = $this->dbProvider->getReplicaDatabase();
659
660                # No article exists either
661                # Show deletion log to be consistent with normal articles
662                LogEventsList::showLogExtract(
663                    $out,
664                    [ 'delete', 'move', 'protect', 'merge' ],
665                    $this->getTitle()->getPrefixedText(),
666                    '',
667                    [ 'lim' => 10,
668                        'conds' => [ $dbr->expr( 'log_action', '!=', 'revision' ) ],
669                        'showIfEmpty' => false,
670                        'msgKey' => [ 'moveddeleted-notice' ]
671                    ]
672                );
673            }
674
675            if ( $enableUploads &&
676                $context->getAuthority()->isAllowed( 'upload' )
677            ) {
678                // Only show an upload link if the user can upload
679                $uploadTitle = SpecialPage::getTitleFor( 'Upload' );
680                $nofile = [
681                    'filepage-nofile-link',
682                    $uploadTitle->getFullURL( [
683                        'wpDestFile' => $this->getFile()->getName()
684                    ] )
685                ];
686            } else {
687                $nofile = 'filepage-nofile';
688            }
689            // Note, if there is an image description page, but
690            // no image, then this setRobotPolicy is overridden
691            // by Article::View().
692            $out->setRobotPolicy( 'noindex,nofollow' );
693            $out->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile );
694            if ( !$this->getPage()->getId() && $send404Code ) {
695                // If there is no image, no shared image, and no description page,
696                // output a 404, to be consistent with Article::showMissingArticle.
697                $request->response()->statusHeader( 404 );
698            }
699        }
700        $out->setFileVersion( $this->displayImg );
701    }
702
703    /**
704     * Make the text under the image to say what size preview
705     *
706     * @param array $params parameters for thumbnail
707     * @param string $sizeLinkBigImagePreview HTML for the current size
708     * @return string HTML output
709     */
710    protected function getThumbPrevText( $params, $sizeLinkBigImagePreview ) {
711        if ( $sizeLinkBigImagePreview ) {
712            // Show a different message of preview is different format from original.
713            $previewTypeDiffers = false;
714            $origExt = $thumbExt = $this->displayImg->getExtension();
715            if ( $this->displayImg->getHandler() ) {
716                $origMime = $this->displayImg->getMimeType();
717                $typeParams = $params;
718                $this->displayImg->getHandler()->normaliseParams( $this->displayImg, $typeParams );
719                [ $thumbExt, $thumbMime ] = $this->displayImg->getHandler()->getThumbType(
720                    $origExt, $origMime, $typeParams );
721                if ( $thumbMime !== $origMime ) {
722                    $previewTypeDiffers = true;
723                }
724            }
725            if ( $previewTypeDiffers ) {
726                return $this->getContext()->msg( 'show-big-image-preview-differ' )->
727                    rawParams( $sizeLinkBigImagePreview )->
728                    params( strtoupper( $origExt ) )->
729                    params( strtoupper( $thumbExt ) )->
730                    parse();
731            } else {
732                return $this->getContext()->msg( 'show-big-image-preview' )->
733                    rawParams( $sizeLinkBigImagePreview )->
734                parse();
735            }
736        } else {
737            return '';
738        }
739    }
740
741    /**
742     * Creates a thumbnail of specified size and returns an HTML link to it
743     * @param array $params Scaler parameters
744     * @param int $width
745     * @param int $height
746     * @return string
747     */
748    protected function makeSizeLink( $params, $width, $height ) {
749        $params['width'] = $width;
750        $params['height'] = $height;
751        $thumbnail = $this->displayImg->transform( $params );
752        if ( $thumbnail && !$thumbnail->isError() ) {
753            return Html::rawElement( 'a', [
754                'href' => $thumbnail->getUrl(),
755                'class' => 'mw-thumbnail-link'
756                ], $this->getContext()->msg( 'show-big-image-size' )->numParams(
757                    $thumbnail->getWidth(), $thumbnail->getHeight()
758                )->parse() );
759        } else {
760            return '';
761        }
762    }
763
764    /**
765     * Show a notice that the file is from a shared repository
766     */
767    protected function printSharedImageText() {
768        $out = $this->getContext()->getOutput();
769        $this->loadFile();
770
771        $descUrl = $this->getFile()->getDescriptionUrl();
772        $descText = $this->getFile()->getDescriptionText( $this->getContext()->getLanguage() );
773
774        /* Add canonical to head if there is no local page for this shared file */
775        if ( $descUrl && !$this->getPage()->getId() ) {
776            $out->setCanonicalUrl( $descUrl );
777        }
778
779        $wrap = "<div class=\"sharedUploadNotice\">\n$1\n</div>\n";
780        $repo = $this->getFile()->getRepo()->getDisplayName();
781
782        if ( $descUrl &&
783            $descText &&
784            !$this->getContext()->msg( 'sharedupload-desc-here' )->isDisabled()
785        ) {
786            $out->wrapWikiMsg( $wrap, [ 'sharedupload-desc-here', $repo, $descUrl ] );
787        } elseif ( $descUrl &&
788            !$this->getContext()->msg( 'sharedupload-desc-there' )->isDisabled()
789        ) {
790            $out->wrapWikiMsg( $wrap, [ 'sharedupload-desc-there', $repo, $descUrl ] );
791        } else {
792            $out->wrapWikiMsg( $wrap, [ 'sharedupload', $repo ], ''/*BACKCOMPAT*/ );
793        }
794
795        if ( $descText ) {
796            $this->mExtraDescription = $descText;
797        }
798    }
799
800    public function getUploadUrl(): string {
801        $this->loadFile();
802        $uploadTitle = SpecialPage::getTitleFor( 'Upload' );
803        return $uploadTitle->getFullURL( [
804            'wpDestFile' => $this->getFile()->getName(),
805            'wpForReUpload' => 1
806        ] );
807    }
808
809    /**
810     * Add the re-upload link (or message about not being able to re-upload) to the output.
811     */
812    protected function uploadLinksBox() {
813        if ( !$this->getContext()->getConfig()->get( MainConfigNames::EnableUploads ) ) {
814            return;
815        }
816
817        $this->loadFile();
818        if ( !$this->getFile()->isLocal() ) {
819            return;
820        }
821
822        $canUpload = $this->getContext()->getAuthority()
823            ->probablyCan( 'upload', $this->getTitle() );
824        if ( $canUpload && UploadBase::userCanReUpload(
825                $this->getContext()->getAuthority(),
826                $this->getFile() )
827        ) {
828            // "Upload a new version of this file" link
829            $ulink = $this->linkRenderer->makeExternalLink(
830                $this->getUploadUrl(),
831                $this->getContext()->msg( 'uploadnewversion-linktext' ),
832                $this->getTitle()
833            );
834            $attrs = [ 'class' => 'plainlinks', 'id' => 'mw-imagepage-reupload-link' ];
835            $linkPara = Html::rawElement( 'p', $attrs, $ulink );
836        } else {
837            // "You cannot overwrite this file." message
838            $attrs = [ 'id' => 'mw-imagepage-upload-disallowed' ];
839            $msg = $this->getContext()->msg( 'upload-disallowed-here' )->text();
840            $linkPara = Html::element( 'p', $attrs, $msg );
841        }
842
843        $uploadLinks = Html::rawElement( 'div', [ 'class' => 'mw-imagepage-upload-links' ], $linkPara );
844        $this->getContext()->getOutput()->addHTML( $uploadLinks );
845    }
846
847    /**
848     * For overloading
849     */
850    protected function closeShowImage() {
851    }
852
853    /**
854     * If the page we've just displayed is in the "Image" namespace,
855     * we follow it with an upload history of the image and its usage.
856     */
857    protected function imageHistory() {
858        $this->loadFile();
859        $out = $this->getContext()->getOutput();
860        $pager = new ImageHistoryPseudoPager(
861            $this,
862            MediaWikiServices::getInstance()->getLinkBatchFactory()
863        );
864        $out->addHTML( $pager->getBody() );
865        $out->getMetadata()->setPreventClickjacking( $pager->getPreventClickjacking() );
866
867        $this->getFile()->resetHistory(); // free db resources
868
869        # Exist check because we don't want to show this on pages where an image
870        # doesn't exist along with the noimage message, that would suck. -ævar
871        if ( $this->getFile()->exists() ) {
872            $this->uploadLinksBox();
873        }
874    }
875
876    /**
877     * @param string|string[] $target
878     * @param int $limit
879     * @return IResultWrapper
880     */
881    protected function queryImageLinks( $target, $limit ) {
882        $dbr = $this->dbProvider->getReplicaDatabase( ImageLinksTable::VIRTUAL_DOMAIN );
883
884        $migrationStage = MediaWikiServices::getInstance()->getMainConfig()->get(
885            MainConfigNames::ImageLinksSchemaMigrationStage
886        );
887
888        $qb = $dbr->newSelectQueryBuilder()
889            ->select( [ 'page_namespace', 'page_title' ] )
890            ->from( 'imagelinks' )
891            ->join( 'page', null, 'il_from = page_id' )
892            ->orderBy( 'il_from' )
893            ->limit( $limit + 1 )
894            ->caller( __METHOD__ );
895
896        if ( $migrationStage & SCHEMA_COMPAT_READ_OLD ) {
897            $qb->select( 'il_to' );
898            $qb->where( [ 'il_to' => $target ] );
899        } else {
900            $qb->select( [ 'il_to' => 'lt_title' ] );
901            $qb->join( 'linktarget', null, 'il_target_id = lt_id' );
902            $qb->where( [ 'lt_title' => $target, 'lt_namespace' => NS_FILE ] );
903        }
904
905        return $qb->fetchResultSet();
906    }
907
908    protected function imageLinks() {
909        $limit = 100;
910
911        $out = $this->getContext()->getOutput();
912
913        $rows = [];
914        $redirects = [];
915        foreach ( $this->getTitle()->getRedirectsHere( NS_FILE ) as $redir ) {
916            $redirects[$redir->getDBkey()] = [];
917            $rows[] = (object)[
918                'page_namespace' => NS_FILE,
919                'page_title' => $redir->getDBkey(),
920            ];
921        }
922
923        $res = $this->queryImageLinks( $this->getTitle()->getDBkey(), $limit + 1 );
924        foreach ( $res as $row ) {
925            $rows[] = $row;
926        }
927        $count = count( $rows );
928
929        $hasMore = $count > $limit;
930        if ( !$hasMore && count( $redirects ) ) {
931            $res = $this->queryImageLinks( array_keys( $redirects ),
932                $limit - count( $rows ) + 1 );
933            foreach ( $res as $row ) {
934                $redirects[$row->il_to][] = $row;
935                $count++;
936            }
937            $hasMore = ( $res->numRows() + count( $rows ) ) > $limit;
938        }
939
940        if ( $count == 0 ) {
941            $out->wrapWikiMsg(
942                Html::rawElement( 'div',
943                    [ 'id' => 'mw-imagepage-nolinkstoimage' ], "\n$1\n" ),
944                'nolinkstoimage'
945            );
946            return;
947        }
948
949        $out->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" );
950        if ( !$hasMore ) {
951            $out->addWikiMsg( 'linkstoimage', $count );
952        } else {
953            // More links than the limit. Add a link to [[Special:Whatlinkshere]]
954            $out->addWikiMsg( 'linkstoimage-more',
955                $this->getContext()->getLanguage()->formatNum( $limit ),
956                $this->getTitle()->getPrefixedDBkey()
957            );
958        }
959
960        $out->addHTML(
961            Html::openElement( 'ul',
962                [ 'class' => 'mw-imagepage-linkstoimage' ] ) . "\n"
963        );
964        // Sort the list by namespace:title
965        usort( $rows, $this->compare( ... ) );
966
967        // Create links for every element
968        $currentCount = 0;
969        foreach ( $rows as $element ) {
970            $currentCount++;
971            if ( $currentCount > $limit ) {
972                break;
973            }
974
975            $link = $this->linkRenderer->makeKnownLink(
976                Title::makeTitle( $element->page_namespace, $element->page_title ),
977                null,
978                [],
979                // Add a redirect=no to make redirect pages reachable
980                [ 'redirect' => isset( $redirects[$element->page_title] ) ? 'no' : null ]
981            );
982            if ( !isset( $redirects[$element->page_title] ) ) {
983                # No redirects
984                $liContents = $link;
985            } elseif ( count( $redirects[$element->page_title] ) === 0 ) {
986                # Redirect without usages
987                $liContents = $this->getContext()->msg( 'linkstoimage-redirect' )
988                    ->rawParams( $link, '' )
989                    ->parse();
990            } else {
991                # Redirect with usages
992                $li = '';
993                foreach ( $redirects[$element->page_title] as $row ) {
994                    $currentCount++;
995                    if ( $currentCount > $limit ) {
996                        break;
997                    }
998
999                    $link2 = $this->linkRenderer->makeKnownLink(
1000                        Title::makeTitle( $row->page_namespace, $row->page_title ) );
1001                    $li .= Html::rawElement(
1002                        'li',
1003                        [ 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ],
1004                        $link2
1005                        ) . "\n";
1006                }
1007
1008                $ul = Html::rawElement(
1009                    'ul',
1010                    [ 'class' => 'mw-imagepage-redirectstofile' ],
1011                    $li
1012                    ) . "\n";
1013                $liContents = $this->getContext()->msg( 'linkstoimage-redirect' )->rawParams(
1014                    $link, $ul )->parse();
1015            }
1016            $out->addHTML( Html::rawElement(
1017                    'li',
1018                    [ 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ],
1019                    $liContents
1020                ) . "\n"
1021            );
1022
1023        }
1024        $out->addHTML( Html::closeElement( 'ul' ) . "\n" );
1025        $res->free();
1026
1027        // Add a links to [[Special:Whatlinkshere]]
1028        if ( $currentCount > $limit ) {
1029            $out->addWikiMsg( 'morelinkstoimage', $this->getTitle()->getPrefixedDBkey() );
1030        }
1031        $out->addHTML( Html::closeElement( 'div' ) . "\n" );
1032    }
1033
1034    protected function imageDupes() {
1035        $this->loadFile();
1036        $out = $this->getContext()->getOutput();
1037
1038        $dupes = $this->getPage()->getDuplicates();
1039        if ( count( $dupes ) == 0 ) {
1040            return;
1041        }
1042
1043        $out->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" );
1044        $out->addWikiMsg( 'duplicatesoffile',
1045            $this->getContext()->getLanguage()->formatNum( count( $dupes ) ), $this->getTitle()->getDBkey()
1046        );
1047        $out->addHTML( "<ul class='mw-imagepage-duplicates'>\n" );
1048
1049        /**
1050         * @var File $file
1051         */
1052        foreach ( $dupes as $file ) {
1053            $fromSrc = '';
1054            if ( $file->isLocal() ) {
1055                $link = $this->linkRenderer->makeKnownLink( $file->getTitle() );
1056            } else {
1057                $link = $this->linkRenderer->makeExternalLink(
1058                    $file->getDescriptionUrl(),
1059                    $file->getTitle()->getPrefixedText(),
1060                    $this->getTitle()
1061                );
1062                $fromSrc = $this->getContext()->msg(
1063                    'shared-repo-from',
1064                    $file->getRepo()->getDisplayName()
1065                )->escaped();
1066            }
1067            $out->addHTML( "<li>{$link} {$fromSrc}</li>\n" );
1068        }
1069        $out->addHTML( "</ul></div>\n" );
1070    }
1071
1072    /**
1073     * Display an error with a wikitext description
1074     *
1075     * @param string $description
1076     */
1077    public function showError( $description ) {
1078        $out = $this->getContext()->getOutput();
1079        $out->setPageTitleMsg( $this->getContext()->msg( 'internalerror' ) );
1080        $out->setRobotPolicy( 'noindex,nofollow' );
1081        $out->setArticleRelated( false );
1082        $out->disableClientCache();
1083        $out->addWikiTextAsInterface( $description );
1084    }
1085
1086    /**
1087     * Callback for usort() to do link sorts by (namespace, title)
1088     * Function copied from Title::compare()
1089     *
1090     * @param stdClass $a Object page to compare with
1091     * @param stdClass $b Object page to compare with
1092     * @return int Result of string comparison, or namespace comparison
1093     */
1094    protected function compare( $a, $b ) {
1095        return $a->page_namespace <=> $b->page_namespace
1096            ?: strcmp( $a->page_title, $b->page_title );
1097    }
1098
1099    /**
1100     * Output a drop-down box for language options for the file
1101     *
1102     * @param array $langChoices Array of string language codes
1103     * @param string|null $renderLang Language code for the language we want the file to rendered in,
1104     *  it is pre-selected in the drop down box, use null to select the default case in the option list
1105     * @return string HTML to insert underneath image.
1106     */
1107    protected function doRenderLangOpt( array $langChoices, $renderLang ) {
1108        $context = $this->getContext();
1109        $script = $context->getConfig()->get( MainConfigNames::Script );
1110        $opts = '';
1111
1112        $matchedRenderLang = $renderLang === null ? null : $this->displayImg->getMatchedLanguage( $renderLang );
1113
1114        foreach ( $langChoices as $lang ) {
1115            $opts .= $this->createXmlOptionStringForLanguage(
1116                $lang,
1117                $matchedRenderLang === $lang
1118            );
1119        }
1120
1121        // Allow for the default case in an svg <switch> that is displayed if no
1122        // systemLanguage attribute matches
1123        $opts .= "\n" .
1124            Html::element(
1125                'option',
1126                [ 'value' => 'und', 'selected' => $matchedRenderLang === null || $matchedRenderLang === 'und' ],
1127                $context->msg( 'img-lang-default' )->text()
1128            );
1129
1130        $select = Html::rawElement(
1131            'select',
1132            [ 'id' => 'mw-imglangselector', 'name' => 'lang' ],
1133            $opts
1134        );
1135        $submit = Html::submitButton( $context->msg( 'img-lang-go' )->text(), [] );
1136
1137        $formContents = $context->msg( 'img-lang-info' )
1138            ->rawParams( $select, $submit )
1139            ->parse();
1140        $formContents .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() );
1141
1142        $langSelectLine = Html::rawElement( 'div', [ 'id' => 'mw-imglangselector-line' ],
1143            Html::rawElement( 'form', [ 'action' => $script ], $formContents )
1144        );
1145        return $langSelectLine;
1146    }
1147
1148    /**
1149     * @param string $lang
1150     * @param bool $selected
1151     * @return string
1152     */
1153    private function createXmlOptionStringForLanguage( $lang, $selected ) {
1154        // TODO: There is no good way to get the language name of a BCP code,
1155        // as MW language codes take precedence
1156        $name = MediaWikiServices::getInstance()
1157            ->getLanguageNameUtils()
1158            ->getLanguageName( $lang, $this->getContext()->getLanguage()->getCode() );
1159        if ( $name !== '' ) {
1160            $display = $this->getContext()->msg( 'img-lang-opt', $lang, $name )->text();
1161        } else {
1162            $display = $lang;
1163        }
1164        return "\n" . Html::element( 'option', [ 'value' => $lang, 'selected' => $selected ], $display );
1165    }
1166
1167    /**
1168     * Get alternative thumbnail sizes.
1169     *
1170     * @note This will only list several alternatives if thumbnails are rendered on 404
1171     * @param int $origWidth Actual width of image
1172     * @param int $origHeight Actual height of image
1173     * @return int[][] An array of [width, height] pairs.
1174     * @phan-return array<int,array{0:int,1:int}>
1175     */
1176    protected function getThumbSizes( $origWidth, $origHeight ) {
1177        $context = $this->getContext();
1178        $imageLimits = $context->getConfig()->get( MainConfigNames::ImageLimits );
1179        if ( $this->displayImg->getRepo()->canTransformVia404() ) {
1180            $thumbSizes = $imageLimits;
1181            // Also include the full sized resolution in the list, so
1182            // that users know they can get it. This will link to the
1183            // original file asset if mustRender() === false. In the case
1184            // that we mustRender, some users have indicated that they would
1185            // find it useful to have the full size image in the rendered
1186            // image format.
1187            $thumbSizes[] = [ $origWidth, $origHeight ];
1188        } else {
1189            # Creating thumb links triggers thumbnail generation.
1190            # Just generate the thumb for the current users prefs.
1191            $thumbSizes = [
1192                $this->getImageLimitsFromOption( $context->getUser(), 'thumbsize' )
1193            ];
1194            if ( !$this->displayImg->mustRender() ) {
1195                // We can safely include a link to the "full-size" preview,
1196                // without actually rendering.
1197                $thumbSizes[] = [ $origWidth, $origHeight ];
1198            }
1199        }
1200        return $thumbSizes;
1201    }
1202
1203    /**
1204     * @see WikiFilePage::getFile
1205     * @return File
1206     */
1207    public function getFile(): File {
1208        return $this->getPage()->getFile();
1209    }
1210
1211    /**
1212     * @see WikiFilePage::isLocal
1213     * @return bool
1214     */
1215    public function isLocal() {
1216        return $this->getPage()->isLocal();
1217    }
1218
1219    /**
1220     * @see WikiFilePage::getDuplicates
1221     * @return File[]|null
1222     */
1223    public function getDuplicates() {
1224        return $this->getPage()->getDuplicates();
1225    }
1226
1227    /**
1228     * @see WikiFilePage::getForeignCategories
1229     * @return TitleArrayFromResult
1230     */
1231    public function getForeignCategories() {
1232        return $this->getPage()->getForeignCategories();
1233    }
1234
1235}
1236
1237/** @deprecated class alias since 1.44 */
1238class_alias( ImagePage::class, 'ImagePage' );