MediaWiki  master
Exif.php
Go to the documentation of this file.
1 <?php
28 use Wikimedia\AtEase\AtEase;
29 
34 class Exif {
36  private const BYTE = 1;
37 
41  private const ASCII = 2;
42 
44  private const SHORT = 3;
45 
47  private const LONG = 4;
48 
52  private const RATIONAL = 5;
53 
55  private const SHORT_OR_LONG = 6;
56 
58  private const UNDEFINED = 7;
59 
61  private const SLONG = 9;
62 
66  private const SRATIONAL = 10;
67 
69  private const IGNORE = -1;
70 
75  private $mExifTags;
76 
78  private $mRawExifData;
79 
84  private $mFilteredExifData;
85 
87  private $file;
88 
90  private $basename;
91 
93  private $log = false;
94 
98  private $byteOrder;
99 
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 MWException( "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  unset( $this->mFilteredExifData[$prop . 'Ref'] );
672  } else {
673  // if invalid
674  unset( $this->mFilteredExifData[$prop] );
675  unset( $this->mFilteredExifData[$prop . 'Ref'] );
676  }
677  }
678 
689  public function getData() {
690  return $this->mRawExifData;
691  }
692 
697  public function getFilteredData() {
698  return $this->mFilteredExifData;
699  }
700 
715  public static function version() {
716  return 2;
717  }
718 
725  private function isByte( $in ) {
726  if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 255 ) {
727  $this->debug( $in, __FUNCTION__, true );
728 
729  return true;
730  }
731 
732  $this->debug( $in, __FUNCTION__, false );
733 
734  return false;
735  }
736 
741  private function isASCII( $in ) {
742  if ( is_array( $in ) ) {
743  return false;
744  }
745 
746  if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) {
747  $this->debug( $in, __FUNCTION__, 'found a character that is not allowed' );
748 
749  return false;
750  }
751 
752  if ( preg_match( '/^\s*$/', $in ) ) {
753  $this->debug( $in, __FUNCTION__, 'input consisted solely of whitespace' );
754 
755  return false;
756  }
757 
758  return true;
759  }
760 
765  private function isShort( $in ) {
766  if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 65536 ) {
767  $this->debug( $in, __FUNCTION__, true );
768 
769  return true;
770  }
771 
772  $this->debug( $in, __FUNCTION__, false );
773 
774  return false;
775  }
776 
781  private function isLong( $in ) {
782  if ( !is_array( $in ) && sprintf( '%d', $in ) == $in && $in >= 0 && $in <= 4294967296 ) {
783  $this->debug( $in, __FUNCTION__, true );
784 
785  return true;
786  }
787 
788  $this->debug( $in, __FUNCTION__, false );
789 
790  return false;
791  }
792 
797  private function isRational( $in ) {
798  $m = [];
799 
800  # Avoid division by zero
801  if ( !is_array( $in )
802  && preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m )
803  ) {
804  return $this->isLong( $m[1] ) && $this->isLong( $m[2] );
805  }
806 
807  $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
808 
809  return false;
810  }
811 
816  private function isUndefined( $in ) {
817  $this->debug( $in, __FUNCTION__, true );
818 
819  return true;
820  }
821 
826  private function isSlong( $in ) {
827  if ( $this->isLong( abs( (float)$in ) ) ) {
828  $this->debug( $in, __FUNCTION__, true );
829 
830  return true;
831  }
832 
833  $this->debug( $in, __FUNCTION__, false );
834 
835  return false;
836  }
837 
842  private function isSrational( $in ) {
843  $m = [];
844 
845  # Avoid division by zero
846  if ( !is_array( $in ) &&
847  preg_match( '/^(-?\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m )
848  ) {
849  return $this->isSlong( $m[0] ) && $this->isSlong( $m[1] );
850  }
851 
852  $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
853 
854  return false;
855  }
856 
868  private function validate( $section, $tag, $val, $recursive = false ): bool {
869  $debug = "tag is '$tag'";
870  $etype = $this->mExifTags[$section][$tag];
871  $ecount = 1;
872  if ( is_array( $etype ) ) {
873  [ $etype, $ecount ] = $etype;
874  if ( $recursive ) {
875  // checking individual elements
876  $ecount = 1;
877  }
878  }
879 
880  $count = 1;
881  if ( is_array( $val ) ) {
882  $count = count( $val );
883  if ( $ecount != $count ) {
884  $this->debug( $val, __FUNCTION__, "Expected $ecount elements for $tag but got $count" );
885  return false;
886  }
887  }
888  // If there are multiple values, recursively validate each of them.
889  if ( $count > 1 ) {
890  foreach ( $val as $v ) {
891  if ( !$this->validate( $section, $tag, $v, true ) ) {
892  return false;
893  }
894  }
895 
896  return true;
897  }
898 
899  // NULL values are considered valid. T315202.
900  if ( $val === null ) {
901  return true;
902  }
903 
904  // Does not work if not typecast
905  switch ( (string)$etype ) {
906  case (string)self::BYTE:
907  $this->debug( $val, __FUNCTION__, $debug );
908 
909  return $this->isByte( $val );
910  case (string)self::ASCII:
911  $this->debug( $val, __FUNCTION__, $debug );
912 
913  return $this->isASCII( $val );
914  case (string)self::SHORT:
915  $this->debug( $val, __FUNCTION__, $debug );
916 
917  return $this->isShort( $val );
918  case (string)self::LONG:
919  $this->debug( $val, __FUNCTION__, $debug );
920 
921  return $this->isLong( $val );
922  case (string)self::RATIONAL:
923  $this->debug( $val, __FUNCTION__, $debug );
924 
925  return $this->isRational( $val );
926  case (string)self::SHORT_OR_LONG:
927  $this->debug( $val, __FUNCTION__, $debug );
928 
929  return $this->isShort( $val ) || $this->isLong( $val );
930  case (string)self::UNDEFINED:
931  $this->debug( $val, __FUNCTION__, $debug );
932 
933  return $this->isUndefined( $val );
934  case (string)self::SLONG:
935  $this->debug( $val, __FUNCTION__, $debug );
936 
937  return $this->isSlong( $val );
938  case (string)self::SRATIONAL:
939  $this->debug( $val, __FUNCTION__, $debug );
940 
941  return $this->isSrational( $val );
942  case (string)self::IGNORE:
943  $this->debug( $val, __FUNCTION__, $debug );
944 
945  return false;
946  default:
947  $this->debug( $val, __FUNCTION__, "The tag '$tag' is unknown" );
948 
949  return false;
950  }
951  }
952 
960  private function debug( $in, $fname, $action = null ) {
961  if ( !$this->log ) {
962  return;
963  }
964  $type = gettype( $in );
965  $class = ucfirst( __CLASS__ );
966  if ( is_array( $in ) ) {
967  $in = print_r( $in, true );
968  }
969 
970  if ( $action === true ) {
971  wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)" );
972  } elseif ( $action === false ) {
973  wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)" );
974  } elseif ( $action === null ) {
975  wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)" );
976  } else {
977  wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')" );
978  }
979  }
980 
987  private function debugFile( $fname, $io ) {
988  if ( !$this->log ) {
989  return;
990  }
991  $class = ucfirst( __CLASS__ );
992  if ( $io ) {
993  wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'" );
994  } else {
995  wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'" );
996  }
997  }
998 }
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:34
getData()
#-
Definition: Exif.php:689
static version()
#-
Definition: Exif.php:715
getFilteredData()
Get $this->mFilteredExifData.
Definition: Exif.php:697
__construct( $file, $byteOrder='')
Definition: Exif.php:110
MediaWiki exception.
Definition: MWException.php:30
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42