[TASK] Refactor metadata handling in FAL 08/57908/22
authorAndreas Fernandez <a.fernandez@scripting-base.de>
Wed, 15 Aug 2018 07:36:35 +0000 (09:36 +0200)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Fri, 1 Feb 2019 17:53:47 +0000 (18:53 +0100)
Meta data of files handled by FAL is fetched, created and updated in
various places, which makes it hard to maintain the current code base.

Albeit the method `_getMetaData()` is marked as internal, it has been
marked as deprecated as well, because the method is widely used in the
TYPO3 extension universe.

For this reason, a MetaDataAspect is introduced that takes care of meta
data handling on a low-level basis.

In the same run, FAL's `Indexer` is now responsible for creating or
updating such meta data records, the `ResourceStorage` now only tells
whether auto-extraction is enabled. The meta data extraction, based on
registered extractors implementing the `ExtractorInterface` interface,
has been moved into a separate service class.

Resolves: #85895
Releases: master
Change-Id: Icb929a6226777dcea3868ee5c083cf13ff5a71f6
Reviewed-on: https://review.typo3.org/57908
Tested-by: TYPO3com <noreply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
14 files changed:
typo3/sysext/core/Classes/Resource/File.php
typo3/sysext/core/Classes/Resource/Index/Indexer.php
typo3/sysext/core/Classes/Resource/Index/MetaDataRepository.php
typo3/sysext/core/Classes/Resource/MetaDataAspect.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Processing/FileDeletionAspect.php
typo3/sysext/core/Classes/Resource/ResourceStorage.php
typo3/sysext/core/Classes/Resource/Service/ExtractorService.php [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Deprecation-85895-DeprecateFile_getMetaData.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Resource/FileTest.php
typo3/sysext/core/Tests/Unit/Resource/Index/IndexerTest.php
typo3/sysext/core/Tests/Unit/Resource/MetaDataAspectTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Resource/Service/ExtractorServiceTest.php [new file with mode: 0644]
typo3/sysext/filelist/Classes/FileList.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php

index cd46b6a..eaa31dd 100644 (file)
@@ -22,23 +22,6 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class File extends AbstractFile
 {
     /**
-     * @var bool
-     */
-    protected $metaDataLoaded = false;
-
-    /**
-     * @var array
-     */
-    protected $metaDataProperties = [];
-
-    /**
-     * Set to TRUE while this file is being indexed - used to prevent some endless loops
-     *
-     * @var bool
-     */
-    protected $indexingInProgress = false;
-
-    /**
      * Contains the names of all properties that have been update since the
      * instantiation of this object
      *
@@ -47,6 +30,11 @@ class File extends AbstractFile
     protected $updatedProperties = [];
 
     /**
+     * @var MetaDataAspect
+     */
+    private $metaDataAspect;
+
+    /**
      * Constructor for a file object. Should normally not be used directly, use
      * the corresponding factory methods instead.
      *
@@ -60,9 +48,9 @@ class File extends AbstractFile
         $this->name = $fileData['name'] ?? '';
         $this->properties = $fileData;
         $this->storage = $storage;
+
         if (!empty($metaData)) {
-            $this->metaDataLoaded = true;
-            $this->metaDataProperties = $metaData;
+            $this->getMetaData()->add($metaData);
         }
     }
 
@@ -80,8 +68,7 @@ class File extends AbstractFile
         if (parent::hasProperty($key)) {
             return parent::getProperty($key);
         }
-        $metaData = $this->_getMetaData();
-        return $metaData[$key] ?? null;
+        return $this->getMetaData()[$key];
     }
 
     /**
@@ -91,10 +78,10 @@ class File extends AbstractFile
      * @param string $key
      * @return bool
      */
-    public function hasProperty($key)
+    public function hasProperty($key): bool
     {
         if (!parent::hasProperty($key)) {
-            return array_key_exists($key, $this->_getMetaData());
+            return isset($this->getMetaData()[$key]);
         }
         return true;
     }
@@ -104,9 +91,9 @@ class File extends AbstractFile
      *
      * @return array
      */
-    public function getProperties()
+    public function getProperties(): array
     {
-        return array_merge(parent::getProperties(), array_diff_key($this->_getMetaData(), parent::getProperties()));
+        return array_merge(parent::getProperties(), array_diff_key($this->getMetaData()->get(), parent::getProperties()));
     }
 
     /**
@@ -114,13 +101,15 @@ class File extends AbstractFile
      *
      * @return array
      * @internal
+     * @deprecated
      */
     public function _getMetaData()
     {
-        if (!$this->metaDataLoaded) {
-            $this->loadMetaData();
-        }
-        return $this->metaDataProperties;
+        trigger_error(
+            'The method ' . __CLASS__ . '::' . __METHOD__ . ' has been marked as deprecated and will be removed in TYPO3 v11. Use `->getMetaData()->get()` instead.',
+            E_USER_DEPRECATED
+        );
+        return $this->getMetaData()->get();
     }
 
     /******************
@@ -175,19 +164,6 @@ class File extends AbstractFile
     }
 
     /**
-     * Loads MetaData from Repository
-     */
-    protected function loadMetaData()
-    {
-        if (!$this->indexingInProgress) {
-            $this->indexingInProgress = true;
-            $this->metaDataProperties = $this->getMetaDataRepository()->findByFile($this);
-            $this->metaDataLoaded = true;
-            $this->indexingInProgress = false;
-        }
-    }
-
-    /**
      * Updates the properties of this file, e.g. after re-indexing or moving it.
      * By default, only properties that exist as a key in the $properties array
      * are overwritten. If you want to explicitly unset a property, set the
@@ -233,18 +209,6 @@ class File extends AbstractFile
     }
 
     /**
-     * Updates MetaData properties
-     *
-     * @internal Do not use outside the FileAbstraction Layer classes
-     *
-     * @param array $properties
-     */
-    public function _updateMetaDataProperties(array $properties)
-    {
-        $this->metaDataProperties = array_merge($this->metaDataProperties, $properties);
-    }
-
-    /**
      * Returns the names of all properties that have been updated in this record
      *
      * @return array
@@ -370,14 +334,6 @@ class File extends AbstractFile
     }
 
     /**
-     * @return Index\MetaDataRepository
-     */
-    protected function getMetaDataRepository()
-    {
-        return GeneralUtility::makeInstance(Index\MetaDataRepository::class);
-    }
-
-    /**
      * @return Index\FileIndexRepository
      */
     protected function getFileIndexRepository()
@@ -386,15 +342,6 @@ class File extends AbstractFile
     }
 
     /**
-     * @param bool $indexingState
-     * @internal Only for usage in Indexer
-     */
-    public function setIndexingInProgess($indexingState)
-    {
-        $this->indexingInProgress = (bool)$indexingState;
-    }
-
-    /**
      * @param $key
      * @internal Only for use in Repositories and indexer
      * @return mixed
@@ -403,4 +350,17 @@ class File extends AbstractFile
     {
         return parent::getProperty($key);
     }
+
+    /**
+     * Loads the metadata of a file in an encapsulated aspect
+     *
+     * @return MetaDataAspect
+     */
+    public function getMetaData(): MetaDataAspect
+    {
+        if ($this->metaDataAspect === null) {
+            $this->metaDataAspect = GeneralUtility::makeInstance(MetaDataAspect::class, $this);
+        }
+        return $this->metaDataAspect;
+    }
 }
