28 use Wikimedia\Timestamp\TimestampException;
66 $this->singleLang = $val;
88 return $obj->makeFormattedData( $tags );
104 $resolutionunit = !isset( $tags[
'ResolutionUnit'] ) || $tags[
'ResolutionUnit'] == 2 ? 2 : 3;
105 unset( $tags[
'ResolutionUnit'] );
107 foreach ( $tags as $tag => &$vals ) {
110 if ( !is_array( $tags[$tag] ) ) {
115 if ( isset( $tags[$tag][
'_type'] ) ) {
116 $type = $tags[$tag][
'_type'];
117 unset( $vals[
'_type'] );
123 if ( $tag ==
'GPSTimeStamp' && count( $vals ) === 3 ) {
126 $h = explode(
'/', $vals[0] );
127 $m = explode(
'/', $vals[1] );
128 $s = explode(
'/', $vals[2] );
143 $tags[$tag] = str_pad( intval( $h[0] / $h[1] ), 2,
'0', STR_PAD_LEFT )
144 .
':' . str_pad( intval( $m[0] / $m[1] ), 2,
'0', STR_PAD_LEFT )
145 .
':' . str_pad( intval(
$s[0] /
$s[1] ), 2,
'0', STR_PAD_LEFT );
148 $time =
wfTimestamp( TS_MW,
'1971:01:01 ' . $tags[$tag] );
150 if ( $time && intval( $time ) > 0 ) {
153 }
catch ( TimestampException $e ) {
164 if ( $tag ===
'Contact' ) {
169 foreach ( $vals as &$val ) {
184 $val = $this->
exifMsg( $tag, $val );
192 case 'PhotometricInterpretation':
206 $val = $this->
exifMsg( $tag, $val );
224 $val = $this->
exifMsg( $tag, $val );
232 case 'PlanarConfiguration':
236 $val = $this->
exifMsg( $tag, $val );
245 case 'YCbCrPositioning':
249 $val = $this->
exifMsg( $tag, $val );
259 switch ( $resolutionunit ) {
274 case 'FlashpixVersion':
275 $val = (int)$val / 100;
282 $val = $this->
exifMsg( $tag, $val );
290 case 'ComponentsConfiguration':
299 $val = $this->
exifMsg( $tag, $val );
308 case 'DateTimeOriginal':
309 case 'DateTimeDigitized':
310 case 'DateTimeReleased':
311 case 'DateTimeExpires':
314 case 'DateTimeMetadata':
315 if ( $val ==
'0000:00:00 00:00:00' || $val ==
' : : : : ' ) {
316 $val = $this->
msg(
'exif-unknowndate' )->text();
317 } elseif ( preg_match(
318 '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D',
323 if ( $time && intval( $time ) > 0 ) {
326 } elseif ( preg_match(
'/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d)$/D', $val ) ) {
331 if ( $time && intval( $time ) > 0 ) {
334 } elseif ( preg_match(
'/^(?:\d{4}):(?:\d\d):(?:\d\d)$/D', $val ) ) {
337 . substr( $val, 5, 2 )
338 . substr( $val, 8, 2 )
340 if ( $time && intval( $time ) > 0 ) {
347 case 'ExposureProgram':
358 $val = $this->
exifMsg( $tag, $val );
366 case 'SubjectDistance':
381 $val = $this->
exifMsg( $tag, $val );
412 $val = $this->
exifMsg( $tag, $val );
422 'fired' => $val & 0b00000001,
423 'return' => ( $val & 0b00000110 ) >> 1,
424 'mode' => ( $val & 0b00011000 ) >> 3,
425 'function' => ( $val & 0b00100000 ) >> 5,
426 'redeye' => ( $val & 0b01000000 ) >> 6,
430 # We do not need to handle unknown values since all are used.
431 foreach ( $flashDecode as $subTag => $subValue ) {
432 # We do not need any message for zeroed values.
433 if ( $subTag !=
'fired' && $subValue == 0 ) {
436 $fullTag = $tag .
'-' . $subTag;
437 $flashMsgs[] = $this->
exifMsg( $fullTag, $subValue );
439 $val = $this->
getLanguage()->commaList( $flashMsgs );
442 case 'FocalPlaneResolutionUnit':
445 $val = $this->
exifMsg( $tag, $val );
453 case 'SensingMethod':
462 $val = $this->
exifMsg( $tag, $val );
473 $val = $this->
exifMsg( $tag, $val );
484 $val = $this->
exifMsg( $tag, $val );
492 case 'CustomRendered':
504 $val = $this->
exifMsg( $tag, $val );
517 $val = $this->
exifMsg( $tag, $val );
529 $val = $this->
exifMsg( $tag, $val );
537 case 'SceneCaptureType':
543 $val = $this->
exifMsg( $tag, $val );
558 $val = $this->
exifMsg( $tag, $val );
571 $val = $this->
exifMsg( $tag, $val );
584 $val = $this->
exifMsg( $tag, $val );
597 $val = $this->
exifMsg( $tag, $val );
605 case 'SubjectDistanceRange':
611 $val = $this->
exifMsg( $tag, $val );
620 case 'GPSLatitudeRef':
621 case 'GPSDestLatitudeRef':
625 $val = $this->
exifMsg(
'GPSLatitude', $val );
633 case 'GPSLongitudeRef':
634 case 'GPSDestLongitudeRef':
638 $val = $this->
exifMsg(
'GPSLongitude', $val );
648 $val = $this->
exifMsg(
'GPSAltitude',
'below-sealevel', $this->
formatNum( -$val, 3 ) );
650 $val = $this->
exifMsg(
'GPSAltitude',
'above-sealevel', $this->
formatNum( $val, 3 ) );
658 $val = $this->
exifMsg( $tag, $val );
666 case 'GPSMeasureMode':
670 $val = $this->
exifMsg( $tag, $val );
679 case 'GPSImgDirectionRef':
680 case 'GPSDestBearingRef':
684 $val = $this->
exifMsg(
'GPSDirection', $val );
693 case 'GPSDestLatitude':
697 case 'GPSDestLongitude':
706 $val = $this->
exifMsg(
'GPSSpeed', $val );
714 case 'GPSDestDistanceRef':
719 $val = $this->
exifMsg(
'GPSDestDistance', $val );
731 } elseif ( $val <= 5 ) {
733 } elseif ( $val <= 10 ) {
735 } elseif ( $val <= 20 ) {
747 $val = $this->
exifMsg( $tag,
'', $val );
751 if ( is_array( $val ) ) {
752 if ( count( $val ) > 1 ) {
754 $val = $this->
msg(
'exif-software-version-value', $val[0], $val[1] )->text();
757 $val = $this->
exifMsg( $tag,
'', $val[0] );
760 $val = $this->
exifMsg( $tag,
'', $val );
766 $val = $this->
msg(
'exif-exposuretime-format',
769 case 'ISOSpeedRatings':
773 if ( $val ==
'65535' ) {
774 $val = $this->
exifMsg( $tag,
'overflow' );
780 $val = $this->
msg(
'exif-fnumber-format',
785 case 'FocalLengthIn35mmFilm':
786 $val = $this->
msg(
'exif-focallength-format',
790 case 'MaxApertureValue':
791 if ( strpos( $val,
'/' ) !==
false ) {
793 list( $n, $d ) = explode(
'/', $val );
794 if ( is_numeric( $n ) && is_numeric( $d ) ) {
798 if ( is_numeric( $val ) ) {
799 $fNumber = 2 ** ( $val / 2 );
800 if ( $fNumber !==
false ) {
801 $val = $this->
msg(
'exif-maxaperturevalue-value',
810 switch ( strtolower( $val ) ) {
836 case 'SubjectNewsCode':
848 if ( $val == 0 || $val == 9 ) {
850 } elseif ( $val < 5 && $val > 1 ) {
852 } elseif ( $val == 5 ) {
854 } elseif ( $val <= 8 && $val > 5 ) {
858 if ( $urgency !==
'' ) {
859 $val = $this->
exifMsg(
'urgency',
866 case 'OriginalImageHeight':
867 case 'OriginalImageWidth':
868 case 'PixelXDimension':
869 case 'PixelYDimension':
872 $val = $this->
formatNum( $val ) .
' ' . $this->
msg(
'unit-pixel' )->text();
882 case 'ImageDescription':
886 case 'RelatedSoundFile':
887 case 'ImageUniqueID':
888 case 'SpectralSensitivity':
889 case 'GPSSatellites':
893 case 'WorldRegionDest':
895 case 'CountryCodeDest':
896 case 'ProvinceOrStateDest':
898 case 'SublocationDest':
899 case 'WorldRegionCreated':
900 case 'CountryCreated':
901 case 'CountryCodeCreated':
902 case 'ProvinceOrStateCreated':
904 case 'SublocationCreated':
906 case 'SpecialInstructions':
911 case 'FixtureIdentifier':
913 case 'LocationDestCode':
915 case 'JPEGFileComment':
916 case 'iimSupplementalCategory':
917 case 'OriginalTransmissionRef':
919 case 'dc-contributor':
928 case 'CameraOwnerName':
931 case 'RightsCertificate':
932 case 'CopyrightOwner':
935 case 'OriginalDocumentID':
937 case 'MorePermissionsUrl':
938 case 'AttributionUrl':
939 case 'PreferredAttributionName':
940 case 'PNGFileComment':
942 case 'ContentWarning':
943 case 'GIFFileComment':
945 case 'IntellectualGenre':
947 case 'OrginisationInImage':
948 case 'PersonInImage':
950 $val = htmlspecialchars( $val );
958 $val = $this->
exifMsg( $tag, $val );
961 $val = htmlspecialchars( $val );
969 $val = $this->
exifMsg( $tag, $val );
974 if ( $val ==
'-1' ) {
975 $val = $this->
exifMsg( $tag,
'rejected' );
983 $val = htmlspecialchars(
$lang ?: $val );
1020 $context->setLanguage( MediaWikiServices::getInstance()->getContentLanguage() );
1023 return $obj->flattenArrayReal( $vals,
$type, $noHtml );
1043 if ( !is_array( $vals ) ) {
1047 if ( isset( $vals[
'_type'] ) ) {
1048 $type = $vals[
'_type'];
1049 unset( $vals[
'_type'] );
1052 if ( !is_array( $vals ) ) {
1054 } elseif ( count( $vals ) === 1 &&
$type !==
'lang' && isset( $vals[0] ) ) {
1056 } elseif ( count( $vals ) === 0 ) {
1057 wfDebug( __METHOD__ .
" metadata array with 0 elements!\n" );
1076 $defaultItem =
false;
1077 $defaultLang =
false;
1086 if ( isset( $vals[
'x-default'] ) ) {
1087 $defaultItem = $vals[
'x-default'];
1088 unset( $vals[
'x-default'] );
1090 foreach ( $priorityLanguages as $pLang ) {
1091 if ( isset( $vals[$pLang] ) ) {
1093 if ( $vals[$pLang] === $defaultItem ) {
1094 $defaultItem =
false;
1098 $vals[$pLang], $pLang,
1099 $isDefault, $noHtml );
1101 unset( $vals[$pLang] );
1103 if ( $this->singleLang ) {
1104 return Html::rawElement(
'span',
1105 [
'lang' => $pLang ], $vals[$pLang] );
1111 foreach ( $vals as
$lang => $item ) {
1112 if ( $item === $defaultItem ) {
1113 $defaultLang =
$lang;
1117 $lang,
false, $noHtml );
1118 if ( $this->singleLang ) {
1119 return Html::rawElement(
'span',
1120 [
'lang' =>
$lang ], $item );
1123 if ( $defaultItem !==
false ) {
1125 $defaultLang,
true, $noHtml ) .
1127 if ( $this->singleLang ) {
1128 return $defaultItem;
1135 return '<ul class="metadata-langlist">' .
1140 return "\n#" . implode(
"\n#", $vals );
1143 return "<ol><li>" . implode(
"</li>\n<li>", $vals ) .
'</li></ol>';
1147 return "\n*" . implode(
"\n*", $vals );
1150 return "<ul><li>" . implode(
"</li>\n<li>", $vals ) .
'</li></ul>';
1166 if (
$lang ===
false && $default ===
false ) {
1167 throw new MWException(
'$lang and $default cannot both '
1172 $wrappedValue = $value;
1174 $wrappedValue =
'<span class="mw-metadata-lang-value">'
1175 . $value .
'</span>';
1178 if (
$lang ===
false ) {
1179 $msg = $this->
msg(
'metadata-langitem-default', $wrappedValue );
1181 return $msg->text() .
"\n\n";
1184 return '<li class="mw-metadata-lang-default">'
1189 $lowLang = strtolower(
$lang );
1191 if ( $langName ===
'' ) {
1193 $langPrefix = explode(
'-', $lowLang, 2 )[0];
1195 if ( $langName ===
'' ) {
1202 $msg = $this->
msg(
'metadata-langitem', $wrappedValue, $langName,
$lang );
1204 return '*' . $msg->text();
1207 $item =
'<li class="mw-metadata-lang-code-'
1210 $item .=
' mw-metadata-lang-default';
1212 $item .=
'" lang="' .
$lang .
'">';
1213 $item .= $msg->text();
1228 private function exifMsg( $tag, $val, $arg =
null, $arg2 =
null ) {
1229 if ( $val ===
'' ) {
1234 MediaWikiServices::getInstance()->getContentLanguage()->lc(
"exif-$tag-$val" ),
1250 if ( is_array( $num ) ) {
1252 foreach ( $num as $number ) {
1258 if ( preg_match(
'/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
1260 $newNum = $m[1] / $m[2];
1261 if ( $round !==
false ) {
1262 $newNum = round( $newNum, $round );
1268 return $this->
getLanguage()->formatNum( $newNum );
1270 if ( is_numeric( $num ) && $round !==
false ) {
1271 $num = round( $num, $round );
1286 if ( preg_match(
'/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
1287 $numerator = intval( $m[1] );
1288 $denominator = intval( $m[2] );
1289 $gcd = $this->
gcd( abs( $numerator ), $denominator );
1292 return $this->
formatNum( $numerator / $gcd ) .
'/' . $this->
formatNum( $denominator / $gcd );
1306 private function gcd( $a, $b ) {
1316 $remainder = $a % $b;
1339 if ( !preg_match(
'/^\d{8}$/D', $val ) ) {
1344 switch ( substr( $val, 0, 2 ) ) {
1397 if ( $cat !==
'' ) {
1398 $catMsg = $this->
exifMsg(
'iimcategory', $cat );
1399 $val = $this->
exifMsg(
'subjectnewscode',
'', $val, $catMsg );
1414 if ( !is_numeric( $coord ) ) {
1415 wfDebugLog(
'exif', __METHOD__ .
": \"$coord\" is not a number" );
1416 return (
string)$coord;
1422 if (
$type ===
'latitude' ) {
1424 } elseif (
$type ===
'longitude' ) {
1428 $nCoord = (float)$coord;
1429 if (
$type ===
'latitude' ) {
1431 } elseif (
$type ===
'longitude' ) {
1436 $deg = floor( $nCoord );
1437 $min = floor( ( $nCoord - $deg ) * 60 );
1438 $sec = round( ( ( $nCoord - $deg ) * 60 - $min ) * 60, 2 );
1445 return $this->
msg(
'exif-coordinate-format', $deg, $min, $sec, $ref, $coord )->text();
1463 if ( !( isset( $vals[
'CiAdrExtadr'] )
1464 || isset( $vals[
'CiAdrCity'] )
1465 || isset( $vals[
'CiAdrCtry'] )
1466 || isset( $vals[
'CiEmailWork'] )
1467 || isset( $vals[
'CiTelWork'] )
1468 || isset( $vals[
'CiAdrPcode'] )
1469 || isset( $vals[
'CiAdrRegion'] )
1470 || isset( $vals[
'CiUrlWork'] )
1481 foreach ( $vals as &$val ) {
1482 $val = htmlspecialchars( $val );
1490 $url = $tel = $street = $city = $country =
'';
1491 $email = $postal = $region =
'';
1500 if ( isset( $vals[
'CiAdrExtadr'] ) ) {
1503 $street =
'<span class="extended-address">'
1505 $vals[
'CiAdrExtadr'] )
1508 if ( isset( $vals[
'CiAdrCity'] ) ) {
1509 $city =
'<span class="locality">'
1510 . htmlspecialchars( $vals[
'CiAdrCity'] )
1513 if ( isset( $vals[
'CiAdrCtry'] ) ) {
1514 $country =
'<span class="country-name">'
1515 . htmlspecialchars( $vals[
'CiAdrCtry'] )
1518 if ( isset( $vals[
'CiEmailWork'] ) ) {
1521 $splitEmails = explode(
"\n", $vals[
'CiEmailWork'] );
1522 foreach ( $splitEmails as $e1 ) {
1524 foreach ( explode(
',', $e1 ) as $e2 ) {
1525 $finalEmail = trim( $e2 );
1526 if ( $finalEmail ==
',' || $finalEmail ==
'' ) {
1529 if ( strpos( $finalEmail,
'<' ) !==
false ) {
1532 $emails[] = $finalEmail;
1534 $emails[] =
'[mailto:'
1536 .
' <span class="email">'
1542 $email = implode(
', ', $emails );
1544 if ( isset( $vals[
'CiTelWork'] ) ) {
1545 $tel =
'<span class="tel">'
1546 . htmlspecialchars( $vals[
'CiTelWork'] )
1549 if ( isset( $vals[
'CiAdrPcode'] ) ) {
1550 $postal =
'<span class="postal-code">'
1552 $vals[
'CiAdrPcode'] )
1555 if ( isset( $vals[
'CiAdrRegion'] ) ) {
1557 $region =
'<span class="region">'
1559 $vals[
'CiAdrRegion'] )
1562 if ( isset( $vals[
'CiUrlWork'] ) ) {
1563 $url =
'<span class="url">'
1564 . htmlspecialchars( $vals[
'CiUrlWork'] )
1568 return $this->
msg(
'exif-contact-value', $email, $url,
1569 $street, $city, $region, $postal, $country,
1582 $lines = explode(
"\n",
wfMessage(
'metadata-fields' )->inContentLanguage()->text() );
1585 if ( preg_match(
'/^\\*\s*(.*?)\s*$/',
$line,
$matches ) ) {
1589 $fields = array_map(
'strtolower', $fields );
1602 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1609 $cacheKey =
$cache->makeKey(
1610 'getExtendedMetadata',
1612 (
int)$this->singleLang,
1616 $cachedValue =
$cache->get( $cacheKey );
1619 &&
Hooks::run(
'ValidateExtendedMetadataCache', [ $cachedValue[
'timestamp'],
$file ] )
1621 $extendedMetadata = $cachedValue[
'data'];
1626 if ( $this->singleLang ) {
1635 $valueToCache = [
'data' => $extendedMetadata,
'timestamp' =>
wfTimestampNow() ];
1636 $cache->set( $cacheKey, $valueToCache, $maxCacheTime );
1639 return $extendedMetadata;
1657 return $file->getExtendedMetadata() ?: [];
1665 'value' => $uploadDate,
1666 'source' =>
'mediawiki-metadata',
1672 $text =
$title->getText();
1673 $pos = strrpos( $text,
'.' );
1676 $name = substr( $text, 0, $pos );
1681 $fileMetadata[
'ObjectName'] = [
1683 'source' =>
'mediawiki-metadata',
1687 return $fileMetadata;
1711 $visible = array_flip( self::getVisibleFields() );
1712 foreach ( $extendedMetadata as $key => $value ) {
1713 if ( !isset( $visible[strtolower( $key )] ) ) {
1714 $extendedMetadata[$key][
'hidden'] =
'';
1718 return $extendedMetadata;
1732 || !isset( $value[
'_type'] )
1733 || $value[
'_type'] !=
'lang'
1740 foreach ( $priorityLanguages as
$lang ) {
1741 if ( isset( $value[
$lang] ) ) {
1742 return $value[
$lang];
1747 if ( isset( $value[
'x-default'] ) ) {
1748 return $value[
'x-default'];
1752 unset( $value[
'_type'] );
1753 if ( !empty( $value ) ) {
1754 return reset( $value );
1771 if ( !is_array( $value ) ) {
1773 } elseif ( isset( $value[
'_type'] ) && $value[
'_type'] ===
'lang' ) {
1776 foreach ( $value as $k => $v ) {
1781 $v = reset( $value );
1782 if ( key( $value ) ===
'_type' ) {
1783 $v = next( $value );
1796 if ( !is_array( $metadata ) ) {
1799 foreach ( $metadata as &$field ) {
1800 if ( isset( $field[
'value'] ) ) {
1813 if ( !is_array( $metadata ) ) {
1816 foreach ( $metadata as $key => &$field ) {
1817 if ( $key ===
'Software' || $key ===
'Contact' ) {
1822 if ( isset( $field[
'value'] ) ) {
1833 if ( !is_array( $arr ) ) {
1838 foreach ( $arr as $key => &$value ) {
1840 if ( $sanitizedKey !== $key ) {
1841 if ( isset( $arr[$sanitizedKey] ) ) {
1846 $sanitizedKey .= $counter;
1849 $arr[$sanitizedKey] = $arr[$key];
1850 unset( $arr[$key] );
1852 if ( is_array( $value ) ) {
1858 $keys = array_filter( array_keys( $arr ),
'ApiResult::isMetadataKey' );
1874 $key = preg_replace(
'/[^a-zA-z0-9_:.\-]/',
'', $key );
1876 $key = preg_replace(
'/^[\d\-.]+/',
'', $key );
1883 if ( $key ==
'_element' ) {
1897 $priorityLanguages =
1899 $priorityLanguages = array_merge(
1901 $priorityLanguages[0],
1902 $priorityLanguages[1]
1905 return $priorityLanguages;