MediaWiki master
Exif.php
Go to the documentation of this file.
1<?php
14namespace MediaWiki\Media;
15
17
22class Exif {
24 private const BYTE = 1;
25
29 private const ASCII = 2;
30
32 private const SHORT = 3;
33
35 private const LONG = 4;
36
40 private const RATIONAL = 5;
41
43 private const SHORT_OR_LONG = 6;
44
46 private const UNDEFINED = 7;
47
49 private const SLONG = 9;
50
54 private const SRATIONAL = 10;
55
57 private const IGNORE = -1;
58
63 private $mExifTags;
64
66 private $mRawExifData;
67
72 private $mFilteredExifData;
73
75 private $file;
76
78 private $basename;
79
81 private $log = false;
82
86 private $byteOrder;
87
97 public function __construct( $file, $byteOrder = '' ) {
98 if ( !function_exists( 'exif_read_data' ) ) {
99 throw new ConfigException(
100 "Internal error: exif_read_data not present. " .
101 "\$wgShowEXIF may be incorrectly set or not checked by an extension."
102 );
103 }
104
113 $this->mExifTags = [
114 # TIFF Rev. 6.0 Attribute Information (p22)
115 'IFD0' => [
116 # Tags relating to image structure
117 # Image width
118 'ImageWidth' => self::SHORT_OR_LONG,
119 # Image height
120 'ImageLength' => self::SHORT_OR_LONG,
121 # Number of bits per component
122 'BitsPerSample' => [ self::SHORT, 3 ],
123
124 # "When a primary image is JPEG compressed, this designation is not"
125 # "necessary and is omitted." (p23)
126 # Compression scheme #p23
127 'Compression' => self::SHORT,
128 # Pixel composition #p23
129 'PhotometricInterpretation' => self::SHORT,
130 # Orientation of image #p24
131 'Orientation' => self::SHORT,
132 # Number of components
133 'SamplesPerPixel' => self::SHORT,
134 # Image data arrangement #p24
135 'PlanarConfiguration' => self::SHORT,
136 # Subsampling ratio of Y to C #p24
137 'YCbCrSubSampling' => [ self::SHORT, 2 ],
138 # Y and C positioning #p24-25
139 'YCbCrPositioning' => self::SHORT,
140 # Image resolution in width direction
141 'XResolution' => self::RATIONAL,
142 # Image resolution in height direction
143 'YResolution' => self::RATIONAL,
144 # Unit of X and Y resolution #(p26)
145 'ResolutionUnit' => self::SHORT,
146
147 # Tags relating to recording offset
148 # Image data location
149 'StripOffsets' => self::SHORT_OR_LONG,
150 # Number of rows per strip
151 'RowsPerStrip' => self::SHORT_OR_LONG,
152 # Bytes per compressed strip
153 'StripByteCounts' => self::SHORT_OR_LONG,
154 # Offset to JPEG SOI
155 'JPEGInterchangeFormat' => self::SHORT_OR_LONG,
156 # Bytes of JPEG data
157 'JPEGInterchangeFormatLength' => self::SHORT_OR_LONG,
158
159 # Tags relating to image data characteristics
160 # Transfer function
161 'TransferFunction' => self::IGNORE,
162 # White point chromaticity
163 'WhitePoint' => [ self::RATIONAL, 2 ],
164 # Chromaticities of primarities
165 'PrimaryChromaticities' => [ self::RATIONAL, 6 ],
166 # Color space transformation matrix coefficients #p27
167 'YCbCrCoefficients' => [ self::RATIONAL, 3 ],
168 # Pair of black and white reference values
169 'ReferenceBlackWhite' => [ self::RATIONAL, 6 ],
170
171 # Other tags
172 # File change date and time
173 'DateTime' => self::ASCII,
174 # Image title
175 'ImageDescription' => self::ASCII,
176 # Image input equipment manufacturer
177 'Make' => self::ASCII,
178 # Image input equipment model
179 'Model' => self::ASCII,
180 # Software used
181 'Software' => self::ASCII,
182 # Person who created the image
183 'Artist' => self::ASCII,
184 # Copyright holder
185 'Copyright' => self::ASCII,
186 ],
187
188 # Exif IFD Attribute Information (p30-31)
189 'EXIF' => [
190 # @todo NOTE: Nonexistence of this field is taken to mean non-conformance
191 # to the Exif 2.1 AND 2.2 standards
192 'ExifVersion' => self::UNDEFINED,
193 # Supported Flashpix version #p32
194 'FlashPixVersion' => self::UNDEFINED,
195
196 # Tags relating to Image Data Characteristics
197 # Color space information #p32
198 'ColorSpace' => self::SHORT,
199
200 # Tags relating to image configuration
201 # Meaning of each component #p33
202 'ComponentsConfiguration' => self::UNDEFINED,
203 # Image compression mode
204 'CompressedBitsPerPixel' => self::RATIONAL,
205 # Valid image height
206 'PixelYDimension' => self::SHORT_OR_LONG,
207 # Valid image width
208 'PixelXDimension' => self::SHORT_OR_LONG,
209
210 # Tags relating to related user information
211 # Manufacturer notes
212 'MakerNote' => self::IGNORE,
213 # User comments #p34
214 'UserComment' => self::UNDEFINED,
215
216 # Tags relating to related file information
217 # Related audio file
218 'RelatedSoundFile' => self::ASCII,
219
220 # Tags relating to date and time
221 # Date and time of original data generation #p36
222 'DateTimeOriginal' => self::ASCII,
223 # Date and time of original data generation
224 'DateTimeDigitized' => self::ASCII,
225 # DateTime subseconds
226 'SubSecTime' => self::ASCII,
227 # DateTimeOriginal subseconds
228 'SubSecTimeOriginal' => self::ASCII,
229 # DateTimeDigitized subseconds
230 'SubSecTimeDigitized' => self::ASCII,
231
232 # Tags relating to picture-taking conditions (p31)
233 # Exposure time
234 'ExposureTime' => self::RATIONAL,
235 # F Number
236 'FNumber' => self::RATIONAL,
237 # Exposure Program #p38
238 'ExposureProgram' => self::SHORT,
239 # Spectral sensitivity
240 'SpectralSensitivity' => self::ASCII,
241 # ISO speed rating
242 'ISOSpeedRatings' => self::SHORT,
243
244 # Optoelectronic conversion factor. Note: We don't have support for this atm.
245 'OECF' => self::IGNORE,
246
247 # Shutter speed
248 'ShutterSpeedValue' => self::SRATIONAL,
249 # Aperture
250 'ApertureValue' => self::RATIONAL,
251 # Brightness
252 'BrightnessValue' => self::SRATIONAL,
253 # Exposure bias
254 'ExposureBiasValue' => self::SRATIONAL,
255 # Maximum land aperture
256 'MaxApertureValue' => self::RATIONAL,
257 # Subject distance
258 'SubjectDistance' => self::RATIONAL,
259 # Metering mode #p40
260 'MeteringMode' => self::SHORT,
261 # Light source #p40-41
262 'LightSource' => self::SHORT,
263 # Flash #p41-42
264 'Flash' => self::SHORT,
265 # Lens focal length
266 'FocalLength' => self::RATIONAL,
267 # Subject area
268 'SubjectArea' => [ self::SHORT, 4 ],
269 # Flash energy
270 'FlashEnergy' => self::RATIONAL,
271 # Spatial frequency response. Not supported atm.
272 'SpatialFrequencyResponse' => self::IGNORE,
273 # Focal plane X resolution
274 'FocalPlaneXResolution' => self::RATIONAL,
275 # Focal plane Y resolution
276 'FocalPlaneYResolution' => self::RATIONAL,
277 # Focal plane resolution unit #p46
278 'FocalPlaneResolutionUnit' => self::SHORT,
279 # Subject location
280 'SubjectLocation' => [ self::SHORT, 2 ],
281 # Exposure index
282 'ExposureIndex' => self::RATIONAL,
283 # Sensing method #p46
284 'SensingMethod' => self::SHORT,
285 # File source #p47
286 'FileSource' => self::UNDEFINED,
287 # Scene type #p47
288 'SceneType' => self::UNDEFINED,
289 # CFA pattern. not supported atm.
290 'CFAPattern' => self::IGNORE,
291 # Custom image processing #p48
292 'CustomRendered' => self::SHORT,
293 # Exposure mode #p48
294 'ExposureMode' => self::SHORT,
295 # White Balance #p49
296 'WhiteBalance' => self::SHORT,
297 # Digital zoom ratio
298 'DigitalZoomRatio' => self::RATIONAL,
299 # Focal length in 35 mm film
300 'FocalLengthIn35mmFilm' => self::SHORT,
301 # Scene capture type #p49
302 'SceneCaptureType' => self::SHORT,
303 # Scene control #p49-50
304 'GainControl' => self::SHORT,
305 # Contrast #p50
306 'Contrast' => self::SHORT,
307 # Saturation #p50
308 'Saturation' => self::SHORT,
309 # Sharpness #p50
310 'Sharpness' => self::SHORT,
311
312 # Device settings description. This could maybe be supported. Need to find an
313 # example file that uses this to see if it has stuff of interest in it.
314 'DeviceSettingDescription' => self::IGNORE,
315
316 # Subject distance range #p51
317 'SubjectDistanceRange' => self::SHORT,
318
319 # Unique image ID
320 'ImageUniqueID' => self::ASCII,
321 ],
322
323 # GPS Attribute Information (p52)
324 'GPS' => [
325 'GPSVersion' => self::UNDEFINED,
326 # Should be an array of 4 Exif::BYTE's. However, php treats it as an undefined
327 # Note exif standard calls this GPSVersionID, but php doesn't like the id suffix
328 # North or South Latitude #p52-53
329 'GPSLatitudeRef' => self::ASCII,
330 # Latitude
331 'GPSLatitude' => [ self::RATIONAL, 3 ],
332 # East or West Longitude #p53
333 'GPSLongitudeRef' => self::ASCII,
334 # Longitude
335 'GPSLongitude' => [ self::RATIONAL, 3 ],
336 'GPSAltitudeRef' => self::UNDEFINED,
337
338 # Altitude reference. Note, the exif standard says this should be an EXIF::Byte,
339 # but php seems to disagree.
340 # Altitude
341 'GPSAltitude' => self::RATIONAL,
342 # GPS time (atomic clock)
343 'GPSTimeStamp' => [ self::RATIONAL, 3 ],
344 # Satellites used for measurement
345 'GPSSatellites' => self::ASCII,
346 # Receiver status #p54
347 'GPSStatus' => self::ASCII,
348 # Measurement mode #p54-55
349 'GPSMeasureMode' => self::ASCII,
350 # Measurement precision
351 'GPSDOP' => self::RATIONAL,
352 # Speed unit #p55
353 'GPSSpeedRef' => self::ASCII,
354 # Speed of GPS receiver
355 'GPSSpeed' => self::RATIONAL,
356 # Reference for direction of movement #p55
357 'GPSTrackRef' => self::ASCII,
358 # Direction of movement
359 'GPSTrack' => self::RATIONAL,
360 # Reference for direction of image #p56
361 'GPSImgDirectionRef' => self::ASCII,
362 # Direction of image
363 'GPSImgDirection' => self::RATIONAL,
364 # Geodetic survey data used
365 'GPSMapDatum' => self::ASCII,
366 # Reference for latitude of destination #p56
367 'GPSDestLatitudeRef' => self::ASCII,
368 # Latitude destination
369 'GPSDestLatitude' => [ self::RATIONAL, 3 ],
370 # Reference for longitude of destination #p57
371 'GPSDestLongitudeRef' => self::ASCII,
372 # Longitude of destination
373 'GPSDestLongitude' => [ self::RATIONAL, 3 ],
374 # Reference for bearing of destination #p57
375 'GPSDestBearingRef' => self::ASCII,
376 # Bearing of destination
377 'GPSDestBearing' => self::RATIONAL,
378 # Reference for distance to destination #p57-58
379 'GPSDestDistanceRef' => self::ASCII,
380 # Distance to destination
381 'GPSDestDistance' => self::RATIONAL,
382 # Name of GPS processing method
383 'GPSProcessingMethod' => self::UNDEFINED,
384 # Name of GPS area
385 'GPSAreaInformation' => self::UNDEFINED,
386 # GPS date
387 'GPSDateStamp' => self::ASCII,
388 # GPS differential correction
389 'GPSDifferential' => self::SHORT,
390 ],
391 ];
392
393 $this->file = $file;
394 $this->basename = wfBaseName( $this->file );
395 if ( $byteOrder === 'BE' || $byteOrder === 'LE' ) {
396 $this->byteOrder = $byteOrder;
397 } else {
398 // Only give a warning for b/c, since originally we didn't
399 // require this. The number of things affected by this is
400 // rather small.
401 wfWarn( 'Exif class did not have byte order specified. ' .
402 'Some properties may be decoded incorrectly.' );
403 // BE seems about twice as popular as LE in jpg's.
404 $this->byteOrder = 'BE';
405 }
406
407 $this->debugFile( __FUNCTION__, true );
408
409 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
410 $data = @exif_read_data( $this->file, '', true );
411
417 $this->mRawExifData = $data ?: [];
418 $this->makeFilteredData();
419 $this->collapseData();
420 $this->debugFile( __FUNCTION__, false );
421 }
422
426 private function makeFilteredData() {
427 $this->mFilteredExifData = [];
428
429 foreach ( $this->mRawExifData as $section => $data ) {
430 if ( !array_key_exists( $section, $this->mExifTags ) ) {
431 $this->debug( $section, __FUNCTION__, "'$section' is not a valid Exif section" );
432 continue;
433 }
434
435 foreach ( $data as $tag => $value ) {
436 if ( !array_key_exists( $tag, $this->mExifTags[$section] ) ) {
437 $this->debug( $tag, __FUNCTION__, "'$tag' is not a valid tag in '$section'" );
438 continue;
439 }
440
441 if ( $this->validate( $section, $tag, $value ) ) {
442 // This is ok, as the tags in the different sections do not conflict.
443 // except in computed and thumbnail section, which we don't use.
444 $this->mFilteredExifData[$tag] = $value;
445 } else {
446 $this->debug( $value, __FUNCTION__, "'$tag' contained invalid data" );
447 }
448 }
449 }
450 }
451
470 private function collapseData() {
471 $this->exifGPStoNumber( 'GPSLatitude' );
472 $this->exifGPStoNumber( 'GPSDestLatitude' );
473 $this->exifGPStoNumber( 'GPSLongitude' );
474 $this->exifGPStoNumber( 'GPSDestLongitude' );
475
476 if ( isset( $this->mFilteredExifData['GPSAltitude'] ) ) {
477 // We know altitude data is a <num>/<denom> from the validation
478 // functions ran earlier. But multiplying such a string by -1
479 // doesn't work well, so convert.
480 [ $num, $denom ] = explode( '/', $this->mFilteredExifData['GPSAltitude'], 2 );
481 $this->mFilteredExifData['GPSAltitude'] = (int)$num / (int)$denom;
482
483 if ( isset( $this->mFilteredExifData['GPSAltitudeRef'] ) ) {
484 switch ( $this->mFilteredExifData['GPSAltitudeRef'] ) {
485 case "\0":
486 // Above sea level
487 break;
488 case "\1":
489 // Below sea level
490 $this->mFilteredExifData['GPSAltitude'] *= -1;
491 break;
492 default:
493 // Invalid
494 unset( $this->mFilteredExifData['GPSAltitude'] );
495 break;
496 }
497 }
498 }
499 unset( $this->mFilteredExifData['GPSAltitudeRef'] );
500
501 $this->exifPropToOrd( 'FileSource' );
502 $this->exifPropToOrd( 'SceneType' );
503
504 $this->charCodeString( 'UserComment' );
505 $this->charCodeString( 'GPSProcessingMethod' );
506 $this->charCodeString( 'GPSAreaInformation' );
507
508 // ComponentsConfiguration should really be an array instead of a string...
509 // This turns a string of binary numbers into an array of numbers.
510
511 if ( isset( $this->mFilteredExifData['ComponentsConfiguration'] ) ) {
512 $val = $this->mFilteredExifData['ComponentsConfiguration'];
513 $ccVals = [];
514
515 $strLen = strlen( $val );
516 for ( $i = 0; $i < $strLen; $i++ ) {
517 $ccVals[$i] = ord( substr( $val, $i, 1 ) );
518 }
519 // this is for formatting later.
520 $ccVals['_type'] = 'ol';
521 $this->mFilteredExifData['ComponentsConfiguration'] = $ccVals;
522 }
523
524 // GPSVersion(ID) is treated as the wrong type by php exif support.
525 // Go through each byte turning it into a version string.
526 // For example: "\x02\x02\x00\x00" -> "2.2.0.0"
527
528 // Also change exif tag name from GPSVersion (what php exif thinks it is)
529 // to GPSVersionID (what the exif standard thinks it is).
530
531 if ( isset( $this->mFilteredExifData['GPSVersion'] ) ) {
532 $val = $this->mFilteredExifData['GPSVersion'];
533 $newVal = '';
534
535 $strLen = strlen( $val );
536 for ( $i = 0; $i < $strLen; $i++ ) {
537 if ( $i !== 0 ) {
538 $newVal .= '.';
539 }
540 $newVal .= ord( substr( $val, $i, 1 ) );
541 }
542
543 if ( $this->byteOrder === 'LE' ) {
544 // Need to reverse the string
545 $newVal2 = '';
546 for ( $i = strlen( $newVal ) - 1; $i >= 0; $i-- ) {
547 $newVal2 .= substr( $newVal, $i, 1 );
548 }
549 $this->mFilteredExifData['GPSVersionID'] = $newVal2;
550 } else {
551 $this->mFilteredExifData['GPSVersionID'] = $newVal;
552 }
553 unset( $this->mFilteredExifData['GPSVersion'] );
554 }
555 }
556
563 private function charCodeString( $prop ) {
564 if ( isset( $this->mFilteredExifData[$prop] ) ) {
565 if ( strlen( $this->mFilteredExifData[$prop] ) <= 8 ) {
566 // invalid. Must be at least 9 bytes long.
567
568 $this->debug( $this->mFilteredExifData[$prop], __FUNCTION__, false );
569 unset( $this->mFilteredExifData[$prop] );
570
571 return;
572 }
573 $charCode = substr( $this->mFilteredExifData[$prop], 0, 8 );
574 $val = substr( $this->mFilteredExifData[$prop], 8 );
575
576 $charset = match ( $charCode ) {
577 "JIS\x00\x00\x00\x00\x00" => 'Shift-JIS',
578 "UNICODE\x00" => 'UTF-16' . $this->byteOrder,
579 default => null
580 };
581 if ( $charset ) {
582 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
583 $val = @iconv( $charset, 'UTF-8//IGNORE', $val );
584 } else {
585 // if valid utf-8, assume that, otherwise assume windows-1252
586 $valCopy = $val;
587 \UtfNormal\Validator::quickIsNFCVerify( $valCopy );
588 if ( $valCopy !== $val ) {
589 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
590 $val = @iconv( 'Windows-1252', 'UTF-8//IGNORE', $val );
591 }
592 }
593
594 // trim and check to make sure not only whitespace.
595 $val = trim( $val );
596 if ( $val === '' ) {
597 // only whitespace.
598 $this->debug( $this->mFilteredExifData[$prop], __FUNCTION__, "$prop: Is only whitespace" );
599 unset( $this->mFilteredExifData[$prop] );
600
601 return;
602 }
603
604 // all's good.
605 $this->mFilteredExifData[$prop] = $val;
606 }
607 }
608
615 private function exifPropToOrd( $prop ) {
616 if ( isset( $this->mFilteredExifData[$prop] ) ) {
617 $this->mFilteredExifData[$prop] = ord( $this->mFilteredExifData[$prop] );
618 }
619 }
620
626 private function exifGPStoNumber( $prop ) {
627 $loc = $this->mFilteredExifData[$prop] ?? null;
628 $dir = $this->mFilteredExifData[$prop . 'Ref'] ?? null;
629 $res = false;
630
631 if ( $loc !== null && in_array( $dir, [ 'N', 'S', 'E', 'W' ] ) ) {
632 if ( is_array( $loc ) && count( $loc ) === 3 ) {
633 [ $num, $denom ] = explode( '/', $loc[0], 2 );
634 $res = (int)$num / (int)$denom;
635 [ $num, $denom ] = explode( '/', $loc[1], 2 );
636 $res += ( (int)$num / (int)$denom ) * ( 1 / 60 );
637 [ $num, $denom ] = explode( '/', $loc[2], 2 );
638 $res += ( (int)$num / (int)$denom ) * ( 1 / 3600 );
639 } elseif ( is_string( $loc ) ) {
640 // This is non-standard, but occurs in the wild (T386208)
641 [ $num, $denom ] = explode( '/', $loc, 2 );
642 $res = (int)$num / (int)$denom;
643 }
644
645 if ( $res && ( $dir === 'S' || $dir === 'W' ) ) {
646 // make negative
647 $res *= -1;
648 }
649 }
650
651 // update the exif records.
652
653 // using !== as $res could potentially be 0
654 if ( $res !== false ) {
655 $this->mFilteredExifData[$prop] = $res;
656 } else {
657 // if invalid
658 unset( $this->mFilteredExifData[$prop] );
659 }
660 unset( $this->mFilteredExifData[$prop . 'Ref'] );
661 }
662
667 public function getData() {
668 return $this->mRawExifData;
669 }
670
675 public function getFilteredData() {
676 return $this->mFilteredExifData;
677 }
678
691 public static function version() {
692 return 2;
693 }
694
701 private function isByte( $in ) {
702 if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 255 ) {
703 $this->debug( $in, __FUNCTION__, true );
704
705 return true;
706 }
707
708 $this->debug( $in, __FUNCTION__, false );
709
710 return false;
711 }
712
717 private function isASCII( $in ) {
718 if ( is_array( $in ) ) {
719 return false;
720 }
721
722 if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) {
723 $this->debug( $in, __FUNCTION__, 'found a character that is not allowed' );
724
725 return false;
726 }
727
728 if ( preg_match( '/^\s*$/', $in ) ) {
729 $this->debug( $in, __FUNCTION__, 'input consisted solely of whitespace' );
730
731 return false;
732 }
733
734 return true;
735 }
736
741 private function isShort( $in ) {
742 if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 65536 ) {
743 $this->debug( $in, __FUNCTION__, true );
744
745 return true;
746 }
747
748 $this->debug( $in, __FUNCTION__, false );
749
750 return false;
751 }
752
757 private function isLong( $in ) {
758 if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 4_294_967_296 ) {
759 $this->debug( $in, __FUNCTION__, true );
760
761 return true;
762 }
763
764 $this->debug( $in, __FUNCTION__, false );
765
766 return false;
767 }
768
773 private function isRational( $in ) {
774 $m = [];
775
776 # Avoid division by zero
777 if ( !is_array( $in )
778 && preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m )
779 ) {
780 return $this->isLong( $m[1] ) && $this->isLong( $m[2] );
781 }
782
783 $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
784
785 return false;
786 }
787
792 private function isUndefined( $in ) {
793 $this->debug( $in, __FUNCTION__, true );
794
795 return true;
796 }
797
802 private function isSlong( $in ) {
803 if ( $this->isLong( abs( (float)$in ) ) ) {
804 $this->debug( $in, __FUNCTION__, true );
805
806 return true;
807 }
808
809 $this->debug( $in, __FUNCTION__, false );
810
811 return false;
812 }
813
818 private function isSrational( $in ) {
819 $m = [];
820
821 # Avoid division by zero
822 if ( !is_array( $in ) &&
823 preg_match( '/^(-?\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m )
824 ) {
825 return $this->isSlong( $m[0] ) && $this->isSlong( $m[1] );
826 }
827
828 $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
829
830 return false;
831 }
832
842 private function validate( $section, $tag, $val, $recursive = false ): bool {
843 $debug = "tag is '$tag'";
844 $etype = $this->mExifTags[$section][$tag];
845 $ecount = 1;
846 if ( is_array( $etype ) ) {
847 [ $etype, $ecount ] = $etype;
848 if ( $recursive ) {
849 // checking individual elements
850 $ecount = 1;
851 }
852 }
853
854 $count = 1;
855 if ( is_array( $val ) ) {
856 $count = count( $val );
857 if ( $ecount !== $count ) {
858 $this->debug( $val, __FUNCTION__, "Expected $ecount elements for $tag but got $count" );
859 return false;
860 }
861 }
862 // If there are multiple values, recursively validate each of them.
863 if ( $count > 1 ) {
864 foreach ( $val as $v ) {
865 if ( !$this->validate( $section, $tag, $v, true ) ) {
866 return false;
867 }
868 }
869
870 return true;
871 }
872
873 // NULL values are considered valid. T315202.
874 if ( $val === null ) {
875 return true;
876 }
877
878 // Does not work if not typecast
879 switch ( (string)$etype ) {
880 case (string)self::BYTE:
881 $this->debug( $val, __FUNCTION__, $debug );
882
883 return $this->isByte( $val );
884 case (string)self::ASCII:
885 $this->debug( $val, __FUNCTION__, $debug );
886
887 return $this->isASCII( $val );
888 case (string)self::SHORT:
889 $this->debug( $val, __FUNCTION__, $debug );
890
891 return $this->isShort( $val );
892 case (string)self::LONG:
893 $this->debug( $val, __FUNCTION__, $debug );
894
895 return $this->isLong( $val );
896 case (string)self::RATIONAL:
897 $this->debug( $val, __FUNCTION__, $debug );
898
899 return $this->isRational( $val );
900 case (string)self::SHORT_OR_LONG:
901 $this->debug( $val, __FUNCTION__, $debug );
902
903 return $this->isShort( $val ) || $this->isLong( $val );
904 case (string)self::UNDEFINED:
905 $this->debug( $val, __FUNCTION__, $debug );
906
907 return $this->isUndefined( $val );
908 case (string)self::SLONG:
909 $this->debug( $val, __FUNCTION__, $debug );
910
911 return $this->isSlong( $val );
912 case (string)self::SRATIONAL:
913 $this->debug( $val, __FUNCTION__, $debug );
914
915 return $this->isSrational( $val );
916 case (string)self::IGNORE:
917 $this->debug( $val, __FUNCTION__, $debug );
918
919 return false;
920 default:
921 $this->debug( $val, __FUNCTION__, "The tag '$tag' is unknown" );
922
923 return false;
924 }
925 }
926
934 private function debug( $in, $fname, $action = null ) {
935 if ( !$this->log ) {
936 return;
937 }
938 $type = get_debug_type( $in );
939 $class = ucfirst( __CLASS__ );
940 if ( is_array( $in ) ) {
941 $in = print_r( $in, true );
942 }
943
944 if ( $action === true ) {
945 wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)" );
946 } elseif ( $action === false ) {
947 wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)" );
948 } elseif ( $action === null ) {
949 wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)" );
950 } else {
951 wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')" );
952 }
953 }
954
961 private function debugFile( $fname, $io ) {
962 if ( !$this->log ) {
963 return;
964 }
965 $class = ucfirst( __CLASS__ );
966 if ( $io ) {
967 wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'" );
968 } else {
969 wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'" );
970 }
971 }
972}
973
975class_alias( Exif::class, 'Exif' );
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfBaseName( $path, $suffix='')
Return the final portion of a pathname.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Exceptions for config failures.
Class to extract and validate Exif data from jpeg (and possibly tiff) files.
Definition Exif.php:22
static version()
The version of the output format.
Definition Exif.php:691
getFilteredData()
Get $this->mFilteredExifData.
Definition Exif.php:675
__construct( $file, $byteOrder='')
Definition Exif.php:97
getData()
Get $this->mRawExifData.
Definition Exif.php:667