index e8d7f7f..041a527 100644 (file)
@@ -19,6 +19,7 @@ use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException
 use TYPO3\CMS\Core\Resource\File;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\CMS\Core\Resource\Service\ExtractorService;
 use TYPO3\CMS\Core\Type\File\ImageInfo;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
@@ -43,9 +44,9 @@ class Indexer
     protected $storage;
 
     /**
-     * @var ExtractorInterface[]
+     * @var ExtractorService
      */
-    protected $extractionServices;
+    protected $extractorService;
 
     /**
      * @param ResourceStorage $storage
@@ -62,19 +63,25 @@ class Indexer
      * @return File
      * @throws \InvalidArgumentException
      */
-    public function createIndexEntry($identifier)
+    public function createIndexEntry($identifier): File
     {
         if (!isset($identifier) || !is_string($identifier) || $identifier === '') {
-            throw new \InvalidArgumentException('Invalid file identifier given. It must be of type string and not empty. "' . gettype($identifier) . '" given.', 1401732565);
+            throw new \InvalidArgumentException(
+                'Invalid file identifier given. It must be of type string and not empty. "' . gettype($identifier) . '" given.',
+                1401732565
+            );
         }
+
         $fileProperties = $this->gatherFileInformationArray($identifier);
         $record = $this->getFileIndexRepository()->addRaw($fileProperties);
+
         $fileObject = $this->getResourceFactory()->getFileObject($record['uid'], $record);
-        $this->extractRequiredMetaData($fileObject);
+        $metaData = $this->extractRequiredMetaData($fileObject);
 
         if ($this->storage->autoExtractMetadataEnabled()) {
-            $this->extractMetaData($fileObject);
+            $metaData = array_merge($metaData, $this->getExtractorService()->extractMetaData($fileObject));
         }
+        $fileObject->getMetaData()->add($metaData)->save();
 
         return $fileObject;
     }
@@ -83,13 +90,21 @@ class Indexer
      * Update index entry
      *
      * @param File $fileObject
+     * @return File
      */
-    public function updateIndexEntry(File $fileObject)
+    public function updateIndexEntry(File $fileObject): File
     {
         $updatedInformation = $this->gatherFileInformationArray($fileObject->getIdentifier());
         $fileObject->updateProperties($updatedInformation);
+
         $this->getFileIndexRepository()->update($fileObject);
-        $this->extractRequiredMetaData($fileObject);
+        $metaData = $this->extractRequiredMetaData($fileObject);
+
+        if ($this->storage->autoExtractMetadataEnabled()) {
+            $metaData = array_merge($metaData, $this->getExtractorService()->extractMetaData($fileObject));
+        }
+        $fileObject->getMetaData()->add($metaData)->save();
+        return $fileObject;
     }
 
     /**
@@ -135,44 +150,16 @@ class Indexer
      */
     public function extractMetaData(File $fileObject)
     {
-        $newMetaData = [
-            0 => $fileObject->_getMetaData()
-        ];
-
-        // Loop through available extractors and fetch metadata for the given file.
-        foreach ($this->getExtractionServices() as $service) {
-            if ($this->isFileTypeSupportedByExtractor($fileObject, $service) && $service->canProcess($fileObject)) {
-                $newMetaData[$service->getPriority()] = $service->extractMetaData($fileObject, $newMetaData);
-            }
-        }
+        $metaData = array_merge([
+            $fileObject->getMetaData()->get()
+        ], $this->getExtractorService()->extractMetaData($fileObject));
 
-        // Sort metadata by priority so that merging happens in order of precedence.
-        ksort($newMetaData);
+        $fileObject->getMetaData()->add($metaData)->save();
 
-        // Merge the collected metadata.
-        $metaData = [];
-        foreach ($newMetaData as $data) {
-            $metaData = array_merge($metaData, $data);
-        }
-        $fileObject->_updateMetaDataProperties($metaData);
-        $this->getMetaDataRepository()->update($fileObject->getUid(), $metaData);
         $this->getFileIndexRepository()->updateIndexingTime($fileObject->getUid());
     }
 
     /**
-     * Get available extraction services
-     *
-     * @return ExtractorInterface[]
-     */
-    protected function getExtractionServices()
-    {
-        if ($this->extractionServices === null) {
-            $this->extractionServices = $this->getExtractorRegistry()->getExtractorsWithDriverSupport($this->storage->getDriverType());
-        }
-        return $this->extractionServices;
-    }
-
-    /**
      * Since by now all files in filesystem have been looked at it is save to assume,
      * that files that are in indexed but not touched in this run are missing
      */
@@ -280,27 +267,28 @@ class Indexer
      * This should be called after every "content" update and "record" creation
      *
      * @param File $fileObject
+     * @return array
      */
