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