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