-    protected function extractRequiredMetaData(File $fileObject)
+    protected function extractRequiredMetaData(File $fileObject): array
     {
+        $metaData = [];
+
         // since the core desperately needs image sizes in metadata table do this manually
         // prevent doing this for remote storages, remote storages must provide the data with extractors
-        if ($fileObject->getType() == File::FILETYPE_IMAGE && $this->storage->getDriverType() === 'Local') {
+        if ($fileObject->getType() === File::FILETYPE_IMAGE && $this->storage->getDriverType() === 'Local') {
             $rawFileLocation = $fileObject->getForLocalProcessing(false);
             $imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $rawFileLocation);
             $metaData = [
                 'width' => $imageInfo->getWidth(),
                 'height' => $imageInfo->getHeight(),
             ];
-            $this->getMetaDataRepository()->update($fileObject->getUid(), $metaData);
-            $fileObject->_updateMetaDataProperties($metaData);
         }
+
+        return $metaData;
     }
 
     /****************************
-     *
      *         UTILITY
-     *
      ****************************/
 
     /**
@@ -359,7 +347,6 @@ class Indexer
      * Therefore a mapping must happen.
      *
      * @param array $fileInfo
-     *
      * @return array
      */
     protected function transformFromDriverFileInfoArrayToFileObjectFormat(array $fileInfo)
@@ -416,12 +403,13 @@ class Indexer
     }
 
     /**
-     * Returns an instance of the FileIndexRepository
-     *
-     * @return ExtractorRegistry
+     * @return ExtractorService
      */
-    protected function getExtractorRegistry()
+    protected function getExtractorService(): ExtractorService
     {
-        return ExtractorRegistry::getInstance();
+        if ($this->extractorService === null) {
+            $this->extractorService = GeneralUtility::makeInstance(ExtractorService::class);
+        }
+        return $this->extractorService;
     }
 }
index 011a758..ba7853b 100644 (file)
@@ -109,7 +109,7 @@ class MetaDataRepository implements SingletonInterface
             ->fetch();
 
         if (empty($record)) {
-            $record = $this->createMetaDataRecord($uid);
+            return [];
         }
 
         $passedData = new \ArrayObject($record);
@@ -135,6 +135,7 @@ class MetaDataRepository implements SingletonInterface
             'cruser_id' => isset($GLOBALS['BE_USER']->user['uid']) ? (int)$GLOBALS['BE_USER']->user['uid'] : 0,
             'l10n_diffsource' => ''
         ];
+        $additionalFields = array_intersect_key($additionalFields, $this->getTableFields());
         $emptyRecord = array_merge($emptyRecord, $additionalFields);
 
         $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->tableName);
@@ -162,13 +163,7 @@ class MetaDataRepository implements SingletonInterface
      */
     public function update($fileUid, array $data)
     {
-        if (empty($this->tableFields)) {
-            $this->tableFields = GeneralUtility::makeInstance(ConnectionPool::class)
-                ->getConnectionForTable($this->tableName)
-                ->getSchemaManager()
-                ->listTableColumns($this->tableName);
-        }
-        $updateRow = array_intersect_key($data, $this->tableFields);
+        $updateRow = array_intersect_key($data, $this->getTableFields());
         if (array_key_exists('uid', $updateRow)) {
             unset($updateRow['uid']);
         }
@@ -185,13 +180,13 @@ class MetaDataRepository implements SingletonInterface
                 }
             }
             $connection->update(
-                    $this->tableName,
-                    $updateRow,
-                    [
-                        'uid' => (int)$row['uid']
-                    ],
-                    $types
-                );
+                $this->tableName,
+                $updateRow,
+                [
+                    'uid' => (int)$row['uid']
+                ],
+                $types
+            );
 
             $this->emitRecordUpdatedSignal(array_merge($row, $updateRow));
         }
@@ -278,6 +273,23 @@ class MetaDataRepository implements SingletonInterface
     }
 
     /**
+     * Gets the fields that are available in the table
+     *
+     * @return array
+     */
+    protected function getTableFields(): array
+    {
+        if (empty($this->tableFields)) {
+            $this->tableFields = GeneralUtility::makeInstance(ConnectionPool::class)
+                ->getConnectionForTable($this->tableName)
+                ->getSchemaManager()
+                ->listTableColumns($this->tableName);
+        }
+
+        return $this->tableFields;
+    }
+
+    /**
      * @return MetaDataRepository
      */
     public static function getInstance()
