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