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