$scale = $this->getScale( $params );
+ $metadataOpts = array(
+ 'version' => $params['metadataversion'],
+ );
+
$pageIds = $this->getPageSet()->getAllTitlesByNamespace();
if ( !empty( $pageIds[NS_FILE] ) ) {
$titles = array_keys( $pageIds[NS_FILE] );
$fit = $this->addPageSubItem( $pageId,
self::getInfo( $img, $prop, $result,
- $finalThumbParams, $params['metadataversion'] ) );
+ $finalThumbParams, $metadataOpts
+ )
+ );
if ( !$fit ) {
if ( count( $pageIds[NS_FILE] ) == 1 ) {
// See the 'the user is screwed' comment above
$fit = self::getTransformCount() < self::TRANSFORM_LIMIT &&
$this->addPageSubItem( $pageId,
self::getInfo( $oldie, $prop, $result,
- $finalThumbParams, $params['metadataversion']
+ $finalThumbParams, $metadataOpts
)
);
if ( !$fit ) {
* @param array $prop of properties to get (in the keys)
* @param $result ApiResult object
* @param array $thumbParams containing 'width' and 'height' items, or null
- * @param string $version Version of image metadata (for things like jpeg which have different versions).
+ * @param string|array $metadataOpts Options for metadata fetching.
+ * This is an array consisting of the keys:
+ * 'version': The metadata version for the metadata option
* @return Array: result array
*/
- static function getInfo( $file, $prop, $result, $thumbParams = null, $version = 'latest' ) {
+ static function getInfo( $file, $prop, $result, $thumbParams = null, $metadataOpts = false ) {
+ if ( !$metadataOpts || is_string( $metadataOpts ) ) {
+ $metadataOpts = array(
+ 'version' => $metadataOpts ?: 'latest',
+ );
+ }
+ $version = $metadataOpts['version'];
$vals = array();
// Timestamp is shown even if the file is revdelete'd in interface
// so do same here.
$url = isset( $prop['url'] );
$sha1 = isset( $prop['sha1'] );
$meta = isset( $prop['metadata'] );
+ $extmetadata = isset( $prop['extmetadata'] );
$mime = isset( $prop['mime'] );
$mediatype = isset( $prop['mediatype'] );
$archive = isset( $prop['archivename'] );
$vals['metadata'] = $metadata ? self::processMetaData( $metadata, $result ) : null;
}
+ if ( $extmetadata ) {
+ // Note, this should return an array where all the keys
+ // start with a letter, and all the values are strings.
+ // Thus there should be no issue with format=xml.
+ $format = new FormatMetadata;
+ $extmetaArray = $format->fetchExtendedMetadata( $file );
+ $vals['extmetadata'] = $extmetaArray;
+ }
+
if ( $mime ) {
$vals['mime'] = $file->getMimeType();
}
' (requires url and param ' . $modulePrefix . 'urlwidth)',
'mediatype' => ' mediatype - Adds the media type of the image',
'metadata' => ' metadata - Lists Exif metadata for the version of the image',
+ 'extmetadata' => ' extmetadata - Lists formatted metadata combined from multiple sources. Results are HTML formatted.',
'archivename' => ' archivename - Adds the file name of the archive version for non-latest versions',
'bitdepth' => ' bitdepth - Adds the bit depth of the version',
'uploadwarning' => ' uploadwarning - Used by the Special:Upload page to get information about an existing file. Not intended for use outside MediaWiki core',
$tel )->text();
}
}
+
+ /**
+ * Get a list of fields that are visible by default.
+ *
+ * @return array
+ * @since 1.23
+ */
+ public static function getVisibleFields() {
+ $fields = array();
+ $lines = explode( "\n", wfMessage( 'metadata-fields' )->inContentLanguage()->text() );
+ foreach ( $lines as $line ) {
+ $matches = array();
+ if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
+ $fields[] = $matches[1];
+ }
+ }
+ $fields = array_map( 'strtolower', $fields );
+ return $fields;
+ }
+
+ /**
+ * Get an array of extended metadata. (See the imageinfo API for format.)
+ *
+ * @param File $file File to use
+ * @return array [<property name> => ['value' => <value>]], or [] on error
+ * @since 1.23
+ */
+ public function fetchExtendedMetadata( File $file ) {
+ global $wgMemc;
+
+ wfProfileIn( __METHOD__ );
+
+ // If revision deleted, exit immediately
+ if ( $file->isDeleted( File::DELETED_FILE ) ) {
+ return array();
+ }
+
+ $cacheKey = wfMemcKey(
+ 'getExtendedMetadata',
+ $this->getLanguage()->getCode(),
+ $file->getSha1()
+ );
+
+ $cachedValue = $wgMemc->get( $cacheKey );
+ if (
+ $cachedValue
+ && wfRunHooks( 'ValidateExtendedMetadataCache', array( $cachedValue['timestamp'], $file ) )
+ ) {
+ $extendedMetadata = $cachedValue['data'];
+ } else {
+ $maxCacheTime = ( $file instanceof ForeignAPIFile ) ? 60 * 60 * 12 : 60 * 60 * 24 * 30;
+ $fileMetadata = $this->getExtendedMetadataFromFile( $file );
+ $extendedMetadata = $this->getExtendedMetadataFromHook( $file, $fileMetadata, $maxCacheTime );
+ // Make sure the metadata won't break the API when an XML format is used.
+ // This is an API-specific function so it would be cleaner to call it from
+ // outside fetchExtendedMetadata, but this way we don't need to redo the
+ // computation on a cache hit.
+ $this->sanitizeArrayForXml($extendedMetadata);
+ $valueToCache = array( 'data' => $extendedMetadata, 'timestamp' => wfTimestampNow() );
+ $wgMemc->set( $cacheKey, $valueToCache, $maxCacheTime );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $extendedMetadata;
+ }
+
+ /**
+ * Get file-based metadata in standardized format.
+ *
+ * Note that for a remote file, this might return metadata supplied by extensions.
+ *
+ * @param File $file File to use
+ * @return array [<property name> => ['value' => <value>]], or [] on error
+ * @since 1.23
+ */
+ protected function getExtendedMetadataFromFile( File $file ) {
+ // If this is a remote file accessed via an API request, we already
+ // have remote metadata so we just ignore any local one
+ if ( $file instanceof ForeignAPIFile ) {
+ // in case of error we pretend no metadata - this will get cached. Might or might not be a good idea.
+ return $file->getExtendedMetadata() ?: array();
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ $uploadDate = wfTimestamp( TS_ISO_8601, $file->getTimestamp() );
+
+ $fileMetadata = array(
+ // This is modification time, which is close to "upload" time.
+ 'DateTime' => array(
+ 'value' => $uploadDate,
+ 'source' => 'mediawiki-metadata',
+ ),
+ );
+
+ $title = $file->getTitle();
+ if ( $title ) {
+ $text = $title->getText();
+ $pos = strrpos( $text, '.' );
+
+ if ( $pos ) {
+ $name = substr( $text, 0, $pos );
+ } else {
+ $name = $text;
+ }
+
+ $fileMetadata[ 'ObjectName' ] = array(
+ 'value' => $name,
+ 'source' => 'mediawiki-metadata',
+ );
+ }
+
+ $common = $file->getCommonMetaArray();
+
+ foreach ( $common as $key => $value ) {
+ $fileMetadata[$key] = array(
+ 'value' => $value,
+ 'source' => 'file-metadata',
+ );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $fileMetadata;
+ }
+
+ /**
+ * Get additional metadata from hooks in standardized format.
+ *
+ * @param File $file File to use
+ * @param array $extendedMetadata
+ * @param int $maxCacheTime hook handlers might use this parameter to override cache time
+ *
+ * @return array [<property name> => ['value' => <value>]], or [] on error
+ * @since 1.23
+ */
+ protected function getExtendedMetadataFromHook( File $file, array $extendedMetadata, &$maxCacheTime ) {
+ wfProfileIn( __METHOD__ );
+
+ wfRunHooks( 'GetExtendedMetadata', array(
+ &$extendedMetadata,
+ $file,
+ $this->getContext(),
+ &$maxCacheTime
+ ) );
+
+ $visible = array_flip( self::getVisibleFields() );
+ foreach ( $extendedMetadata as $key => $value ) {
+ if ( !isset( $visible[ strtolower( $key ) ] ) ) {
+ $extendedMetadata[$key]['hidden'] = '';
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $extendedMetadata;
+ }
+
+ /**
+ * Makes sure the given array is a valid API response fragment
+ * (can be transformed into XML)
+ * @param array $arr
+ */
+ protected function sanitizeArrayForXml( &$arr ) {
+ if ( !is_array( $arr ) ) {
+ return;
+ }
+
+ $counter = 1;
+ foreach ( $arr as $key => &$value ) {
+ $sanitizedKey = $this->sanitizeKeyForXml( $key );
+ if ( $sanitizedKey !== $key ) {
+ if ( isset( $arr[$sanitizedKey] ) ) {
+ // Make the sanitized keys hopefully unique.
+ // To make it definitely unique would be too much effort, given that
+ // sanitizing is only needed for misformatted metadata anyway, but
+ // this at least covers the case when $arr is numeric.
+ $sanitizedKey .= $counter;
+ ++$counter;
+ }
+ $arr[$sanitizedKey] = $arr[$key];
+ unset( $arr[$key] );
+ }
+ if ( is_array( $value ) ) {
+ $this->sanitizeArrayForXml( $value );
+ }
+ }
+ }
+
+ /**
+ * Turns a string into a valid XML identifier.
+ * Used to ensure that keys of an associative array in the
+ * API response do not break the XML formatter.
+ * @param string $key
+ * @return string
+ * @since 1.23
+ */
+ protected function sanitizeKeyForXml( $key ) {
+ // drop all characters which are not valid in an XML tag name
+ // a bunch of non-ASCII letters would be valid but probably won't
+ // be used so we take the easy way
+ $key = preg_replace( '/[^a-zA-z0-9_:.-]/', '', $key );
+ // drop characters which are invalid at the first position
+ $key = preg_replace( '/^[\d-.]+/', '', $key );
+
+ if ( $key == '' ) {
+ $key = '_';
+ }
+
+ // special case for an internal keyword
+ if ( $key == '_element' ) {
+ $key = 'element';
+ }
+
+ return $key;
+ }
+
+ /**
+ * Returns a list of languages (first is best) to use when formatting multilang fields,
+ * based on user and site preferences.
+ * @return array
+ * @since 1.23
+ */
+ protected function getPriorityLanguages()
+ {
+ $priorityLanguages = Language::getFallbacksIncludingSiteLanguage( $this->getLanguage()->getCode() );
+ $priorityLanguages = array_merge( (array) $this->getLanguage()->getCode(), $priorityLanguages[0], $priorityLanguages[1] );
+ return $priorityLanguages;
+ }
}
/** For compatability with old FormatExif class