diff --git a/typo3/sysext/core/Classes/Resource/MetaDataAspect.php b/typo3/sysext/core/Classes/Resource/MetaDataAspect.php
new file mode 100644 (file)
index 0000000..45baeea
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Resource;
+
+/*
+* This file is part of the TYPO3 CMS project.
+*
+* It is free software; you can redistribute it and/or modify it under
+* the terms of the GNU General Public License, either version 2
+* of the License, or any later version.
+*
+* For the full copyright and license information, please read the
+* LICENSE.txt file that was distributed with this source code.
+*
+* The TYPO3 project - inspiring people to share!
+*/
+
+use TYPO3\CMS\Core\Resource\Index\MetaDataRepository;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Aspect that takes care of a file's metadata
+ */
+class MetaDataAspect implements \ArrayAccess, \Countable, \Iterator
+{
+    /**
+     * @var File
+     */
+    private $file;
+
+    /**
+     * @var array
+     */
+    private $metaData = [];
+
+    /**
+     * This flag is used to treat a possible recursion between $this->get() and $this->file->getUid()
+     *
+     * @var bool
+     */
+    private $loaded = false;
+
+    /**
+     * @var int
+     */
+    private $indexPosition = 0;
+
+    /**
+     * Constructor
+     *
+     * @param File $file
+     */
+    public function __construct(File $file)
+    {
+        $this->file = $file;
+    }
+
+    /**
+     * Adds already known metadata to the aspect
+     *
+     * @param array $metaData
+     * @return self
+     * @internal
+     */
+    public function add(array $metaData): self
+    {
+        $this->loaded = true;
+        $this->metaData = array_merge($this->metaData, $metaData);
+
+        return $this;
+    }
+
+    /**
+     * Gets the metadata of a file. If not metadata is loaded yet, the database gets queried
+     *
+     * @return array
+     */
+    public function get(): array
+    {
+        if (!$this->loaded) {
+            $this->loaded = true;
+            $this->metaData = $this->loadFromRepository();
+        }
+        return $this->metaData;
+    }
+
+    /**
+     * @param mixed $offset
+     * @return bool
+     */
+    public function offsetExists($offset): bool
+    {
+        return array_key_exists($offset, $this->get());
+    }
+
+    /**
+     * @param mixed $offset
+     * @return mixed
+     */
+    public function offsetGet($offset)
+    {
+        return $this->get()[$offset];
+    }
+
+    /**
+     * @param mixed $offset
+     * @param mixed $value
+     */
+    public function offsetSet($offset, $value): void
+    {
+        $this->loaded = true;
+        $this->metaData[$offset] = $value;
+    }
+
+    /**
+     * @param mixed $offset
+     */
+    public function offsetUnset($offset): void
+    {
+        $this->metaData[$offset] = null;
+    }
+
+    /**
+     * @return int
+     */
+    public function count(): int
+    {
+        return count($this->get());
+    }
+
+    /**
+     * Resets the internal iterator counter
+     */
+    public function rewind(): void
+    {
+        $this->indexPosition = 0;
+    }
+
+    /**
+     * Gets the current value of iteration
+     *
+     * @return mixed
+     */
+    public function current()
+    {
+        $key = array_keys($this->metaData)[$this->indexPosition];
+        return $this->metaData[$key];
+    }
+
+    /**
+     * Returns the key of the current iteration
+     *
+     * @return string
+     */
+    public function key(): string
+    {
+        return array_keys($this->metaData)[$this->indexPosition];
+    }
+
+    /**
+     * Increases the index for iteration
+     */
+    public function next(): void
+    {
+        ++$this->indexPosition;
+    }
+
+    /**
+     * @return bool
+     */
+    public function valid(): bool
+    {
+        $key = array_keys($this->metaData)[$this->indexPosition];
+        return array_key_exists($key, $this->metaData);
+    }
+
+    /**
+     * Creates new or updates existing meta data
+     *
+     * @internal
+     */
+    public function save(): void
+    {
+        $metaDataInDatabase = $this->loadFromRepository();
+        if ($metaDataInDatabase === []) {
+            $this->metaData = $this->getMetaDataRepository()->createMetaDataRecord($this->file->getUid(), $this->metaData);
+        } else {
+            $this->getMetaDataRepository()->update($this->file->getUid(), $this->metaData);
+            $this->metaData = array_merge($metaDataInDatabase, $this->metaData);
+        }
+    }
+
+    /**
+     * Removes a meta data record
+     *
+     * @internal
+     */
+    public function remove(): void
+    {
+        $this->getMetaDataRepository()->removeByFileUid($this->file->getUid());
+        $this->metaData = [];
+    }
+
+    /**
+     * @return MetaDataRepository
+     */
+    protected function getMetaDataRepository(): MetaDataRepository
+    {
+        return GeneralUtility::makeInstance(MetaDataRepository::class);
+    }
+
+    /**
+     * @return array
+     */
+    protected function loadFromRepository(): array
+    {
+        return $this->getMetaDataRepository()->findByFileUid((int)$this->file->getUid());
+    }
+}
index 17a947c..b3c1b14 100644 (file)
@@ -125,7 +125,7 @@ class FileDeletionAspect
     protected function cleanupCategoryReferences(File $fileObject)
     {
         // Retrieve the file metadata uid which is different from the file uid.
-        $metadataProperties = $fileObject->_getMetaData();
+        $metadataProperties = $fileObject->getMetaData()->get();
         $metaDataUid = $metadataProperties['_ORIG_uid'] ?? $metadataProperties['uid'];
 
         GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_category_record_mm')
index abf8e66..1fb9c6c 100644 (file)
@@ -1193,9 +1193,6 @@ class ResourceStorage implements ResourceStorageInterface
         if ($replaceExisting && $file instanceof File) {
             $this->getIndexer()->updateIndexEntry($file);
         }
-        if ($this->autoExtractMetadataEnabled()) {
-            $this->getIndexer()->extractMetaData($file);
-        }
 
         $this->emitPostFileAddSignal($file, $targetFolder);
 
@@ -1971,9 +1968,6 @@ class ResourceStorage implements ResourceStorageInterface
         if ($file instanceof File) {
             $this->getIndexer()->updateIndexEntry($file);
         }
-        if ($this->autoExtractMetadataEnabled()) {
-            $this->getIndexer()->extractMetaData($file);
-        }
         $this->emitPostFileReplaceSignal($file, $localFilePath);
 
         return $file;
diff --git a/typo3/sysext/core/Classes/Resource/Service/ExtractorService.php b/typo3/sysext/core/Classes/Resource/Service/ExtractorService.php
new file mode 100644 (file)
index 0000000..166bc14
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Resource\Service;
+
+/*
+* This file is part of the TYPO3 CMS project.
+*
+* It is free software; you can redistribute it and/or modify it under
+* the terms of the GNU General Public License, either version 2
+* of the License, or any later version.
+*
+* For the full copyright and license information, please read the
+* LICENSE.txt file that was distributed with this source code.
+*
+* The TYPO3 project - inspiring people to share!
+*/
+
+use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Resource\Index\ExtractorInterface;
+use TYPO3\CMS\Core\Resource\Index\ExtractorRegistry;
+
+/**
+ * Service class to extract metadata
+ */
+class ExtractorService
+{
+    /**
+     * @var ExtractorInterface[][]
+     */
+    private $extractionServices;
+
+    /**
+     * @param File $fileObject
+     * @return array
+     */
+    public function extractMetaData(File $fileObject): array
+    {
+        $newMetaData = [];
+        // Loop through available extractors and fetch metadata for the given file.
+        foreach ($this->getExtractionServices($fileObject->getStorage()->getDriverType()) as $service) {
+            if ($this->isFileTypeSupportedByExtractor($fileObject, $service) && $service->canProcess($fileObject)) {
+                $newMetaData[$service->getPriority()] = $service->extractMetaData($fileObject, $newMetaData);
+            }
+        }
+        // Sort metadata by priority so that merging happens in order of precedence.
+        ksort($newMetaData);
+        // Merge the collected metadata.
+        $metaData = [[]];
+        foreach ($newMetaData as $data) {
+            $metaData[] = $data;
+        }
+        return array_filter(array_merge(...$metaData));
+    }
+
+    /**
+     * Get available extraction services
+     *
+     * @param string $driverType
+     * @return ExtractorInterface[]
+     */
+    protected function getExtractionServices(string $driverType): array
+    {
+        if (empty($this->extractionServices[$driverType])) {
+            $this->extractionServices[$driverType] = $this->getExtractorRegistry()->getExtractorsWithDriverSupport($driverType);
+        }
+        return $this->extractionServices[$driverType];
+    }
+
+    /**
+     * Check whether the extractor service supports this file according to file type restrictions.
+     *
+     * @param File $file
+     * @param ExtractorInterface $extractor
+     * @return bool
+     */
+    private function isFileTypeSupportedByExtractor(File $file, ExtractorInterface $extractor): bool
+    {
+        $isSupported = true;
+        $fileTypeRestrictions = $extractor->getFileTypeRestrictions();
+        if (!empty($fileTypeRestrictions) && !in_array($file->getType(), $fileTypeRestrictions, true)) {
+            $isSupported = false;
+        }
+        return $isSupported;
+    }
+
+    /**
+     * Returns an instance of the FileIndexRepository
+     *
+     * @return ExtractorRegistry
+     */
+    protected function getExtractorRegistry(): ExtractorRegistry
+    {
+        return ExtractorRegistry::getInstance();
+    }
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85895-DeprecateFile_getMetaData.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85895-DeprecateFile_getMetaData.rst
new file mode 100644 (file)
index 0000000..61c1025
--- /dev/null
@@ -0,0 +1,32 @@
+.. include:: ../../Includes.txt
+
+====================================================
+Deprecation: #85895 - Deprecate File::_getMetaData()
+====================================================
+
+See :issue:`85895`
+
+Description
+===========
+
+The internal method :php:`File::_getMetaData()` which is used to fetch meta data of a file has been marked as deprecated. This method has been superseded by the :php:`MetaDataAspect`.
+
+
+Impact
+======
+
+Using this method will trigger a deprecation entry.
+
+
+Affected Installations
+======================
+
+Any 3rd party extension calling :php:`_getMetaData()` is affected.
+
+
+Migration
+=========
+
+To fetch the meta data, call :php:`$fileObject->getMetaData()->get()` instead.
+
+.. index:: FAL, PHP-API, FullyScanned, ext:core
index 32232de..2f31420 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\Resource;
 
 use TYPO3\CMS\Core\Resource\File;
 use TYPO3\CMS\Core\Resource\Index\MetaDataRepository;
+use TYPO3\CMS\Core\Resource\MetaDataAspect;
 use TYPO3\CMS\Core\Resource\ResourceStorage;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
@@ -250,9 +251,20 @@ class FileTest extends UnitTestCase
      */
     public function hasPropertyReturnsTrueIfMetadataPropertyExists(): void
     {
-        $fixture = $this->getAccessibleMock(File::class, ['dummy'], [[], $this->storageMock]);
-        $fixture->_set('metaDataLoaded', true);
-        $fixture->_set('metaDataProperties', ['testproperty' => 'testvalue']);
+        $fixture = $this->getMockBuilder(File::class)
+            ->setConstructorArgs([[], $this->storageMock])
+            ->setMethods(['getMetaData'])
+            ->getMock();
+
+        $metaDataAspectMock = $this->getMockBuilder(MetaDataAspect::class)
+            ->setConstructorArgs([$fixture])
+            ->setMethods(['get'])
+            ->getMock();
+
+        $metaDataAspectMock->expects($this->any())->method('get')->willReturn(['testproperty' => 'testvalue']);
+        $fixture->expects($this->any())->method('getMetaData')->willReturn($metaDataAspectMock);
+
         $this->assertTrue($fixture->hasProperty('testproperty'));
+        $this->assertSame('testvalue', $fixture->getProperty('testproperty'));
     }
 }
index 7af158f..d16d7b5 100644 (file)
@@ -15,8 +15,10 @@ namespace TYPO3\CMS\Core\Tests\Unit\Resource\Index;
  */
 
 use TYPO3\CMS\Core\Resource\File;
-use TYPO3\CMS\Core\Resource\Index\ExtractorInterface;
+use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
 use TYPO3\CMS\Core\Resource\Index\Indexer;
+use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\CMS\Core\Resource\Service\ExtractorService;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
@@ -27,81 +29,30 @@ class IndexerTest extends UnitTestCase
     /**
      * @test
      */
-    public function isFileTypeSupportedByExtractorReturnsFalesForFileTypeTextAndExtractorLimitedToFileTypeImage()
+    public function extractMetaDataCallsSubsequentMethodsWithCorrectArguments(): void
     {
-        $mockStorage = $this->createMock(\TYPO3\CMS\Core\Resource\ResourceStorage::class);
-        $mockFile = $this->createMock(File::class);
-        $mockFile->expects($this->any())->method('getType')->will($this->returnValue(
-            File::FILETYPE_TEXT
-        ));
+        $mockStorage = $this->createMock(ResourceStorage::class);
 
-        $mockExtractor = $this->createMock(ExtractorInterface::class);
-        $mockExtractor->expects($this->any())->method('getFileTypeRestrictions')->will($this->returnValue(
-            [File::FILETYPE_IMAGE]
-        ));
+        /** @var Indexer|\PHPUnit\Framework\MockObject\MockObject $subject */
+        $subject = $this->getMockBuilder(Indexer::class)
+            ->setConstructorArgs([$mockStorage])
+            ->setMethods(['getFileIndexRepository', 'extractRequiredMetaData', 'getExtractorService'])
+            ->getMock();
 
-        $method = new \ReflectionMethod(Indexer::class, 'isFileTypeSupportedByExtractor');
-        $method->setAccessible(true);
-        $arguments = [
-            $mockFile,
-            $mockExtractor
-        ];
+        $indexFileRepositoryMock = $this->createMock(FileIndexRepository::class);
+        $subject->expects($this->any())->method('getFileIndexRepository')->willReturn($indexFileRepositoryMock);
 
-        $result = $method->invokeArgs(new Indexer($mockStorage), $arguments);
-        $this->assertFalse($result);
-    }
-
-    /**
-     * @test
-     */
-    public function isFileTypeSupportedByExtractorReturnsTrueForFileTypeImageAndExtractorLimitedToFileTypeImage()
-    {
-        $mockStorage = $this->createMock(\TYPO3\CMS\Core\Resource\ResourceStorage::class);
-        $mockFile = $this->createMock(File::class);
-        $mockFile->expects($this->any())->method('getType')->will($this->returnValue(
-            File::FILETYPE_IMAGE
-        ));
-
-        $mockExtractor = $this->createMock(ExtractorInterface::class);
-        $mockExtractor->expects($this->any())->method('getFileTypeRestrictions')->will($this->returnValue(
-            [File::FILETYPE_IMAGE]
-        ));
-
-        $method = new \ReflectionMethod(Indexer::class, 'isFileTypeSupportedByExtractor');
-        $method->setAccessible(true);
-        $arguments = [
-            $mockFile,
-            $mockExtractor
-        ];
-
-        $result = $method->invokeArgs(new Indexer($mockStorage), $arguments);
-        $this->assertTrue($result);
-    }
-
-    /**
-     * @test
-     */
-    public function isFileTypeSupportedByExtractorReturnsTrueForFileTypeTextAndExtractorHasNoFileTypeLimitation()
-    {
-        $mockStorage = $this->createMock(\TYPO3\CMS\Core\Resource\ResourceStorage::class);
-        $mockFile = $this->createMock(File::class);
-        $mockFile->expects($this->any())->method('getType')->will($this->returnValue(
-            File::FILETYPE_TEXT
-        ));
+        $fileMock = $this->createMock(File::class);
+        $fileMock->expects($this->any())->method('getUid')->willReturn(42);
+        $fileMock->expects($this->any())->method('getType')->willReturn(File::FILETYPE_TEXT);
+        $fileMock->expects($this->any())->method('getStorage')->willReturn($mockStorage);
 
-        $mockExtractor = $this->createMock(ExtractorInterface::class);
-        $mockExtractor->expects($this->any())->method('getFileTypeRestrictions')->will($this->returnValue(
-            []
-        ));
+        $extractorServiceMock = $this->getMockBuilder(ExtractorService::class)->getMock();
+        $extractorServiceMock->expects($this->once())->method('extractMetaData')->with($fileMock);
+        $subject->expects($this->any())->method('getExtractorService')->willReturn($extractorServiceMock);
 
-        $method = new \ReflectionMethod(Indexer::class, 'isFileTypeSupportedByExtractor');
-        $method->setAccessible(true);
-        $arguments = [
-            $mockFile,
-            $mockExtractor
-        ];
+        $indexFileRepositoryMock->expects($this->once())->method('updateIndexingTime')->with($fileMock->getUid());
 
-        $result = $method->invokeArgs(new Indexer($mockStorage), $arguments);
-        $this->assertTrue($result);
+        $subject->extractMetaData($fileMock);
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Resource/MetaDataAspectTest.php b/typo3/sysext/core/Tests/Unit/Resource/MetaDataAspectTest.php
new file mode 100644 (file)
index 0000000..db1ff32
--- /dev/null
@@ -0,0 +1,256 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Resource;
+
+/*
+* This file is part of the TYPO3 CMS project.
+*
+* It is free software; you can redistribute it and/or modify it under
+* the terms of the GNU General Public License, either version 2
+* of the License, or any later version.
+*
+* For the full copyright and license information, please read the
+* LICENSE.txt file that was distributed with this source code.
+*
+* The TYPO3 project - inspiring people to share!
+*/
+
+use Prophecy\Argument;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Resource\Exception\InvalidUidException;
+use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Resource\Index\MetaDataRepository;
+use TYPO3\CMS\Core\Resource\MetaDataAspect;
+use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class MetaDataAspectTest extends UnitTestCase
+{
+    /**
+     * @var ResourceStorage|\PHPUnit\Framework\MockObject\MockObject
+     */
+    protected $storageMock;
+
+    /**
+     * Set up
+     */
+    protected function setUp(): void
+    {
+        $this->storageMock = $this->createMock(ResourceStorage::class);
+        $this->storageMock->expects($this->any())->method('getUid')->will($this->returnValue(12));
+    }
+
+    /**
+     * Tear down
+     */
+    protected function tearDown(): void
+    {
+        $this->resetSingletonInstances = true;
+
+        GeneralUtility::purgeInstances();
+        parent::tearDown();
+    }
+
+    /**
+     * @test
+     */
+    public function knownMetaDataIsAdded(): void
+    {
+        $metaData = [
+            'width' => 4711,
+            'title' => 'Lorem ipsum meta sit amet',
+        ];
+        $file = new File([], $this->storageMock, $metaData);
+
+        $this->assertSame($metaData, $file->getMetaData()->get());
+    }
+
+    /**
+     * @test
+     */
+    public function manuallyAddedMetaDataIsMerged(): void
+    {
+        $metaData = [
+            'width' => 4711,
+            'title' => 'Lorem ipsum meta sit amet',
+        ];
+        $file = new File([], $this->storageMock, $metaData);
+        $file->getMetaData()->add([
+            'height' => 900,
+            'description' => 'This file is presented by TYPO3',
+        ]);
+
+        $expected = [
+            'width' => 4711,
+            'title' => 'Lorem ipsum meta sit amet',
+            'height' => 900,
+            'description' => 'This file is presented by TYPO3',
+        ];
+
+        $this->assertSame($expected, $file->getMetaData()->get());
+    }
+
+    /**
+     * @test
+     */
+    public function metaDataGetsRemoved(): void
+    {
+        $metaData = ['foo' => 'bar'];
+
+        $file = new File(['uid' => 12], $this->storageMock);
+
+        /** @var MetaDataAspect|\PHPUnit\Framework\MockObject\MockObject $metaDataAspectMock */
+        $metaDataAspectMock = $this->getMockBuilder(MetaDataAspect::class)
+            ->setConstructorArgs([$file])
+            ->setMethods(['getMetaDataRepository'])
+            ->getMock();
+
+        $metaDataAspectMock->add($metaData);
+        $metaDataAspectMock->remove();
+
+        $this->assertEmpty($metaDataAspectMock->get());
+    }
+
+    /**
+     * @test
+     */
+    public function positiveUidOfFileIsExpectedToLoadMetaData(): void
+    {
+        $this->expectException(InvalidUidException::class);
+        $this->expectExceptionCode(1381590731);
+
+        $file = new File(['uid' => -3], $this->storageMock);
+        $file->getMetaData()->get();
+    }
+
+    /**
+     * @test
+     */
+    public function newMetaDataIsCreated(): void
+    {
+        $GLOBALS['EXEC_TIME'] = 1534530781;
+        $metaData = [
+            'title' => 'Hooray',
+            // This value is ignored on purpose, we simulate the non-existence of the field "description"
+            'description' => 'Yipp yipp yipp',
+        ];
+
+        $file = new File(['uid' => 12], $this->storageMock);
+
+        $connectionProphecy = $this->prophesize(Connection::class);
+        $connectionProphecy->insert(Argument::cetera())->willReturn(1);
+        $connectionProphecy->lastInsertId(Argument::cetera())->willReturn(5);
+        $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
+        $connectionPoolProphecy->getConnectionForTable(Argument::cetera())->willReturn($connectionProphecy->reveal());
+        GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
+
+        $metaDataRepositoryMock = $this->getMockBuilder(MetaDataRepository::class)
+            ->setMethods(['findByFileUid', 'getTableFields', 'update'])
+            ->getMock();
+        $metaDataRepositoryMock->expects($this->any())->method('findByFileUid')->willReturn([]);
+        $metaDataRepositoryMock->expects($this->any())->method('getTableFields')->willReturn(['title' => 'sometype']);
+        $metaDataRepositoryMock->expects($this->never())->method('update');
+        GeneralUtility::setSingletonInstance(MetaDataRepository::class, $metaDataRepositoryMock);
+
+        $file->getMetaData()->add($metaData)->save();
+
+        $expected = [
+            'file' => $file->getUid(),
+            'pid' => 0,
+            'crdate' => 1534530781,
+            'tstamp' => 1534530781,
+            'cruser_id' => 0,
+            'l10n_diffsource' => '',
+            'title' => 'Hooray',
+            'uid' => '5',
+            'newlyCreated' => true,
+        ];
+
+        $this->assertSame($expected, $file->getMetaData()->get());
+    }
+
+    /**
+     * @test
+     */
+    public function existingMetaDataGetsUpdated(): void
+    {
+        $metaData = ['foo' => 'bar'];
+
+        $file = new File(['uid' => 12], $this->storageMock);
+
+        $metaDataRepositoryMock = $this->getMockBuilder(MetaDataRepository::class)
+            ->setMethods(['loadFromRepository', 'createMetaDataRecord', 'update'])
+            ->getMock();
+
+        $metaDataRepositoryMock->expects($this->any())->method('createMetaDataRecord')->willReturn($metaData);
+        GeneralUtility::setSingletonInstance(MetaDataRepository::class, $metaDataRepositoryMock);
+
+        $metaDataAspectMock = $this->getMockBuilder(MetaDataAspect::class)
+            ->setConstructorArgs([$file])
+            ->setMethods(['loadFromRepository'])
+            ->getMock();
+
+        $metaDataAspectMock->expects($this->any())->method('loadFromRepository')->will($this->onConsecutiveCalls([], $metaData));
+        $metaDataAspectMock->add($metaData)->save();
+        $metaDataAspectMock->add(['testproperty' => 'testvalue'])->save();
+
+        $this->assertSame(['foo' => 'bar', 'testproperty' => 'testvalue'], $metaDataAspectMock->get());
+    }
+
+    /**
+     * @return array
+     */
+    public function propertyDataProvider(): array
+    {
+        return [
+            [
+                [
+                    'width' => 4711,
+                    'title' => 'Lorem ipsum meta sit amet',
+                ],
+                [
+                    'property' => 'width',
+                    'expected' => true,
+                ],
+                [
+                    'property' => 'width',
+                    'expected' => 4711,
+                ],
+            ],
+            [
+                [
+                    'foo' => 'bar',
+                ],
+                [
+                    'property' => 'husel',
+                    'expected' => false,
+                ],
+                [
+                    'property' => 'husel',
+                    'expected' => null,
+                ],
+            ],
+        ];
+    }
+
+    /**
+     * @param $metaData
+     * @param $has
+     * @param $get
+     * @test
+     * @dataProvider propertyDataProvider
+     */
+    public function propertyIsFetchedProperly($metaData, $has, $get): void
+    {
+        $file = new File([], $this->storageMock, $metaData);
+
+        $this->assertSame($has['expected'], isset($file->getMetaData()[$has['property']]));
+        $this->assertSame($get['expected'], $file->getMetaData()[$get['property']] ?? null);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Resource/Service/ExtractorServiceTest.php b/typo3/sysext/core/Tests/Unit/Resource/Service/ExtractorServiceTest.php
new file mode 100644 (file)
index 0000000..a480c6c
--- /dev/null
@@ -0,0 +1,165 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Resource\Service;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Resource\Index\ExtractorInterface;
+use TYPO3\CMS\Core\Resource\Index\ExtractorRegistry;
+use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\CMS\Core\Resource\Service\ExtractorService;
+
+/**
+ * Class ExtractorServiceTest
+ */
+class ExtractorServiceTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function isFileTypeSupportedByExtractorReturnsFalesForFileTypeTextAndExtractorLimitedToFileTypeImage(): void
+    {
+        $fileMock = $this->createMock(File::class);
+        $fileMock->expects($this->any())->method('getType')->willReturn(File::FILETYPE_TEXT);
+
+        $extractorMock = $this->createMock(ExtractorInterface::class);
+        $extractorMock->expects($this->any())->method('getFileTypeRestrictions')->willReturn([File::FILETYPE_IMAGE]);
+
+        $extractorService = new ExtractorService();
+        $method = new \ReflectionMethod($extractorService, 'isFileTypeSupportedByExtractor');
+        $method->setAccessible(true);
+        $arguments = [
+            $fileMock,
+            $extractorMock
+        ];
+
+        $result = $method->invokeArgs($extractorService, $arguments);
+        $this->assertFalse($result);
+    }
+
+    /**
+     * @test
+     */
+    public function isFileTypeSupportedByExtractorReturnsTrueForFileTypeImageAndExtractorLimitedToFileTypeImage(): void
+    {
+        $fileMock = $this->createMock(File::class);
+        $fileMock->expects($this->any())->method('getType')->willReturn(File::FILETYPE_IMAGE);
+
+        $extractorMock = $this->createMock(ExtractorInterface::class);
+        $extractorMock->expects($this->any())->method('getFileTypeRestrictions')->willReturn([File::FILETYPE_IMAGE]);
+
+        $extractorService = new ExtractorService();
+        $method = new \ReflectionMethod($extractorService, 'isFileTypeSupportedByExtractor');
+        $method->setAccessible(true);
+        $arguments = [
+            $fileMock,
+            $extractorMock
+        ];
+
+        $result = $method->invokeArgs($extractorService, $arguments);
+        $this->assertTrue($result);
+    }
+
+    /**
+     * @test
+     */
+    public function isFileTypeSupportedByExtractorReturnsTrueForFileTypeTextAndExtractorHasNoFileTypeLimitation(): void
+    {
+        $fileMock = $this->createMock(File::class);
+        $fileMock->expects($this->any())->method('getType')->willReturn(File::FILETYPE_TEXT);
+
+        $extractorMock = $this->createMock(ExtractorInterface::class);
+        $extractorMock->expects($this->any())->method('getFileTypeRestrictions')->willReturn([]);
+
+        $extractorService = new ExtractorService();
+        $method = new \ReflectionMethod($extractorService, 'isFileTypeSupportedByExtractor');
+        $method->setAccessible(true);
+        $arguments = [
+            $fileMock,
+            $extractorMock
+        ];
+
+        $result = $method->invokeArgs($extractorService, $arguments);
+        $this->assertTrue($result);
+    }
+
+    /**
+     * @test
+     */
+    public function extractMetaDataComposesDataByAvailableExtractors(): void
+    {
+        $storageMock = $this->createMock(ResourceStorage::class);
+        $storageMock->method('getDriverType')->willReturn('Local');
+
+        /** @var ExtractorService|\PHPUnit\Framework\MockObject\MockObject $subject */
+        $subject = $this->getMockBuilder(ExtractorService::class)
+            ->setMethods(['getExtractorRegistry'])
+            ->getMock()
+        ;
+
+        $fileMock = $this->createMock(File::class);
+        $fileMock->expects($this->any())->method('getUid')->willReturn(4711);
+        $fileMock->expects($this->any())->method('getType')->willReturn(File::FILETYPE_IMAGE);
+        $fileMock->expects($this->any())->method('getStorage')->willReturn($storageMock);
+
+        $extractorClass1 = md5('1');
+        $extractorObject1 = $this->getMockBuilder(ExtractorInterface::class)
+            ->setMockClassName($extractorClass1)
+            ->getMock();
+
+        $extractorObject1->expects($this->any())->method('getPriority')->willReturn(10);
+        $extractorObject1->expects($this->any())->method('getExecutionPriority')->willReturn(10);
+        $extractorObject1->expects($this->any())->method('canProcess')->willReturn(true);
+        $extractorObject1->expects($this->any())->method('getFileTypeRestrictions')->willReturn([File::FILETYPE_IMAGE]);
+        $extractorObject1->expects($this->any())->method('getDriverRestrictions')->willReturn([$storageMock->getDriverType()]);
+        $extractorObject1->expects($this->any())->method('extractMetaData')->with($fileMock)->willReturn([
+            'width' => 800,
+            'height' => 600,
+        ]);
+
+        $extractorClass2 = md5('2');
+        $extractorObject2 = $this->getMockBuilder(ExtractorInterface::class)
+            ->setMockClassName($extractorClass2)
+            ->getMock();
+
+        $extractorObject2->expects($this->any())->method('getPriority')->willReturn(20);
+        $extractorObject2->expects($this->any())->method('getExecutionPriority')->willReturn(20);
+        $extractorObject2->expects($this->any())->method('canProcess')->willReturn(true);
+        $extractorObject2->expects($this->any())->method('getFileTypeRestrictions')->willReturn([File::FILETYPE_IMAGE]);
+        $extractorObject2->expects($this->any())->method('getDriverRestrictions')->willReturn([$storageMock->getDriverType()]);
+        $extractorObject2->expects($this->any())->method('extractMetaData')->with($fileMock)->willReturn([
+            'keywords' => 'typo3, cms',
+        ]);
+
+        /** @var ExtractorRegistry|\PHPUnit\Framework\MockObject\MockObject $extractorRegistryMock */
+        $extractorRegistryMock = $this->getMockBuilder(ExtractorRegistry::class)
+            ->setMethods(['createExtractorInstance'])
+            ->getMock();
+
+        $extractorRegistryMock->expects($this->any())->method('createExtractorInstance')->will($this->returnValueMap(
+            [
+                [$extractorClass1, $extractorObject1],
+                [$extractorClass2, $extractorObject2]
+            ]
+        ));
+        $extractorRegistryMock->registerExtractionService($extractorClass1);
+        $extractorRegistryMock->registerExtractionService($extractorClass2);
+
+        $subject->expects($this->any())->method('getExtractorRegistry')->willReturn($extractorRegistryMock);
+
+        $this->assertSame(['width' => 800, 'height' => 600, 'keywords' => 'typo3, cms'], $subject->extractMetaData($fileMock));
+    }
+}
index 9ebe847..073ef46 100644 (file)
@@ -956,7 +956,7 @@ class FileList
     {
         try {
             if ($fileObject instanceof File && $fileObject->isIndexed() && $fileObject->checkActionPermission('editMeta') && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata')) {
-                $metaData = $fileObject->_getMetaData();
+                $metaData = $fileObject->getMetaData()->get();
                 $urlParameters = [
                     'edit' => [
                         'sys_file_metadata' => [
@@ -1050,7 +1050,7 @@ class FileList
                         break;
                     case '_LOCALIZATION_':
                         if (!empty($systemLanguages) && $fileObject->isIndexed() && $fileObject->checkActionPermission('editMeta') && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata')) {
-                            $metaDataRecord = $fileObject->_getMetaData();
+                            $metaDataRecord = $fileObject->getMetaData()->get();
                             $translations = $this->getTranslationsForMetaData($metaDataRecord);
                             $languageCode = '';
 
@@ -1315,7 +1315,7 @@ class FileList
 
         // Edit metadata of file
         if ($fileOrFolderObject instanceof File && $fileOrFolderObject->checkActionPermission('editMeta') && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata')) {
-            $metaData = $fileOrFolderObject->_getMetaData();
+            $metaData = $fileOrFolderObject->getMetaData()->get();
             $urlParameters = [
                 'edit' => [
                     'sys_file_metadata' => [
index 610a124..6d3264a 100644 (file)
@@ -3318,6 +3318,13 @@ return [
             'Breaking-87193-DeprecatedFunctionalityRemoved.rst',
         ],
     ],
+    'TYPO3\CMS\Core\Resource\File->_getMetaData' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85895-DeprecateFile_getMetaData.rst',
+        ],
+    ],
     'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->initFEuser' => [
         'numberOfMandatoryArguments' => 0,
         'maximumNumberOfArguments' => 0,