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