Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.43% covered (warning)
54.43%
43 / 79
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExifBitmapHandler
55.13% covered (warning)
55.13%
43 / 78
0.00% covered (danger)
0.00%
0 / 8
218.91
0.00% covered (danger)
0.00%
0 / 1
 convertMetadataVersion
80.77% covered (warning)
80.77%
21 / 26
0.00% covered (danger)
0.00%
0 / 1
20.30
 isFileMetadataValid
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
9.24
 formatMetadata
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getCommonMetaArray
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getMetadataType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 applyExifRotation
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getRotation
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getRotationForExifFromOrientation
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace MediaWiki\Media;
4
5/**
6 * Handler for bitmap images with exif metadata.
7 *
8 * @license GPL-2.0-or-later
9 * @file
10 * @ingroup Media
11 */
12
13use MediaWiki\Context\IContextSource;
14use MediaWiki\FileRepo\File\File;
15use MediaWiki\MainConfigNames;
16use MediaWiki\MediaWikiServices;
17
18/**
19 * Stuff specific to JPEG and (built-in) TIFF handler.
20 * All metadata related, since both JPEG and TIFF support Exif.
21 *
22 * @stable to extend
23 * @ingroup Media
24 */
25class ExifBitmapHandler extends BitmapHandler {
26    /** Error extracting metadata */
27    public const BROKEN_FILE = '-1';
28
29    /** Outdated error extracting metadata */
30    public const OLD_BROKEN_FILE = '0';
31
32    /** @inheritDoc */
33    public function convertMetadataVersion( $metadata, $version = 1 ) {
34        // basically flattens arrays.
35        $version = is_int( $version ) ? $version : (int)explode( ';', $version, 2 )[0];
36        if ( $version < 1 || $version >= 2 ) {
37            return $metadata;
38        }
39
40        if ( !isset( $metadata['MEDIAWIKI_EXIF_VERSION'] ) || $metadata['MEDIAWIKI_EXIF_VERSION'] !== 2 ) {
41            return $metadata;
42        }
43
44        // Treat Software as a special case because in can contain
45        // an array of (SoftwareName, Version).
46        if ( isset( $metadata['Software'] )
47            && is_array( $metadata['Software'] )
48            && is_array( $metadata['Software'][0] )
49            && isset( $metadata['Software'][0][0] )
50            && isset( $metadata['Software'][0][1] )
51        ) {
52            $metadata['Software'] = $metadata['Software'][0][0] . ' (Version '
53                . $metadata['Software'][0][1] . ')';
54        }
55
56        $formatter = new FormatMetadata;
57
58        // ContactInfo also has to be dealt with specially
59        if ( isset( $metadata['Contact'] ) ) {
60            $metadata['Contact'] = $formatter->collapseContactInfo(
61                is_array( $metadata['Contact'] ) ? $metadata['Contact'] : [ $metadata['Contact'] ]
62            );
63        }
64
65        // Ignore Location shown/created if they are not simple strings
66        foreach ( [ 'LocationShown', 'LocationCreated' ] as $metadataKey ) {
67            if ( isset( $metadata[ $metadataKey ] ) && !is_string( $metadata[ $metadataKey ] ) ) {
68                unset( $metadata[ $metadataKey ] );
69            }
70        }
71
72        foreach ( $metadata as &$val ) {
73            if ( is_array( $val ) ) {
74                // @phan-suppress-next-line SecurityCheck-DoubleEscaped Ambiguous with the true for nohtml
75                $val = $formatter->flattenArrayReal( $val, 'ul', true );
76            }
77        }
78        unset( $val );
79        $metadata['MEDIAWIKI_EXIF_VERSION'] = 1;
80
81        return $metadata;
82    }
83
84    /**
85     * @param File $image
86     * @return bool|int
87     */
88    public function isFileMetadataValid( $image ) {
89        $showEXIF = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ShowEXIF );
90        if ( !$showEXIF ) {
91            # Metadata disabled and so an empty field is expected
92            return self::METADATA_GOOD;
93        }
94        $exif = $image->getMetadataArray();
95        if ( !$exif ) {
96            wfDebug( __METHOD__ . ': error unserializing?' );
97            return self::METADATA_BAD;
98        }
99        if ( $exif === [ '_error' => self::OLD_BROKEN_FILE ] ) {
100            # Old special value indicating that there is no Exif data in the file.
101            # or that there was an error well extracting the metadata.
102            wfDebug( __METHOD__ . ": back-compat version" );
103            return self::METADATA_COMPATIBLE;
104        }
105
106        if ( $exif === [ '_error' => self::BROKEN_FILE ] ) {
107            return self::METADATA_GOOD;
108        }
109
110        if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] )
111            || $exif['MEDIAWIKI_EXIF_VERSION'] !== Exif::version()
112        ) {
113            if ( isset( $exif['MEDIAWIKI_EXIF_VERSION'] )
114                && $exif['MEDIAWIKI_EXIF_VERSION'] === 1
115            ) {
116                // back-compatible but old
117                wfDebug( __METHOD__ . ": back-compat version" );
118
119                return self::METADATA_COMPATIBLE;
120            }
121            # Wrong (non-compatible) version
122            wfDebug( __METHOD__ . ": wrong version" );
123
124            return self::METADATA_BAD;
125        }
126
127        return self::METADATA_GOOD;
128    }
129
130    /**
131     * @param File $image
132     * @param IContextSource|false $context
133     * @return array[]|false
134     */
135    public function formatMetadata( $image, $context = false ) {
136        $meta = $this->getCommonMetaArray( $image );
137        if ( !$meta ) {
138            return false;
139        }
140
141        return $this->formatMetadataHelper( $meta, $context );
142    }
143
144    /** @inheritDoc */
145    public function getCommonMetaArray( File $file ) {
146        $exif = $file->getMetadataArray();
147        if ( !$exif ) {
148            return [];
149        }
150        unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
151
152        return $exif;
153    }
154
155    /** @inheritDoc */
156    public function getMetadataType( $image ) {
157        return 'exif';
158    }
159
160    /**
161     * @param array $info
162     * @param array $metadata
163     * @return array
164     */
165    protected function applyExifRotation( $info, $metadata ) {
166        if ( $this->autoRotateEnabled() ) {
167            $rotation = $this->getRotationForExifFromOrientation( $metadata['Orientation'] ?? null );
168        } else {
169            $rotation = 0;
170        }
171
172        if ( $rotation === 90 || $rotation === 270 ) {
173            $width = $info['width'];
174            $info['width'] = $info['height'];
175            $info['height'] = $width;
176        }
177        return $info;
178    }
179
180    /**
181     * On supporting image formats, try to read out the low-level orientation
182     * of the file and return the angle that the file needs to be rotated to
183     * be viewed.
184     *
185     * This information is only useful when manipulating the original file;
186     * the width and height we normally work with is logical, and will match
187     * any produced output views.
188     *
189     * @param File $file
190     * @return int 0, 90, 180 or 270
191     */
192    public function getRotation( $file ) {
193        if ( !$this->autoRotateEnabled() ) {
194            return 0;
195        }
196
197        $orientation = $file->getMetadataItem( 'Orientation' );
198        return $this->getRotationForExifFromOrientation( $orientation );
199    }
200
201    /**
202     * Given a chunk of serialized Exif metadata, return the orientation as
203     * degrees of rotation.
204     *
205     * @param int|null $orientation
206     * @return int 0, 90, 180 or 270
207     * @todo FIXME: Orientation can include flipping as well; see if this is an issue!
208     */
209    protected function getRotationForExifFromOrientation( $orientation ) {
210        if ( $orientation === null ) {
211            return 0;
212        }
213        # See http://sylvana.net/jpegcrop/exif_orientation.html
214        switch ( $orientation ) {
215            case 8:
216                return 90;
217            case 3:
218                return 180;
219            case 6:
220                return 270;
221            default:
222                return 0;
223        }
224    }
225}
226
227/** @deprecated class alias since 1.46 */
228class_alias( ExifBitmapHandler::class, 'ExifBitmapHandler' );