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