MediaWiki  master
FormatMetadata.php
Go to the documentation of this file.
1 <?php
28 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
31 use Wikimedia\Timestamp\TimestampException;
32 
55  use ProtectedHookAccessorTrait;
56 
62  protected $singleLang = false;
63 
70  public function setSingleLanguage( $val ) {
71  $this->singleLang = $val;
72  }
73 
87  public static function getFormattedData( $tags, $context = false ) {
88  $obj = new FormatMetadata;
89  if ( $context ) {
90  $obj->setContext( $context );
91  }
92 
93  return $obj->makeFormattedData( $tags );
94  }
95 
107  public function makeFormattedData( $tags ) {
108  $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3;
109  unset( $tags['ResolutionUnit'] );
110 
111  // Ignore these complex values
112  unset( $tags['HasExtendedXMP'] );
113  unset( $tags['AuthorsPosition'] );
114  unset( $tags['LocationCreated'] );
115  unset( $tags['LocationShown'] );
116  unset( $tags['GPSAltitudeRef'] );
117 
118  foreach ( $tags as $tag => &$vals ) {
119  // This seems ugly to wrap non-array's in an array just to unwrap again,
120  // especially when most of the time it is not an array
121  if ( !is_array( $vals ) ) {
122  $vals = [ $vals ];
123  }
124 
125  // _type is a special value to say what array type
126  if ( isset( $vals['_type'] ) ) {
127  $type = $vals['_type'];
128  unset( $vals['_type'] );
129  } else {
130  $type = 'ul'; // default unordered list.
131  }
132 
133  // _formatted is a special value to indicate the subclass
134  // already handled & formatted this tag as wikitext
135  if ( isset( $tags[$tag]['_formatted'] ) ) {
136  $tags[$tag] = $this->flattenArrayReal(
137  $tags[$tag]['_formatted'], $type
138  );
139  continue;
140  }
141 
142  // This is done differently as the tag is an array.
143  if ( $tag == 'GPSTimeStamp' && count( $vals ) === 3 ) {
144  // hour min sec array
145 
146  $h = explode( '/', $vals[0] );
147  $m = explode( '/', $vals[1] );
148  $s = explode( '/', $vals[2] );
149 
150  // this should already be validated
151  // when loaded from file, but it could
152  // come from a foreign repo, so be
153  // paranoid.
154  if ( !isset( $h[1] )
155  || !isset( $m[1] )
156  || !isset( $s[1] )
157  || $h[1] == 0
158  || $m[1] == 0
159  || $s[1] == 0
160  ) {
161  continue;
162  }
163  $vals = str_pad( intval( $h[0] / $h[1] ), 2, '0', STR_PAD_LEFT )
164  . ':' . str_pad( intval( $m[0] / $m[1] ), 2, '0', STR_PAD_LEFT )
165  . ':' . str_pad( intval( $s[0] / $s[1] ), 2, '0', STR_PAD_LEFT );
166 
167  try {
168  $time = wfTimestamp( TS_MW, '1971:01:01 ' . $vals );
169  // the 1971:01:01 is just a placeholder, and not shown to user.
170  if ( $time && intval( $time ) > 0 ) {
171  $vals = $this->getLanguage()->time( $time );
172  }
173  } catch ( TimestampException $e ) {
174  // This shouldn't happen, but we've seen bad formats
175  // such as 4-digit seconds in the wild.
176  // leave $vals as-is
177  }
178  continue;
179  }
180 
181  // The contact info is a multi-valued field
182  // instead of the other props which are single
183  // valued (mostly) so handle as a special case.
184  if ( $tag === 'Contact' || $tag === 'CreatorContactInfo' ) {
185  $vals = $this->collapseContactInfo( $vals );
186  continue;
187  }
188 
189  foreach ( $vals as &$val ) {
190  switch ( $tag ) {
191  case 'Compression':
192  switch ( $val ) {
193  case 1:
194  case 2:
195  case 3:
196  case 4:
197  case 5:
198  case 6:
199  case 7:
200  case 8:
201  case 32773:
202  case 32946:
203  case 34712:
204  $val = $this->exifMsg( $tag, $val );
205  break;
206  default:
207  /* If not recognized, display as is. */
208  $val = $this->literal( $val );
209  break;
210  }
211  break;
212 
213  case 'PhotometricInterpretation':
214  switch ( $val ) {
215  case 0:
216  case 1:
217  case 2:
218  case 3:
219  case 4:
220  case 5:
221  case 6:
222  case 8:
223  case 9:
224  case 10:
225  case 32803:
226  case 34892:
227  $val = $this->exifMsg( $tag, $val );
228  break;
229  default:
230  /* If not recognized, display as is. */
231  $val = $this->literal( $val );
232  break;
233  }
234  break;
235 
236  case 'Orientation':
237  switch ( $val ) {
238  case 1:
239  case 2:
240  case 3:
241  case 4:
242  case 5:
243  case 6:
244  case 7:
245  case 8:
246  $val = $this->exifMsg( $tag, $val );
247  break;
248  default:
249  /* If not recognized, display as is. */
250  $val = $this->literal( $val );
251  break;
252  }
253  break;
254 
255  case 'PlanarConfiguration':
256  switch ( $val ) {
257  case 1:
258  case 2:
259  $val = $this->exifMsg( $tag, $val );
260  break;
261  default:
262  /* If not recognized, display as is. */
263  $val = $this->literal( $val );
264  break;
265  }
266  break;
267 
268  // TODO: YCbCrSubSampling
269  case 'YCbCrPositioning':
270  switch ( $val ) {
271  case 1:
272  case 2:
273  $val = $this->exifMsg( $tag, $val );
274  break;
275  default:
276  /* If not recognized, display as is. */
277  $val = $this->literal( $val );
278  break;
279  }
280  break;
281 
282  case 'XResolution':
283  case 'YResolution':
284  switch ( $resolutionunit ) {
285  case 2:
286  $val = $this->exifMsg( 'XYResolution', 'i', $this->formatNum( $val ) );
287  break;
288  case 3:
289  $val = $this->exifMsg( 'XYResolution', 'c', $this->formatNum( $val ) );
290  break;
291  default:
292  /* If not recognized, display as is. */
293  $val = $this->literal( $val );
294  break;
295  }
296  break;
297 
298  // TODO: YCbCrCoefficients #p27 (see annex E)
299  case 'ExifVersion':
300  // PHP likes to be the odd one out with casing of FlashPixVersion;
301  // https://www.exif.org/Exif2-2.PDF#page=32 and
302  // https://www.digitalgalen.net/Documents/External/XMP/XMPSpecificationPart2.pdf#page=51
303  // both use FlashpixVersion. However, since at least 2002, PHP has used FlashPixVersion at
304  // https://github.com/php/php-src/blame/master/ext/exif/exif.c#L725
305  case 'FlashPixVersion':
306  // But we can still get the correct casing from
307  // Wikimedia\XMPReader on PDFs
308  case 'FlashpixVersion':
309  $val = $this->literal( (int)$val / 100 );
310  break;
311 
312  case 'ColorSpace':
313  switch ( $val ) {
314  case 1:
315  case 65535:
316  $val = $this->exifMsg( $tag, $val );
317  break;
318  default:
319  /* If not recognized, display as is. */
320  $val = $this->literal( $val );
321  break;
322  }
323  break;
324 
325  case 'ComponentsConfiguration':
326  switch ( $val ) {
327  case 0:
328  case 1:
329  case 2:
330  case 3:
331  case 4:
332  case 5:
333  case 6:
334  $val = $this->exifMsg( $tag, $val );
335  break;
336  default:
337  /* If not recognized, display as is. */
338  $val = $this->literal( $val );
339  break;
340  }
341  break;
342 
343  case 'DateTime':
344  case 'DateTimeOriginal':
345  case 'DateTimeDigitized':
346  case 'DateTimeReleased':
347  case 'DateTimeExpires':
348  case 'GPSDateStamp':
349  case 'dc-date':
350  case 'DateTimeMetadata':
351  case 'FirstPhotoDate':
352  case 'LastPhotoDate':
353  if ( $val == '0000:00:00 00:00:00' || $val == ' : : : : ' ) {
354  $val = $this->msg( 'exif-unknowndate' )->text();
355  break;
356  } elseif ( preg_match(
357  '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D',
358  $val
359  ) ) {
360  // Full date.
361  $time = wfTimestamp( TS_MW, $val );
362  if ( $time && intval( $time ) > 0 ) {
363  $val = $this->getLanguage()->timeanddate( $time );
364  break;
365  }
366  } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d)$/D', $val ) ) {
367  // No second field. Still format the same
368  // since timeanddate doesn't include seconds anyways,
369  // but second still available in api
370  $time = wfTimestamp( TS_MW, $val . ':00' );
371  if ( $time && intval( $time ) > 0 ) {
372  $val = $this->getLanguage()->timeanddate( $time );
373  break;
374  }
375  } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d)$/D', $val ) ) {
376  // If only the date but not the time is filled in.
377  $time = wfTimestamp( TS_MW, substr( $val, 0, 4 )
378  . substr( $val, 5, 2 )
379  . substr( $val, 8, 2 )
380  . '000000' );
381  if ( $time && intval( $time ) > 0 ) {
382  $val = $this->getLanguage()->date( $time );
383  break;
384  }
385  }
386  // else it will just output $val without formatting it.
387  $val = $this->literal( $val );
388  break;
389 
390  case 'ExposureProgram':
391  switch ( $val ) {
392  case 0:
393  case 1:
394  case 2:
395  case 3:
396  case 4:
397  case 5:
398  case 6:
399  case 7:
400  case 8:
401  $val = $this->exifMsg( $tag, $val );
402  break;
403  default:
404  /* If not recognized, display as is. */
405  $val = $this->literal( $val );
406  break;
407  }
408  break;
409 
410  case 'SubjectDistance':
411  $val = $this->exifMsg( $tag, '', $this->formatNum( $val ) );
412  break;
413 
414  case 'MeteringMode':
415  switch ( $val ) {
416  case 0:
417  case 1:
418  case 2:
419  case 3:
420  case 4:
421  case 5:
422  case 6:
423  case 7:
424  case 255:
425  $val = $this->exifMsg( $tag, $val );
426  break;
427  default:
428  /* If not recognized, display as is. */
429  $val = $this->literal( $val );
430  break;
431  }
432  break;
433 
434  case 'LightSource':
435  switch ( $val ) {
436  case 0:
437  case 1:
438  case 2:
439  case 3:
440  case 4:
441  case 9:
442  case 10:
443  case 11:
444  case 12:
445  case 13:
446  case 14:
447  case 15:
448  case 17:
449  case 18:
450  case 19:
451  case 20:
452  case 21:
453  case 22:
454  case 23:
455  case 24:
456  case 255:
457  $val = $this->exifMsg( $tag, $val );
458  break;
459  default:
460  /* If not recognized, display as is. */
461  $val = $this->literal( $val );
462  break;
463  }
464  break;
465 
466  case 'Flash':
467  $flashDecode = [
468  'fired' => $val & 0b00000001,
469  'return' => ( $val & 0b00000110 ) >> 1,
470  'mode' => ( $val & 0b00011000 ) >> 3,
471  'function' => ( $val & 0b00100000 ) >> 5,
472  'redeye' => ( $val & 0b01000000 ) >> 6,
473  // 'reserved' => ( $val & 0b10000000 ) >> 7,
474  ];
475  $flashMsgs = [];
476  # We do not need to handle unknown values since all are used.
477  foreach ( $flashDecode as $subTag => $subValue ) {
478  # We do not need any message for zeroed values.
479  if ( $subTag != 'fired' && $subValue == 0 ) {
480  continue;
481  }
482  $fullTag = $tag . '-' . $subTag;
483  $flashMsgs[] = $this->exifMsg( $fullTag, $subValue );
484  }
485  $val = $this->getLanguage()->commaList( $flashMsgs );
486  break;
487 
488  case 'FocalPlaneResolutionUnit':
489  switch ( $val ) {
490  case 2:
491  $val = $this->exifMsg( $tag, $val );
492  break;
493  default:
494  /* If not recognized, display as is. */
495  $val = $this->literal( $val );
496  break;
497  }
498  break;
499 
500  case 'SensingMethod':
501  switch ( $val ) {
502  case 1:
503  case 2:
504  case 3:
505  case 4:
506  case 5:
507  case 7:
508  case 8:
509  $val = $this->exifMsg( $tag, $val );
510  break;
511  default:
512  /* If not recognized, display as is. */
513  $val = $this->literal( $val );
514  break;
515  }
516  break;
517 
518  case 'FileSource':
519  switch ( $val ) {
520  case 3:
521  $val = $this->exifMsg( $tag, $val );
522  break;
523  default:
524  /* If not recognized, display as is. */
525  $val = $this->literal( $val );
526  break;
527  }
528  break;
529 
530  case 'SceneType':
531  switch ( $val ) {
532  case 1:
533  $val = $this->exifMsg( $tag, $val );
534  break;
535  default:
536  /* If not recognized, display as is. */
537  $val = $this->literal( $val );
538  break;
539  }
540  break;
541 
542  case 'CustomRendered':
543  switch ( $val ) {
544  case 0: /* normal */
545  case 1: /* custom */
546  /* The following are unofficial Apple additions */
547  case 2: /* HDR (no original saved) */
548  case 3: /* HDR (original saved) */
549  case 4: /* Original (for HDR) */
550  /* Yes 5 is not present ;) */
551  case 6: /* Panorama */
552  case 7: /* Portrait HDR */
553  case 8: /* Portrait */
554  $val = $this->exifMsg( $tag, $val );
555  break;
556  default:
557  /* If not recognized, display as is. */
558  $val = $this->literal( $val );
559  break;
560  }
561  break;
562 
563  case 'ExposureMode':
564  switch ( $val ) {
565  case 0:
566  case 1:
567  case 2:
568  $val = $this->exifMsg( $tag, $val );
569  break;
570  default:
571  /* If not recognized, display as is. */
572  break;
573  }
574  break;
575 
576  case 'WhiteBalance':
577  switch ( $val ) {
578  case 0:
579  case 1:
580  $val = $this->exifMsg( $tag, $val );
581  break;
582  default:
583  /* If not recognized, display as is. */
584  $val = $this->literal( $val );
585  break;
586  }
587  break;
588 
589  case 'SceneCaptureType':
590  switch ( $val ) {
591  case 0:
592  case 1:
593  case 2:
594  case 3:
595  $val = $this->exifMsg( $tag, $val );
596  break;
597  default:
598  /* If not recognized, display as is. */
599  $val = $this->literal( $val );
600  break;
601  }
602  break;
603 
604  case 'GainControl':
605  switch ( $val ) {
606  case 0:
607  case 1:
608  case 2:
609  case 3:
610  case 4:
611  $val = $this->exifMsg( $tag, $val );
612  break;
613  default:
614  /* If not recognized, display as is. */
615  $val = $this->literal( $val );
616  break;
617  }
618  break;
619 
620  case 'Contrast':
621  switch ( $val ) {
622  case 0:
623  case 1:
624  case 2:
625  $val = $this->exifMsg( $tag, $val );
626  break;
627  default:
628  /* If not recognized, display as is. */
629  $val = $this->literal( $val );
630  break;
631  }
632  break;
633 
634  case 'Saturation':
635  switch ( $val ) {
636  case 0:
637  case 1:
638  case 2:
639  $val = $this->exifMsg( $tag, $val );
640  break;
641  default:
642  /* If not recognized, display as is. */
643  $val = $this->literal( $val );
644  break;
645  }
646  break;
647 
648  case 'Sharpness':
649  switch ( $val ) {
650  case 0:
651  case 1:
652  case 2:
653  $val = $this->exifMsg( $tag, $val );
654  break;
655  default:
656  /* If not recognized, display as is. */
657  $val = $this->literal( $val );
658  break;
659  }
660  break;
661 
662  case 'SubjectDistanceRange':
663  switch ( $val ) {
664  case 0:
665  case 1:
666  case 2:
667  case 3:
668  $val = $this->exifMsg( $tag, $val );
669  break;
670  default:
671  /* If not recognized, display as is. */
672  $val = $this->literal( $val );
673  break;
674  }
675  break;
676 
677  // The GPS...Ref values are kept for compatibility, probably won't be reached.
678  case 'GPSLatitudeRef':
679  case 'GPSDestLatitudeRef':
680  switch ( $val ) {
681  case 'N':
682  case 'S':
683  $val = $this->exifMsg( 'GPSLatitude', $val );
684  break;
685  default:
686  /* If not recognized, display as is. */
687  $val = $this->literal( $val );
688  break;
689  }
690  break;
691 
692  case 'GPSLongitudeRef':
693  case 'GPSDestLongitudeRef':
694  switch ( $val ) {
695  case 'E':
696  case 'W':
697  $val = $this->exifMsg( 'GPSLongitude', $val );
698  break;
699  default:
700  /* If not recognized, display as is. */
701  $val = $this->literal( $val );
702  break;
703  }
704  break;
705 
706  case 'GPSAltitude':
707  if ( $val < 0 ) {
708  $val = $this->exifMsg( 'GPSAltitude', 'below-sealevel', $this->formatNum( -$val, 3 ) );
709  } else {
710  $val = $this->exifMsg( 'GPSAltitude', 'above-sealevel', $this->formatNum( $val, 3 ) );
711  }
712  break;
713 
714  case 'GPSStatus':
715  switch ( $val ) {
716  case 'A':
717  case 'V':
718  $val = $this->exifMsg( $tag, $val );
719  break;
720  default:
721  /* If not recognized, display as is. */
722  $val = $this->literal( $val );
723  break;
724  }
725  break;
726 
727  case 'GPSMeasureMode':
728  switch ( $val ) {
729  case 2:
730  case 3:
731  $val = $this->exifMsg( $tag, $val );
732  break;
733  default:
734  /* If not recognized, display as is. */
735  $val = $this->literal( $val );
736  break;
737  }
738  break;
739 
740  case 'GPSTrackRef':
741  case 'GPSImgDirectionRef':
742  case 'GPSDestBearingRef':
743  switch ( $val ) {
744  case 'T':
745  case 'M':
746  $val = $this->exifMsg( 'GPSDirection', $val );
747  break;
748  default:
749  /* If not recognized, display as is. */
750  $val = $this->literal( $val );
751  break;
752  }
753  break;
754 
755  case 'GPSLatitude':
756  case 'GPSDestLatitude':
757  $val = $this->formatCoords( $val, 'latitude' );
758  break;
759  case 'GPSLongitude':
760  case 'GPSDestLongitude':
761  $val = $this->formatCoords( $val, 'longitude' );
762  break;
763 
764  case 'GPSSpeedRef':
765  switch ( $val ) {
766  case 'K':
767  case 'M':
768  case 'N':
769  $val = $this->exifMsg( 'GPSSpeed', $val );
770  break;
771  default:
772  /* If not recognized, display as is. */
773  $val = $this->literal( $val );
774  break;
775  }
776  break;
777 
778  case 'GPSDestDistanceRef':
779  switch ( $val ) {
780  case 'K':
781  case 'M':
782  case 'N':
783  $val = $this->exifMsg( 'GPSDestDistance', $val );
784  break;
785  default:
786  /* If not recognized, display as is. */
787  $val = $this->literal( $val );
788  break;
789  }
790  break;
791 
792  case 'GPSDOP':
793  // See https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)
794  if ( $val <= 2 ) {
795  $val = $this->exifMsg( $tag, 'excellent', $this->formatNum( $val ) );
796  } elseif ( $val <= 5 ) {
797  $val = $this->exifMsg( $tag, 'good', $this->formatNum( $val ) );
798  } elseif ( $val <= 10 ) {
799  $val = $this->exifMsg( $tag, 'moderate', $this->formatNum( $val ) );
800  } elseif ( $val <= 20 ) {
801  $val = $this->exifMsg( $tag, 'fair', $this->formatNum( $val ) );
802  } else {
803  $val = $this->exifMsg( $tag, 'poor', $this->formatNum( $val ) );
804  }
805  break;
806 
807  // This is not in the Exif standard, just a special
808  // case for our purposes which enables wikis to wikify
809  // the make, model and software name to link to their articles.
810  case 'Make':
811  case 'Model':
812  $val = $this->exifMsg( $tag, '', $this->literal( $val ) );
813  break;
814 
815  case 'Software':
816  if ( is_array( $val ) ) {
817  if ( count( $val ) > 1 ) {
818  // if its a software, version array.
819  $val = $this->msg(
820  'exif-software-version-value',
821  $this->literal( $val[0] ),
822  $this->literal( $val[1] )
823  )->text();
824  } else {
825  // https://phabricator.wikimedia.org/T178130
826  $val = $this->exifMsg( $tag, '', $this->literal( $val[0] ) );
827  }
828  } else {
829  $val = $this->exifMsg( $tag, '', $this->literal( $val ) );
830  }
831  break;
832 
833  case 'ExposureTime':
834  // Show the pretty fraction as well as decimal version
835  $val = $this->msg( 'exif-exposuretime-format',
836  $this->formatFraction( $val ), $this->formatNum( $val ) )->text();
837  break;
838  case 'ISOSpeedRatings':
839  // If its = 65535 that means its at the
840  // limit of the size of Exif::short and
841  // is really higher.
842  if ( $val == '65535' ) {
843  $val = $this->exifMsg( $tag, 'overflow' );
844  } else {
845  $val = $this->formatNum( $val );
846  }
847  break;
848  case 'FNumber':
849  $val = $this->msg( 'exif-fnumber-format',
850  $this->formatNum( $val ) )->text();
851  break;
852 
853  case 'FocalLength':
854  case 'FocalLengthIn35mmFilm':
855  $val = $this->msg( 'exif-focallength-format',
856  $this->formatNum( $val ) )->text();
857  break;
858 
859  case 'MaxApertureValue':
860  if ( strpos( $val, '/' ) !== false ) {
861  // need to expand this earlier to calculate fNumber
862  list( $n, $d ) = explode( '/', $val );
863  if ( is_numeric( $n ) && is_numeric( $d ) ) {
864  $val = $n / $d;
865  }
866  }
867  if ( is_numeric( $val ) ) {
868  $fNumber = 2 ** ( $val / 2 );
869  if ( is_finite( $fNumber ) ) {
870  $val = $this->msg( 'exif-maxaperturevalue-value',
871  $this->formatNum( $val ),
872  $this->formatNum( $fNumber, 2 )
873  )->text();
874  break;
875  }
876  }
877  $val = $this->literal( $val );
878  break;
879 
880  case 'iimCategory':
881  switch ( strtolower( $val ) ) {
882  // See pg 29 of IPTC photo
883  // metadata standard.
884  case 'ace':
885  case 'clj':
886  case 'dis':
887  case 'fin':
888  case 'edu':
889  case 'evn':
890  case 'hth':
891  case 'hum':
892  case 'lab':
893  case 'lif':
894  case 'pol':
895  case 'rel':
896  case 'sci':
897  case 'soi':
898  case 'spo':
899  case 'war':
900  case 'wea':
901  $val = $this->exifMsg(
902  'iimcategory',
903  $val
904  );
905  break;
906  default:
907  $val = $this->literal( $val );
908  }
909  break;
910  case 'SubjectNewsCode':
911  // Essentially like iimCategory.
912  // 8 (numeric) digit hierarchical
913  // classification. We decode the
914  // first 2 digits, which provide
915  // a broad category.
916  $val = $this->convertNewsCode( $val );
917  break;
918  case 'Urgency':
919  // 1-8 with 1 being highest, 5 normal
920  // 0 is reserved, and 9 is 'user-defined'.
921  $urgency = '';
922  if ( $val == 0 || $val == 9 ) {
923  $urgency = 'other';
924  } elseif ( $val < 5 && $val > 1 ) {
925  $urgency = 'high';
926  } elseif ( $val == 5 ) {
927  $urgency = 'normal';
928  } elseif ( $val <= 8 && $val > 5 ) {
929  $urgency = 'low';
930  }
931 
932  if ( $urgency !== '' ) {
933  $val = $this->exifMsg( 'urgency',
934  $urgency, $this->literal( $val )
935  );
936  } else {
937  $val = $this->literal( $val );
938  }
939  break;
940 
941  // Things that have a unit of pixels.
942  case 'OriginalImageHeight':
943  case 'OriginalImageWidth':
944  case 'PixelXDimension':
945  case 'PixelYDimension':
946  case 'ImageWidth':
947  case 'ImageLength':
948  $val = $this->formatNum( $val ) . ' ' . $this->msg( 'unit-pixel' )->text();
949  break;
950 
951  // Do not transform fields with pure text.
952  // For some languages the formatNum()
953  // conversion results to wrong output like
954  // foo,bar@example,com or foo٫bar@example٫com.
955  // Also some 'numeric' things like Scene codes
956  // are included here as we really don't want
957  // commas inserted.
958  case 'ImageDescription':
959  case 'UserComment':
960  case 'Artist':
961  case 'Copyright':
962  case 'RelatedSoundFile':
963  case 'ImageUniqueID':
964  case 'SpectralSensitivity':
965  case 'GPSSatellites':
966  case 'GPSVersionID':
967  case 'GPSMapDatum':
968  case 'Keywords':
969  case 'WorldRegionDest':
970  case 'CountryDest':
971  case 'CountryCodeDest':
972  case 'ProvinceOrStateDest':
973  case 'CityDest':
974  case 'SublocationDest':
975  case 'WorldRegionCreated':
976  case 'CountryCreated':
977  case 'CountryCodeCreated':
978  case 'ProvinceOrStateCreated':
979  case 'CityCreated':
980  case 'SublocationCreated':
981  case 'ObjectName':
982  case 'SpecialInstructions':
983  case 'Headline':
984  case 'Credit':
985  case 'Source':
986  case 'EditStatus':
987  case 'FixtureIdentifier':
988  case 'LocationDest':
989  case 'LocationDestCode':
990  case 'Writer':
991  case 'JPEGFileComment':
992  case 'iimSupplementalCategory':
993  case 'OriginalTransmissionRef':
994  case 'Identifier':
995  case 'dc-contributor':
996  case 'dc-coverage':
997  case 'dc-publisher':
998  case 'dc-relation':
999  case 'dc-rights':
1000  case 'dc-source':
1001  case 'dc-type':
1002  case 'Lens':
1003  case 'SerialNumber':
1004  case 'CameraOwnerName':
1005  case 'Label':
1006  case 'Nickname':
1007  case 'RightsCertificate':
1008  case 'CopyrightOwner':
1009  case 'UsageTerms':
1010  case 'WebStatement':
1011  case 'OriginalDocumentID':
1012  case 'LicenseUrl':
1013  case 'MorePermissionsUrl':
1014  case 'AttributionUrl':
1015  case 'PreferredAttributionName':
1016  case 'PNGFileComment':
1017  case 'Disclaimer':
1018  case 'ContentWarning':
1019  case 'GIFFileComment':
1020  case 'SceneCode':
1021  case 'IntellectualGenre':
1022  case 'Event':
1023  case 'OrganisationInImage':
1024  case 'PersonInImage':
1025  case 'CaptureSoftware':
1026  case 'GPSAreaInformation':
1027  case 'GPSProcessingMethod':
1028  case 'StitchingSoftware':
1029  case 'SubSecTime':
1030  case 'SubSecTimeOriginal':
1031  case 'SubSecTimeDigitized':
1032  $val = $this->literal( $val );
1033  break;
1034 
1035  case 'ProjectionType':
1036  switch ( $val ) {
1037  case 'equirectangular':
1038  $val = $this->exifMsg( $tag, $val );
1039  break;
1040  default:
1041  $val = $this->literal( $val );
1042  break;
1043  }
1044  break;
1045  case 'ObjectCycle':
1046  switch ( $val ) {
1047  case 'a':
1048  case 'p':
1049  case 'b':
1050  $val = $this->exifMsg( $tag, $val );
1051  break;
1052  default:
1053  $val = $this->literal( $val );
1054  break;
1055  }
1056  break;
1057  case 'Copyrighted':
1058  case 'UsePanoramaViewer':
1059  case 'ExposureLockUsed':
1060  switch ( $val ) {
1061  case 'True':
1062  case 'False':
1063  $val = $this->exifMsg( $tag, $val );
1064  break;
1065  default:
1066  $val = $this->literal( $val );
1067  break;
1068  }
1069  break;
1070  case 'Rating':
1071  if ( $val == '-1' ) {
1072  $val = $this->exifMsg( $tag, 'rejected' );
1073  } else {
1074  $val = $this->formatNum( $val );
1075  }
1076  break;
1077 
1078  case 'LanguageCode':
1079  $lang = MediaWikiServices::getInstance()
1080  ->getLanguageNameUtils()
1081  ->getLanguageName( strtolower( $val ), $this->getLanguage()->getCode() );
1082  $val = $this->literal( $lang ?: $val );
1083  break;
1084 
1085  default:
1086  $val = $this->formatNum( $val, false, $tag );
1087  break;
1088  }
1089  }
1090  // End formatting values, start flattening arrays.
1091  $vals = $this->flattenArrayReal( $vals, $type );
1092  }
1093 
1094  return $tags;
1095  }
1096 
1115  public static function flattenArrayContentLang( $vals, $type = 'ul',
1116  $noHtml = false, $context = false
1117  ) {
1118  // Allow $noHtml to be omitted.
1119  if ( $noHtml instanceof IContextSource ) {
1120  $context = $noHtml;
1121  $noHtml = false;
1122  }
1123  if ( $noHtml ) {
1124  wfDeprecated( __METHOD__ . ' with $noHtml = true', '1.36' );
1125  }
1126  $obj = new FormatMetadata;
1127  if ( $context ) {
1128  $obj->setContext( $context );
1129  }
1130  $context = new DerivativeContext( $obj->getContext() );
1131  $context->setLanguage( MediaWikiServices::getInstance()->getContentLanguage() );
1132  $obj->setContext( $context );
1133 
1134  return $obj->flattenArrayReal( $vals, $type, $noHtml );
1135  }
1136 
1154  public function flattenArrayReal( $vals, $type = 'ul', $noHtml = false ) {
1155  if ( !is_array( $vals ) ) {
1156  return $vals; // do nothing if not an array;
1157  }
1158 
1159  if ( isset( $vals['_type'] ) ) {
1160  $type = $vals['_type'];
1161  unset( $vals['_type'] );
1162  }
1163 
1164  if ( count( $vals ) === 1 && $type !== 'lang' && isset( $vals[0] ) ) {
1165  return $vals[0];
1166  } elseif ( count( $vals ) === 0 ) {
1167  wfDebug( __METHOD__ . " metadata array with 0 elements!" );
1168 
1169  return ""; // paranoia. This should never happen
1170  } else {
1171  /* @todo FIXME: This should hide some of the list entries if there are
1172  * say more than four. Especially if a field is translated into 20
1173  * languages, we don't want to show them all by default
1174  */
1175  switch ( $type ) {
1176  case 'lang':
1177  // Display default, followed by ContentLanguage,
1178  // followed by the rest in no particular
1179  // order.
1180 
1181  // Todo: hide some items if really long list.
1182 
1183  $content = '';
1184 
1185  $priorityLanguages = $this->getPriorityLanguages();
1186  $defaultItem = false;
1187  $defaultLang = false;
1188 
1189  // If default is set, save it for later,
1190  // as we don't know if it's equal to
1191  // one of the lang codes. (In xmp
1192  // you specify the language for a
1193  // default property by having both
1194  // a default prop, and one in the language
1195  // that are identical)
1196  if ( isset( $vals['x-default'] ) ) {
1197  $defaultItem = $vals['x-default'];
1198  unset( $vals['x-default'] );
1199  }
1200  foreach ( $priorityLanguages as $pLang ) {
1201  if ( isset( $vals[$pLang] ) ) {
1202  $isDefault = false;
1203  if ( $vals[$pLang] === $defaultItem ) {
1204  $defaultItem = false;
1205  $isDefault = true;
1206  }
1207  $content .= $this->langItem(
1208  $vals[$pLang], $pLang,
1209  $isDefault, $noHtml );
1210 
1211  unset( $vals[$pLang] );
1212 
1213  if ( $this->singleLang ) {
1214  return Html::rawElement( 'span',
1215  [ 'lang' => $pLang ], $vals[$pLang] );
1216  }
1217  }
1218  }
1219 
1220  // Now do the rest.
1221  foreach ( $vals as $lang => $item ) {
1222  if ( $item === $defaultItem ) {
1223  $defaultLang = $lang;
1224  continue;
1225  }
1226  $content .= $this->langItem( $item,
1227  $lang, false, $noHtml );
1228  if ( $this->singleLang ) {
1229  return Html::rawElement( 'span',
1230  [ 'lang' => $lang ], $item );
1231  }
1232  }
1233  if ( $defaultItem !== false ) {
1234  $content = $this->langItem( $defaultItem,
1235  $defaultLang, true, $noHtml ) .
1236  $content;
1237  if ( $this->singleLang ) {
1238  return $defaultItem;
1239  }
1240  }
1241  if ( $noHtml ) {
1242  return $content;
1243  }
1244 
1245  return '<ul class="metadata-langlist">' .
1246  $content .
1247  '</ul>';
1248  case 'ol':
1249  if ( $noHtml ) {
1250  return "\n#" . implode( "\n#", $vals );
1251  }
1252 
1253  return "<ol><li>" . implode( "</li>\n<li>", $vals ) . '</li></ol>';
1254  case 'ul':
1255  default:
1256  if ( $noHtml ) {
1257  return "\n*" . implode( "\n*", $vals );
1258  }
1259 
1260  return "<ul><li>" . implode( "</li>\n<li>", $vals ) . '</li></ul>';
1261  }
1262  }
1263  }
1264 
1275  private function langItem( $value, $lang, $default = false, $noHtml = false ) {
1276  if ( $lang === false && $default === false ) {
1277  throw new MWException( '$lang and $default cannot both '
1278  . 'be false.' );
1279  }
1280 
1281  if ( $noHtml ) {
1282  $wrappedValue = $this->literal( $value );
1283  } else {
1284  $wrappedValue = '<span class="mw-metadata-lang-value">'
1285  . $this->literal( $value ) . '</span>';
1286  }
1287 
1288  if ( $lang === false ) {
1289  $msg = $this->msg( 'metadata-langitem-default', $wrappedValue );
1290  if ( $noHtml ) {
1291  return $msg->text() . "\n\n";
1292  } /* else */
1293 
1294  return '<li class="mw-metadata-lang-default">'
1295  . $msg->text()
1296  . "</li>\n";
1297  }
1298 
1299  $lowLang = strtolower( $lang );
1300  $languageNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
1301  $langName = $languageNameUtils->getLanguageName( $lowLang );
1302  if ( $langName === '' ) {
1303  // try just the base language name. (aka en-US -> en ).
1304  $langPrefix = explode( '-', $lowLang, 2 )[0];
1305  $langName = $languageNameUtils->getLanguageName( $langPrefix );
1306  if ( $langName === '' ) {
1307  // give up.
1308  $langName = $lang;
1309  }
1310  }
1311  // else we have a language specified
1312 
1313  $msg = $this->msg( 'metadata-langitem', $wrappedValue, $langName, $lang );
1314  if ( $noHtml ) {
1315  return '*' . $msg->text();
1316  } /* else: */
1317 
1318  $item = '<li class="mw-metadata-lang-code-'
1319  . $lang;
1320  if ( $default ) {
1321  $item .= ' mw-metadata-lang-default';
1322  }
1323  $item .= '" lang="' . $lang . '">';
1324  $item .= $msg->text();
1325  $item .= "</li>\n";
1326 
1327  return $item;
1328  }
1329 
1337  protected function literal( $val ) {
1338  // T266707: historically this has used htmlspecialchars to protect
1339  // the string contents, but it should probably be changed to use
1340  // wfEscapeWikitext() instead -- however, "we still want to auto-link
1341  // urls" so wfEscapeWikitext isn't *quite* right...
1342  return htmlspecialchars( $val );
1343  }
1344 
1354  private function exifMsg( $tag, $val, $arg = null, $arg2 = null ) {
1355  if ( $val === '' ) {
1356  $val = 'value';
1357  }
1358 
1359  return $this->msg(
1360  MediaWikiServices::getInstance()->getContentLanguage()->lc( "exif-$tag-$val" ),
1361  $arg,
1362  $arg2
1363  )->text();
1364  }
1365 
1375  private function formatNum( $num, $round = false, $tagName = null ) {
1376  $m = [];
1377  if ( is_array( $num ) ) {
1378  $out = [];
1379  foreach ( $num as $number ) {
1380  $out[] = $this->formatNum( $number, $round, $tagName );
1381  }
1382 
1383  return $this->getLanguage()->commaList( $out );
1384  }
1385  if ( is_numeric( $num ) ) {
1386  if ( $round !== false ) {
1387  $num = round( $num, $round );
1388  }
1389  return $this->getLanguage()->formatNum( $num );
1390  }
1391  if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
1392  if ( $m[2] != 0 ) {
1393  $newNum = $m[1] / $m[2];
1394  if ( $round !== false ) {
1395  $newNum = round( $newNum, $round );
1396  }
1397  } else {
1398  $newNum = $num;
1399  }
1400 
1401  return $this->getLanguage()->formatNum( $newNum );
1402  }
1403  # T267370: there are a lot of strange EXIF tags floating around.
1404  LoggerFactory::getInstance( 'formatnum' )->warning(
1405  'FormatMetadata::formatNum with non-numeric value',
1406  [
1407  'tag' => $tagName,
1408  'value' => $num,
1409  ]
1410  );
1411  return $this->literal( $num );
1412  }
1413 
1420  private function formatFraction( $num ) {
1421  $m = [];
1422  if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
1423  $numerator = intval( $m[1] );
1424  $denominator = intval( $m[2] );
1425  $gcd = $this->gcd( abs( $numerator ), $denominator );
1426  if ( $gcd != 0 ) {
1427  // 0 shouldn't happen! ;)
1428  return $this->formatNum( $numerator / $gcd ) . '/' . $this->formatNum( $denominator / $gcd );
1429  }
1430  }
1431 
1432  return $this->formatNum( $num );
1433  }
1434 
1442  private function gcd( $a, $b ) {
1443  /*
1444  // https://en.wikipedia.org/wiki/Euclidean_algorithm
1445  // Recursive form would be:
1446  if( $b == 0 )
1447  return $a;
1448  else
1449  return gcd( $b, $a % $b );
1450  */
1451  while ( $b != 0 ) {
1452  $remainder = $a % $b;
1453 
1454  // tail recursion...
1455  $a = $b;
1456  $b = $remainder;
1457  }
1458 
1459  return $a;
1460  }
1461 
1474  private function convertNewsCode( $val ) {
1475  if ( !preg_match( '/^\d{8}$/D', $val ) ) {
1476  // Not a valid news code.
1477  return $val;
1478  }
1479  $cat = '';
1480  switch ( substr( $val, 0, 2 ) ) {
1481  case '01':
1482  $cat = 'ace';
1483  break;
1484  case '02':
1485  $cat = 'clj';
1486  break;
1487  case '03':
1488  $cat = 'dis';
1489  break;
1490  case '04':
1491  $cat = 'fin';
1492  break;
1493  case '05':
1494  $cat = 'edu';
1495  break;
1496  case '06':
1497  $cat = 'evn';
1498  break;
1499  case '07':
1500  $cat = 'hth';
1501  break;
1502  case '08':
1503  $cat = 'hum';
1504  break;
1505  case '09':
1506  $cat = 'lab';
1507  break;
1508  case '10':
1509  $cat = 'lif';
1510  break;
1511  case '11':
1512  $cat = 'pol';
1513  break;
1514  case '12':
1515  $cat = 'rel';
1516  break;
1517  case '13':
1518  $cat = 'sci';
1519  break;
1520  case '14':
1521  $cat = 'soi';
1522  break;
1523  case '15':
1524  $cat = 'spo';
1525  break;
1526  case '16':
1527  $cat = 'war';
1528  break;
1529  case '17':
1530  $cat = 'wea';
1531  break;
1532  }
1533  if ( $cat !== '' ) {
1534  $catMsg = $this->exifMsg( 'iimcategory', $cat );
1535  $val = $this->exifMsg( 'subjectnewscode', '', $this->literal( $val ), $catMsg );
1536  }
1537 
1538  return $val;
1539  }
1540 
1549  private function formatCoords( $coord, string $type ) {
1550  if ( !is_numeric( $coord ) ) {
1551  wfDebugLog( 'exif', __METHOD__ . ": \"$coord\" is not a number" );
1552  return $this->literal( (string)$coord );
1553  }
1554 
1555  $ref = '';
1556  if ( $coord < 0 ) {
1557  $nCoord = -$coord;
1558  if ( $type === 'latitude' ) {
1559  $ref = 'S';
1560  } elseif ( $type === 'longitude' ) {
1561  $ref = 'W';
1562  }
1563  } else {
1564  $nCoord = (float)$coord;
1565  if ( $type === 'latitude' ) {
1566  $ref = 'N';
1567  } elseif ( $type === 'longitude' ) {
1568  $ref = 'E';
1569  }
1570  }
1571 
1572  $deg = floor( $nCoord );
1573  $min = floor( ( $nCoord - $deg ) * 60 );
1574  $sec = round( ( ( $nCoord - $deg ) * 60 - $min ) * 60, 2 );
1575 
1576  $deg = $this->formatNum( $deg );
1577  $min = $this->formatNum( $min );
1578  $sec = $this->formatNum( $sec );
1579 
1580  // Note the default message "$1° $2′ $3″ $4" ignores the 5th parameter
1581  return $this->msg( 'exif-coordinate-format', $deg, $min, $sec, $ref, $this->literal( $coord ) )->text();
1582  }
1583 
1598  public function collapseContactInfo( array $vals ) {
1599  if ( !( isset( $vals['CiAdrExtadr'] )
1600  || isset( $vals['CiAdrCity'] )
1601  || isset( $vals['CiAdrCtry'] )
1602  || isset( $vals['CiEmailWork'] )
1603  || isset( $vals['CiTelWork'] )
1604  || isset( $vals['CiAdrPcode'] )
1605  || isset( $vals['CiAdrRegion'] )
1606  || isset( $vals['CiUrlWork'] )
1607  ) ) {
1608  // We don't have any sub-properties
1609  // This could happen if its using old
1610  // iptc that just had this as a free-form
1611  // text value.
1612  // Note: people often insert >, etc into
1613  // the metadata which should not be interpreted
1614  // but we still want to auto-link urls.
1615  foreach ( $vals as &$val ) {
1616  $val = $this->literal( $val );
1617  }
1618 
1619  return $this->flattenArrayReal( $vals );
1620  } else {
1621  // We have a real ContactInfo field.
1622  // Its unclear if all these fields have to be
1623  // set, so assume they do not.
1624  $url = $tel = $street = $city = $country = '';
1625  $email = $postal = $region = '';
1626 
1627  // Also note, some of the class names this uses
1628  // are similar to those used by hCard. This is
1629  // mostly because they're sensible names. This
1630  // does not (and does not attempt to) output
1631  // stuff in the hCard microformat. However it
1632  // might output in the adr microformat.
1633 
1634  if ( isset( $vals['CiAdrExtadr'] ) ) {
1635  // Todo: This can potentially be multi-line.
1636  // Need to check how that works in XMP.
1637  $street = '<span class="extended-address">'
1638  . $this->literal(
1639  $vals['CiAdrExtadr'] )
1640  . '</span>';
1641  }
1642  if ( isset( $vals['CiAdrCity'] ) ) {
1643  $city = '<span class="locality">'
1644  . $this->literal( $vals['CiAdrCity'] )
1645  . '</span>';
1646  }
1647  if ( isset( $vals['CiAdrCtry'] ) ) {
1648  $country = '<span class="country-name">'
1649  . $this->literal( $vals['CiAdrCtry'] )
1650  . '</span>';
1651  }
1652  if ( isset( $vals['CiEmailWork'] ) ) {
1653  $emails = [];
1654  // Have to split multiple emails at commas/new lines.
1655  $splitEmails = explode( "\n", $vals['CiEmailWork'] );
1656  foreach ( $splitEmails as $e1 ) {
1657  // Also split on comma
1658  foreach ( explode( ',', $e1 ) as $e2 ) {
1659  $finalEmail = trim( $e2 );
1660  if ( $finalEmail == ',' || $finalEmail == '' ) {
1661  continue;
1662  }
1663  if ( strpos( $finalEmail, '<' ) !== false ) {
1664  // Don't do fancy formatting to
1665  // "My name" <foo@bar.com> style stuff
1666  $emails[] = $this->literal( $finalEmail );
1667  } else {
1668  $emails[] = '[mailto:'
1669  . $finalEmail
1670  . ' <span class="email">'
1671  . $this->literal( $finalEmail )
1672  . '</span>]';
1673  }
1674  }
1675  }
1676  $email = implode( ', ', $emails );
1677  }
1678  if ( isset( $vals['CiTelWork'] ) ) {
1679  $tel = '<span class="tel">'
1680  . $this->literal( $vals['CiTelWork'] )
1681  . '</span>';
1682  }
1683  if ( isset( $vals['CiAdrPcode'] ) ) {
1684  $postal = '<span class="postal-code">'
1685  . $this->literal(
1686  $vals['CiAdrPcode'] )
1687  . '</span>';
1688  }
1689  if ( isset( $vals['CiAdrRegion'] ) ) {
1690  // Note this is province/state.
1691  $region = '<span class="region">'
1692  . $this->literal(
1693  $vals['CiAdrRegion'] )
1694  . '</span>';
1695  }
1696  if ( isset( $vals['CiUrlWork'] ) ) {
1697  $url = '<span class="url">'
1698  . $this->literal( $vals['CiUrlWork'] )
1699  . '</span>';
1700  }
1701 
1702  return $this->msg( 'exif-contact-value', $email, $url,
1703  $street, $city, $region, $postal, $country,
1704  $tel )->text();
1705  }
1706  }
1707 
1714  public static function getVisibleFields() {
1715  $fields = [];
1716  $lines = explode( "\n", wfMessage( 'metadata-fields' )->inContentLanguage()->text() );
1717  foreach ( $lines as $line ) {
1718  $matches = [];
1719  if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
1720  $fields[] = $matches[1];
1721  }
1722  }
1723  $fields = array_map( 'strtolower', $fields );
1724 
1725  return $fields;
1726  }
1727 
1735  public function fetchExtendedMetadata( File $file ) {
1736  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1737 
1738  // If revision deleted, exit immediately
1739  if ( $file->isDeleted( File::DELETED_FILE ) ) {
1740  return [];
1741  }
1742 
1743  $cacheKey = $cache->makeKey(
1744  'getExtendedMetadata',
1745  $this->getLanguage()->getCode(),
1746  (int)$this->singleLang,
1747  $file->getSha1()
1748  );
1749 
1750  $cachedValue = $cache->get( $cacheKey );
1751  if (
1752  $cachedValue
1753  && $this->getHookRunner()->onValidateExtendedMetadataCache( $cachedValue['timestamp'], $file )
1754  ) {
1755  $extendedMetadata = $cachedValue['data'];
1756  } else {
1757  $maxCacheTime = ( $file instanceof ForeignAPIFile ) ? 60 * 60 * 12 : 60 * 60 * 24 * 30;
1758  $fileMetadata = $this->getExtendedMetadataFromFile( $file );
1759  $extendedMetadata = $this->getExtendedMetadataFromHook( $file, $fileMetadata, $maxCacheTime );
1760  if ( $this->singleLang ) {
1761  $this->resolveMultilangMetadata( $extendedMetadata );
1762  }
1763  $this->discardMultipleValues( $extendedMetadata );
1764  // Make sure the metadata won't break the API when an XML format is used.
1765  // This is an API-specific function so it would be cleaner to call it from
1766  // outside fetchExtendedMetadata, but this way we don't need to redo the
1767  // computation on a cache hit.
1768  $this->sanitizeArrayForAPI( $extendedMetadata );
1769  $valueToCache = [ 'data' => $extendedMetadata, 'timestamp' => wfTimestampNow() ];
1770  $cache->set( $cacheKey, $valueToCache, $maxCacheTime );
1771  }
1772 
1773  return $extendedMetadata;
1774  }
1775 
1785  protected function getExtendedMetadataFromFile( File $file ) {
1786  // If this is a remote file accessed via an API request, we already
1787  // have remote metadata so we just ignore any local one
1788  if ( $file instanceof ForeignAPIFile ) {
1789  // In case of error we pretend no metadata - this will get cached.
1790  // Might or might not be a good idea.
1791  return $file->getExtendedMetadata() ?: [];
1792  }
1793 
1794  $uploadDate = wfTimestamp( TS_ISO_8601, $file->getTimestamp() );
1795 
1796  $fileMetadata = [
1797  // This is modification time, which is close to "upload" time.
1798  'DateTime' => [
1799  'value' => $uploadDate,
1800  'source' => 'mediawiki-metadata',
1801  ],
1802  ];
1803 
1804  $title = $file->getTitle();
1805  if ( $title ) {
1806  $text = $title->getText();
1807  $pos = strrpos( $text, '.' );
1808 
1809  if ( $pos ) {
1810  $name = substr( $text, 0, $pos );
1811  } else {
1812  $name = $text;
1813  }
1814 
1815  $fileMetadata['ObjectName'] = [
1816  'value' => $name,
1817  'source' => 'mediawiki-metadata',
1818  ];
1819  }
1820 
1821  return $fileMetadata;
1822  }
1823 
1834  protected function getExtendedMetadataFromHook( File $file, array $extendedMetadata,
1835  &$maxCacheTime
1836  ) {
1837  $this->getHookRunner()->onGetExtendedMetadata(
1838  $extendedMetadata,
1839  $file,
1840  $this->getContext(),
1841  $this->singleLang,
1842  $maxCacheTime
1843  );
1844 
1845  $visible = array_fill_keys( self::getVisibleFields(), true );
1846  foreach ( $extendedMetadata as $key => $value ) {
1847  if ( !isset( $visible[strtolower( $key )] ) ) {
1848  $extendedMetadata[$key]['hidden'] = '';
1849  }
1850  }
1851 
1852  return $extendedMetadata;
1853  }
1854 
1863  protected function resolveMultilangValue( $value ) {
1864  if (
1865  !is_array( $value )
1866  || !isset( $value['_type'] )
1867  || $value['_type'] != 'lang'
1868  ) {
1869  return $value; // do nothing if not a multilang array
1870  }
1871 
1872  // choose the language best matching user or site settings
1873  $priorityLanguages = $this->getPriorityLanguages();
1874  foreach ( $priorityLanguages as $lang ) {
1875  if ( isset( $value[$lang] ) ) {
1876  return $value[$lang];
1877  }
1878  }
1879 
1880  // otherwise go with the default language, if set
1881  if ( isset( $value['x-default'] ) ) {
1882  return $value['x-default'];
1883  }
1884 
1885  // otherwise just return any one language
1886  unset( $value['_type'] );
1887  if ( !empty( $value ) ) {
1888  return reset( $value );
1889  }
1890 
1891  // this should not happen; signal error
1892  return null;
1893  }
1894 
1904  protected function resolveMultivalueValue( $value ) {
1905  if ( !is_array( $value ) ) {
1906  return $value;
1907  } elseif ( isset( $value['_type'] ) && $value['_type'] === 'lang' ) {
1908  // if this is a multilang array, process fields separately
1909  $newValue = [];
1910  foreach ( $value as $k => $v ) {
1911  $newValue[$k] = $this->resolveMultivalueValue( $v );
1912  }
1913  return $newValue;
1914  } else { // _type is 'ul' or 'ol' or missing in which case it defaults to 'ul'
1915  $v = reset( $value );
1916  if ( key( $value ) === '_type' ) {
1917  $v = next( $value );
1918  }
1919  return $v;
1920  }
1921  }
1922 
1929  protected function resolveMultilangMetadata( &$metadata ) {
1930  if ( !is_array( $metadata ) ) {
1931  return;
1932  }
1933  foreach ( $metadata as &$field ) {
1934  if ( isset( $field['value'] ) ) {
1935  $field['value'] = $this->resolveMultilangValue( $field['value'] );
1936  }
1937  }
1938  }
1939 
1946  protected function discardMultipleValues( &$metadata ) {
1947  if ( !is_array( $metadata ) ) {
1948  return;
1949  }
1950  foreach ( $metadata as $key => &$field ) {
1951  if ( $key === 'Software' || $key === 'Contact' ) {
1952  // we skip some fields which have composite values. They are not particularly interesting
1953  // and you can get them via the metadata / commonmetadata APIs anyway.
1954  continue;
1955  }
1956  if ( isset( $field['value'] ) ) {
1957  $field['value'] = $this->resolveMultivalueValue( $field['value'] );
1958  }
1959  }
1960  }
1961 
1966  protected function sanitizeArrayForAPI( &$arr ) {
1967  if ( !is_array( $arr ) ) {
1968  return;
1969  }
1970 
1971  $counter = 1;
1972  foreach ( $arr as $key => &$value ) {
1973  $sanitizedKey = $this->sanitizeKeyForAPI( $key );
1974  if ( $sanitizedKey !== $key ) {
1975  if ( isset( $arr[$sanitizedKey] ) ) {
1976  // Make the sanitized keys hopefully unique.
1977  // To make it definitely unique would be too much effort, given that
1978  // sanitizing is only needed for misformatted metadata anyway, but
1979  // this at least covers the case when $arr is numeric.
1980  $sanitizedKey .= $counter;
1981  ++$counter;
1982  }
1983  $arr[$sanitizedKey] = $arr[$key];
1984  unset( $arr[$key] );
1985  }
1986  if ( is_array( $value ) ) {
1987  $this->sanitizeArrayForAPI( $value );
1988  }
1989  }
1990 
1991  // Handle API metadata keys (particularly "_type")
1992  $keys = array_filter( array_keys( $arr ), [ ApiResult::class, 'isMetadataKey' ] );
1993  if ( $keys ) {
1995  }
1996  }
1997 
2004  protected function sanitizeKeyForAPI( $key ) {
2005  // drop all characters which are not valid in an XML tag name
2006  // a bunch of non-ASCII letters would be valid but probably won't
2007  // be used so we take the easy way
2008  $key = preg_replace( '/[^a-zA-z0-9_:.\-]/', '', $key );
2009  // drop characters which are invalid at the first position
2010  $key = preg_replace( '/^[\d\-.]+/', '', $key );
2011 
2012  if ( $key === '' ) {
2013  $key = '_';
2014  // special case for an internal keyword
2015  } elseif ( $key === '_element' ) {
2016  $key = 'element';
2017  }
2018 
2019  return $key;
2020  }
2021 
2028  protected function getPriorityLanguages() {
2029  $priorityLanguages = MediaWikiServices::getInstance()
2030  ->getLanguageFallback()
2031  ->getAllIncludingSiteLanguage( $this->getLanguage()->getCode() );
2032  $priorityLanguages = array_merge(
2033  (array)$this->getLanguage()->getCode(),
2034  $priorityLanguages[0],
2035  $priorityLanguages[1]
2036  );
2037 
2038  return $priorityLanguages;
2039  }
2040 }
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:39
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:47
FormatMetadata\formatNum
formatNum( $num, $round=false, $tagName=null)
Format a number, convert numbers from fractions into floating point numbers, joins arrays of numbers ...
Definition: FormatMetadata.php:1375
FormatMetadata\sanitizeKeyForAPI
sanitizeKeyForAPI( $key)
Turns a string into a valid API identifier.
Definition: FormatMetadata.php:2004
FormatMetadata\gcd
gcd( $a, $b)
Calculate the greatest common divisor of two integers.
Definition: FormatMetadata.php:1442
FormatMetadata\getFormattedData
static getFormattedData( $tags, $context=false)
Numbers given by Exif user agents are often magical, that is they should be replaced by a detailed ex...
Definition: FormatMetadata.php:87
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:193
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
FormatMetadata\collapseContactInfo
collapseContactInfo(array $vals)
Format the contact info field into a single value.
Definition: FormatMetadata.php:1598
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1668
FormatMetadata\fetchExtendedMetadata
fetchExtendedMetadata(File $file)
Get an array of extended metadata.
Definition: FormatMetadata.php:1735
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1186
FormatMetadata\literal
literal( $val)
Convenience function for getFormattedData()
Definition: FormatMetadata.php:1337
FormatMetadata\getExtendedMetadataFromFile
getExtendedMetadataFromFile(File $file)
Get file-based metadata in standardized format.
Definition: FormatMetadata.php:1785
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:958
FormatMetadata\formatFraction
formatFraction( $num)
Format a rational number, reducing fractions.
Definition: FormatMetadata.php:1420
FormatMetadata\setSingleLanguage
setSingleLanguage( $val)
Trigger only outputting single language for multilanguage fields.
Definition: FormatMetadata.php:70
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:153
DerivativeContext
An IContextSource implementation which will inherit context from another source but allow individual ...
Definition: DerivativeContext.php:32
FormatMetadata\discardMultipleValues
discardMultipleValues(&$metadata)
Takes an array returned by the getExtendedMetadata* functions, and turns all fields into single-value...
Definition: FormatMetadata.php:1946
FormatMetadata\convertNewsCode
convertNewsCode( $val)
Fetch the human readable version of a news code.
Definition: FormatMetadata.php:1474
File
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:66
MWException
MediaWiki exception.
Definition: MWException.php:29
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
FormatMetadata\resolveMultivalueValue
resolveMultivalueValue( $value)
Turns an XMP-style multivalue array into a single value by dropping all but the first value.
Definition: FormatMetadata.php:1904
FormatMetadata\getPriorityLanguages
getPriorityLanguages()
Returns a list of languages (first is best) to use when formatting multilang fields,...
Definition: FormatMetadata.php:2028
FormatMetadata\langItem
langItem( $value, $lang, $default=false, $noHtml=false)
Helper function for creating lists of translations.
Definition: FormatMetadata.php:1275
$matches
$matches
Definition: NoLocalSettings.php:24
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:33
FormatMetadata\getExtendedMetadataFromHook
getExtendedMetadataFromHook(File $file, array $extendedMetadata, &$maxCacheTime)
Get additional metadata from hooks in standardized format.
Definition: FormatMetadata.php:1834
$title
$title
Definition: testCompression.php:38
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1697
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:63
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:197
$content
$content
Definition: router.php:76
FormatMetadata\$singleLang
bool $singleLang
Only output a single language for multi-language fields.
Definition: FormatMetadata.php:62
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:206
$line
$line
Definition: mcc.php:119
FormatMetadata\flattenArrayContentLang
static flattenArrayContentLang( $vals, $type='ul', $noHtml=false, $context=false)
Flatten an array, using the content language for any messages.
Definition: FormatMetadata.php:1115
$lines
if(!file_exists( $CREDITS)) $lines
Definition: updateCredits.php:45
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:58
FormatMetadata
Format Image metadata values into a human readable form.
Definition: FormatMetadata.php:54
FormatMetadata\exifMsg
exifMsg( $tag, $val, $arg=null, $arg2=null)
Convenience function for getFormattedData()
Definition: FormatMetadata.php:1354
FormatMetadata\sanitizeArrayForAPI
sanitizeArrayForAPI(&$arr)
Makes sure the given array is a valid API response fragment.
Definition: FormatMetadata.php:1966
ApiResult\setPreserveKeysList
static setPreserveKeysList(array &$arr, $names)
Preserve specified keys.
Definition: ApiResult.php:662
FormatMetadata\resolveMultilangMetadata
resolveMultilangMetadata(&$metadata)
Takes an array returned by the getExtendedMetadata* functions, and resolves multi-language values in ...
Definition: FormatMetadata.php:1929
$cache
$cache
Definition: mcc.php:33
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
$keys
$keys
Definition: testCompression.php:72
FormatMetadata\flattenArrayReal
flattenArrayReal( $vals, $type='ul', $noHtml=false)
A function to collapse multivalued tags into a single value.
Definition: FormatMetadata.php:1154
File\DELETED_FILE
const DELETED_FILE
Definition: File.php:70
FormatMetadata\formatCoords
formatCoords( $coord, string $type)
Format a coordinate value, convert numbers from floating point into degree minute second representati...
Definition: FormatMetadata.php:1549
ForeignAPIFile
Foreign file accessible through api.php requests.
Definition: ForeignAPIFile.php:32
FormatMetadata\getVisibleFields
static getVisibleFields()
Get a list of fields that are visible by default.
Definition: FormatMetadata.php:1714
FormatMetadata\resolveMultilangValue
resolveMultilangValue( $value)
Turns an XMP-style multilang array into a single value.
Definition: FormatMetadata.php:1863
FormatMetadata\makeFormattedData
makeFormattedData( $tags)
Numbers given by Exif user agents are often magical, that is they should be replaced by a detailed ex...
Definition: FormatMetadata.php:107
$type
$type
Definition: testCompression.php:52