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