Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
13.86% covered (danger)
13.86%
28 / 202
1.69% covered (danger)
1.69%
1 / 59
CRAP
0.00% covered (danger)
0.00%
0 / 1
MediaHandler
13.93% covered (danger)
13.93%
28 / 201
1.69% covered (danger)
1.69%
1 / 59
6221.54
0.00% covered (danger)
0.00%
0 / 1
 getHandler
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getLanguage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 setLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getParamMap
n/a
0 / 0
n/a
0 / 0
0
 validateParam
n/a
0 / 0
n/a
0 / 0
0
 makeParamString
n/a
0 / 0
n/a
0 / 0
0
 parseParamString
n/a
0 / 0
n/a
0 / 0
0
 normaliseParams
n/a
0 / 0
n/a
0 / 0
0
 getImageSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSizeAndMetadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMetadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 useLegacyMetadata
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 hasMostDerivedMethod
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getSizeAndMetadataWithFallback
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
182
 getMetadataVersion
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 convertMetadataVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMetadataType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isMetadataValid
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isFileMetadataValid
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCommonMetaArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getScriptedTransform
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTransform
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doTransform
n/a
0 / 0
n/a
0 / 0
0
 getThumbType
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 canRender
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mustRender
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isMultiPage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pageCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isVectorized
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isAnimatedImage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canAnimateThumbnail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPageDimensions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPageText
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEntireText
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 formatMetadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatMetadataHelper
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
7.03
 formatTag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 visibleMetadataFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addMeta
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getShortDesc
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLongDesc
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGeneralShortDesc
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGeneralLongDesc
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 fitBoxWidth
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getDimensionsString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parserTransformHook
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 verifyUpload
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 removeBadFile
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 filterThumbnailPurgeList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canRotate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRotation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMirrored
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logErrorForExternalProcess
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getAvailableLanguages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMatchedLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultRenderLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLength
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isExpensiveToThumbnail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 supportsBucketing
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sanitizeParamsForBucketing
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWarningConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPageRangesByDimensions
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 getContentHeaders
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 useSplitMetadata
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\Media;
8
9use InvalidArgumentException;
10use MediaWiki\Context\IContextSource;
11use MediaWiki\Context\RequestContext;
12use MediaWiki\FileRepo\File\File;
13use MediaWiki\FileRepo\File\LocalFile;
14use MediaWiki\HookContainer\HookRunner;
15use MediaWiki\Language\Language;
16use MediaWiki\MediaWikiServices;
17use MediaWiki\Parser\Parser;
18use MediaWiki\Status\Status;
19use ReflectionClass;
20use ReflectionMethod;
21use Wikimedia\FileBackend\FSFile\FSFile;
22
23/**
24 * @defgroup Media Media
25 *
26 * Media handlers and other classes relating to Multimedia support,
27 * with the exception of FileRepo and FileBackend, which have their own groups.
28 */
29
30/**
31 * Base media handler class
32 *
33 * @stable to extend
34 * @ingroup Media
35 */
36abstract class MediaHandler {
37    public const TRANSFORM_LATER = 1;
38    public const METADATA_GOOD = true;
39    public const METADATA_BAD = false;
40    public const METADATA_COMPATIBLE = 2; // for old but backwards compatible.
41    /**
42     * Max length of error logged by logErrorForExternalProcess()
43     */
44    private const MAX_ERR_LOG_SIZE = 65535;
45
46    private ?Language $lang = null;
47
48    /**
49     * Get a MediaHandler for a given MIME type from the instance cache
50     *
51     * @param string $type
52     * @param Language|null $lang
53     * @return MediaHandler|false
54     */
55    public static function getHandler( $type, ?Language $lang = null ) {
56        return MediaWikiServices::getInstance()
57            ->getMediaHandlerFactory()->getHandler( $type, $lang );
58    }
59
60    /**
61     * Get the language to be used by this media handler for text formatting.
62     */
63    public function getLanguage(): Language {
64        if ( !$this->lang ) {
65            wfDeprecatedMsg(
66                "Accessing the language without explicitly setting it via " .
67                "MediaHandler:setLanguage, MediaHandler::getHandler, or " .
68                "MediaHandlerFactory::getHandler was deprecated in 1.46.",
69                '1.46'
70            );
71            $this->lang = RequestContext::getMain()->getLanguage();
72            // throw new \RuntimeException( "Need to set language before accessing." );
73        }
74        return $this->lang;
75    }
76
77     /**
78      * Set the language to be used by this media handler for text formatting.
79      * Must be executed before getLanguage is called, getHandler automatically takes
80      * care of that if a language is passed.
81      */
82    public function setLanguage( Language $lang ): void {
83        $this->lang = $lang;
84    }
85
86    /**
87     * Get an associative array mapping magic word IDs to parameter names.
88     * Will be used by the parser to identify parameters.
89     * @return array<string,string>
90     */
91    abstract public function getParamMap();
92
93    /**
94     * Validate a thumbnail parameter at parse time.
95     * Return true to accept the parameter, and false to reject it.
96     * If you return false, the parser will do something quiet and forgiving.
97     *
98     * @param string $name
99     * @param mixed $value
100     * @return bool
101     */
102    abstract public function validateParam( $name, $value );
103
104    /**
105     * Merge a parameter array into a string appropriate for inclusion in filenames
106     *
107     * @param array $params Array of parameters that have been through normaliseParams.
108     * @return string|false
109     */
110    abstract public function makeParamString( $params );
111
112    /**
113     * Parse a param string made with makeParamString back into an array
114     *
115     * @param string $str The parameter string without file name (e.g. 122px)
116     * @return array|false Array of parameters or false on failure.
117     */
118    abstract public function parseParamString( $str );
119
120    /**
121     * Changes the parameter array as necessary, ready for transformation.
122     * Should be idempotent.
123     * Returns false if the parameters are unacceptable and the transform should fail
124     * @param File $image
125     * @param array &$params
126     * @return bool
127     */
128    abstract public function normaliseParams( $image, &$params );
129
130    /**
131     * Get an image size array like that returned by getimagesize(), or false if it
132     * can't be determined.
133     *
134     * This function is used for determining the width, height and bitdepth directly
135     * from an image. The results are stored in the database in the img_width,
136     * img_height, img_bits fields.
137     *
138     * @note If this is a multipage file, return the width and height of the
139     *  first page.
140     *
141     * @deprecated since 1.37, override getSizeAndMetadata instead
142     *
143     * @param File|FSFile|false $image The image object, or false if there isn't one.
144     *   Warning, FSFile::getPropsFromPath might pass an FSFile instead of File (!)
145     * @param string $path The filename
146     * @return array|false Follow the format of PHP getimagesize() internal function.
147     *   See https://www.php.net/getimagesize. MediaWiki will only ever use the
148     *   first two array keys (the width and height), and the 'bits' associative
149     *   key. All other array keys are ignored. Returning a 'bits' key is optional
150     *   as not all formats have a notion of "bitdepth". Returns false on failure.
151     */
152    public function getImageSize( $image, $path ) {
153        return false;
154    }
155
156    /**
157     * Get image size information and metadata array.
158     *
159     * If this returns null, the caller will fall back to getImageSize() and
160     * getMetadata().
161     *
162     * If getImageSize() or getMetadata() are implemented in the most derived
163     * class, they will be used instead of this function. To override this
164     * behaviour, override useLegacyMetadata().
165     *
166     * @stable to override
167     * @since 1.37
168     *
169     * @param MediaHandlerState $state An object for saving process-local state.
170     *   This is normally a File object which will be passed back to other
171     *   MediaHandler methods like pageCount(), if they are called in the same
172     *   request. The handler can use this object to save its state.
173     * @param string $path The filename
174     * @return array|null Null to fall back to getImageSize(), or an array with
175     *   the following keys. All keys are optional.
176     *   - width: The width. If multipage, return the first page width. (optional)
177     *   - height: The height. If multipage, return the first page height. (optional)
178     *   - bits: The number of bits for each color (optional)
179     *   - metadata: A JSON-serializable array of metadata (optional)
180     */
181    public function getSizeAndMetadata( $state, $path ) {
182        return null;
183    }
184
185    /**
186     * Get handler-specific metadata which will be saved in the img_metadata field.
187     * @deprecated since 1.37 override getSizeAndMetadata() instead
188     *
189     * @param File|FSFile|false $image The image object, or false if there isn't one.
190     *   Warning, FSFile::getPropsFromPath might pass an FSFile instead of File (!)
191     * @param string $path The filename
192     * @return string A string of metadata in php serialized form (Run through serialize())
193     */
194    public function getMetadata( $image, $path ) {
195        return '';
196    }
197
198    /**
199     * If this returns true, the new method getSizeAndMetadata() will not be
200     * called. The legacy methods getMetadata() and getImageSize() will be used
201     * instead.
202     *
203     * @since 1.37
204     * @stable to override
205     * @return bool
206     */
207    protected function useLegacyMetadata() {
208        return $this->hasMostDerivedMethod( 'getMetadata' )
209            || $this->hasMostDerivedMethod( 'getImageSize' );
210    }
211
212    /**
213     * Check whether a method is implemented in the most derived class.
214     *
215     * @since 1.37
216     * @param string $name
217     * @return bool
218     */
219    protected function hasMostDerivedMethod( $name ) {
220        $rc = new ReflectionClass( $this );
221        $rm = new ReflectionMethod( $this, $name );
222        return $rm->getDeclaringClass()->getName() === $rc->getName();
223    }
224
225    /**
226     * Get the metadata array and the image size, with b/c fallback.
227     *
228     * The legacy methods will be used if useLegacyMetadata() returns true or
229     * if getSizeAndMetadata() returns null.
230     *
231     * Absent metadata will be normalized to an empty array. Absent width and
232     * height will be normalized to zero.
233     *
234     * @param File|FSFile $file This must be a File or FSFile to support the
235     *   legacy methods. When the legacy methods are removed, this will be
236     *   narrowed to MediaHandlerState.
237     * @param string $path
238     * @return array|false|null False on failure, or an array with the following keys:
239     *   - width: The width. If multipage, return the first page width.
240     *   - height: The height. If multipage, return the first page height.
241     *   - bits: The number of bits for each color (optional)
242     *   - metadata: A JSON-serializable array of metadata
243     * @since 1.37
244     */
245    final public function getSizeAndMetadataWithFallback( $file, $path ) {
246        if ( !$this->useLegacyMetadata() ) {
247            if ( $file instanceof MediaHandlerState ) {
248                $state = $file;
249            } else {
250                $state = new TrivialMediaHandlerState;
251            }
252            $info = $this->getSizeAndMetadata( $state, $path );
253            if ( $info === false ) {
254                return false;
255            }
256            if ( $info !== null ) {
257                $info += [ 'width' => 0, 'height' => 0, 'metadata' => [] ];
258                if ( !is_array( $info['metadata'] ) ) {
259                    throw new InvalidArgumentException( 'Media handler ' .
260                        static::class . ' returned ' . get_debug_type( $info['metadata'] ) .
261                        ' for metadata, should be array' );
262                }
263                return $info;
264            }
265        }
266
267        $blob = $this->getMetadata( $file, $path );
268        // @phan-suppress-next-line PhanParamTooMany
269        $size = $this->getImageSize(
270            $file,
271            $path,
272            $blob // Secret TimedMediaHandler parameter
273        );
274        if ( $blob === false && $size === false ) {
275            return false;
276        }
277        if ( $size ) {
278            $info = [
279                'width' => $size[0] ?? 0,
280                'height' => $size[1] ?? 0
281            ];
282            if ( isset( $size['bits'] ) ) {
283                $info['bits'] = $size['bits'];
284            }
285        } else {
286            $info = [ 'width' => 0, 'height' => 0 ];
287        }
288        if ( $blob !== false ) {
289            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
290            $metadata = @unserialize( $blob );
291            if ( $metadata === false ) {
292                // Unserialize error
293                $metadata = [ '_error' => $blob ];
294            } elseif ( !is_array( $metadata ) ) {
295                $metadata = [];
296            }
297            $info['metadata'] = $metadata;
298        } else {
299            $info['metadata'] = [];
300        }
301        return $info;
302    }
303
304    /**
305     * Get metadata version.
306     *
307     * This is not used for validating metadata, this is used for the api when returning
308     * metadata, since api content formats should stay the same over time, and so things
309     * using ForeignApiRepo can keep backwards compatibility
310     *
311     * All core media handlers share a common version number, and extensions can
312     * use the GetMetadataVersion hook to append to the array (they should append a unique
313     * string so not to get confusing). If there was a media handler named 'foo' with metadata
314     * version 3 it might add to the end of the array the element 'foo=3'. if the core metadata
315     * version is 2, the end version string would look like '2;foo=3'.
316     *
317     * @stable to override
318     *
319     * @return string Version string
320     */
321    public static function getMetadataVersion() {
322        $version = [ '2' ]; // core metadata version
323        ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onGetMetadataVersion( $version );
324
325        return implode( ';', $version );
326    }
327
328    /**
329     * Convert metadata version.
330     *
331     * By default just returns $metadata, but can be used to allow
332     * media handlers to convert between metadata versions.
333     * @stable to override
334     *
335     * @param array $metadata Metadata array
336     * @param int|string $version Target version
337     * @return array Serialized metadata in specified version, or $metadata on fail.
338     */
339    public function convertMetadataVersion( $metadata, $version = 1 ) {
340        return $metadata;
341    }
342
343    /**
344     * Get a string describing the type of metadata, for display purposes.
345     * @stable to override
346     *
347     * @note This method is currently unused.
348     * @param File $image
349     * @return string|false
350     */
351    public function getMetadataType( $image ) {
352        return false;
353    }
354
355    /**
356     * Check if the metadata string is valid for this handler.
357     * If it returns MediaHandler::METADATA_BAD (or false), Image
358     * will reload the metadata from the file and update the database.
359     * MediaHandler::METADATA_GOOD for if the metadata is a-ok,
360     * MediaHandler::METADATA_COMPATIBLE if metadata is old but backwards
361     * compatible (which may or may not trigger a metadata reload).
362     *
363     * @note Returning self::METADATA_BAD will trigger a metadata reload from
364     *  file on page view. Always returning this from a broken file, or suddenly
365     *  triggering as bad metadata for a large number of files can cause
366     *  performance problems.
367     *
368     * @deprecated since 1.37 use isFileMetadataValid
369     * @param File $image
370     * @param string $metadata The metadata in serialized form
371     * @return bool|int
372     */
373    public function isMetadataValid( $image, $metadata ) {
374        return self::METADATA_GOOD;
375    }
376
377    /**
378     * Check if the metadata is valid for this handler.
379     * If it returns MediaHandler::METADATA_BAD (or false), Image
380     * will reload the metadata from the file and update the database.
381     * MediaHandler::METADATA_GOOD for if the metadata is a-ok,
382     * MediaHandler::METADATA_COMPATIBLE if metadata is old but backwards
383     * compatible (which may or may not trigger a metadata reload).
384     *
385     * @note Returning self::METADATA_BAD will trigger a metadata reload from
386     *  file on page view. Always returning this from a broken file, or suddenly
387     *  triggering as bad metadata for a large number of files can cause
388     *  performance problems.
389     *
390     * This was introduced in 1.37 to replace isMetadataValid(), which took a
391     * serialized string as a parameter. Handlers overriding this method are
392     * expected to use accessors to get the metadata out of the File. The
393     * reasons for the change were to get rid of serialization, and to allow
394     * handlers to partially load metadata with getMetadataItem(). For example
395     * a handler could just validate a version number.
396     *
397     * @stable to override
398     * @since 1.37
399     * @param File $image
400     * @return bool|int
401     */
402    public function isFileMetadataValid( $image ) {
403        return self::METADATA_GOOD;
404    }
405
406    /**
407     * Get an array of standard (FormatMetadata type) metadata values.
408     *
409     * The returned data is largely the same as that from getMetadata(),
410     * but formatted in a standard, stable, handler-independent way.
411     * The idea being that some values like ImageDescription or Artist
412     * are universal and should be retrievable in a handler generic way.
413     *
414     * The specific properties are the type of properties that can be
415     * handled by the FormatMetadata class. These values are exposed to the
416     * user via the filemetadata parser function.
417     *
418     * Details of the response format of this function can be found at
419     * https://www.mediawiki.org/wiki/Manual:File_metadata_handling
420     * tl/dr: the response is an associative array of
421     * properties keyed by name, but the value can be complex. You probably
422     * want to call one of the FormatMetadata::flatten* functions on the
423     * property values before using them, or call
424     * FormatMetadata::getFormattedData() on the full response array, which
425     * transforms all values into prettified, human-readable text.
426     *
427     * Subclasses overriding this function must return a value which is a
428     * valid API response fragment (all associative array keys are valid
429     * XML tagnames).
430     *
431     * Note, if the file simply has no metadata, but the handler supports
432     * this interface, it should return an empty array, not false.
433     *
434     * @stable to override
435     *
436     * @param File $file
437     * @return array|false False if interface not supported
438     * @since 1.23
439     */
440    public function getCommonMetaArray( File $file ) {
441        return false;
442    }
443
444    /**
445     * Get a MediaTransformOutput object representing an alternate of the transformed
446     * output which will call an intermediary thumbnail assist script.
447     *
448     * Used when the repository has a thumbnailScriptUrl option configured.
449     *
450     * Return false to fall back to the regular getTransform().
451     *
452     * @stable to override
453     *
454     * @param File $image
455     * @param string $script
456     * @param array $params
457     * @return ThumbnailImage|false
458     */
459    public function getScriptedTransform( $image, $script, $params ) {
460        return false;
461    }
462
463    /**
464     * Get a MediaTransformOutput object representing the transformed output. Does not
465     * actually do the transform.
466     *
467     * @stable to override
468     *
469     * @param File $image
470     * @param string $dstPath Filesystem destination path
471     * @param string $dstUrl Destination URL to use in output HTML
472     * @param array $params Arbitrary set of parameters validated by $this->validateParam()
473     * @return MediaTransformOutput
474     */
475    final public function getTransform( $image, $dstPath, $dstUrl, $params ) {
476        return $this->doTransform( $image, $dstPath, $dstUrl, $params, self::TRANSFORM_LATER );
477    }
478
479    /**
480     * Get a MediaTransformOutput object representing the transformed output. Does the
481     * transform unless $flags contains self::TRANSFORM_LATER.
482     *
483     * @stable to override
484     *
485     * @param File $image
486     * @param string $dstPath Filesystem destination path
487     * @param string $dstUrl Destination URL to use in output HTML
488     * @param array $params Arbitrary set of parameters validated by $this->validateParam()
489     *   Note: These parameters have *not* gone through $this->normaliseParams()
490     * @param int $flags A bitfield, may contain self::TRANSFORM_LATER
491     * @return MediaTransformOutput
492     */
493    abstract public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 );
494
495    /**
496     * Get the thumbnail extension and MIME type for a given source MIME type
497     *
498     * @stable to override
499     *
500     * @param string $ext Extension of original file
501     * @param string $mime MIME type of original file
502     * @param array|null $params Handler specific rendering parameters
503     * @return array Thumbnail extension and MIME type
504     */
505    public function getThumbType( $ext, $mime, $params = null ) {
506        $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
507        if ( !$ext || $magic->isMatchingExtension( $ext, $mime ) === false ) {
508            // The extension is not valid for this MIME type and we do
509            // recognize the MIME type
510            $knownExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
511            if ( $knownExt !== null ) {
512                return [ $knownExt, $mime ];
513            }
514        }
515
516        // The extension is correct (true) or the MIME type is unknown to
517        // MediaWiki (null)
518        return [ $ext, $mime ];
519    }
520
521    /**
522     * True if the handled types can be transformed
523     *
524     * @stable to override
525     *
526     * @param File $file
527     * @return bool
528     */
529    public function canRender( $file ) {
530        return true;
531    }
532
533    /**
534     * True if handled types cannot be displayed directly in a browser
535     * but can be rendered
536     *
537     * @stable to override
538     *
539     * @param File $file
540     * @return bool
541     */
542    public function mustRender( $file ) {
543        return false;
544    }
545
546    /**
547     * True if the type has multi-page capabilities
548     *
549     * @stable to override
550     *
551     * @param File $file
552     * @return bool
553     */
554    public function isMultiPage( $file ) {
555        return false;
556    }
557
558    /**
559     * Page count for a multi-page document, false if unsupported or unknown
560     *
561     * @stable to override
562     *
563     * @param File $file
564     * @return int|false
565     */
566    public function pageCount( File $file ) {
567        return false;
568    }
569
570    /**
571     * The material is vectorized and thus scaling is lossless
572     *
573     * @stable to override
574     *
575     * @param File $file
576     * @return bool
577     */
578    public function isVectorized( $file ) {
579        return false;
580    }
581
582    /**
583     * The material is an image, and is animated.
584     * In particular, video material need not return true.
585     * @note Before 1.20, this was a method of ImageHandler only
586     *
587     * @stable to override
588     *
589     * @param File $file
590     * @return bool
591     */
592    public function isAnimatedImage( $file ) {
593        return false;
594    }
595
596    /**
597     * If the material is animated, we can animate the thumbnail
598     * @since 1.20
599     *
600     * @stable to override
601     *
602     * @param File $file
603     * @return bool If material is not animated, handler may return any value.
604     */
605    public function canAnimateThumbnail( $file ) {
606        return true;
607    }
608
609    /**
610     * False if the handler is disabled for all files
611     * @stable to override
612     *
613     * @return bool
614     */
615    public function isEnabled() {
616        return true;
617    }
618
619    /**
620     * Get an associative array of page dimensions
621     * Currently "width" and "height" are understood, but this might be
622     * expanded in the future.
623     * Returns false if unknown.
624     *
625     * For a single page document format (!isMultipage()), this should return
626     * false.
627     *
628     * @note For non-paged media, use getImageSize.
629     *
630     * @stable to override
631     *
632     * @param File $image
633     * @param int $page What page to get dimensions of
634     * @return array{width: int, height: int}|false
635     */
636    public function getPageDimensions( File $image, $page ) {
637        return false;
638    }
639
640    /**
641     * Generic getter for text layer.
642     * Currently overloaded by PDF and DjVu handlers
643     * @stable to override
644     *
645     * @param File $image
646     * @param int $page Page number to get information for
647     * @return string|false Page text or false when no text found or if
648     *   unsupported.
649     */
650    public function getPageText( File $image, $page ) {
651        return false;
652    }
653
654    /**
655     * Get the text of the entire document.
656     * @param File $file
657     * @return string|false The text of the document or false if unsupported.
658     */
659    public function getEntireText( File $file ) {
660        $numPages = $file->pageCount();
661        if ( !$numPages ) {
662            // Not a multipage document
663            return $this->getPageText( $file, 1 );
664        }
665        $document = '';
666        for ( $i = 1; $i <= $numPages; $i++ ) {
667            $curPage = $this->getPageText( $file, $i );
668            if ( is_string( $curPage ) ) {
669                $document .= $curPage . "\n";
670            }
671        }
672        if ( $document !== '' ) {
673            return $document;
674        }
675        return false;
676    }
677
678    /**
679     * Get an array structure that looks like this:
680     *
681     * [
682     *    'visible' => [
683     *       'Human-readable name' => 'Human readable value',
684     *       ...
685     *    ],
686     *    'collapsed' => [
687     *       'Human-readable name' => 'Human readable value',
688     *       ...
689     *    ]
690     * ]
691     * The UI will format this into a table where the visible fields are always
692     * visible, and the collapsed fields are optionally visible.
693     *
694     * The function should return false if there is no metadata to display.
695     */
696
697    /**
698     * @todo FIXME: This interface is not very flexible. The media handler
699     * should generate HTML instead. It can do all the formatting according
700     * to some standard. That makes it possible to do things like visual
701     * indication of grouped and chained streams in ogg container files.
702     * @stable to override
703     *
704     * @param File $image
705     * @param IContextSource|false $context
706     * @return array<string,array[]>|false
707     */
708    public function formatMetadata( $image, $context = false ) {
709        return false;
710    }
711
712    /** sorts the visible/invisible field.
713     * Split off from ImageHandler::formatMetadata, as used by more than
714     * one type of handler.
715     *
716     * This is used by the media handlers that use the FormatMetadata class
717     *
718     * @stable to override
719     *
720     * @param array $metadataArray
721     * @param IContextSource|false $context
722     * @return array<string,array[]> Array for use displaying metadata.
723     */
724    protected function formatMetadataHelper( $metadataArray, $context = false ) {
725        $result = [
726            'visible' => [],
727            'collapsed' => []
728        ];
729
730        // Allow this MediaHandler to override formatting on certain values
731        foreach ( $metadataArray as $tag => $vals ) {
732            $v = $this->formatTag( $tag, $vals, $context );
733            if ( $v === false ) {
734                // Use default formatting
735                continue;
736            }
737            if ( $v === null ) {
738                // Remove this tag, don't format it for display
739                unset( $metadataArray[$tag] );
740            } else {
741                // Allow subclass to override default formatting.
742                $metadataArray[$tag] = [ '_formatted' => $v ];
743                if ( isset( $v['_type'] ) ) {
744                    $metadataArray[$tag]['_type'] = $v['_type'];
745                    unset( $metadataArray[$tag]['_formatted']['_type'] );
746                }
747            }
748        }
749
750        $formatted = FormatMetadata::getFormattedData( $metadataArray, $context );
751        // Sort fields into visible and collapsed
752        $visibleFields = $this->visibleMetadataFields();
753        foreach ( $formatted as $name => $value ) {
754            $tag = strtolower( $name );
755            self::addMeta( $result,
756                in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
757                'exif',
758                $tag,
759                $value
760            );
761        }
762
763        return $result;
764    }
765
766    /**
767     * Override default formatting for the given metadata field.
768     *
769     * @stable to override
770     *
771     * @param string $key The metadata field key
772     * @param string|array $vals The unformatted value of this metadata field
773     * @param IContextSource|false $context Context to use (optional)
774     * @return false|null|string|array False to use default formatting, null
775     *   to remove this tag from the formatted list; otherwise return
776     *   a formatted HTML string (or array of them).
777     */
778    protected function formatTag( string $key, $vals, $context = false ) {
779        return false; // Use default formatting
780    }
781
782    /**
783     * Get a list of metadata items which should be displayed when
784     * the metadata table is collapsed.
785     *
786     * @stable to override
787     *
788     * @return string[]
789     */
790    protected function visibleMetadataFields() {
791        return FormatMetadata::getVisibleFields();
792    }
793
794    /**
795     * This is used to generate an array element for each metadata value
796     * That array is then used to generate the table of metadata values
797     * on the image page
798     *
799     * @param array &$array An array containing elements for each type of visibility
800     *   and each of those elements being an array of metadata items. This function adds
801     *   a value to that array.
802     * @param string $visibility ('visible' or 'collapsed') if this value is hidden
803     *   by default.
804     * @param string $type Type of metadata tag (currently always 'exif')
805     * @param string $id The name of the metadata tag (like 'artist' for example).
806     *   its name in the table displayed is the message "$type-$id" (Ex exif-artist ).
807     * @param string $value Thingy goes into a wikitext table; it used to be escaped but
808     *   that was incompatible with previous practise of customized display
809     *   with wikitext formatting via messages such as 'exif-model-value'.
810     *   So the escaping is taken back out, but generally this seems a confusing
811     *   interface.
812     * @param bool|string $param Value to pass to the message for the name of the field
813     *   as $1. Currently this parameter doesn't seem to ever be used.
814     *
815     * Note, everything here is passed through the parser later on (!)
816     */
817    protected static function addMeta( &$array, $visibility, $type, $id, $value, $param = false ) {
818        $msg = wfMessage( "$type-$id", (string)$param );
819        if ( $msg->exists() ) {
820            $name = $msg->text();
821        } else {
822            // This is for future compatibility when using instant commons.
823            // So as to not display as ugly a name if a new metadata
824            // property is defined that we don't know about
825            // (not a major issue since such a property would be collapsed
826            // by default).
827            wfDebug( __METHOD__ . ' Unknown metadata name: ' . $id );
828            $name = wfEscapeWikiText( $id );
829        }
830        $array[$visibility][] = [
831            'id' => "$type-$id",
832            'name' => $name,
833            'value' => $value
834        ];
835    }
836
837    /**
838     * Short description. Shown on Special:Search results.
839     *
840     * Until MediaWiki 1.45, the return value was poorly documented, and some subclasses returned HTML
841     * while others returned plain text. When overriding this method, you should return safe HTML,
842     * e.g. using `Message::escaped()`. When calling this method, you should treat it as returning
843     * unsafe HTML, and call `Sanitizer::removeSomeTags()` on the result.
844     *
845     * @stable to override
846     *
847     * @param File $file
848     * @return string HTML (possibly unsafe, call `Sanitizer::removeSomeTags()` on the result)
849     * @return-taint tainted
850     */
851    public function getShortDesc( $file ) {
852        return self::getGeneralShortDesc( $file, $this->getLanguage() );
853    }
854
855    /**
856     * Long description. Shown under image on image description page surrounded by ().
857     *
858     * Until MediaWiki 1.45, the return value was poorly documented, and some subclasses returned HTML
859     * while others returned plain text. When overriding this method, you should return safe HTML,
860     * e.g. using `Message::escaped()`. When calling this method, you should treat it as returning
861     * unsafe HTML, and call `Sanitizer::removeSomeTags()` on the result.
862     *
863     * @stable to override
864     *
865     * @param File $file
866     * @return string HTML (possibly unsafe, call `Sanitizer::removeSomeTags()` on the result)
867     * @return-taint tainted
868     */
869    public function getLongDesc( $file ) {
870        return self::getGeneralLongDesc( $file, $this->getLanguage() );
871    }
872
873    /**
874     * Used instead of getShortDesc if there is no handler registered for file.
875     *
876     * @param File $file
877     * @param Language $lang
878     * @return string HTML
879     */
880    public static function getGeneralShortDesc( $file, Language $lang ) {
881        return htmlspecialchars( $lang->formatSize( $file->getSize() ), ENT_QUOTES );
882    }
883
884    /**
885     * Used instead of getLongDesc if there is no handler registered for file.
886     *
887     * @param File $file
888     * @param Language $lang
889     * @return string HTML
890     */
891    public static function getGeneralLongDesc( $file, Language $lang ) {
892        return wfMessage( 'file-info' )
893            ->sizeParams( $file->getSize() )
894            ->params( '<span class="mime-type">' . $file->getMimeType() . '</span>' )
895            ->inLanguage( $lang )
896            ->parse();
897    }
898
899    /**
900     * Calculate the largest thumbnail width for a given original file size
901     * such that the thumbnail's height is at most $maxHeight.
902     * @param int $boxWidth Width of the thumbnail box.
903     * @param int $boxHeight Height of the thumbnail box.
904     * @param int $maxHeight Maximum height expected for the thumbnail.
905     * @return int
906     */
907    public static function fitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) {
908        $idealWidth = $boxWidth * $maxHeight / $boxHeight;
909        $roundedUp = ceil( $idealWidth );
910        if ( round( $roundedUp * $boxHeight / $boxWidth ) > $maxHeight ) {
911            return (int)floor( $idealWidth );
912        }
913        return (int)$roundedUp;
914    }
915
916    /**
917     * Shown in file history box on image description page.
918     *
919     * @stable to override
920     *
921     * @param File $file
922     * @return string Dimensions (plain text)
923     */
924    public function getDimensionsString( $file ) {
925        return '';
926    }
927
928    /**
929     * Modify the parser object post-transform.
930     *
931     * This is often used to do $parser->addOutputHook(),
932     * in order to add some javascript to render a viewer.
933     * See TimedMediaHandler or OggHandler for an example.
934     *
935     * @stable to override
936     *
937     * @param Parser $parser
938     * @param File $file
939     */
940    public function parserTransformHook( $parser, $file ) {
941    }
942
943    /**
944     * File validation hook called on upload.
945     *
946     * If the file at the given local path is not valid, or its MIME type does not
947     * match the handler class, a Status object should be returned containing
948     * relevant errors.
949     *
950     * @stable to override
951     *
952     * @param string $fileName The local path to the file.
953     * @return Status
954     */
955    public function verifyUpload( $fileName ) {
956        return Status::newGood();
957    }
958
959    /**
960     * Check for zero-sized thumbnails. These can be generated when
961     * no disk space is available or some other error occurs
962     *
963     * @stable to override
964     *
965     * @param string $dstPath The location of the suspect file
966     * @param int $retval Return value of some shell process, file will be deleted if this is non-zero
967     * @return bool True if removed, false otherwise
968     */
969    public function removeBadFile( $dstPath, $retval = 0 ) {
970        if ( file_exists( $dstPath ) ) {
971            $thumbstat = stat( $dstPath );
972            if ( $thumbstat['size'] == 0 || $retval != 0 ) {
973                $result = unlink( $dstPath );
974
975                if ( $result ) {
976                    wfDebugLog( 'thumbnail',
977                        sprintf( 'Removing bad %d-byte thumbnail "%s". unlink() succeeded',
978                            $thumbstat['size'], $dstPath ) );
979                } else {
980                    wfDebugLog( 'thumbnail',
981                        sprintf( 'Removing bad %d-byte thumbnail "%s". unlink() failed',
982                            $thumbstat['size'], $dstPath ) );
983                }
984
985                return true;
986            }
987        }
988
989        return false;
990    }
991
992    /**
993     * Remove files from the purge list.
994     *
995     * This is used by some video handlers to prevent ?action=purge
996     * from removing a transcoded video, which is expensive to
997     * regenerate.
998     *
999     * @see LocalFile::purgeThumbnails
1000     * @stable to override
1001     *
1002     * @param array &$files
1003     * @param array $options Purge options. Currently will always be
1004     *  an array with a single key 'forThumbRefresh' set to true.
1005     */
1006    public function filterThumbnailPurgeList( &$files, $options ) {
1007        // Do nothing
1008    }
1009
1010    /**
1011     * True if the handler can rotate the media
1012     * @since 1.24 non-static. From 1.21-1.23 was static
1013     * @stable to override
1014     *
1015     * @return bool
1016     */
1017    public function canRotate() {
1018        return false;
1019    }
1020
1021    /**
1022     * On supporting image formats, try to read out the low-level orientation
1023     * of the file and return the angle that the file needs to be rotated to
1024     * be viewed.
1025     *
1026     * This information is only useful when manipulating the original file;
1027     * the width and height we normally work with is logical, and will match
1028     * any produced output views.
1029     *
1030     * For files we don't know, we return 0.
1031     *
1032     * @stable to override
1033     *
1034     * @param File $file
1035     * @return int 0, 90, 180 or 270
1036     */
1037    public function getRotation( $file ) {
1038        return 0;
1039    }
1040
1041    /**
1042     * On supporting image formats, determine if the image is mirrored
1043     *
1044     * This information is only useful when manipulating the original file;
1045     * the width and height we normally work with is logical, and will match
1046     * any produced output views.
1047     *
1048     * For files we don't know, we return ''.
1049     *
1050     * @stable to override
1051     *
1052     * @param File $file
1053     * @return string 'vertical' or 'horizontal' or ''
1054     * @since 1.46
1055     */
1056    public function getMirrored( $file ): string {
1057        return '';
1058    }
1059
1060    /**
1061     * Log an error that occurred in an external process
1062     *
1063     * Moved from BitmapHandler to MediaHandler with MediaWiki 1.23
1064     *
1065     * @since 1.23
1066     * @param int $retval
1067     * @param string $err Error reported by command. Anything longer than
1068     * MediaHandler::MAX_ERR_LOG_SIZE is stripped off.
1069     * @param string $cmd
1070     */
1071    protected function logErrorForExternalProcess( $retval, $err, $cmd ) {
1072        # Keep error output limited (T59985)
1073        $errMessage = trim( substr( $err, 0, self::MAX_ERR_LOG_SIZE ) );
1074
1075        wfDebugLog( 'thumbnail',
1076            sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
1077                    wfHostname(), $retval, $errMessage, $cmd ) );
1078    }
1079
1080    /**
1081     * Get list of languages file can be viewed in.
1082     *
1083     * @stable to override
1084     *
1085     * @param File $file
1086     * @return string[] Array of IETF language codes, or empty array if unsupported.
1087     * @since 1.23
1088     */
1089    public function getAvailableLanguages( File $file ) {
1090        return [];
1091    }
1092
1093    /**
1094     * When overridden in a descendant class, returns a language code most suiting
1095     *
1096     * @stable to override
1097     *
1098     * @since 1.32
1099     *
1100     * @param string $userPreferredLanguage IETF Language code requested
1101     * @param string[] $availableLanguages IETF Languages present in the file
1102     * @return string|null IETF Language code picked or null if not supported/available
1103     */
1104    public function getMatchedLanguage( $userPreferredLanguage, array $availableLanguages ) {
1105        return null;
1106    }
1107
1108    /**
1109     * On file types that support renderings in multiple languages,
1110     * which language is used by default if unspecified.
1111     *
1112     * If getAvailableLanguages returns a non-empty array, this must return
1113     * a valid language code. Otherwise can return null if files of this
1114     * type do not support alternative language renderings.
1115     * It can also return 'und' for explicitly requesting an undetermined language
1116     *
1117     * @stable to override
1118     *
1119     * @param File $file
1120     * @return string|null IETF Language code or null if multi-language not supported for filetype.
1121     * @since 1.23
1122     */
1123    public function getDefaultRenderLanguage( File $file ) {
1124        return null;
1125    }
1126
1127    /**
1128     * If it's an audio file, return the length of the file. Otherwise 0.
1129     *
1130     * File::getLength() existed for a long time, but was calling a method
1131     * that only existed in some subclasses of this class (The TMH ones).
1132     *
1133     * @stable to override
1134     *
1135     * @param File $file
1136     * @return float Length in seconds
1137     * @since 1.23
1138     */
1139    public function getLength( $file ) {
1140        return 0.0;
1141    }
1142
1143    /**
1144     * True if creating thumbnails from the file is large or otherwise resource-intensive.
1145     * @stable to override
1146     *
1147     * @param File $file
1148     * @return bool
1149     */
1150    public function isExpensiveToThumbnail( $file ) {
1151        return false;
1152    }
1153
1154    /**
1155     * Returns whether or not this handler supports the chained generation of thumbnails according
1156     * to buckets
1157     * @stable to override
1158     *
1159     * @return bool
1160     * @since 1.24
1161     */
1162    public function supportsBucketing() {
1163        return false;
1164    }
1165
1166    /**
1167     * Returns a normalised params array for which parameters have been cleaned up for bucketing
1168     * purposes
1169     * @stable to override
1170     *
1171     * @param array $params
1172     * @return array
1173     */
1174    public function sanitizeParamsForBucketing( $params ) {
1175        return $params;
1176    }
1177
1178    /**
1179     * Gets configuration for the file warning message. Return value of
1180     * the following structure:
1181     *   [
1182     *     // Required, module with messages loaded for the client
1183     *     'module' => 'example.filewarning.messages',
1184     *     // Required, array of names of messages
1185     *     'messages' => [
1186     *       // Required, main warning message
1187     *       'main' => 'example-filewarning-main',
1188     *       // Optional, header for warning dialog
1189     *       'header' => 'example-filewarning-header',
1190     *       // Optional, footer for warning dialog
1191     *       'footer' => 'example-filewarning-footer',
1192     *       // Optional, text for more-information link (see below)
1193     *       'info' => 'example-filewarning-info',
1194     *     ],
1195     *     // Optional, link for more information
1196     *     'link' => 'http://example.com',
1197     *   ]
1198     *
1199     * Returns null if no warning is necessary.
1200     * @stable to override
1201     * @param File $file
1202     * @return array|null
1203     */
1204    public function getWarningConfig( $file ) {
1205        return null;
1206    }
1207
1208    /**
1209     * Converts a dimensions array about a potentially multipage document from an
1210     * exhaustive list of ordered page numbers to a list of page ranges
1211     * @param array[] $pagesByDimensions
1212     * @return string
1213     * @since 1.30
1214     */
1215    public static function getPageRangesByDimensions( $pagesByDimensions ) {
1216        $pageRangesByDimensions = [];
1217
1218        foreach ( $pagesByDimensions as $dimensions => $pageList ) {
1219            $ranges = [];
1220            $firstPage = $pageList[0];
1221            $lastPage = $firstPage - 1;
1222
1223            foreach ( $pageList as $page ) {
1224                if ( $page > $lastPage + 1 ) {
1225                    if ( $firstPage !== $lastPage ) {
1226                        $ranges[] = "$firstPage-$lastPage";
1227                    } else {
1228                        $ranges[] = "$firstPage";
1229                    }
1230
1231                    $firstPage = $page;
1232                }
1233
1234                $lastPage = $page;
1235            }
1236
1237            if ( $firstPage != $lastPage ) {
1238                $ranges[] = "$firstPage-$lastPage";
1239            } else {
1240                $ranges[] = "$firstPage";
1241            }
1242
1243            $pageRangesByDimensions[ $dimensions ] = $ranges;
1244        }
1245
1246        $dimensionsString = [];
1247        foreach ( $pageRangesByDimensions as $dimensions => $pageRanges ) {
1248            $dimensionsString[] = "$dimensions:" . implode( ',', $pageRanges );
1249        }
1250
1251        return implode( '/', $dimensionsString );
1252    }
1253
1254    /**
1255     * Get useful response headers for GET/HEAD requests for a file with the given metadata
1256     * @stable to override
1257     *
1258     * @param array $metadata Contains this handler's unserialized getMetadata() for a file
1259     * @return array
1260     * @since 1.30
1261     */
1262    public function getContentHeaders( $metadata ) {
1263        return [ 'X-Content-Dimensions' => '' ]; // T175689
1264    }
1265
1266    /**
1267     * If this returns true, LocalFile may split metadata up and store its
1268     * constituent items separately. This only makes sense if the handler calls
1269     * File::getMetadataItem() or File::getMetadataItems() instead of
1270     * requesting the whole array at once.
1271     *
1272     * @return bool
1273     */
1274    public function useSplitMetadata() {
1275        return false;
1276    }
1277}
1278
1279/** @deprecated class alias since 1.46 */
1280class_alias( MediaHandler::class, 'MediaHandler' );