This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * PNG frame counter and metadata extractor. |
||
4 | * |
||
5 | * Slightly derived from GIFMetadataExtractor.php |
||
6 | * Deliberately not using MWExceptions to avoid external dependencies, encouraging |
||
7 | * redistribution. |
||
8 | * |
||
9 | * This program is free software; you can redistribute it and/or modify |
||
10 | * it under the terms of the GNU General Public License as published by |
||
11 | * the Free Software Foundation; either version 2 of the License, or |
||
12 | * (at your option) any later version. |
||
13 | * |
||
14 | * This program is distributed in the hope that it will be useful, |
||
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
17 | * GNU General Public License for more details. |
||
18 | * |
||
19 | * You should have received a copy of the GNU General Public License along |
||
20 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
21 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
22 | * http://www.gnu.org/copyleft/gpl.html |
||
23 | * |
||
24 | * @file |
||
25 | * @ingroup Media |
||
26 | */ |
||
27 | |||
28 | /** |
||
29 | * PNG frame counter. |
||
30 | * |
||
31 | * @ingroup Media |
||
32 | */ |
||
33 | class PNGMetadataExtractor { |
||
34 | /** @var string */ |
||
35 | private static $pngSig; |
||
36 | |||
37 | /** @var int */ |
||
38 | private static $crcSize; |
||
39 | |||
40 | /** @var array */ |
||
41 | private static $textChunks; |
||
42 | |||
43 | const VERSION = 1; |
||
44 | const MAX_CHUNK_SIZE = 3145728; // 3 megabytes |
||
45 | |||
46 | static function getMetadata( $filename ) { |
||
47 | self::$pngSig = pack( "C8", 137, 80, 78, 71, 13, 10, 26, 10 ); |
||
48 | self::$crcSize = 4; |
||
49 | /* based on list at http://owl.phy.queensu.ca/~phil/exiftool/TagNames/PNG.html#TextualData |
||
50 | * and https://www.w3.org/TR/PNG/#11keywords |
||
51 | */ |
||
52 | self::$textChunks = [ |
||
53 | 'xml:com.adobe.xmp' => 'xmp', |
||
54 | # Artist is unofficial. Author is the recommended |
||
55 | # keyword in the PNG spec. However some people output |
||
56 | # Artist so support both. |
||
57 | 'artist' => 'Artist', |
||
58 | 'model' => 'Model', |
||
59 | 'make' => 'Make', |
||
60 | 'author' => 'Artist', |
||
61 | 'comment' => 'PNGFileComment', |
||
62 | 'description' => 'ImageDescription', |
||
63 | 'title' => 'ObjectName', |
||
64 | 'copyright' => 'Copyright', |
||
65 | # Source as in original device used to make image |
||
66 | # not as in who gave you the image |
||
67 | 'source' => 'Model', |
||
68 | 'software' => 'Software', |
||
69 | 'disclaimer' => 'Disclaimer', |
||
70 | 'warning' => 'ContentWarning', |
||
71 | 'url' => 'Identifier', # Not sure if this is best mapping. Maybe WebStatement. |
||
72 | 'label' => 'Label', |
||
73 | 'creation time' => 'DateTimeDigitized', |
||
74 | /* Other potentially useful things - Document */ |
||
75 | ]; |
||
76 | |||
77 | $frameCount = 0; |
||
78 | $loopCount = 1; |
||
79 | $text = []; |
||
80 | $duration = 0.0; |
||
81 | $bitDepth = 0; |
||
82 | $colorType = 'unknown'; |
||
83 | |||
84 | View Code Duplication | if ( !$filename ) { |
|
85 | throw new Exception( __METHOD__ . ": No file name specified" ); |
||
86 | } elseif ( !file_exists( $filename ) || is_dir( $filename ) ) { |
||
87 | throw new Exception( __METHOD__ . ": File $filename does not exist" ); |
||
88 | } |
||
89 | |||
90 | $fh = fopen( $filename, 'rb' ); |
||
91 | |||
92 | if ( !$fh ) { |
||
93 | throw new Exception( __METHOD__ . ": Unable to open file $filename" ); |
||
94 | } |
||
95 | |||
96 | // Check for the PNG header |
||
97 | $buf = fread( $fh, 8 ); |
||
98 | if ( $buf != self::$pngSig ) { |
||
99 | throw new Exception( __METHOD__ . ": Not a valid PNG file; header: $buf" ); |
||
100 | } |
||
101 | |||
102 | // Read chunks |
||
103 | while ( !feof( $fh ) ) { |
||
104 | $buf = fread( $fh, 4 ); |
||
105 | if ( !$buf || strlen( $buf ) < 4 ) { |
||
106 | throw new Exception( __METHOD__ . ": Read error" ); |
||
107 | } |
||
108 | $chunk_size = unpack( "N", $buf )[1]; |
||
109 | |||
110 | if ( $chunk_size < 0 ) { |
||
111 | throw new Exception( __METHOD__ . ": Chunk size too big for unpack" ); |
||
112 | } |
||
113 | |||
114 | $chunk_type = fread( $fh, 4 ); |
||
115 | if ( !$chunk_type || strlen( $chunk_type ) < 4 ) { |
||
116 | throw new Exception( __METHOD__ . ": Read error" ); |
||
117 | } |
||
118 | |||
119 | if ( $chunk_type == "IHDR" ) { |
||
120 | $buf = self::read( $fh, $chunk_size ); |
||
121 | if ( !$buf || strlen( $buf ) < $chunk_size ) { |
||
122 | throw new Exception( __METHOD__ . ": Read error" ); |
||
123 | } |
||
124 | $bitDepth = ord( substr( $buf, 8, 1 ) ); |
||
125 | // Detect the color type in British English as per the spec |
||
126 | // https://www.w3.org/TR/PNG/#11IHDR |
||
127 | switch ( ord( substr( $buf, 9, 1 ) ) ) { |
||
128 | case 0: |
||
129 | $colorType = 'greyscale'; |
||
130 | break; |
||
131 | case 2: |
||
132 | $colorType = 'truecolour'; |
||
133 | break; |
||
134 | case 3: |
||
135 | $colorType = 'index-coloured'; |
||
136 | break; |
||
137 | case 4: |
||
138 | $colorType = 'greyscale-alpha'; |
||
139 | break; |
||
140 | case 6: |
||
141 | $colorType = 'truecolour-alpha'; |
||
142 | break; |
||
143 | default: |
||
144 | $colorType = 'unknown'; |
||
145 | break; |
||
146 | } |
||
147 | } elseif ( $chunk_type == "acTL" ) { |
||
148 | $buf = fread( $fh, $chunk_size ); |
||
149 | if ( !$buf || strlen( $buf ) < $chunk_size || $chunk_size < 4 ) { |
||
150 | throw new Exception( __METHOD__ . ": Read error" ); |
||
151 | } |
||
152 | |||
153 | $actl = unpack( "Nframes/Nplays", $buf ); |
||
154 | $frameCount = $actl['frames']; |
||
155 | $loopCount = $actl['plays']; |
||
156 | } elseif ( $chunk_type == "fcTL" ) { |
||
157 | $buf = self::read( $fh, $chunk_size ); |
||
158 | if ( !$buf || strlen( $buf ) < $chunk_size ) { |
||
159 | throw new Exception( __METHOD__ . ": Read error" ); |
||
160 | } |
||
161 | $buf = substr( $buf, 20 ); |
||
162 | if ( strlen( $buf ) < 4 ) { |
||
163 | throw new Exception( __METHOD__ . ": Read error" ); |
||
164 | } |
||
165 | |||
166 | $fctldur = unpack( "ndelay_num/ndelay_den", $buf ); |
||
167 | if ( $fctldur['delay_den'] == 0 ) { |
||
168 | $fctldur['delay_den'] = 100; |
||
169 | } |
||
170 | if ( $fctldur['delay_num'] ) { |
||
171 | $duration += $fctldur['delay_num'] / $fctldur['delay_den']; |
||
172 | } |
||
173 | } elseif ( $chunk_type == "iTXt" ) { |
||
174 | // Extracts iTXt chunks, uncompressing if necessary. |
||
175 | $buf = self::read( $fh, $chunk_size ); |
||
176 | $items = []; |
||
177 | if ( preg_match( |
||
178 | '/^([^\x00]{1,79})\x00(\x00|\x01)\x00([^\x00]*)(.)[^\x00]*\x00(.*)$/Ds', |
||
179 | $buf, $items ) |
||
180 | ) { |
||
181 | /* $items[1] = text chunk name, $items[2] = compressed flag, |
||
182 | * $items[3] = lang code (or ""), $items[4]= compression type. |
||
183 | * $items[5] = content |
||
184 | */ |
||
185 | |||
186 | // Theoretically should be case-sensitive, but in practise... |
||
187 | $items[1] = strtolower( $items[1] ); |
||
188 | if ( !isset( self::$textChunks[$items[1]] ) ) { |
||
189 | // Only extract textual chunks on our list. |
||
190 | fseek( $fh, self::$crcSize, SEEK_CUR ); |
||
191 | continue; |
||
192 | } |
||
193 | |||
194 | $items[3] = strtolower( $items[3] ); |
||
195 | if ( $items[3] == '' ) { |
||
196 | // if no lang specified use x-default like in xmp. |
||
197 | $items[3] = 'x-default'; |
||
198 | } |
||
199 | |||
200 | // if compressed |
||
201 | if ( $items[2] == "\x01" ) { |
||
202 | if ( function_exists( 'gzuncompress' ) && $items[4] === "\x00" ) { |
||
203 | MediaWiki\suppressWarnings(); |
||
204 | $items[5] = gzuncompress( $items[5] ); |
||
205 | MediaWiki\restoreWarnings(); |
||
206 | |||
207 | View Code Duplication | if ( $items[5] === false ) { |
|
208 | // decompression failed |
||
209 | wfDebug( __METHOD__ . ' Error decompressing iTxt chunk - ' . $items[1] . "\n" ); |
||
210 | fseek( $fh, self::$crcSize, SEEK_CUR ); |
||
211 | continue; |
||
212 | } |
||
213 | View Code Duplication | } else { |
|
214 | wfDebug( __METHOD__ . ' Skipping compressed png iTXt chunk due to lack of zlib,' |
||
215 | . " or potentially invalid compression method\n" ); |
||
216 | fseek( $fh, self::$crcSize, SEEK_CUR ); |
||
217 | continue; |
||
218 | } |
||
219 | } |
||
220 | $finalKeyword = self::$textChunks[$items[1]]; |
||
221 | $text[$finalKeyword][$items[3]] = $items[5]; |
||
222 | $text[$finalKeyword]['_type'] = 'lang'; |
||
223 | } else { |
||
224 | // Error reading iTXt chunk |
||
225 | throw new Exception( __METHOD__ . ": Read error on iTXt chunk" ); |
||
226 | } |
||
227 | } elseif ( $chunk_type == 'tEXt' ) { |
||
228 | $buf = self::read( $fh, $chunk_size ); |
||
229 | |||
230 | // In case there is no \x00 which will make explode fail. |
||
231 | if ( strpos( $buf, "\x00" ) === false ) { |
||
232 | throw new Exception( __METHOD__ . ": Read error on tEXt chunk" ); |
||
233 | } |
||
234 | |||
235 | list( $keyword, $content ) = explode( "\x00", $buf, 2 ); |
||
236 | if ( $keyword === '' || $content === '' ) { |
||
237 | throw new Exception( __METHOD__ . ": Read error on tEXt chunk" ); |
||
238 | } |
||
239 | |||
240 | // Theoretically should be case-sensitive, but in practise... |
||
241 | $keyword = strtolower( $keyword ); |
||
242 | if ( !isset( self::$textChunks[$keyword] ) ) { |
||
243 | // Don't recognize chunk, so skip. |
||
244 | fseek( $fh, self::$crcSize, SEEK_CUR ); |
||
245 | continue; |
||
246 | } |
||
247 | MediaWiki\suppressWarnings(); |
||
248 | $content = iconv( 'ISO-8859-1', 'UTF-8', $content ); |
||
249 | MediaWiki\restoreWarnings(); |
||
250 | |||
251 | if ( $content === false ) { |
||
252 | throw new Exception( __METHOD__ . ": Read error (error with iconv)" ); |
||
253 | } |
||
254 | |||
255 | $finalKeyword = self::$textChunks[$keyword]; |
||
256 | $text[$finalKeyword]['x-default'] = $content; |
||
257 | $text[$finalKeyword]['_type'] = 'lang'; |
||
258 | } elseif ( $chunk_type == 'zTXt' ) { |
||
259 | if ( function_exists( 'gzuncompress' ) ) { |
||
260 | $buf = self::read( $fh, $chunk_size ); |
||
261 | |||
262 | // In case there is no \x00 which will make explode fail. |
||
263 | if ( strpos( $buf, "\x00" ) === false ) { |
||
264 | throw new Exception( __METHOD__ . ": Read error on zTXt chunk" ); |
||
265 | } |
||
266 | |||
267 | list( $keyword, $postKeyword ) = explode( "\x00", $buf, 2 ); |
||
268 | if ( $keyword === '' || $postKeyword === '' ) { |
||
269 | throw new Exception( __METHOD__ . ": Read error on zTXt chunk" ); |
||
270 | } |
||
271 | // Theoretically should be case-sensitive, but in practise... |
||
272 | $keyword = strtolower( $keyword ); |
||
273 | |||
274 | if ( !isset( self::$textChunks[$keyword] ) ) { |
||
275 | // Don't recognize chunk, so skip. |
||
276 | fseek( $fh, self::$crcSize, SEEK_CUR ); |
||
277 | continue; |
||
278 | } |
||
279 | $compression = substr( $postKeyword, 0, 1 ); |
||
280 | $content = substr( $postKeyword, 1 ); |
||
281 | View Code Duplication | if ( $compression !== "\x00" ) { |
|
282 | wfDebug( __METHOD__ . " Unrecognized compression method in zTXt ($keyword). Skipping.\n" ); |
||
283 | fseek( $fh, self::$crcSize, SEEK_CUR ); |
||
284 | continue; |
||
285 | } |
||
286 | |||
287 | MediaWiki\suppressWarnings(); |
||
288 | $content = gzuncompress( $content ); |
||
289 | MediaWiki\restoreWarnings(); |
||
290 | |||
291 | View Code Duplication | if ( $content === false ) { |
|
292 | // decompression failed |
||
293 | wfDebug( __METHOD__ . ' Error decompressing zTXt chunk - ' . $keyword . "\n" ); |
||
294 | fseek( $fh, self::$crcSize, SEEK_CUR ); |
||
295 | continue; |
||
296 | } |
||
297 | |||
298 | MediaWiki\suppressWarnings(); |
||
299 | $content = iconv( 'ISO-8859-1', 'UTF-8', $content ); |
||
300 | MediaWiki\restoreWarnings(); |
||
301 | |||
302 | if ( $content === false ) { |
||
303 | throw new Exception( __METHOD__ . ": Read error (error with iconv)" ); |
||
304 | } |
||
305 | |||
306 | $finalKeyword = self::$textChunks[$keyword]; |
||
307 | $text[$finalKeyword]['x-default'] = $content; |
||
308 | $text[$finalKeyword]['_type'] = 'lang'; |
||
309 | } else { |
||
310 | wfDebug( __METHOD__ . " Cannot decompress zTXt chunk due to lack of zlib. Skipping.\n" ); |
||
311 | fseek( $fh, $chunk_size, SEEK_CUR ); |
||
312 | } |
||
313 | } elseif ( $chunk_type == 'tIME' ) { |
||
314 | // last mod timestamp. |
||
315 | if ( $chunk_size !== 7 ) { |
||
316 | throw new Exception( __METHOD__ . ": tIME wrong size" ); |
||
317 | } |
||
318 | $buf = self::read( $fh, $chunk_size ); |
||
319 | if ( !$buf || strlen( $buf ) < $chunk_size ) { |
||
320 | throw new Exception( __METHOD__ . ": Read error" ); |
||
321 | } |
||
322 | |||
323 | // Note: spec says this should be UTC. |
||
324 | $t = unpack( "ny/Cm/Cd/Ch/Cmin/Cs", $buf ); |
||
325 | $strTime = sprintf( "%04d%02d%02d%02d%02d%02d", |
||
326 | $t['y'], $t['m'], $t['d'], $t['h'], |
||
327 | $t['min'], $t['s'] ); |
||
328 | |||
329 | $exifTime = wfTimestamp( TS_EXIF, $strTime ); |
||
330 | |||
331 | if ( $exifTime ) { |
||
0 ignored issues
–
show
|
|||
332 | $text['DateTime'] = $exifTime; |
||
333 | } |
||
334 | } elseif ( $chunk_type == 'pHYs' ) { |
||
335 | // how big pixels are (dots per meter). |
||
336 | if ( $chunk_size !== 9 ) { |
||
337 | throw new Exception( __METHOD__ . ": pHYs wrong size" ); |
||
338 | } |
||
339 | |||
340 | $buf = self::read( $fh, $chunk_size ); |
||
341 | if ( !$buf || strlen( $buf ) < $chunk_size ) { |
||
342 | throw new Exception( __METHOD__ . ": Read error" ); |
||
343 | } |
||
344 | |||
345 | $dim = unpack( "Nwidth/Nheight/Cunit", $buf ); |
||
346 | if ( $dim['unit'] == 1 ) { |
||
347 | // Need to check for negative because php |
||
348 | // doesn't deal with super-large unsigned 32-bit ints well |
||
349 | if ( $dim['width'] > 0 && $dim['height'] > 0 ) { |
||
350 | // unit is meters |
||
351 | // (as opposed to 0 = undefined ) |
||
352 | $text['XResolution'] = $dim['width'] |
||
353 | . '/100'; |
||
354 | $text['YResolution'] = $dim['height'] |
||
355 | . '/100'; |
||
356 | $text['ResolutionUnit'] = 3; |
||
357 | // 3 = dots per cm (from Exif). |
||
358 | } |
||
359 | } |
||
360 | } elseif ( $chunk_type == "IEND" ) { |
||
361 | break; |
||
362 | } else { |
||
363 | fseek( $fh, $chunk_size, SEEK_CUR ); |
||
364 | } |
||
365 | fseek( $fh, self::$crcSize, SEEK_CUR ); |
||
366 | } |
||
367 | fclose( $fh ); |
||
368 | |||
369 | if ( $loopCount > 1 ) { |
||
370 | $duration *= $loopCount; |
||
371 | } |
||
372 | |||
373 | if ( isset( $text['DateTimeDigitized'] ) ) { |
||
374 | // Convert date format from rfc2822 to exif. |
||
375 | foreach ( $text['DateTimeDigitized'] as $name => &$value ) { |
||
376 | if ( $name === '_type' ) { |
||
377 | continue; |
||
378 | } |
||
379 | |||
380 | // @todo FIXME: Currently timezones are ignored. |
||
381 | // possibly should be wfTimestamp's |
||
382 | // responsibility. (at least for numeric TZ) |
||
383 | $formatted = wfTimestamp( TS_EXIF, $value ); |
||
384 | if ( $formatted ) { |
||
0 ignored issues
–
show
The expression
$formatted of type false|string is loosely compared to true ; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
385 | // Only change if we could convert the |
||
386 | // date. |
||
387 | // The png standard says it should be |
||
388 | // in rfc2822 format, but not required. |
||
389 | // In general for the exif stuff we |
||
390 | // prettify the date if we can, but we |
||
391 | // display as-is if we cannot or if |
||
392 | // it is invalid. |
||
393 | // So do the same here. |
||
394 | |||
395 | $value = $formatted; |
||
396 | } |
||
397 | } |
||
398 | } |
||
399 | |||
400 | return [ |
||
401 | 'frameCount' => $frameCount, |
||
402 | 'loopCount' => $loopCount, |
||
403 | 'duration' => $duration, |
||
404 | 'text' => $text, |
||
405 | 'bitDepth' => $bitDepth, |
||
406 | 'colorType' => $colorType, |
||
407 | ]; |
||
408 | } |
||
409 | |||
410 | /** |
||
411 | * Read a chunk, checking to make sure its not too big. |
||
412 | * |
||
413 | * @param resource $fh The file handle |
||
414 | * @param int $size Size in bytes. |
||
415 | * @throws Exception If too big |
||
416 | * @return string The chunk. |
||
417 | */ |
||
418 | private static function read( $fh, $size ) { |
||
419 | if ( $size > self::MAX_CHUNK_SIZE ) { |
||
420 | throw new Exception( __METHOD__ . ': Chunk size of ' . $size . |
||
421 | ' too big. Max size is: ' . self::MAX_CHUNK_SIZE ); |
||
422 | } |
||
423 | |||
424 | return fread( $fh, $size ); |
||
425 | } |
||
426 | } |
||
427 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
string
values, the empty string''
is a special case, in particular the following results might be unexpected: