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