TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
|
||||
/**
|
||||
* Immutable value object that represents a search demand for files.
|
||||
*/
|
||||
class FileSearchDemand
|
||||
{
|
||||
private ?string $searchTerm;
|
||||
private ?Folder $folder = null;
|
||||
private ?int $firstResult = null;
|
||||
private ?int $maxResults = null;
|
||||
private ?array $searchFields = null;
|
||||
private ?array $orderings = null;
|
||||
private bool $recursive = false;
|
||||
|
||||
/**
|
||||
* Only factory methods are allowed to be used to create this object
|
||||
*/
|
||||
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 hasSearchTerm(): bool
|
||||
{
|
||||
return $this->searchTerm !== null;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search;
|
||||
|
||||
use Doctrine\DBAL\Result;
|
||||
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\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* 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 string FILES_TABLE = 'sys_file';
|
||||
|
||||
private const string FILES_META_TABLE = 'sys_file_metadata';
|
||||
|
||||
private QueryBuilder $queryBuilder;
|
||||
|
||||
/**
|
||||
* @var QueryRestrictionInterface[]
|
||||
*/
|
||||
private array $additionalRestrictions = [];
|
||||
|
||||
private ?Result $result = null;
|
||||
|
||||
private TcaSchemaFactory $tcaSchemaFactory;
|
||||
|
||||
public function __construct(?QueryBuilder $queryBuilder = null)
|
||||
{
|
||||
$this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::FILES_TABLE);
|
||||
$this->tcaSchemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a query based on a search demand to be used to fetch rows.
|
||||
*/
|
||||
public static function createForSearchDemand(FileSearchDemand $searchDemand, ?QueryBuilder $queryBuilder = null): self
|
||||
{
|
||||
$query = new self($queryBuilder);
|
||||
$query->additionalRestriction(
|
||||
new SearchTermRestriction($searchDemand, $query->queryBuilder)
|
||||
);
|
||||
$folder = $searchDemand->getFolder();
|
||||
if ($folder !== null) {
|
||||
$query->additionalRestriction(
|
||||
new FolderRestriction($folder, $searchDemand->isRecursive())
|
||||
);
|
||||
} else {
|
||||
$query->additionalRestriction(
|
||||
new FolderMountsRestriction($GLOBALS['BE_USER'])
|
||||
);
|
||||
}
|
||||
|
||||
$query->queryBuilder->getConcreteQueryBuilder()->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) {
|
||||
$schema = $query->tcaSchemaFactory->get(self::FILES_TABLE);
|
||||
if ($schema->hasCapability(TcaSchemaCapability::SortByField)) {
|
||||
$orderBy = $schema->getCapability(TcaSchemaCapability::SortByField)->getFieldName();
|
||||
} elseif ($schema->hasCapability(TcaSchemaCapability::DefaultSorting)) {
|
||||
$orderBy = $schema->getCapability(TcaSchemaCapability::DefaultSorting)->getValue();
|
||||
} else {
|
||||
$orderBy = '';
|
||||
}
|
||||
foreach (QueryHelper::parseOrderBy($orderBy) as [$fieldName, $order]) {
|
||||
if (is_string($fieldName) && $fieldName !== '') {
|
||||
// Call add ordering only for valid field names
|
||||
$searchDemand = $searchDemand->addOrdering(self::FILES_TABLE, $fieldName, $order ?? 'ASC');
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($searchDemand->getOrderings() as [$tableName, $fieldName, $direction]) {
|
||||
if (!$query->tcaSchemaFactory->has($tableName)
|
||||
|| !$query->tcaSchemaFactory->get($tableName)->hasField($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->getConcreteQueryBuilder()->addSelect(
|
||||
...$query->queryBuilder->quoteIdentifiersForSelect([
|
||||
$tableName . '.' . $fieldName
|
||||
. ' AS '
|
||||
. preg_replace(
|
||||
'/[^a-z0-9]/',
|
||||
'',
|
||||
StringUtility::getUniqueId($tableName . $fieldName)
|
||||
),
|
||||
])
|
||||
);
|
||||
$query->queryBuilder->addOrderBy($tableName . '.' . $fieldName, $direction);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a query based on a search demand to be used to count rows.
|
||||
*/
|
||||
public static function createCountForSearchDemand(FileSearchDemand $searchDemand, ?QueryBuilder $queryBuilder = null): self
|
||||
{
|
||||
$query = new self($queryBuilder);
|
||||
$query->additionalRestriction(
|
||||
new SearchTermRestriction($searchDemand, $query->queryBuilder)
|
||||
);
|
||||
$folder = $searchDemand->getFolder();
|
||||
if ($folder !== null) {
|
||||
$query->additionalRestriction(
|
||||
new FolderRestriction($folder, $searchDemand->isRecursive())
|
||||
);
|
||||
}
|
||||
|
||||
$query->queryBuilder->getConcreteQueryBuilder()->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.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function additionalRestriction(QueryRestrictionInterface $additionalRestriction): void
|
||||
{
|
||||
$this->ensureQueryNotExecuted();
|
||||
$this->additionalRestrictions[get_class($additionalRestriction)] = $additionalRestriction;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Result
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
if ($this->result === null) {
|
||||
$this->initializeQueryBuilder();
|
||||
$this->result = $this->queryBuilder->executeQuery();
|
||||
}
|
||||
|
||||
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 !== null) {
|
||||
throw new \RuntimeException('Cannot modify file query once it was executed. Create a new query instead.', 1555944032);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
|
||||
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
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, Connection::PARAM_INT));
|
||||
}
|
||||
}
|
||||
|
||||
return $expressionBuilder->and(...$constraints);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
|
||||
|
||||
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->or(...$constraints);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
|
||||
|
||||
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->or(...$constraints);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
|
||||
|
||||
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;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* 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->and('1=0');
|
||||
}
|
||||
$constraints = [];
|
||||
foreach ($this->restrictions as $restriction) {
|
||||
$constraints[] = $restriction->buildExpression($queriedTables, $expressionBuilder);
|
||||
}
|
||||
return $expressionBuilder->or(...$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[] = GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier($fileMount['identifier'] ?? '');
|
||||
}
|
||||
|
||||
return $this->folderMounts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
|
||||
|
||||
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(false)->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 = [];
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
|
||||
|
||||
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\Schema\SearchableSchemaFieldsCollector;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Filters result by a given search term, respecting search fields defined in search demand or in TCA.
|
||||
*/
|
||||
readonly class SearchTermRestriction implements QueryRestrictionInterface
|
||||
{
|
||||
public function __construct(
|
||||
private FileSearchDemand $searchDemand,
|
||||
private 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->or(...$constraints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the MySql where clause by table.
|
||||
*
|
||||
* @param string $tableName Record table name
|
||||
*/
|
||||
private function makeQuerySearchByTable(string $tableName, string $tableAlias): CompositeExpression
|
||||
{
|
||||
$constraints = [];
|
||||
$fieldsToSearchWithin = GeneralUtility::makeInstance(SearchableSchemaFieldsCollector::class)->getFields(
|
||||
$tableName,
|
||||
$this->searchDemand->getSearchFields()[$tableName] ?? []
|
||||
);
|
||||
if ($fieldsToSearchWithin->count() > 0) {
|
||||
$searchTerm = (string)$this->searchDemand->getSearchTerm();
|
||||
$searchTermParts = str_getcsv($searchTerm, ' ', '"', '\\');
|
||||
foreach ($searchTermParts as $searchTermPart) {
|
||||
$searchTermPart = trim($searchTermPart);
|
||||
if ($searchTermPart === '') {
|
||||
continue;
|
||||
}
|
||||
$constraintsForParts = [];
|
||||
$like = '%' . $this->queryBuilder->escapeLikeWildcards($searchTermPart) . '%';
|
||||
foreach ($fieldsToSearchWithin as $fieldName => $field) {
|
||||
$constraintsForParts[] = $this->queryBuilder->expr()->and(
|
||||
$this->queryBuilder->expr()->comparison(
|
||||
sprintf(
|
||||
'LOWER(%s)',
|
||||
// Ensure to cast `$fieldName` to a text value, otherwise picky databases like
|
||||
// postgres would complain about trying to use `LOWER()` on incompatible field
|
||||
// like integer fields, something MariaDB/MySQL is silently allowed and hidden
|
||||
// away from the consumer. We avoid doing database field type checks here for
|
||||
// all or specific database and adding a value conversion by default for all
|
||||
// fields to be on the safe side.
|
||||
//
|
||||
// The lower() construct here is used to enforce "case-insensitive" search for
|
||||
// all database vendors unrelated to charset/collation configurations on field
|
||||
// level.
|
||||
$this->queryBuilder->expr()->castText($this->queryBuilder->quoteIdentifier($tableAlias . '.' . $fieldName))
|
||||
),
|
||||
'LIKE',
|
||||
$this->queryBuilder->createNamedParameter(mb_strtolower($like))
|
||||
)
|
||||
);
|
||||
}
|
||||
$constraints[] = $this->queryBuilder->expr()->or(...$constraintsForParts);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->queryBuilder->expr()->and(...$constraints);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\QueryRestrictions;
|
||||
|
||||
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->or(...$constraints);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\Result;
|
||||
|
||||
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
|
||||
{
|
||||
private ?array $result = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly FileSearchResultInterface $searchResult,
|
||||
private readonly DriverInterface $driver,
|
||||
/**
|
||||
* @var callable[]
|
||||
*/
|
||||
private readonly array $filters
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @see Countable::count()
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
return count($this->result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Iterator::current()
|
||||
*/
|
||||
public function current(): File
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
return current($this->result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
* @return array<int, File>
|
||||
*/
|
||||
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 use -1 as the "don't include“ return value, for historic reasons,
|
||||
// as call_user_func() used to return FALSE if calling the method failed.
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\Result;
|
||||
|
||||
/**
|
||||
* Represents an empty search result (no matches found)
|
||||
*/
|
||||
class EmptyFileSearchResult implements FileSearchResultInterface
|
||||
{
|
||||
public function count(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return null
|
||||
*/
|
||||
public function current(): mixed
|
||||
{
|
||||
// Noop
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return null
|
||||
*/
|
||||
public function key(): mixed
|
||||
{
|
||||
// Noop
|
||||
return null;
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
// Noop
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
// Noop
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\Result;
|
||||
|
||||
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;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Represents a search result for a given search query
|
||||
* being an iterable and countable list of file objects.
|
||||
*/
|
||||
class FileSearchResult implements FileSearchResultInterface
|
||||
{
|
||||
/**
|
||||
* @var FileSearchDemand
|
||||
*/
|
||||
private $searchDemand;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $result;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $resultCount;
|
||||
|
||||
public function __construct(FileSearchDemand $searchDemand)
|
||||
{
|
||||
$this->searchDemand = $searchDemand;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Countable::count()
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
if ($this->resultCount !== null) {
|
||||
return $this->resultCount;
|
||||
}
|
||||
|
||||
$this->resultCount = (int)FileSearchQuery::createCountForSearchDemand($this->searchDemand)->execute()->fetchOne();
|
||||
|
||||
return $this->resultCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Iterator::current()
|
||||
*/
|
||||
public function current(): File
|
||||
{
|
||||
$this->initialize();
|
||||
return current($this->result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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()->fetchAllAssociative();
|
||||
$this->resultCount = count($this->result);
|
||||
$this->result = array_map(
|
||||
static function (array $fileRow): File {
|
||||
return GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject($fileRow['uid'], $fileRow);
|
||||
},
|
||||
$this->result
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Resource\Search\Result;
|
||||
|
||||
/**
|
||||
* Representation of a result for a search for files performed by FileSearchQuery,
|
||||
* which is a collection of matching files.
|
||||
*/
|
||||
interface FileSearchResultInterface extends \Countable, \Iterator {}
|
||||
Reference in New Issue
Block a user