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