[FEATURE] API to search for files including meta data based on folder 85/58985/38
authorHelmut Hummel <typo3@helhum.io>
Thu, 29 Nov 2018 17:49:04 +0000 (18:49 +0100)
committerGeorg Ringer <georg.ringer@gmail.com>
Tue, 7 May 2019 03:57:27 +0000 (05:57 +0200)
Instead of globally selecting all files with matching
meta data, we now base each search on the selected folder.

To do so, search is now completely based on the persisted
files in sys_file (and their corresponding meta data
in sys_file_metadata).

Additionally we properly evaluate search fields from TCA
so that we now search in all fields defined for sys_file
and sys_file_metadata table.

To achieve that, a new capability "CAPABILITY_HIERARCHICAL_IDENTIFIERS"
is introduced, which drivers can set, that build identifiers
that reflect the directory structure.
For such drivers, the search can be optimized by using
like queries on identifiers, instead of recursively traversing folders,
which can be an expensive operation especially for drivers
handling a remote file system.

Resolves: #87610
Releases: master, 9.5
Change-Id: Ia132465437827b2fdb56004eb73348ce4a05b336
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/58985
Reviewed-by: Markus Klein <markus.klein@typo3.org>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Markus Klein <markus.klein@typo3.org>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
27 files changed:
typo3/sysext/core/Classes/Database/Query/QueryBuilder.php
typo3/sysext/core/Classes/Resource/Driver/LocalDriver.php
typo3/sysext/core/Classes/Resource/FileRepository.php
typo3/sysext/core/Classes/Resource/Folder.php
typo3/sysext/core/Classes/Resource/Index/FileIndexRepository.php
typo3/sysext/core/Classes/Resource/ResourceStorage.php
typo3/sysext/core/Classes/Resource/ResourceStorageInterface.php
typo3/sysext/core/Classes/Resource/Search/FileSearchDemand.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/FileSearchQuery.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/ConsistencyRestriction.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderHashesRestriction.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderIdentifierRestriction.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderMountsRestriction.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderRestriction.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/StorageRestriction.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/Result/DriverFilteredSearchResult.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/Result/EmptyFileSearchResult.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/Result/FileSearchResult.php [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/Search/Result/FileSearchResultInterface.php [new file with mode: 0644]
typo3/sysext/core/Configuration/TCA/sys_file_metadata.php
typo3/sysext/core/Documentation/Changelog/9.3/Feature-71644-AddMetadataToFilebrowserSearch.rst
typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-87610-New-FAL-API-to-search-for-files.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Resource/Fixtures/FileSearch.xml [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Resource/ResourceStorageTest.php
typo3/sysext/filelist/Classes/Controller/FileListController.php
typo3/sysext/recordlist/Classes/Browser/FileBrowser.php

index b902ac1..6a9aebc 100644 (file)
@@ -351,12 +351,12 @@ class QueryBuilder
      * 'groupBy', 'having' and 'orderBy'.
      *
      * @param string $sqlPartName
-     * @param string $sqlPart
+     * @param string|array $sqlPart
      * @param bool $append
      *
      * @return QueryBuilder This QueryBuilder instance.
      */
-    public function add(string $sqlPartName, string $sqlPart, bool $append = false): QueryBuilder
+    public function add(string $sqlPartName, $sqlPart, bool $append = false): QueryBuilder
     {
         $this->concreteQueryBuilder->add($sqlPartName, $sqlPart, $append);
 
index 7a53c5e..99aa9c2 100644 (file)
@@ -75,7 +75,8 @@ class LocalDriver extends AbstractHierarchicalFilesystemDriver implements Stream
         $this->capabilities =
             ResourceStorage::CAPABILITY_BROWSABLE
             | ResourceStorage::CAPABILITY_PUBLIC
-            | ResourceStorage::CAPABILITY_WRITABLE;
+            | ResourceStorage::CAPABILITY_WRITABLE
+            | ResourceStorage::CAPABILITY_HIERARCHICAL_IDENTIFIERS;
     }
 
     /**
@@ -89,6 +90,7 @@ class LocalDriver extends AbstractHierarchicalFilesystemDriver implements Stream
     public function mergeConfigurationCapabilities($capabilities)
     {
         $this->capabilities &= $capabilities;
+
         return $this->capabilities;
     }
 
index 390fc43..3ff1cd4 100644 (file)
@@ -20,6 +20,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
 use TYPO3\CMS\Core\Database\RelationHandler;
 use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
 use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
+use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 
@@ -161,32 +162,15 @@ class FileRepository extends AbstractRepository
      * @param string $fileName
      * @return File[]
      * @internal
+     * @deprecated Use ResourceStorage::searchFiles instead
      */
     public function searchByName(Folder $folder, $fileName)
     {
-        /** @var ResourceFactory $fileFactory */
-        $fileFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+        trigger_error(__METHOD__ . ' is deprecated. Use ResourceStorage::searchFiles instead', \E_USER_DEPRECATED);
+        $searchDemand = FileSearchDemand::createForSearchTerm($fileName)
+            ->addSearchField('sys_file', 'name');
 
-        $storage = $folder->getStorage();
-        $folders = $storage->getFoldersInFolder($folder, 0, 0, true, true);
-        $folders[$folder->getIdentifier()] = $folder;
-
-        $fileRecords = $this->getFileIndexRepository()->findByFolders($folders, false, $fileName);
-        $fileRecords = array_merge($fileRecords, $this->getFileIndexRepository()->findBySearchWordInMetaData($fileName));
-
-        $files = [];
-        foreach ($fileRecords as $fileRecord) {
-            try {
-                $file = $fileFactory->getFileObject($fileRecord['uid'], $fileRecord);
-                if ($storage->checkFileAndFolderNameFilters($file)) {
-                    $files[] = $file;
-                }
-            } catch (Exception\FileDoesNotExistException $ignoredException) {
-                continue;
-            }
-        }
-
-        return $files;
+        return iterator_to_array($folder->searchFiles($searchDemand));
     }
 
     /**
index 7a125db..edcf5a0 100644 (file)
@@ -14,6 +14,8 @@ namespace TYPO3\CMS\Core\Resource;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
+use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResultInterface;
 use TYPO3\CMS\Core\Utility\PathUtility;
 
 /**
@@ -232,6 +234,23 @@ class Folder implements FolderInterface
     }
 
     /**
+     * Returns a file search result based on the given demand.
+     * The result also includes matches in meta data fields that are defined in TCA.
+     *
+     * @param FileSearchDemand $searchDemand
+     * @param int $filterMode The filter mode to use for the found files
+     * @return FileSearchResultInterface
+     */
+    public function searchFiles(FileSearchDemand $searchDemand, int $filterMode = self::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS): FileSearchResultInterface
+    {
+        list($backedUpFilters, $useFilters) = $this->prepareFiltersInStorage($filterMode);
+        $searchResult = $this->storage->searchFiles($searchDemand, $this, $useFilters);
+        $this->restoreBackedUpFiltersInStorage($backedUpFilters);
+
+        return $searchResult;
+    }
+
+    /**
      * Returns amount of all files within this folder, optionally filtered by
      * the given pattern
      *
index 41e1cd0..9fba616 100644 (file)
@@ -22,6 +22,8 @@ use TYPO3\CMS\Core\Resource\FileInterface;
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
+use TYPO3\CMS\Core\Resource\Search\FileSearchQuery;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Object\ObjectManager;
@@ -561,57 +563,15 @@ class FileIndexRepository implements SingletonInterface
      * Search for files by search word in metadata
      *
      * @param string $searchWord search word
-     *
+     * @deprecated Use FileSearchQuery instead
      * @return array
      */
     public function findBySearchWordInMetaData($searchWord)
     {
-        $metaDataTableName = 'sys_file_metadata';
-        $sysFileTableName = 'sys_file';
-
-        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($metaDataTableName);
-        $queryBuilder
-            ->select($sysFileTableName . '.*')
-            ->from($metaDataTableName)
-            ->join(
-                $metaDataTableName,
-                $sysFileTableName,
-                $sysFileTableName,
-                $queryBuilder->expr()->eq($metaDataTableName . '.file', $queryBuilder->quoteIdentifier($sysFileTableName . '.uid'))
-            );
-
-        if (null !== $searchWord) {
-            $nameParts = str_getcsv($searchWord, ' ');
-            foreach ($nameParts as $part) {
-                $part = trim($part);
-                if ($part !== '') {
-                    $queryBuilder->orWhere(
-                        $queryBuilder->expr()->like(
-                            $metaDataTableName . '.title',
-                            $queryBuilder->createNamedParameter(
-                                '%' . $queryBuilder->escapeLikeWildcards($part) . '%',
-                                \PDO::PARAM_STR
-                            )
-                        ),
-                        $queryBuilder->expr()->like(
-                            $metaDataTableName . '.description',
-                            $queryBuilder->createNamedParameter(
-                                '%' . $queryBuilder->escapeLikeWildcards($part) . '%',
-                                \PDO::PARAM_STR
-                            )
-                        ),
-                        $queryBuilder->expr()->like(
-                            $metaDataTableName . '.alternative',
-                            $queryBuilder->createNamedParameter(
-                                '%' . $queryBuilder->escapeLikeWildcards($part) . '%',
-                                \PDO::PARAM_STR
-                            )
-                        )
-                    );
-                }
-            }
-        }
-        $result = $queryBuilder->execute();
+        trigger_error(__METHOD__ . ' is deprecated. Use FileSearchQuery instead', \E_USER_DEPRECATED);
+        $searchDemand = FileSearchDemand::createForSearchTerm($searchWord);
+        $searchQuery = FileSearchQuery::createForSearchDemand($searchDemand);
+        $result = $searchQuery->execute();
         $fileRecords = [];
         while ($fileRecord = $result->fetch()) {
             $fileRecords[$fileRecord['identifier']] = $fileRecord;
index 011ea05..77dfa49 100644 (file)
@@ -26,6 +26,11 @@ use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException;
 use TYPO3\CMS\Core\Resource\Exception\InvalidTargetFolderException;
 use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
 use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
+use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
+use TYPO3\CMS\Core\Resource\Search\Result\DriverFilteredSearchResult;
+use TYPO3\CMS\Core\Resource\Search\Result\EmptyFileSearchResult;
+use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResult;
+use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResultInterface;
 use TYPO3\CMS\Core\Utility\Exception\NotImplementedMethodException;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
@@ -179,7 +184,9 @@ class ResourceStorage implements ResourceStorageInterface
         $this->capabilities =
             ($this->storageRecord['is_browsable'] ?? null ? self::CAPABILITY_BROWSABLE : 0) |
             ($this->storageRecord['is_public'] ?? null ? self::CAPABILITY_PUBLIC : 0) |
-            ($this->storageRecord['is_writable'] ?? null ? self::CAPABILITY_WRITABLE : 0);
+            ($this->storageRecord['is_writable'] ?? null ? self::CAPABILITY_WRITABLE : 0) |
+            // Always let the driver decide whether to set this capability
+            self::CAPABILITY_HIERARCHICAL_IDENTIFIERS;
 
         $this->driver = $driver;
         $this->driver->setStorageUid($storageRecord['uid'] ?? null);
@@ -349,6 +356,41 @@ class ResourceStorage implements ResourceStorageInterface
     }
 
     /**
+     * Returns TRUE if this storage stores folder structure in file identifiers.
+     *
+     * @return bool
+     */
+    public function hasHierarchicalIdentifiers(): bool
+    {
+        return $this->hasCapability(self::CAPABILITY_HIERARCHICAL_IDENTIFIERS);
+    }
+
+    /**
+     * Search for files in a storage based on given restrictions
+     * and a possibly given folder.
+     *
+     * @param FileSearchDemand $searchDemand
+     * @param Folder|null $folder
+     * @param bool $useFilters Whether storage filters should be applied
+     * @return FileSearchResultInterface
+     */
+    public function searchFiles(FileSearchDemand $searchDemand, Folder $folder = null, bool $useFilters = true): FileSearchResultInterface
+    {
+        $folder = $folder ?? $this->getRootLevelFolder();
+        if (!$folder->checkActionPermission('read')) {
+            return new EmptyFileSearchResult();
+        }
+
+        return new DriverFilteredSearchResult(
+            new FileSearchResult(
+                $searchDemand->withFolder($folder)
+            ),
+            $this->driver,
+            $useFilters ? $this->getFileAndFolderNameFilters() : []
+        );
+    }
+
+    /**
      * Returns TRUE if the identifiers used by this storage are case-sensitive.
      *
      * @return bool
@@ -721,10 +763,12 @@ class ResourceStorage implements ResourceStorageInterface
 
     /**
      * @param ResourceInterface $fileOrFolder
+     * @deprecated This method will be removed without substitution, use the ResourceStorage API instead
      * @return bool
      */
     public function checkFileAndFolderNameFilters(ResourceInterface $fileOrFolder)
     {
+        trigger_error(__METHOD__ . ' is deprecated. This method will be removed without substitution, use the ResourceStorage API instead', \E_USER_DEPRECATED);
         foreach ($this->fileAndFolderNameFilters as $filter) {
             if (is_callable($filter)) {
                 $result = call_user_func($filter, $fileOrFolder->getName(), $fileOrFolder->getIdentifier(), $fileOrFolder->getParentFolder()->getIdentifier(), [], $this->driver);
index 1ee4c98..65eb4a8 100644 (file)
@@ -62,6 +62,10 @@ interface ResourceStorageInterface
      */
     const CAPABILITY_WRITABLE = 4;
     /**
+     * Whether identifiers contain hierarchy information (folder structure).
+     */
+    const CAPABILITY_HIERARCHICAL_IDENTIFIERS = 8;
+    /**
      * Name of the default processing folder
      */
     const DEFAULT_ProcessingFolder = '_processed_';
diff --git a/typo3/sysext/core/Classes/Resource/Search/FileSearchDemand.php b/typo3/sysext/core/Classes/Resource/Search/FileSearchDemand.php
new file mode 100644 (file)
index 0000000..625a7fa
--- /dev/null
@@ -0,0 +1,177 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search;
+
+/*
+ * 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\Folder;
+
+/**
+ * Immutable value object that represents a search demand for files.
+ */
+class FileSearchDemand
+{
+    /**
+     * @var string|null
+     */
+    private $searchTerm;
+
+    /**
+     * @var Folder|null
+     */
+    private $folder;
+
+    /**
+     * @var int|null
+     */
+    private $firstResult;
+
+    /**
+     * @var int|null
+     */
+    private $maxResults;
+
+    /**
+     * @var array|null
+     */
+    private $searchFields;
+
+    /**
+     * @var array|null
+     */
+    private $orderings;
+
+    /**
+     * @var bool
+     */
+    private $recursive = false;
+
+    /**
+     * Only factory methods are allowed to be used to create this object
+     *
+     * @param string|null $searchTerm
+     */
+    private function __construct(string $searchTerm = null)
+    {
+        $this->searchTerm = $searchTerm;
+    }
+
+    public static function create(): self
+    {
+        return new self();
+    }
+
+    public static function createForSearchTerm(string $searchTerm): self
+    {
+        return new self($searchTerm);
+    }
+
+    public function getSearchTerm(): ?string
+    {
+        return $this->searchTerm;
+    }
+
+    public function getFolder(): ?Folder
+    {
+        return $this->folder;
+    }
+
+    public function getFirstResult(): ?int
+    {
+        return $this->firstResult;
+    }
+
+    public function getMaxResults(): ?int
+    {
+        return $this->maxResults;
+    }
+
+    public function getSearchFields(): ?array
+    {
+        return $this->searchFields;
+    }
+
+    public function getOrderings(): ?array
+    {
+        return $this->orderings;
+    }
+
+    public function isRecursive(): bool
+    {
+        return $this->recursive;
+    }
+
+    public function withSearchTerm(string $searchTerm): self
+    {
+        $demand = clone $this;
+        $demand->searchTerm = $searchTerm;
+
+        return $demand;
+    }
+
+    public function withFolder(Folder $folder): self
+    {
+        $demand = clone $this;
+        $demand->folder = $folder;
+
+        return $demand;
+    }
+
+    /**
+     * Requests the position of the first result to retrieve (the "offset").
+     * Same as in QueryBuilder it is the index of the result set, with 0 being the first result.
+     *
+     * @param int $firstResult
+     * @return FileSearchDemand
+     */
+    public function withStartResult(int $firstResult): self
+    {
+        $demand = clone $this;
+        $demand->firstResult = $firstResult;
+
+        return $demand;
+    }
+
+    public function withMaxResults(int $maxResults): self
+    {
+        $demand = clone $this;
+        $demand->maxResults = $maxResults;
+
+        return $demand;
+    }
+
+    public function addSearchField(string $tableName, string $field): self
+    {
+        $demand = clone $this;
+        $demand->searchFields[$tableName] = $field;
+
+        return $demand;
+    }
+
+    public function addOrdering(string $tableName, string $fieldName, string $direction = 'ASC'): self
+    {
+        $demand = clone $this;
+        $demand->orderings[] = [$tableName, $fieldName, $direction];
+
+        return $demand;
+    }
+
+    public function withRecursive(): self
+    {
+        $demand = clone $this;
+        $demand->recursive = true;
+
+        return $demand;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/FileSearchQuery.php b/typo3/sysext/core/Classes/Resource/Search/FileSearchQuery.php
new file mode 100644 (file)
index 0000000..042fd27
--- /dev/null
@@ -0,0 +1,207 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search;
+
+/*
+ * 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\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Database\Query\QueryHelper;
+use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface;
+use TYPO3\CMS\Core\Resource\Search\QueryRestrictions\ConsistencyRestriction;
+use TYPO3\CMS\Core\Resource\Search\QueryRestrictions\FolderMountsRestriction;
+use TYPO3\CMS\Core\Resource\Search\QueryRestrictions\FolderRestriction;
+use TYPO3\CMS\Core\Resource\Search\QueryRestrictions\SearchTermRestriction;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Represents an SQL query to search for files.
+ * Acts as facade to a QueryBuilder and comes with factory methods
+ * to preconfigure the query for a search demand.
+ */
+class FileSearchQuery
+{
+    private const FILES_TABLE = 'sys_file';
+
+    private const FILES_META_TABLE = 'sys_file_metadata';
+
+    /**
+     * @var QueryBuilder
+     */
+    private $queryBuilder;
+
+    /**
+     * @var QueryRestrictionInterface[]
+     */
+    private $additionalRestrictions = [];
+
+    /**
+     * @var \Doctrine\DBAL\Driver\Statement|int
+     */
+    private $result;
+
+    public function __construct(QueryBuilder $queryBuilder = null)
+    {
+        $this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::FILES_TABLE);
+    }
+
+    /**
+     * Prepares a query based on a search demand to be used to fetch rows.
+     *
+     * @param FileSearchDemand $searchDemand
+     * @param QueryBuilder|null $queryBuilder
+     * @return FileSearchQuery
+     */
+    public static function createForSearchDemand(FileSearchDemand $searchDemand, QueryBuilder $queryBuilder = null): self
+    {
+        $query = new self($queryBuilder);
+        $query->additionalRestriction(
+            new SearchTermRestriction($searchDemand, $query->queryBuilder)
+        );
+        if ($searchDemand->getFolder()) {
+            $query->additionalRestriction(
+                new FolderRestriction($searchDemand->getFolder(), $searchDemand->isRecursive())
+            );
+        } else {
+            $query->additionalRestriction(
+                new FolderMountsRestriction($GLOBALS['BE_USER'])
+            );
+        }
+
+        $query->queryBuilder->add(
+            'select',
+            [
+                'DISTINCT ' . $query->queryBuilder->quoteIdentifier(self::FILES_TABLE . '.identifier'),
+                $query->queryBuilder->quoteIdentifier(self::FILES_TABLE) . '.*',
+            ]
+        );
+
+        if ($searchDemand->getFirstResult() !== null) {
+            $query->queryBuilder->setFirstResult($searchDemand->getFirstResult());
+        }
+        if ($searchDemand->getMaxResults() !== null) {
+            $query->queryBuilder->setMaxResults($searchDemand->getMaxResults());
+        }
+
+        if ($searchDemand->getOrderings() === null) {
+            $orderBy = $GLOBALS['TCA'][self::FILES_TABLE]['ctrl']['sortby'] ?: $GLOBALS['TCA'][self::FILES_TABLE]['ctrl']['default_sortby'];
+            foreach (QueryHelper::parseOrderBy((string)$orderBy) as [$fieldName, $order]) {
+                $searchDemand = $searchDemand->addOrdering(self::FILES_TABLE, $fieldName, $order);
+            }
+        }
+        foreach ($searchDemand->getOrderings() as [$tableName, $fieldName, $direction]) {
+            if (!isset($GLOBALS['TCA'][$tableName]['columns'][$fieldName]) || !in_array($direction, ['ASC', 'DESC'], true)) {
+                // This exception is essential to avoid SQL injections based on ordering field names, which could be input controlled by an attacker.
+                throw new \RuntimeException(sprintf('Invalid file search ordering given table: "%s", field: "%s", direction: "%s".', $tableName, $fieldName, $direction), 1555850106);
+            }
+            // Add order by fields to select, to make postgres happy and use random names to make sure to not interfere with file fields
+            $query->queryBuilder->add(
+                'select',
+                $query->queryBuilder->quoteIdentifiersForSelect([
+                    $tableName . '.' . $fieldName
+                    . ' AS '
+                    . preg_replace(
+                        '/[^a-z0-9]/',
+                        '',
+                        uniqid($tableName . $fieldName, true)
+                    )
+                ]),
+                true
+            );
+            $query->queryBuilder->addOrderBy($tableName . '.' . $fieldName, $direction);
+        }
+
+        return $query;
+    }
+
+    /**
+     * Prepares a query based on a search demand to be used to count rows.
+     *
+     * @param FileSearchDemand $searchDemand
+     * @param QueryBuilder|null $queryBuilder
+     * @return FileSearchQuery
+     */
+    public static function createCountForSearchDemand(FileSearchDemand $searchDemand, QueryBuilder $queryBuilder = null): self
+    {
+        $query = new self($queryBuilder);
+        $query->additionalRestriction(
+            new SearchTermRestriction($searchDemand, $query->queryBuilder)
+        );
+        if ($searchDemand->getFolder()) {
+            $query->additionalRestriction(
+                new FolderRestriction($searchDemand->getFolder(), $searchDemand->isRecursive())
+            );
+        }
+
+        $query->queryBuilder->add(
+            'select',
+            'COUNT(DISTINCT ' . $query->queryBuilder->quoteIdentifier(self::FILES_TABLE . '.identifier') . ')'
+         );
+
+        return $query;
+    }
+
+    /**
+     * Limit the result set of identifiers, by adding further SQL restrictions.
+     * Note that no further restrictions can be added once result is initialized,
+     * by starting the iteration over the result.
+     * Can be accessed by subclasses to add further restrictions to the query.
+     *
+     * @param QueryRestrictionInterface $additionalRestriction
+     * @throws |RuntimeException
+     */
+    public function additionalRestriction(QueryRestrictionInterface $additionalRestriction): void
+    {
+        $this->ensureQueryNotExecuted();
+        $this->additionalRestrictions[get_class($additionalRestriction)] = $additionalRestriction;
+    }
+
+    public function execute()
+    {
+        if ($this->result === null) {
+            $this->initializeQueryBuilder();
+            $this->result = $this->queryBuilder->execute();
+        }
+
+        return $this->result;
+    }
+
+    /**
+     * Create and initialize QueryBuilder for SQL based file search.
+     * Can be accessed by subclasses for example to add further joins to the query.
+     */
+    private function initializeQueryBuilder(): void
+    {
+        $this->queryBuilder->from(self::FILES_TABLE);
+        $this->queryBuilder->join(
+            self::FILES_TABLE,
+            self::FILES_META_TABLE,
+            self::FILES_META_TABLE,
+            $this->queryBuilder->expr()->eq(self::FILES_META_TABLE . '.file', $this->queryBuilder->quoteIdentifier(self::FILES_TABLE . '.uid'))
+        );
+
+        $restrictionContainer = $this->queryBuilder->getRestrictions()
+            ->add(new ConsistencyRestriction($this->queryBuilder));
+        foreach ($this->additionalRestrictions as $additionalRestriction) {
+            $restrictionContainer->add($additionalRestriction);
+        }
+    }
+
+    private function ensureQueryNotExecuted(): void
+    {
+        if ($this->result) {
+            throw new \RuntimeException('Cannot modify file query once it was executed. Create a new query instead.', 1555944032);
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/ConsistencyRestriction.php b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/ConsistencyRestriction.php
new file mode 100644 (file)
index 0000000..ed7d9b8
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
+
+/*
+ * 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\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface;
+
+/**
+ * Filters missing files from search result
+ */
+class ConsistencyRestriction implements QueryRestrictionInterface
+{
+    /**
+     * @var QueryBuilder
+     */
+    private $queryBuilder;
+
+    public function __construct(QueryBuilder $queryBuilder)
+    {
+        $this->queryBuilder = $queryBuilder;
+    }
+
+    public function buildExpression(array $queriedTables, ExpressionBuilder $expressionBuilder): CompositeExpression
+    {
+        $constraints = [];
+        foreach ($queriedTables as $tableAlias => $tableName) {
+            if ($tableName === 'sys_file') {
+                $constraints[] = $this->queryBuilder->expr()->eq($tableAlias . '.missing', $this->queryBuilder->createNamedParameter(0, \PDO::PARAM_INT));
+            }
+        }
+
+        return $expressionBuilder->andX(...$constraints);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderHashesRestriction.php b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderHashesRestriction.php
new file mode 100644 (file)
index 0000000..bc0c153
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
+
+/*
+ * 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\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Limits search result to files with given folder hashes
+ */
+class FolderHashesRestriction implements QueryRestrictionInterface
+{
+    /**
+     * @var array
+     */
+    private $folderHashes;
+
+    public function __construct(array $folderHashes)
+    {
+        $this->folderHashes = $folderHashes;
+    }
+
+    public function buildExpression(array $queriedTables, ExpressionBuilder $expressionBuilder): CompositeExpression
+    {
+        $constraints = [];
+        foreach ($queriedTables as $tableAlias => $tableName) {
+            if ($tableName !== 'sys_file') {
+                continue;
+            }
+            $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
+            $quotedHashes = array_map([$connection, 'quote'], $this->folderHashes);
+            $constraints[] = $expressionBuilder->in($tableAlias . '.folder_hash', $quotedHashes);
+        }
+
+        return $expressionBuilder->orX(...$constraints);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderIdentifierRestriction.php b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderIdentifierRestriction.php
new file mode 100644 (file)
index 0000000..f340f78
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
+
+/*
+ * 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\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Assumes identifiers carrying hierarchical information and
+ * filters files with identifiers starting with given identifier.
+ */
+class FolderIdentifierRestriction implements QueryRestrictionInterface
+{
+    /**
+     * @var string
+     */
+    private $folderIdentifier;
+
+    public function __construct(string $folderIdentifier)
+    {
+        $this->folderIdentifier = $folderIdentifier;
+    }
+
+    public function buildExpression(array $queriedTables, ExpressionBuilder $expressionBuilder): CompositeExpression
+    {
+        $constraints = [];
+        foreach ($queriedTables as $tableAlias => $tableName) {
+            if ($tableName !== 'sys_file') {
+                continue;
+            }
+            $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
+            $folderIdentifier = $connection->createQueryBuilder()->escapeLikeWildcards($this->folderIdentifier);
+            $constraints[] = $expressionBuilder->like(
+                $tableAlias . '.identifier',
+                $connection->quote($folderIdentifier . '%')
+            );
+        }
+
+        return $expressionBuilder->orX(...$constraints);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderMountsRestriction.php b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderMountsRestriction.php
new file mode 100644 (file)
index 0000000..b6ade2a
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
+
+/*
+ * 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\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\Restriction\AbstractRestrictionContainer;
+use TYPO3\CMS\Core\Resource\Folder;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
+
+/**
+ * Restricts the result to available file mounts.
+ * No restriction is added if the user is admin.
+ */
+class FolderMountsRestriction extends AbstractRestrictionContainer
+{
+    /**
+     * @var BackendUserAuthentication
+     */
+    private $backendUser;
+
+    /**
+     * @var Folder[]|null
+     */
+    private $folderMounts;
+
+    public function __construct(BackendUserAuthentication $backendUser)
+    {
+        $this->backendUser = $backendUser;
+        $this->populateRestrictions();
+    }
+
+    private function populateRestrictions(): void
+    {
+        if ($this->backendUser->isAdmin()) {
+            return;
+        }
+        foreach ($this->getFolderMounts() as $folder) {
+            $this->add(new FolderRestriction($folder, true));
+        }
+    }
+
+    /**
+     * Same as parent method, but using OR composite, as files in either mounted folder should be found.
+     *
+     * @param array $queriedTables Array of tables, where array key is table alias and value is a table name
+     * @param ExpressionBuilder $expressionBuilder Expression builder instance to add restrictions with
+     * @return CompositeExpression The result of query builder expression(s)
+     */
+    public function buildExpression(array $queriedTables, ExpressionBuilder $expressionBuilder): CompositeExpression
+    {
+        if (!$this->backendUser->isAdmin() && empty($this->getFolderMounts())) {
+            // If the user isn't an admin but has no mounted folders, add an expression leading to an empty result
+            return $expressionBuilder->andX('1=0');
+        }
+        $constraints = [];
+        foreach ($this->restrictions as $restriction) {
+            $constraints[] = $restriction->buildExpression($queriedTables, $expressionBuilder);
+        }
+        return $expressionBuilder->orX(...$constraints);
+    }
+
+    /**
+     * @return Folder[]
+     */
+    private function getFolderMounts(): array
+    {
+        if ($this->folderMounts !== null) {
+            return $this->folderMounts;
+        }
+        $this->folderMounts = [];
+        $fileMounts = $this->backendUser->getFileMountRecords();
+        foreach ($fileMounts as $fileMount) {
+            $this->folderMounts[] = ResourceFactory::getInstance()->getFolderObjectFromCombinedIdentifier($fileMount['base'] . ':' . $fileMount['path']);
+        }
+
+        return $this->folderMounts;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderRestriction.php b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/FolderRestriction.php
new file mode 100644 (file)
index 0000000..b880e68
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
+
+/*
+ * 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\Database\Query\Restriction\AbstractRestrictionContainer;
+use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface;
+use TYPO3\CMS\Core\Resource\Folder;
+
+/**
+ * Limits result to storage given by the folder
+ * and also restricts result to the given folder, respecting whether the storage
+ * has hierarchical identifiers or not.
+ */
+class FolderRestriction extends AbstractRestrictionContainer
+{
+    /**
+     * @var Folder
+     */
+    private $folder;
+
+    /**
+     * @var bool
+     */
+    private $recursive;
+
+    public function __construct(Folder $folder, bool $recursive)
+    {
+        $this->folder = $folder;
+        $this->recursive = $recursive;
+        $this->populateRestrictions();
+    }
+
+    private function populateRestrictions(): void
+    {
+        $storage = $this->folder->getStorage();
+        $this->add(new StorageRestriction($storage));
+        if (!$this->recursive) {
+            $this->add($this->createFolderRestriction());
+            return;
+        }
+        if ($this->folder->getIdentifier() === $storage->getRootLevelFolder()->getIdentifier()) {
+            return;
+        }
+        if ($storage->hasHierarchicalIdentifiers()) {
+            $this->add($this->createHierarchicalFolderRestriction());
+        } else {
+            $this->add($this->createFolderRestriction());
+        }
+    }
+
+    private function createHierarchicalFolderRestriction(): QueryRestrictionInterface
+    {
+        return $this->recursive ? new FolderIdentifierRestriction($this->folder->getIdentifier()) : new FolderHashesRestriction([$this->folder->getHashedIdentifier()]);
+    }
+
+    private function createFolderRestriction(): QueryRestrictionInterface
+    {
+        $hashedFolderIdentifiers[] = $this->folder->getHashedIdentifier();
+        if ($this->recursive) {
+            foreach ($this->folder->getSubfolders(0, 0, Folder::FILTER_MODE_NO_FILTERS, true) as $subFolder) {
+                $hashedFolderIdentifiers[] = $subFolder->getHashedIdentifier();
+            }
+        }
+
+        return new FolderHashesRestriction($hashedFolderIdentifiers);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php
new file mode 100644 (file)
index 0000000..7eb9ad0
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
+
+/*
+ * 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\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface;
+use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Filters result by a given search term, respecting search fields defined in search demand or in TCA.
+ */
+class SearchTermRestriction implements QueryRestrictionInterface
+{
+    /**
+     * @var FileSearchDemand
+     */
+    private $searchDemand;
+
+    /**
+     * @var QueryBuilder
+     */
+    private $queryBuilder;
+
+    public function __construct(FileSearchDemand $searchDemand, QueryBuilder $queryBuilder)
+    {
+        $this->searchDemand = $searchDemand;
+        $this->queryBuilder = $queryBuilder;
+    }
+
+    public function buildExpression(array $queriedTables, ExpressionBuilder $expressionBuilder): CompositeExpression
+    {
+        $constraints = [];
+        foreach ($queriedTables as $tableAlias => $tableName) {
+            if (!in_array($tableName, ['sys_file', 'sys_file_metadata'])) {
+                continue;
+            }
+            $constraints[] = $this->makeQuerySearchByTable($tableName, $tableAlias);
+        }
+
+        return $expressionBuilder->orX(...$constraints);
+    }
+
+    /**
+     * Build the MySql where clause by table.
+     *
+     * @param string $tableName Record table name
+     * @param string $tableAlias
+     * @return CompositeExpression
+     */
+    private function makeQuerySearchByTable(string $tableName, string $tableAlias): CompositeExpression
+    {
+        $fieldsToSearchWithin = $this->extractSearchableFieldsFromTable($tableName);
+        $searchTerm = $this->searchDemand->getSearchTerm();
+        $constraints = [];
+
+        $like = '%' . $this->queryBuilder->escapeLikeWildcards($searchTerm) . '%';
+        foreach ($fieldsToSearchWithin as $fieldName) {
+            if (!isset($GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
+                continue;
+            }
+            $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
+            $fieldType = $fieldConfig['type'];
+            $evalRules = $fieldConfig['eval'] ?? '';
+
+            // Check whether search should be case-sensitive or not
+            if (is_array($fieldConfig['search']) && in_array('case', $fieldConfig['search'], true)) {
+                // case sensitive
+                $searchConstraint = $this->queryBuilder->expr()->andX(
+                    $this->queryBuilder->expr()->like(
+                        $tableAlias . '.' . $fieldName,
+                        $this->queryBuilder->createNamedParameter($like, \PDO::PARAM_STR)
+                    )
+                );
+            } else {
+                $searchConstraint = $this->queryBuilder->expr()->andX(
+                    // case insensitive
+                    $this->queryBuilder->expr()->comparison(
+                        'LOWER(' . $this->queryBuilder->quoteIdentifier($tableAlias . '.' . $fieldName) . ')',
+                        'LIKE',
+                        $this->queryBuilder->createNamedParameter(mb_strtolower($like), \PDO::PARAM_STR)
+                    )
+                );
+            }
+
+            // Assemble the search condition only if the field makes sense to be searched
+            if ($fieldType === 'text'
+                || $fieldType === 'flex'
+                || ($fieldType === 'input' && (!$evalRules || !preg_match('/date|time|int/', $evalRules)))
+            ) {
+                $constraints[] = $searchConstraint;
+            }
+        }
+
+        return $this->queryBuilder->expr()->orX(...$constraints);
+    }
+
+    /**
+     * Get all fields from given table where we can search for.
+     *
+     * @param string $tableName Name of the table for which to get the searchable fields
+     * @return array
+     */
+    private function extractSearchableFieldsFromTable(string $tableName): array
+    {
+        if ($searchFields = $this->searchDemand->getSearchFields()) {
+            if (empty($searchFields[$tableName])) {
+                return [];
+            }
+            foreach ($searchFields[$tableName] as $searchField) {
+                if (!isset($GLOBALS['TCA'][$tableName]['columns'][$searchField])) {
+                    throw new \RuntimeException(sprintf('Cannot use search field "%s" because it is not defined in TCA.', $searchField), 1556367071);
+                }
+            }
+
+            return $searchFields;
+        }
+        $fieldListArray = [];
+        // Get the list of fields to search in from the TCA, if any
+        if (isset($GLOBALS['TCA'][$tableName]['ctrl']['searchFields'])) {
+            $fieldListArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$tableName]['ctrl']['searchFields'], true);
+        }
+
+        return $fieldListArray;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/StorageRestriction.php b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/StorageRestriction.php
new file mode 100644 (file)
index 0000000..22abd99
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
+
+/*
+ * 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\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface;
+use TYPO3\CMS\Core\Resource\ResourceStorage;
+
+/**
+ * Limits search result to a give storage
+ */
+class StorageRestriction implements QueryRestrictionInterface
+{
+    /**
+     * @var ResourceStorage
+     */
+    private $storage;
+
+    public function __construct(ResourceStorage $storage)
+    {
+        $this->storage = $storage;
+    }
+
+    public function buildExpression(array $queriedTables, ExpressionBuilder $expressionBuilder): CompositeExpression
+    {
+        $constraints = [];
+        foreach ($queriedTables as $tableAlias => $tableName) {
+            if ($tableName !== 'sys_file') {
+                continue;
+            }
+            $constraints[] = $expressionBuilder->eq(
+                $tableAlias . '.storage',
+                (int)$this->storage->getUid()
+            );
+        }
+
+        return $expressionBuilder->orX(...$constraints);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/Result/DriverFilteredSearchResult.php b/typo3/sysext/core/Classes/Resource/Search/Result/DriverFilteredSearchResult.php
new file mode 100644 (file)
index 0000000..64c6880
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\Result;
+
+/*
+ * 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\Driver\DriverInterface;
+use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Utility\PathUtility;
+
+/**
+ * Decorator for a search result with files, which filters
+ * the result based on given filters.
+ */
+class DriverFilteredSearchResult implements FileSearchResultInterface
+{
+    /**
+     * @var FileSearchResultInterface
+     */
+    private $searchResult;
+
+    /**
+     * @var DriverInterface
+     */
+    private $driver;
+
+    /**
+     * @var callable[]
+     */
+    private $filters;
+
+    /**
+     * @var array
+     */
+    private $result;
+
+    public function __construct(FileSearchResultInterface $searchResult, DriverInterface $driver, array $filters)
+    {
+        $this->searchResult = $searchResult;
+        $this->driver = $driver;
+        $this->filters = $filters;
+    }
+
+    /**
+     * @return int
+     * @see Countable::count()
+     */
+    public function count(): int
+    {
+        $this->initialize();
+
+        return count($this->result);
+    }
+
+    /**
+     * @return File
+     * @see Iterator::current()
+     */
+    public function current(): File
+    {
+        $this->initialize();
+
+        return current($this->result);
+    }
+
+    /**
+     * @return int
+     * @see Iterator::key()
+     */
+    public function key(): int
+    {
+        $this->initialize();
+
+        return key($this->result);
+    }
+
+    /**
+     * @see Iterator::next()
+     */
+    public function next(): void
+    {
+        $this->initialize();
+        next($this->result);
+    }
+
+    /**
+     * @see Iterator::rewind()
+     */
+    public function rewind(): void
+    {
+        $this->initialize();
+        reset($this->result);
+    }
+
+    /**
+     * @return bool
+     * @see Iterator::valid()
+     */
+    public function valid(): bool
+    {
+        $this->initialize();
+
+        return current($this->result) !== false;
+    }
+
+    private function initialize(): void
+    {
+        if ($this->result === null) {
+            $this->result = $this->applyFilters(...iterator_to_array($this->searchResult));
+        }
+    }
+
+    /**
+     * Filter out identifiers by calling all attached filters
+     *
+     * @param File[] $files
+     * @return array
+     */
+    private function applyFilters(File ...$files): array
+    {
+        $filteredFiles = [];
+        foreach ($files as $file) {
+            $itemIdentifier = $file->getIdentifier();
+            $itemName = PathUtility::basename($itemIdentifier);
+            $parentIdentifier = PathUtility::dirname($itemIdentifier);
+            $matches = true;
+            foreach ($this->filters as $filter) {
+                if (!is_callable($filter)) {
+                    continue;
+                }
+                $result = $filter($itemName, $itemIdentifier, $parentIdentifier, [], $this->driver);
+                // We have to use -1 as the „don't include“ return value, as call_user_func() will return FALSE
+                // If calling the method succeeded and thus we can't use that as a return value.
+                if ($result === -1) {
+                    $matches = false;
+                }
+                if ($result === false) {
+                    throw new \RuntimeException(
+                        'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1],
+                        1543617278
+                    );
+                }
+            }
+            if ($matches) {
+                $filteredFiles[] = $file;
+            }
+        }
+
+        return $filteredFiles;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/Result/EmptyFileSearchResult.php b/typo3/sysext/core/Classes/Resource/Search/Result/EmptyFileSearchResult.php
new file mode 100644 (file)
index 0000000..ece3da1
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\Result;
+
+/*
+ * 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!
+ */
+
+/**
+ * Represents an empty search result (no matches found)
+ */
+class EmptyFileSearchResult implements FileSearchResultInterface
+{
+    /**
+     * @return int
+     * @see Countable::count()
+     */
+    public function count(): int
+    {
+        return 0;
+    }
+
+    /**
+     * @see Iterator::current()
+     */
+    public function current(): void
+    {
+        // Noop
+    }
+
+    /**
+     * @see Iterator::key()
+     */
+    public function key(): void
+    {
+        // Noop
+    }
+
+    /**
+     * @see Iterator::next()
+     */
+    public function next(): void
+    {
+        // Noop
+    }
+
+    /**
+     * @see Iterator::rewind()
+     */
+    public function rewind(): void
+    {
+        // Noop
+    }
+
+    /**
+     * @return bool
+     * @see Iterator::valid()
+     */
+    public function valid(): bool
+    {
+        return false;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/Result/FileSearchResult.php b/typo3/sysext/core/Classes/Resource/Search/Result/FileSearchResult.php
new file mode 100644 (file)
index 0000000..22f579d
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\Result;
+
+/*
+ * 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\ResourceFactory;
+use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
+use TYPO3\CMS\Core\Resource\Search\FileSearchQuery;
+
+/**
+ * Represents a search result for a given search query
+ * being an iterable and countable list of file objects.
+ */
+class FileSearchResult implements FileSearchResultInterface
+{
+    /**
+     * @var FileSearchQuery
+     */
+    private $searchDemand;
+
+    /**
+     * @var array
+     */
+    private $result;
+
+    /**
+     * @var int
+     */
+    private $resultCount;
+
+    public function __construct(FileSearchDemand $searchDemand)
+    {
+        $this->searchDemand = $searchDemand;
+    }
+
+    /**
+     * @return int
+     * @see Countable::count()
+     */
+    public function count(): int
+    {
+        if ($this->resultCount !== null) {
+            return $this->resultCount;
+        }
+
+        $this->resultCount = (int)FileSearchQuery::createCountForSearchDemand($this->searchDemand)->execute()->fetchColumn(0);
+
+        return $this->resultCount;
+    }
+
+    /**
+     * @return File
+     * @see Iterator::current()
+     */
+    public function current(): File
+    {
+        $this->initialize();
+        return current($this->result);
+    }
+
+    /**
+     * @return int
+     * @see Iterator::key()
+     */
+    public function key(): int
+    {
+        $this->initialize();
+        return key($this->result);
+    }
+
+    /**
+     * @see Iterator::next()
+     */
+    public function next(): void
+    {
+        $this->initialize();
+        next($this->result);
+    }
+
+    /**
+     * @see Iterator::rewind()
+     */
+    public function rewind(): void
+    {
+        $this->initialize();
+        reset($this->result);
+    }
+
+    /**
+     * @return bool
+     * @see Iterator::valid()
+     */
+    public function valid(): bool
+    {
+        $this->initialize();
+        return current($this->result) !== false;
+    }
+
+    /**
+     * Perform the SQL query and apply filters on the resulting identifiers
+     */
+    private function initialize(): void
+    {
+        if ($this->result !== null) {
+            return;
+        }
+        $this->result = FileSearchQuery::createForSearchDemand($this->searchDemand)->execute()->fetchAll();
+        $this->resultCount = count($this->result);
+        $this->result = array_map(
+            function (array $fileRow) {
+                return ResourceFactory::getInstance()->getFileObject($fileRow['uid'], $fileRow);
+            },
+            $this->result
+        );
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Search/Result/FileSearchResultInterface.php b/typo3/sysext/core/Classes/Resource/Search/Result/FileSearchResultInterface.php
new file mode 100644 (file)
index 0000000..cba003f
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Search\Result;
+
+/*
+ * 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!
+ */
+
+/**
+ * Representation of a result for a search for files performed by FileSearchQuery,
+ * which is a collection of matching files.
+ */
+interface FileSearchResultInterface extends \Countable, \Iterator
+{
+}
index efdcd84..ea4b4bb 100644 (file)
@@ -22,12 +22,17 @@ return [
             'ignoreWebMountRestriction' => true,
             'ignoreRootLevelRestriction' => true,
         ],
-        'searchFields' => 'file,title,description,alternative'
+        'searchFields' => 'title,description,alternative'
     ],
     'interface' => [
         'showRecordFieldList' => 'file, title, description, alternative'
     ],
     'columns' => [
+        'crdate' => [
+            'config' => [
+                'type' => 'passthrough',
+            ],
+        ],
         'sys_language_uid' => [
             'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
             'config' => [
index e56c690..618cb94 100644 (file)
@@ -9,12 +9,6 @@ See :issue:`71644`
 Description
 ===========
 
-Now also searching for `sys_file_metadata` in the filebrowser is possible. The fields title, description and alternative are searched.
-
-
-Impact
-======
-
-Files are displayed whose metadata match the search word.
+This change is now superseded by a more solid solution with `#87610`, which also respects folders in file search.
 
 .. index:: Backend, ext:filelist
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-87610-New-FAL-API-to-search-for-files.rst b/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-87610-New-FAL-API-to-search-for-files.rst
new file mode 100644 (file)
index 0000000..120236a
--- /dev/null
@@ -0,0 +1,54 @@
+.. include:: ../../Includes.txt
+
+===========================================================================
+Feature: #87610 - New FAL API to search for files including their meta data
+===========================================================================
+
+See :issue:`87610`
+
+Description
+===========
+
+A new API is introduced to search for files in a storage or folder, which includes matches in meta data of those files.
+The given search term is looked for in all search fields defined in TCA of `sys_file` and `sys_file_metadata` tables.
+
+A new driver capability `\TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_HIERARCHICAL_IDENTIFIERS`
+is introduced to allow implementing an optimized search with good performance.
+Drivers can optionally add this capability in case the identifiers that are constructed by the driver
+include the directory structure.
+Adding this capability to drivers can provide a big performance boost
+when it comes to recursive search (which is default in the file list and file browser UI).
+
+Impact
+======
+
+This change is fully backwards compatible. Custom driver implementations will continue to work like before,
+but they won't benefit from the performance gain unless the implementers add the new capability.
+
+Searching for files in a folder works like this:
+
+.. code-block:: php
+
+   $searchDemand = FileSearchDemand::createForSearchTerm($searchWord)->withRecursive();
+   $files = $folder->searchFiles($searchDemand);
+
+Searching for files in a complete storage works like this:
+
+.. code-block:: php
+
+   $searchDemand = FileSearchDemand::createForSearchTerm($searchWord)->withRecursive();
+   $files = $storage->searchFiles($searchDemand);
+
+It is possible to further limit the result set, by adding additional restrictions to the `FileSearchDemand`.
+Please note, that `FileSearchDemand` is an immutable value object, but allows chaining methods for ease of use:
+
+.. code-block:: php
+
+   $searchDemand = FileSearchDemand::createForSearchTerm($this->searchWord)
+       ->withRecursive()
+       ->withMaxResults(10)
+       ->withOrdering('fileext');
+   $files = $storage->searchFiles($searchDemand);
+
+
+.. index:: Backend, PHP-API, ext:filelist
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/FileSearch.xml b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/FileSearch.xml
new file mode 100644 (file)
index 0000000..cb1f25f
--- /dev/null
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+       <sys_file>
+               <uid>1</uid>
+               <pid>0</pid>
+               <storage>1</storage>
+               <identifier>/bar/bla/foo.txt</identifier>
+               <folder_hash>d839c094d3294deb5b61cf7e0fe4f1cddaa96579</folder_hash>
+               <name>foo</name>
+               <identifier_hash>84e70e8b11db88cea944e4d7be107092ab148b72</identifier_hash>
+               <extension>txt</extension>
+               <mime_type>text/plain</mime_type>
+               <sha1>6e237d7c3e98387f924b81aada9b8d1f43f5eb5b</sha1>
+       </sys_file>
+       <sys_file_metadata>
+               <uid>1</uid>
+               <pid>0</pid>
+               <crdate>1556435417</crdate>
+               <file>1</file>
+               <sys_language_uid>0</sys_language_uid>
+               <title>Title for foo</title>
+               <description>FoDescription</description>
+       </sys_file_metadata>
+       <sys_file_metadata>
+               <uid>2</uid>
+               <pid>0</pid>
+               <crdate>1556435427</crdate>
+               <file>1</file>
+               <l10n_parent>1</l10n_parent>
+               <sys_language_uid>1</sys_language_uid>
+               <title>Title for foo</title>
+               <description>FoTranslatedDescription</description>
+       </sys_file_metadata>
+
+       <sys_file>
+               <uid>2</uid>
+               <pid>0</pid>
+               <storage>1</storage>
+               <identifier>/bar/blupp.txt</identifier>
+               <folder_hash>04a7e2dfdf6e35eae326a441224010622d589ee9</folder_hash>
+               <name>blupp</name>
+               <identifier_hash>bdbeddb9d7697ff7508674ed4b5e35e0a397286c</identifier_hash>
+               <extension>txt</extension>
+               <mime_type>text/plain</mime_type>
+               <sha1>6e237d7c3e98387f924b81aada9b8d1f43f5eb5b</sha1>
+       </sys_file>
+       <sys_file_metadata>
+               <uid>3</uid>
+               <pid>0</pid>
+               <crdate>1556435499</crdate>
+               <file>2</file>
+               <sys_language_uid>0</sys_language_uid>
+               <title>Title for blupp</title>
+               <description>BlubDescription</description>
+       </sys_file_metadata>
+
+       <sys_file>
+               <uid>3</uid>
+               <pid>0</pid>
+               <storage>1</storage>
+               <identifier>/baz/bla/baz.txt</identifier>
+               <folder_hash>a0296fe46605d9faec617d7d138bd6d4e87d46f1</folder_hash>
+               <name>baz</name>
+               <identifier_hash>f11e0f830ba618ce3a65f8fbb19f09137c755e24</identifier_hash>
+               <extension>txt</extension>
+               <mime_type>text/plain</mime_type>
+               <sha1>6e237d7c3e98387f924b81aada9b8d1f43f5eb5b</sha1>
+       </sys_file>
+       <sys_file_metadata>
+               <uid>4</uid>
+               <pid>0</pid>
+               <crdate>1556435503</crdate>
+               <file>3</file>
+               <sys_language_uid>0</sys_language_uid>
+               <title>Title for baz</title>
+               <description>BaDescription</description>
+       </sys_file_metadata>
+</dataset>
index 2001cf8..b7dd6d5 100644 (file)
@@ -19,6 +19,7 @@ use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\FolderInterface;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
 use TYPO3\CMS\Core\Resource\StorageRepository;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
@@ -251,4 +252,173 @@ class ResourceStorageTest extends FunctionalTestCase
 
         $this->assertFalse(file_exists(Environment::getPublicPath() . '/fileadmin/foo/bar.txt'));
     }
+
+    public function searchFilesFindsFilesInFolderDataProvider(): array
+    {
+        return [
+            'Finds foo recursive by name' => [
+                'foo',
+                '/bar/',
+                true,
+                [],
+                [
+                    '/bar/bla/foo.txt',
+                ],
+            ],
+            'Finds foo not recursive by name' => [
+                'foo',
+                '/bar/bla/',
+                false,
+                [],
+                [
+                    '/bar/bla/foo.txt',
+                ],
+            ],
+            'Finds nothing when not recursive for top level folder' => [
+                'foo',
+                '/bar/',
+                false,
+                [],
+                [],
+            ],
+            'Finds foo by description' => [
+                'fodescrip',
+                '/bar/',
+                true,
+                [],
+                [
+                    '/bar/bla/foo.txt',
+                ],
+            ],
+            'Finds foo by translated description' => [
+                'fotranslated',
+                '/bar/',
+                true,
+                [],
+                [
+                    '/bar/bla/foo.txt',
+                ],
+            ],
+            'Finds blupp by name' => [
+                'blupp',
+                '/bar/',
+                false,
+                [],
+                [
+                    '/bar/blupp.txt',
+                ],
+            ],
+            'Finds only blupp by title for non recursive' => [
+                'title',
+                '/bar/',
+                false,
+                [],
+                [
+                    '/bar/blupp.txt',
+                ],
+            ],
+            'Finds foo and blupp by title for recursive' => [
+                'title',
+                '/bar/',
+                true,
+                [],
+                [
+                    '/bar/blupp.txt',
+                    '/bar/bla/foo.txt',
+                ],
+            ],
+            'Finds foo, baz and blupp with no folder' => [
+                'title',
+                null,
+                true,
+                [],
+                [
+                    '/baz/bla/baz.txt',
+                    '/bar/blupp.txt',
+                    '/bar/bla/foo.txt',
+                ],
+            ],
+            'Finds nothing for not existing' => [
+                'baz',
+                '/bar/',
+                true,
+                [],
+                [],
+            ],
+            'Finds nothing in root, when not recursive' => [
+                'title',
+                '/',
+                false,
+                [],
+                [],
+            ],
+            'Finds nothing, when not recursive and no folder given' => [
+                'title',
+                null,
+                false,
+                [],
+                [],
+            ],
+            'Filter is applied to result' => [
+                'title',
+                null,
+                true,
+                [
+                    function ($itemName) {
+                        return strpos($itemName, 'blupp') !== false ? true : -1;
+                    }
+                ],
+                [
+                    '/bar/blupp.txt',
+                ],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider searchFilesFindsFilesInFolderDataProvider
+     * @param string $searchTerm
+     * @param string $searchFolder
+     * @param bool $recursive
+     * @param array $filters
+     * @param string[] $expectedIdentifiers
+     * @throws \TYPO3\TestingFramework\Core\Exception
+     */
+    public function searchFilesFindsFilesInFolder(string $searchTerm, ?string $searchFolder, bool $recursive, array $filters, array $expectedIdentifiers)
+    {
+        try {
+            $this->importDataSet('PACKAGE:typo3/testing-framework/Resources/Core/Functional/Fixtures/sys_file_storage.xml');
+            $this->importDataSet(__DIR__ . '/Fixtures/FileSearch.xml');
+            $this->setUpBackendUserFromFixture(1);
+            $subject = (new StorageRepository())->findByUid(1);
+            $subject->setFileAndFolderNameFilters($filters);
+
+            GeneralUtility::mkdir_deep(Environment::getPublicPath() . '/fileadmin/bar/bla');
+            GeneralUtility::mkdir_deep(Environment::getPublicPath() . '/fileadmin/baz/bla');
+            file_put_contents(Environment::getPublicPath() . '/fileadmin/bar/bla/foo.txt', 'myData');
+            file_put_contents(Environment::getPublicPath() . '/fileadmin/baz/bla/baz.txt', 'myData');
+            file_put_contents(Environment::getPublicPath() . '/fileadmin/bar/blupp.txt', 'myData');
+            clearstatcache();
+
+            $folder = $searchFolder ? ResourceFactory::getInstance()->getFolderObjectFromCombinedIdentifier('1:' . $searchFolder) : null;
+            $search = FileSearchDemand::createForSearchTerm($searchTerm);
+            if ($recursive) {
+                $search = $search->withRecursive();
+            }
+
+            $result = $subject->searchFiles($search, $folder);
+            $expectedFiles = array_map([$subject, 'getFile'], $expectedIdentifiers);
+            $this->assertSame($expectedFiles, iterator_to_array($result));
+
+            // Check if search also works for non hierarchical storages/drivers
+            $this->inject($subject, 'capabilities', $subject->getCapabilities() & 7);
+            $result = $subject->searchFiles($search, $folder);
+            $expectedFiles = array_map([$subject, 'getFile'], $expectedIdentifiers);
+            $this->assertSame($expectedFiles, iterator_to_array($result));
+        } finally {
+            GeneralUtility::rmdir(Environment::getPublicPath() . '/fileadmin/bar', true);
+            GeneralUtility::rmdir(Environment::getPublicPath() . '/fileadmin/baz', true);
+        }
+    }
 }
index a9d2eda..a7baef4 100644 (file)
@@ -27,6 +27,7 @@ use TYPO3\CMS\Core\Resource\DuplicationBehavior;
 use TYPO3\CMS\Core\Resource\Exception;
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
 use TYPO3\CMS\Core\Resource\Utility\ListUtility;
 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
 use TYPO3\CMS\Core\Utility\File\ExtendedFileUtility;
@@ -487,11 +488,11 @@ class FileListController extends ActionController
         if (empty($searchWord)) {
             $this->forward('index');
         }
+        $searchDemand = FileSearchDemand::createForSearchTerm($searchWord)->withRecursive();
+        $files = $this->folderObject->searchFiles($searchDemand);
 
         $fileFacades = [];
-        $files = $this->fileRepository->searchByName($this->folderObject, $searchWord);
-
-        if (empty($files)) {
+        if (count($files) === 0) {
             $this->controllerContext->getFlashMessageQueue('core.template.flashMessages')->addMessage(
                 new FlashMessage(
                     LocalizationUtility::translate('flashmessage.no_results', 'filelist'),
index da79e4c..f9ed609 100644 (file)
@@ -25,6 +25,7 @@ use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\ProcessedFile;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
@@ -250,7 +251,8 @@ class FileBrowser extends AbstractElementBrowser implements ElementBrowserInterf
         $titleLen = (int)$this->getBackendUser()->uc['titleLen'];
 
         if ($this->searchWord !== '') {
-            $files = $this->fileRepository->searchByName($folder, $this->searchWord);
+            $searchDemand = FileSearchDemand::createForSearchTerm($this->searchWord)->withRecursive();
+            $files = $folder->searchFiles($searchDemand);
         } else {
             $extensionList = !empty($extensionList) && $extensionList[0] === '*' ? [] : $extensionList;
             $files = $this->getFilesInFolder($folder, $extensionList);