MediaWiki master
Exif.php
Go to the documentation of this file.
1<?php
15use Wikimedia\AtEase\AtEase;
16
21class Exif {
23 private const BYTE = 1;
24
28 private const ASCII = 2;
29
31 private const SHORT = 3;
32
34 private const LONG = 4;
35
39 private const RATIONAL = 5;
40
42 private const SHORT_OR_LONG = 6;
43
45 private const UNDEFINED = 7;
46
48 private const SLONG = 9;
49
53 private const SRATIONAL = 10;
54
56 private const IGNORE = -1;
57
62 private $mExifTags;
63
65 private $mRawExifData;
66
71 private $mFilteredExifData;
72
74 private $file;
75
77 private $basename;
78
80 private $log = false;
81
85 private $byteOrder;
86
96 public function __construct( $file, $byteOrder = '' ) {
97 if ( !function_exists( 'exif_read_data' ) ) {
98 throw new ConfigException(
99 "Internal error: exif_read_data not present. " .
100 "\$wgShowEXIF may be incorrectly set or not checked by an extension."
101 );
102 }
103
112 $this->mExifTags = [
113 # TIFF Rev. 6.0 Attribute Information (p22)
114 'IFD0' => [
115 # Tags relating to image structure
116 # Image width
117 'ImageWidth' => self::SHORT_OR_LONG,
118 # Image height
119 'ImageLength' => self::SHORT_OR_LONG,
120 # Number of bits per component
121 'BitsPerSample' => [ self::SHORT, 3 ],
122
123 # "When a primary image is JPEG compressed, this designation is not"
124 # "necessary and is omitted." (p23)
125 # Compression scheme #p23
126 'Compression' => self::SHORT,
127 # Pixel composition #p23
128 'PhotometricInterpretation' => self::SHORT,
129 # Orientation of image #p24
130 'Orientation' => self::SHORT,
131 # Number of components
132 'SamplesPerPixel' => self::SHORT,
133 # Image data arrangement #p24
134 'PlanarConfiguration' => self::SHORT,
135 # Subsampling ratio of Y to C #p24
136 'YCbCrSubSampling' => [ self::SHORT, 2 ],
137 # Y and C positioning #p24-25
138 'YCbCrPositioning' => self::SHORT,
139 # Image resolution in width direction
140 'XResolution' => self::RATIONAL,
141 # Image resolution in height direction
142 'YResolution' => self::RATIONAL,
143 # Unit of X and Y resolution #(p26)
144 'ResolutionUnit' => self::SHORT,
145
146 # Tags relating to recording offset
147 # Image data location
148 'StripOffsets' => self::SHORT_OR_LONG,
149 # Number of rows per strip
150 'RowsPerStrip' => self::SHORT_OR_LONG,
151 # Bytes per compressed strip
152 'StripByteCounts' => self::SHORT_OR_LONG,
153 # Offset to JPEG SOI
154 'JPEGInterchangeFormat' => self::SHORT_OR_LONG,
155 # Bytes of JPEG data
156 'JPEGInterchangeFormatLength' => self::SHORT_OR_LONG,
157
158 # Tags relating to image data characteristics
159 # Transfer function
160 'TransferFunction' => self::IGNORE,
161 # White point chromaticity
162 'WhitePoint' => [ self::RATIONAL, 2 ],
163 # Chromaticities of primarities
164 'PrimaryChromaticities' => [ self::RATIONAL, 6 ],
165 # Color space transformation matrix coefficients #p27
166 'YCbCrCoefficients' => [ self::RATIONAL, 3 ],
167 # Pair of black and white reference values
168 'ReferenceBlackWhite' => [ self::RATIONAL, 6 ],
169
170 # Other tags
171 # File change date and time
172 'DateTime' => self::ASCII,
173 # Image title
174 'ImageDescription' => self::ASCII,
175 # Image input equipment manufacturer
176 'Make' => self::ASCII,
177 # Image input equipment model
178 'Model' => self::ASCII,
179 # Software used
180 'Software' => self::ASCII,
181 # Person who created the image
182 'Artist' => self::ASCII,
183 # Copyright holder
184 'Copyright' => self::ASCII,
185 ],
186
187 # Exif IFD Attribute Information (p30-31)
188 'EXIF' => [
189 # @todo NOTE: Nonexistence of this field is taken to mean non-conformance
190 # to the Exif 2.1 AND 2.2 standards
191 'ExifVersion' => self::UNDEFINED,
192 # Supported Flashpix version #p32
193 'FlashPixVersion' => self::UNDEFINED,
194
195 # Tags relating to Image Data Characteristics
196 # Color space information #p32
197 'ColorSpace' => self::SHORT,
198
199 # Tags relating to image configuration
200 # Meaning of each component #p33
201 'ComponentsConfiguration' => self::UNDEFINED,
202 # Image compression mode
203 'CompressedBitsPerPixel' => self::RATIONAL,
204 # Valid image height
205 'PixelYDimension' => self::SHORT_OR_LONG,
206 # Valid image width
207 'PixelXDimension' => self::SHORT_OR_LONG,
208
209 # Tags relating to related user information
210 # Manufacturer notes
211 'MakerNote' => self::IGNORE,
212 # User comments #p34
213 'UserComment' => self::UNDEFINED,
214
215 # Tags relating to related file information
216 # Related audio file
217 'RelatedSoundFile' => self::ASCII,
218
219 # Tags relating to date and time
220 # Date and time of original data generation #p36
221 'DateTimeOriginal' => self::ASCII,
222 # Date and time of original data generation
223 'DateTimeDigitized' => self::ASCII,
224 # DateTime subseconds
225 'SubSecTime' => self::ASCII,
226 # DateTimeOriginal subseconds
227 'SubSecTimeOriginal' => self::ASCII,
228 # DateTimeDigitized subseconds
229 'SubSecTimeDigitized' => self::ASCII,
230
231 # Tags relating to picture-taking conditions (p31)
232 # Exposure time
233 'ExposureTime' => self::RATIONAL,
234 # F Number
235 'FNumber' => self::RATIONAL,
236 # Exposure Program #p38
237 'ExposureProgram' => self::SHORT,
238 # Spectral sensitivity
239 'SpectralSensitivity' => self::ASCII,
240 # ISO speed rating
241 'ISOSpeedRatings' => self::SHORT,
242
243 # Optoelectronic conversion factor. Note: We don't have support for this atm.
244 'OECF' => self::IGNORE,
245
246 # Shutter speed
247 'ShutterSpeedValue' => self::SRATIONAL,
248 # Aperture
249 'ApertureValue' => self::RATIONAL,
250 # Brightness
251 'BrightnessValue' => self::SRATIONAL,
252 # Exposure bias
253 'ExposureBiasValue' => self::SRATIONAL,
254 # Maximum land aperture
255 'MaxApertureValue' => self::RATIONAL,
256 # Subject distance
257 'SubjectDistance' => self::RATIONAL,
258 # Metering mode #p40
259 'MeteringMode' => self::SHORT,
260 # Light source #p40-41
261 'LightSource' => self::SHORT,
262 # Flash #p41-42
263 'Flash' => self::SHORT,
264 # Lens focal length
265 'FocalLength' => self::RATIONAL,
266 # Subject area
267 'SubjectArea' => [ self::SHORT, 4 ],
268 # Flash energy
269 'FlashEnergy' => self::RATIONAL,
270 # Spatial frequency response. Not supported atm.
271 'SpatialFrequencyResponse' => self::IGNORE,
272 # Focal plane X resolution
273 'FocalPlaneXResolution' => self::RATIONAL,
274 # Focal plane Y resolution
275 'FocalPlaneYResolution' => self::RATIONAL,
276 # Focal plane resolution unit #p46
277 'FocalPlaneResolutionUnit' => self::SHORT,
278 # Subject location
279 'SubjectLocation' => [ self::SHORT, 2 ],
280 # Exposure index
281 'ExposureIndex' => self::RATIONAL,
282 # Sensing method #p46
283 'SensingMethod' => self::SHORT,
284 # File source #p47
285 'FileSource' => self::UNDEFINED,
286 # Scene type #p47
287 'SceneType' => self::UNDEFINED,
288 # CFA pattern. not supported atm.
289 'CFAPattern' => self::IGNORE,
290 # Custom image processing #p48
291 'CustomRendered' => self::SHORT,
292 # Exposure mode #p48
293 'ExposureMode' => self::SHORT,
294 # White Balance #p49
295 'WhiteBalance' => self::SHORT,
296 # Digital zoom ratio
297 'DigitalZoomRatio' => self::RATIONAL,
298 # Focal length in 35 mm film
299 'FocalLengthIn35mmFilm' => self::SHORT,
300 # Scene capture type #p49
301 'SceneCaptureType' => self::SHORT,
302 # Scene control #p49-50
303 'GainControl' => self::SHORT,
304 # Contrast #p50
305 'Contrast' => self::SHORT,
306 # Saturation #p50
307 'Saturation' => self::SHORT,
308 # Sharpness #p50
309 'Sharpness' => self::SHORT,
310
311 # Device settings description. This could maybe be supported. Need to find an
312 # example file that uses this to see if it has stuff of interest in it.
313 'DeviceSettingDescription' => self::IGNORE,
314
315 # Subject distance range #p51
316 'SubjectDistanceRange' => self::SHORT,
317
318 # Unique image ID
319 'ImageUniqueID' => self::ASCII,
320 ],
321
322 # GPS Attribute Information (p52)
323 'GPS' => [
324 'GPSVersion' => self::UNDEFINED,
325 # Should be an array of 4 Exif::BYTE's. However, php treats it as an undefined
326 # Note exif standard calls this GPSVersionID, but php doesn't like the id suffix
327 # North or South Latitude #p52-53
328 'GPSLatitudeRef' => self::ASCII,
329 # Latitude
330 'GPSLatitude' => [ self::RATIONAL, 3 ],
331 # East or West Longitude #p53
332 'GPSLongitudeRef' => self::ASCII,
333 # Longitude
334 'GPSLongitude' => [ self::RATIONAL, 3 ],
335 'GPSAltitudeRef' => self::UNDEFINED,
336
337 # Altitude reference. Note, the exif standard says this should be an EXIF::Byte,
338 # but php seems to disagree.
339 # Altitude
340 'GPSAltitude' => self::RATIONAL,
341 # GPS time (atomic clock)
342 'GPSTimeStamp' => [ self::RATIONAL, 3 ],
343 # Satellites used for measurement
344 'GPSSatellites' => self::ASCII,
345 # Receiver status #p54
346 'GPSStatus' => self::ASCII,
347 # Measurement mode #p54-55
348 'GPSMeasureMode' => self::ASCII,
349 # Measurement precision
350 'GPSDOP' => self::RATIONAL,
351 # Speed unit #p55
352 'GPSSpeedRef' => self::ASCII,
353 # Speed of GPS receiver
354 'GPSSpeed' => self::RATIONAL,
355 # Reference for direction of movement #p55
356 'GPSTrackRef' => self::ASCII,
357 # Direction of movement
358 'GPSTrack' => self::RATIONAL,
359 # Reference for direction of image #p56
360 'GPSImgDirectionRef' => self::ASCII,
361 # Direction of image
362 'GPSImgDirection' => self::RATIONAL,
363 # Geodetic survey data used
364 'GPSMapDatum' => self::ASCII,
365 # Reference for latitude of destination #p56
366 'GPSDestLatitudeRef' => self::ASCII,
367 # Latitude destination
368 'GPSDestLatitude' => [ self::RATIONAL, 3 ],
369 # Reference for longitude of destination #p57
370 'GPSDestLongitudeRef' => self::ASCII,
371 # Longitude of destination
372 'GPSDestLongitude' => [ self::RATIONAL, 3 ],
373 # Reference for bearing of destination #p57
374 'GPSDestBearingRef' => self::ASCII,
375 # Bearing of destination
376 'GPSDestBearing' => self::RATIONAL,
377 # Reference for distance to destination #p57-58
378 'GPSDestDistanceRef' => self::ASCII,
379 # Distance to destination
380 'GPSDestDistance' => self::RATIONAL,
381 # Name of GPS processing method
382 'GPSProcessingMethod' => self::UNDEFINED,
383 # Name of GPS area
384 'GPSAreaInformation' => self::UNDEFINED,
385 # GPS date
386 'GPSDateStamp' => self::ASCII,
387 # GPS differential correction
388 'GPSDifferential' => self::SHORT,
389 ],
390 ];
391
392 $this->file = $file;
393 $this->basename = wfBaseName( $this->file );
394 if ( $byteOrder === 'BE' || $byteOrder === 'LE' ) {
395 $this->byteOrder = $byteOrder;
396 } else {
397 // Only give a warning for b/c, since originally we didn't
398 // require this. The number of things affected by this is
399 // rather small.
400 wfWarn( 'Exif class did not have byte order specified. ' .
401 'Some properties may be decoded incorrectly.' );
402 // BE seems about twice as popular as LE in jpg's.
403 $this->byteOrder = 'BE';
404 }
405
406 $this->debugFile( __FUNCTION__, true );
407
408 AtEase::suppressWarnings();
409 $data = exif_read_data( $this->file, '', true );
410 AtEase::restoreWarnings();
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 AtEase::suppressWarnings();
583 $val = iconv( $charset, 'UTF-8//IGNORE', $val );
584 AtEase::restoreWarnings();
585 } else {
586 // if valid utf-8, assume that, otherwise assume windows-1252
587 $valCopy = $val;
588 UtfNormal\Validator::quickIsNFCVerify( $valCopy );
589 if ( $valCopy !== $val ) {
590 AtEase::suppressWarnings();
591 $val = iconv( 'Windows-1252', 'UTF-8//IGNORE', $val );
592 AtEase::restoreWarnings();
593 }
594 }
595
596 // trim and check to make sure not only whitespace.
597 $val = trim( $val );
598 if ( $val === '' ) {
599 // only whitespace.
600 $this->debug( $this->mFilteredExifData[$prop], __FUNCTION__, "$prop: Is only whitespace" );
601 unset( $this->mFilteredExifData[$prop] );
602
603 return;
604 }
605
606 // all's good.
607 $this->mFilteredExifData[$prop] = $val;
608 }
609 }
610
617 private function exifPropToOrd( $prop ) {
618 if ( isset( $this->mFilteredExifData[$prop] ) ) {
619 $this->mFilteredExifData[$prop] = ord( $this->mFilteredExifData[$prop] );
620 }
621 }
622
628 private function exifGPStoNumber( $prop ) {
629 $loc = $this->mFilteredExifData[$prop] ?? null;
630 $dir = $this->mFilteredExifData[$prop . 'Ref'] ?? null;
631 $res = false;
632
633 if ( $loc !== null && in_array( $dir, [ 'N', 'S', 'E', 'W' ] ) ) {
634 if ( is_array( $loc ) && count( $loc ) === 3 ) {
635 [ $num, $denom ] = explode( '/', $loc[0], 2 );
636 $res = (int)$num / (int)$denom;
637 [ $num, $denom ] = explode( '/', $loc[1], 2 );
638 $res += ( (int)$num / (int)$denom ) * ( 1 / 60 );
639 [ $num, $denom ] = explode( '/', $loc[2], 2 );
640 $res += ( (int)$num / (int)$denom ) * ( 1 / 3600 );
641 } elseif ( is_string( $loc ) ) {
642 // This is non-standard, but occurs in the wild (T386208)
643 [ $num, $denom ] = explode( '/', $loc, 2 );
644 $res = (int)$num / (int)$denom;
645 }
646
647 if ( $res && ( $dir === 'S' || $dir === 'W' ) ) {
648 // make negative
649 $res *= -1;
650 }
651 }
652
653 // update the exif records.
654
655 // using !== as $res could potentially be 0
656 if ( $res !== false ) {
657 $this->mFilteredExifData[$prop] = $res;
658 } else {
659 // if invalid
660 unset( $this->mFilteredExifData[$prop] );
661 }
662 unset( $this->mFilteredExifData[$prop . 'Ref'] );
663 }
664
669 public function getData() {
670 return $this->mRawExifData;
671 }
672
677 public function getFilteredData() {
678 return $this->mFilteredExifData;
679 }
680
693 public static function version() {
694 return 2;
695 }
696
703 private function isByte( $in ) {
704 if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 255 ) {
705 $this->debug( $in, __FUNCTION__, true );
706
707 return true;
708 }
709
710 $this->debug( $in, __FUNCTION__, false );
711
712 return false;
713 }
714
719 private function isASCII( $in ) {
720 if ( is_array( $in ) ) {
721 return false;
722 }
723
724 if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) {
725 $this->debug( $in, __FUNCTION__, 'found a character that is not allowed' );
726
727 return false;
728 }
729
730 if ( preg_match( '/^\s*$/', $in ) ) {
731 $this->debug( $in, __FUNCTION__, 'input consisted solely of whitespace' );
732
733 return false;
734 }
735
736 return true;
737 }
738
743 private function isShort( $in ) {
744 if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 65536 ) {
745 $this->debug( $in, __FUNCTION__, true );
746
747 return true;
748 }
749
750 $this->debug( $in, __FUNCTION__, false );
751
752 return false;
753 }
754
759 private function isLong( $in ) {
760 if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 4_294_967_296 ) {
761 $this->debug( $in, __FUNCTION__, true );
762
763 return true;
764 }
765
766 $this->debug( $in, __FUNCTION__, false );
767
768 return false;
769 }
770
775 private function isRational( $in ) {
776 $m = [];
777
778 # Avoid division by zero
779 if ( !is_array( $in )
780 && preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m )
781 ) {
782 return $this->isLong( $m[1] ) && $this->isLong( $m[2] );
783 }
784
785 $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
786
787 return false;
788 }
789
794 private function isUndefined( $in ) {
795 $this->debug( $in, __FUNCTION__, true );
796
797 return true;
798 }
799
804 private function isSlong( $in ) {
805 if ( $this->isLong( abs( (float)$in ) ) ) {
806 $this->debug( $in, __FUNCTION__, true );
807
808 return true;
809 }
810
811 $this->debug( $in, __FUNCTION__, false );
812
813 return false;
814 }
815
820 private function isSrational( $in ) {
821 $m = [];
822
823 # Avoid division by zero
824 if ( !is_array( $in ) &&
825 preg_match( '/^(-?\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m )
826 ) {
827 return $this->isSlong( $m[0] ) && $this->isSlong( $m[1] );
828 }
829
830 $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
831
832 return false;
833 }
834
844 private function validate( $section, $tag, $val, $recursive = false ): bool {
845 $debug = "tag is '$tag'";
846 $etype = $this->mExifTags[$section][$tag];
847 $ecount = 1;
848 if ( is_array( $etype ) ) {
849 [ $etype, $ecount ] = $etype;
850 if ( $recursive ) {
851 // checking individual elements
852 $ecount = 1;
853 }
854 }
855
856 $count = 1;
857 if ( is_array( $val ) ) {
858 $count = count( $val );
859 if ( $ecount !== $count ) {
860 $this->debug( $val, __FUNCTION__, "Expected $ecount elements for $tag but got $count" );
861 return false;
862 }
863 }
864 // If there are multiple values, recursively validate each of them.
865 if ( $count > 1 ) {
866 foreach ( $val as $v ) {
867 if ( !$this->validate( $section, $tag, $v, true ) ) {
868 return false;
869 }
870 }
871
872 return true;
873 }
874
875 // NULL values are considered valid. T315202.
876 if ( $val === null ) {
877 return true;
878 }
879
880 // Does not work if not typecast
881 switch ( (string)$etype ) {
882 case (string)self::BYTE:
883 $this->debug( $val, __FUNCTION__, $debug );
884
885 return $this->isByte( $val );
886 case (string)self::ASCII:
887 $this->debug( $val, __FUNCTION__, $debug );
888
889 return $this->isASCII( $val );
890 case (string)self::SHORT:
891 $this->debug( $val, __FUNCTION__, $debug );
892
893 return $this->isShort( $val );
894 case (string)self::LONG:
895 $this->debug( $val, __FUNCTION__, $debug );
896
897 return $this->isLong( $val );
898 case (string)self::RATIONAL:
899 $this->debug( $val, __FUNCTION__, $debug );
900
901 return $this->isRational( $val );
902 case (string)self::SHORT_OR_LONG:
903 $this->debug( $val, __FUNCTION__, $debug );
904
905 return $this->isShort( $val ) || $this->isLong( $val );
906 case (string)self::UNDEFINED:
907 $this->debug( $val, __FUNCTION__, $debug );
908
909 return $this->isUndefined( $val );
910 case (string)self::SLONG:
911 $this->debug( $val, __FUNCTION__, $debug );
912
913 return $this->isSlong( $val );
914 case (string)self::SRATIONAL:
915 $this->debug( $val, __FUNCTION__, $debug );
916
917 return $this->isSrational( $val );
918 case (string)self::IGNORE:
919 $this->debug( $val, __FUNCTION__, $debug );
920
921 return false;
922 default:
923 $this->debug( $val, __FUNCTION__, "The tag '$tag' is unknown" );
924
925 return false;
926 }
927 }
928
936 private function debug( $in, $fname, $action = null ) {
937 if ( !$this->log ) {
938 return;
939 }
940 $type = get_debug_type( $in );
941 $class = ucfirst( __CLASS__ );
942 if ( is_array( $in ) ) {
943 $in = print_r( $in, true );
944 }
945
946 if ( $action === true ) {
947 wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)" );
948 } elseif ( $action === false ) {
949 wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)" );
950 } elseif ( $action === null ) {
951 wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)" );
952 } else {
953 wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')" );
954 }
955 }
956
963 private function debugFile( $fname, $io ) {
964 if ( !$this->log ) {
965 return;
966 }
967 $class = ucfirst( __CLASS__ );
968 if ( $io ) {
969 wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'" );
970 } else {
971 wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'" );
972 }
973 }
974}
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:21
getData()
Get $this->mRawExifData.
Definition Exif.php:669
static version()
The version of the output format.
Definition Exif.php:693
getFilteredData()
Get $this->mFilteredExifData.
Definition Exif.php:677
__construct( $file, $byteOrder='')
Definition Exif.php:96
Exceptions for config failures.