28 use Wikimedia\Timestamp\TimestampException;
66 $this->singleLang = $val;
88 return $obj->makeFormattedData( $tags );
103 $resolutionunit = !isset( $tags[
'ResolutionUnit'] ) || $tags[
'ResolutionUnit'] == 2 ? 2 : 3;
104 unset( $tags[
'ResolutionUnit'] );
106 foreach ( $tags as $tag => &$vals ) {
109 if ( !is_array( $tags[$tag] ) ) {
114 if ( isset( $tags[$tag][
'_type'] ) ) {
115 $type = $tags[$tag][
'_type'];
116 unset( $vals[
'_type'] );
122 if ( $tag ==
'GPSTimeStamp' && count( $vals ) === 3 ) {
125 $h = explode(
'/', $vals[0] );
126 $m = explode(
'/', $vals[1] );
127 $s = explode(
'/', $vals[2] );
142 $tags[$tag] = str_pad( intval( $h[0] / $h[1] ), 2,
'0', STR_PAD_LEFT )
143 .
':' . str_pad( intval( $m[0] / $m[1] ), 2,
'0', STR_PAD_LEFT )
144 .
':' . str_pad( intval(
$s[0] /
$s[1] ), 2,
'0', STR_PAD_LEFT );
147 $time =
wfTimestamp( TS_MW,
'1971:01:01 ' . $tags[$tag] );
149 if ( $time && intval( $time ) > 0 ) {
152 }
catch ( TimestampException $e ) {
163 if ( $tag ===
'Contact' ) {
168 foreach ( $vals as &$val ) {
183 $val = $this->
exifMsg( $tag, $val );
191 case 'PhotometricInterpretation':
205 $val = $this->
exifMsg( $tag, $val );
223 $val = $this->
exifMsg( $tag, $val );
231 case 'PlanarConfiguration':
235 $val = $this->
exifMsg( $tag, $val );
244 case 'YCbCrPositioning':
248 $val = $this->
exifMsg( $tag, $val );
258 switch ( $resolutionunit ) {
273 case 'FlashpixVersion':
274 $val = (int)$val / 100;
281 $val = $this->
exifMsg( $tag, $val );
289 case 'ComponentsConfiguration':
298 $val = $this->
exifMsg( $tag, $val );
307 case 'DateTimeOriginal':
308 case 'DateTimeDigitized':
309 case 'DateTimeReleased':
310 case 'DateTimeExpires':
313 case 'DateTimeMetadata':
314 if ( $val ==
'0000:00:00 00:00:00' || $val ==
' : : : : ' ) {
315 $val = $this->
msg(
'exif-unknowndate' )->text();
316 } elseif ( preg_match(
317 '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D',
322 if ( $time && intval( $time ) > 0 ) {
325 } elseif ( preg_match(
'/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d)$/D', $val ) ) {
330 if ( $time && intval( $time ) > 0 ) {
333 } elseif ( preg_match(
'/^(?:\d{4}):(?:\d\d):(?:\d\d)$/D', $val ) ) {
336 . substr( $val, 5, 2 )
337 . substr( $val, 8, 2 )
339 if ( $time && intval( $time ) > 0 ) {
346 case 'ExposureProgram':
357 $val = $this->
exifMsg( $tag, $val );
365 case 'SubjectDistance':
380 $val = $this->
exifMsg( $tag, $val );
411 $val = $this->
exifMsg( $tag, $val );
421 'fired' => $val & 0b00000001,
422 'return' => ( $val & 0b00000110 ) >> 1,
423 'mode' => ( $val & 0b00011000 ) >> 3,
424 'function' => ( $val & 0b00100000 ) >> 5,
425 'redeye' => ( $val & 0b01000000 ) >> 6,
429 # We do not need to handle unknown values since all are used.
430 foreach ( $flashDecode as $subTag => $subValue ) {
431 # We do not need any message for zeroed values.
432 if ( $subTag !=
'fired' && $subValue == 0 ) {
435 $fullTag = $tag .
'-' . $subTag;
436 $flashMsgs[] = $this->
exifMsg( $fullTag, $subValue );
438 $val = $this->
getLanguage()->commaList( $flashMsgs );
441 case 'FocalPlaneResolutionUnit':
444 $val = $this->
exifMsg( $tag, $val );
452 case 'SensingMethod':
461 $val = $this->
exifMsg( $tag, $val );
472 $val = $this->
exifMsg( $tag, $val );
483 $val = $this->
exifMsg( $tag, $val );
491 case 'CustomRendered':
503 $val = $this->
exifMsg( $tag, $val );
516 $val = $this->
exifMsg( $tag, $val );
528 $val = $this->
exifMsg( $tag, $val );
536 case 'SceneCaptureType':
542 $val = $this->
exifMsg( $tag, $val );
557 $val = $this->
exifMsg( $tag, $val );
570 $val = $this->
exifMsg( $tag, $val );
583 $val = $this->
exifMsg( $tag, $val );
596 $val = $this->
exifMsg( $tag, $val );
604 case 'SubjectDistanceRange':
610 $val = $this->
exifMsg( $tag, $val );
619 case 'GPSLatitudeRef':
620 case 'GPSDestLatitudeRef':
624 $val = $this->
exifMsg(
'GPSLatitude', $val );
632 case 'GPSLongitudeRef':
633 case 'GPSDestLongitudeRef':
637 $val = $this->
exifMsg(
'GPSLongitude', $val );
647 $val = $this->
exifMsg(
'GPSAltitude',
'below-sealevel', $this->
formatNum( -$val, 3 ) );
649 $val = $this->
exifMsg(
'GPSAltitude',
'above-sealevel', $this->
formatNum( $val, 3 ) );
657 $val = $this->
exifMsg( $tag, $val );
665 case 'GPSMeasureMode':
669 $val = $this->
exifMsg( $tag, $val );
678 case 'GPSImgDirectionRef':
679 case 'GPSDestBearingRef':
683 $val = $this->
exifMsg(
'GPSDirection', $val );
692 case 'GPSDestLatitude':
696 case 'GPSDestLongitude':
705 $val = $this->
exifMsg(
'GPSSpeed', $val );
713 case 'GPSDestDistanceRef':
718 $val = $this->
exifMsg(
'GPSDestDistance', $val );
730 } elseif ( $val <= 5 ) {
732 } elseif ( $val <= 10 ) {
734 } elseif ( $val <= 20 ) {
746 $val = $this->
exifMsg( $tag,
'', $val );
750 if ( is_array( $val ) ) {
751 if ( count( $val ) > 1 ) {
753 $val = $this->
msg(
'exif-software-version-value', $val[0], $val[1] )->text();
756 $val = $this->
exifMsg( $tag,
'', $val[0] );
759 $val = $this->
exifMsg( $tag,
'', $val );
765 $val = $this->
msg(
'exif-exposuretime-format',
768 case 'ISOSpeedRatings':
772 if ( $val ==
'65535' ) {
773 $val = $this->
exifMsg( $tag,
'overflow' );
779 $val = $this->
msg(
'exif-fnumber-format',
784 case 'FocalLengthIn35mmFilm':
785 $val = $this->
msg(
'exif-focallength-format',
789 case 'MaxApertureValue':
790 if ( strpos( $val,
'/' ) !==
false ) {
792 list( $n, $d ) = explode(
'/', $val );
793 if ( is_numeric( $n ) && is_numeric( $d ) ) {
797 if ( is_numeric( $val ) ) {
798 $fNumber = 2 ** ( $val / 2 );
799 if ( $fNumber !==
false ) {
800 $val = $this->
msg(
'exif-maxaperturevalue-value',
809 switch ( strtolower( $val ) ) {
835 case 'SubjectNewsCode':
847 if ( $val == 0 || $val == 9 ) {
849 } elseif ( $val < 5 && $val > 1 ) {
851 } elseif ( $val == 5 ) {
853 } elseif ( $val <= 8 && $val > 5 ) {
857 if ( $urgency !==
'' ) {
858 $val = $this->
exifMsg(
'urgency',
865 case 'OriginalImageHeight':
866 case 'OriginalImageWidth':
867 case 'PixelXDimension':
868 case 'PixelYDimension':
871 $val = $this->
formatNum( $val ) .
' ' . $this->
msg(
'unit-pixel' )->text();
881 case 'ImageDescription':
885 case 'RelatedSoundFile':
886 case 'ImageUniqueID':
887 case 'SpectralSensitivity':
888 case 'GPSSatellites':
892 case 'WorldRegionDest':
894 case 'CountryCodeDest':
895 case 'ProvinceOrStateDest':
897 case 'SublocationDest':
898 case 'WorldRegionCreated':
899 case 'CountryCreated':
900 case 'CountryCodeCreated':
901 case 'ProvinceOrStateCreated':
903 case 'SublocationCreated':
905 case 'SpecialInstructions':
910 case 'FixtureIdentifier':
912 case 'LocationDestCode':
914 case 'JPEGFileComment':
915 case 'iimSupplementalCategory':
916 case 'OriginalTransmissionRef':
918 case 'dc-contributor':
927 case 'CameraOwnerName':
930 case 'RightsCertificate':
931 case 'CopyrightOwner':
934 case 'OriginalDocumentID':
936 case 'MorePermissionsUrl':
937 case 'AttributionUrl':
938 case 'PreferredAttributionName':
939 case 'PNGFileComment':
941 case 'ContentWarning':
942 case 'GIFFileComment':
944 case 'IntellectualGenre':
946 case 'OrginisationInImage':
947 case 'PersonInImage':
949 $val = htmlspecialchars( $val );
957 $val = $this->
exifMsg( $tag, $val );
960 $val = htmlspecialchars( $val );
968 $val = $this->
exifMsg( $tag, $val );
973 if ( $val ==
'-1' ) {
974 $val = $this->
exifMsg( $tag,
'rejected' );
982 $val = htmlspecialchars(
$lang ?: $val );
1019 $context->setLanguage( MediaWikiServices::getInstance()->getContentLanguage() );
1022 return $obj->flattenArrayReal( $vals,
$type, $noHtml );
1042 if ( !is_array( $vals ) ) {
1046 if ( isset( $vals[
'_type'] ) ) {
1047 $type = $vals[
'_type'];
1048 unset( $vals[
'_type'] );
1051 if ( !is_array( $vals ) ) {
1053 } elseif ( count( $vals ) === 1 &&
$type !==
'lang' && isset( $vals[0] ) ) {
1055 } elseif ( count( $vals ) === 0 ) {
1056 wfDebug( __METHOD__ .
" metadata array with 0 elements!\n" );
1075 $defaultItem =
false;
1076 $defaultLang =
false;
1085 if ( isset( $vals[
'x-default'] ) ) {
1086 $defaultItem = $vals[
'x-default'];
1087 unset( $vals[
'x-default'] );
1089 foreach ( $priorityLanguages as $pLang ) {
1090 if ( isset( $vals[$pLang] ) ) {
1092 if ( $vals[$pLang] === $defaultItem ) {
1093 $defaultItem =
false;
1097 $vals[$pLang], $pLang,
1098 $isDefault, $noHtml );
1100 unset( $vals[$pLang] );
1102 if ( $this->singleLang ) {
1104 [
'lang' => $pLang ], $vals[$pLang] );
1110 foreach ( $vals as
$lang => $item ) {
1111 if ( $item === $defaultItem ) {
1112 $defaultLang =
$lang;
1116 $lang,
false, $noHtml );
1117 if ( $this->singleLang ) {
1119 [
'lang' =>
$lang ], $item );
1122 if ( $defaultItem !==
false ) {
1124 $defaultLang,
true, $noHtml ) .
1126 if ( $this->singleLang ) {
1127 return $defaultItem;
1134 return '<ul class="metadata-langlist">' .
1139 return "\n#" . implode(
"\n#", $vals );
1142 return "<ol><li>" . implode(
"</li>\n<li>", $vals ) .
'</li></ol>';
1146 return "\n*" . implode(
"\n*", $vals );
1149 return "<ul><li>" . implode(
"</li>\n<li>", $vals ) .
'</li></ul>';
1165 if (
$lang ===
false && $default ===
false ) {
1166 throw new MWException(
'$lang and $default cannot both '
1171 $wrappedValue = $value;
1173 $wrappedValue =
'<span class="mw-metadata-lang-value">'
1174 . $value .
'</span>';
1177 if (
$lang ===
false ) {
1178 $msg = $this->
msg(
'metadata-langitem-default', $wrappedValue );
1180 return $msg->text() .
"\n\n";
1183 return '<li class="mw-metadata-lang-default">'
1188 $lowLang = strtolower(
$lang );
1190 if ( $langName ===
'' ) {
1192 $langPrefix = explode(
'-', $lowLang, 2 )[0];
1194 if ( $langName ===
'' ) {
1201 $msg = $this->
msg(
'metadata-langitem', $wrappedValue, $langName,
$lang );
1203 return '*' . $msg->text();
1206 $item =
'<li class="mw-metadata-lang-code-'
1209 $item .=
' mw-metadata-lang-default';
1211 $item .=
'" lang="' .
$lang .
'">';
1212 $item .= $msg->text();
1227 private function exifMsg( $tag, $val, $arg =
null, $arg2 =
null ) {
1228 if ( $val ===
'' ) {
1233 MediaWikiServices::getInstance()->getContentLanguage()->lc(
"exif-$tag-$val" ),
1249 if ( is_array( $num ) ) {
1251 foreach ( $num as $number ) {
1257 if ( preg_match(
'/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
1259 $newNum = $m[1] / $m[2];
1260 if ( $round !==
false ) {
1261 $newNum = round( $newNum, $round );
1267 return $this->
getLanguage()->formatNum( $newNum );
1269 if ( is_numeric( $num ) && $round !==
false ) {
1270 $num = round( $num, $round );
1285 if ( preg_match(
'/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
1286 $numerator = intval( $m[1] );
1287 $denominator = intval( $m[2] );
1288 $gcd = $this->
gcd( abs( $numerator ), $denominator );
1291 return $this->
formatNum( $numerator / $gcd ) .
'/' . $this->
formatNum( $denominator / $gcd );
1305 private function gcd( $a, $b ) {
1315 $remainder = $a % $b;
1338 if ( !preg_match(
'/^\d{8}$/D', $val ) ) {
1343 switch ( substr( $val, 0, 2 ) ) {
1396 if ( $cat !==
'' ) {
1397 $catMsg = $this->
exifMsg(
'iimcategory', $cat );
1398 $val = $this->
exifMsg(
'subjectnewscode',
'', $val, $catMsg );
1413 if ( !is_numeric( $coord ) ) {
1414 wfDebugLog(
'exif', __METHOD__ .
": \"$coord\" is not a number" );
1415 return (
string)$coord;
1421 if (
$type ===
'latitude' ) {
1423 } elseif (
$type ===
'longitude' ) {
1427 $nCoord = (float)$coord;
1428 if (
$type ===
'latitude' ) {
1430 } elseif (
$type ===
'longitude' ) {
1435 $deg = floor( $nCoord );
1436 $min = floor( ( $nCoord - $deg ) * 60 );
1437 $sec = round( ( ( $nCoord - $deg ) * 60 - $min ) * 60, 2 );
1444 return $this->
msg(
'exif-coordinate-format', $deg, $min, $sec, $ref, $coord )->text();
1462 if ( !( isset( $vals[
'CiAdrExtadr'] )
1463 || isset( $vals[
'CiAdrCity'] )
1464 || isset( $vals[
'CiAdrCtry'] )
1465 || isset( $vals[
'CiEmailWork'] )
1466 || isset( $vals[
'CiTelWork'] )
1467 || isset( $vals[
'CiAdrPcode'] )
1468 || isset( $vals[
'CiAdrRegion'] )
1469 || isset( $vals[
'CiUrlWork'] )
1480 foreach ( $vals as &$val ) {
1481 $val = htmlspecialchars( $val );
1489 $url = $tel = $street = $city = $country =
'';
1490 $email = $postal = $region =
'';
1499 if ( isset( $vals[
'CiAdrExtadr'] ) ) {
1502 $street =
'<span class="extended-address">'
1504 $vals[
'CiAdrExtadr'] )
1507 if ( isset( $vals[
'CiAdrCity'] ) ) {
1508 $city =
'<span class="locality">'
1509 . htmlspecialchars( $vals[
'CiAdrCity'] )
1512 if ( isset( $vals[
'CiAdrCtry'] ) ) {
1513 $country =
'<span class="country-name">'
1514 . htmlspecialchars( $vals[
'CiAdrCtry'] )
1517 if ( isset( $vals[
'CiEmailWork'] ) ) {
1520 $splitEmails = explode(
"\n", $vals[
'CiEmailWork'] );
1521 foreach ( $splitEmails as $e1 ) {
1523 foreach ( explode(
',', $e1 ) as $e2 ) {
1524 $finalEmail = trim( $e2 );
1525 if ( $finalEmail ==
',' || $finalEmail ==
'' ) {
1528 if ( strpos( $finalEmail,
'<' ) !==
false ) {
1531 $emails[] = $finalEmail;
1533 $emails[] =
'[mailto:'
1535 .
' <span class="email">'
1541 $email = implode(
', ', $emails );
1543 if ( isset( $vals[
'CiTelWork'] ) ) {
1544 $tel =
'<span class="tel">'
1545 . htmlspecialchars( $vals[
'CiTelWork'] )
1548 if ( isset( $vals[
'CiAdrPcode'] ) ) {
1549 $postal =
'<span class="postal-code">'
1551 $vals[
'CiAdrPcode'] )
1554 if ( isset( $vals[
'CiAdrRegion'] ) ) {
1556 $region =
'<span class="region">'
1558 $vals[
'CiAdrRegion'] )
1561 if ( isset( $vals[
'CiUrlWork'] ) ) {
1562 $url =
'<span class="url">'
1563 . htmlspecialchars( $vals[
'CiUrlWork'] )
1567 return $this->
msg(
'exif-contact-value', $email, $url,
1568 $street, $city, $region, $postal, $country,
1581 $lines = explode(
"\n",
wfMessage(
'metadata-fields' )->inContentLanguage()->text() );
1584 if ( preg_match(
'/^\\*\s*(.*?)\s*$/',
$line,
$matches ) ) {
1588 $fields = array_map(
'strtolower', $fields );
1601 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1608 $cacheKey =
$cache->makeKey(
1609 'getExtendedMetadata',
1611 (
int)$this->singleLang,
1615 $cachedValue =
$cache->get( $cacheKey );
1618 &&
Hooks::run(
'ValidateExtendedMetadataCache', [ $cachedValue[
'timestamp'],
$file ] )
1620 $extendedMetadata = $cachedValue[
'data'];
1625 if ( $this->singleLang ) {
1634 $valueToCache = [
'data' => $extendedMetadata,
'timestamp' =>
wfTimestampNow() ];
1635 $cache->set( $cacheKey, $valueToCache, $maxCacheTime );
1638 return $extendedMetadata;
1656 return $file->getExtendedMetadata() ?: [];
1664 'value' => $uploadDate,
1665 'source' =>
'mediawiki-metadata',
1671 $text =
$title->getText();
1672 $pos = strrpos( $text,
'.' );
1675 $name = substr( $text, 0, $pos );
1680 $fileMetadata[
'ObjectName'] = [
1682 'source' =>
'mediawiki-metadata',
1686 return $fileMetadata;
1710 $visible = array_flip( self::getVisibleFields() );
1711 foreach ( $extendedMetadata as $key => $value ) {
1712 if ( !isset( $visible[strtolower( $key )] ) ) {
1713 $extendedMetadata[$key][
'hidden'] =
'';
1717 return $extendedMetadata;
1731 || !isset( $value[
'_type'] )
1732 || $value[
'_type'] !=
'lang'
1739 foreach ( $priorityLanguages as
$lang ) {
1740 if ( isset( $value[
$lang] ) ) {
1741 return $value[
$lang];
1746 if ( isset( $value[
'x-default'] ) ) {
1747 return $value[
'x-default'];
1751 unset( $value[
'_type'] );
1752 if ( !empty( $value ) ) {
1753 return reset( $value );
1770 if ( !is_array( $value ) ) {
1772 } elseif ( isset( $value[
'_type'] ) && $value[
'_type'] ===
'lang' ) {
1775 foreach ( $value as $k => $v ) {
1780 $v = reset( $value );
1781 if ( key( $value ) ===
'_type' ) {
1782 $v = next( $value );
1795 if ( !is_array( $metadata ) ) {
1798 foreach ( $metadata as &$field ) {
1799 if ( isset( $field[
'value'] ) ) {
1812 if ( !is_array( $metadata ) ) {
1815 foreach ( $metadata as $key => &$field ) {
1816 if ( $key ===
'Software' || $key ===
'Contact' ) {
1821 if ( isset( $field[
'value'] ) ) {
1832 if ( !is_array( $arr ) ) {
1837 foreach ( $arr as $key => &$value ) {
1839 if ( $sanitizedKey !== $key ) {
1840 if ( isset( $arr[$sanitizedKey] ) ) {
1845 $sanitizedKey .= $counter;
1848 $arr[$sanitizedKey] = $arr[$key];
1849 unset( $arr[$key] );
1851 if ( is_array( $value ) ) {
1857 $keys = array_filter( array_keys( $arr ),
'ApiResult::isMetadataKey' );
1873 $key = preg_replace(
'/[^a-zA-z0-9_:.\-]/',
'', $key );
1875 $key = preg_replace(
'/^[\d\-.]+/',
'', $key );
1882 if ( $key ==
'_element' ) {
1896 $priorityLanguages =
1898 $priorityLanguages = array_merge(
1900 $priorityLanguages[0],
1901 $priorityLanguages[1]
1904 return $priorityLanguages;