TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+255
View File
@@ -0,0 +1,255 @@
<?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\Database\Query;
use Doctrine\DBAL\Connection as DoctrineConnection;
use TYPO3\CMS\Core\Database\Connection as Typo3Connection;
/**
* Provides functionality to generate and execute row based bulk INSERT statements.
*
* Based on work by Steve Müller <st.mueller@dzh-online.de> for the Doctrine project,
* licensed under the MIT license.
*
* This class will be removed from core and the functionality will be provided by
* the upstream implementation once the pull request has been merged into Doctrine DBAL.
*
* @see https://github.com/doctrine/dbal/pull/682
* @internal
*/
class BulkInsertQuery
{
/**
* @var string[]
*/
protected $columns;
/**
* @var DoctrineConnection
*/
protected $connection;
/**
* @var string
*/
protected $table;
/**
* @var array
*/
protected $parameters = [];
/**
* @var array
*/
protected $types = [];
/**
* @var array
*/
protected $values = [];
/**
* Constructor.
*
* @param DoctrineConnection $connection The connection to use for query execution.
* @param string $table The name of the table to insert rows into.
* @param string[] $columns The names of the columns to insert values into.
* Can be left empty to allow arbitrary row inserts based on the table's column order.
*/
public function __construct(DoctrineConnection $connection, string $table, array $columns = [])
{
$this->connection = $connection;
$this->table = $connection->quoteIdentifier($table);
$this->columns = $columns;
}
/**
* Render the bulk insert statement as string.
*/
public function __toString(): string
{
return $this->getSQL();
}
/**
* Adds a set of values to the bulk insert query to be inserted as a row into the specified table.
*
* @param array $values The set of values to be inserted as a row into the table.
* If no columns have been specified for insertion, this can be
* an arbitrary list of values to be inserted into the table.
* Otherwise the values' keys have to match either one of the
* specified column names or indexes.
* @param array $types The types for the given values to bind to the query.
* If no columns have been specified for insertion, the types'
* keys will be matched against the given values' keys.
* Otherwise the types' keys will be matched against the
* specified column names and indexes.
* Non-matching keys will be discarded, missing keys will not
* be bound to a specific type.
*
* @throws \InvalidArgumentException if columns were specified for this query
* and either no value for one of the specified
* columns is given or multiple values are given
* for a single column (named and indexed) or
* multiple types are given for a single column
* (named and indexed).
*/
public function addValues(array $values, array $types = [])
{
$valueSet = [];
if (empty($this->columns)) {
foreach ($values as $index => $value) {
$this->parameters[] = $value;
$this->types[] = $types[$index] ?? null;
$valueSet[] = '?';
}
$this->values[] = $valueSet;
return;
}
foreach ($this->columns as $index => $column) {
$namedValue = isset($values[$column]) || array_key_exists($column, $values);
$positionalValue = isset($values[$index]) || array_key_exists($index, $values);
if (!$namedValue && !$positionalValue) {
throw new \InvalidArgumentException(
sprintf('No value specified for column %s (index %d).', $column, $index),
1476049651
);
}
if ($namedValue && $positionalValue && $values[$column] !== $values[$index]) {
throw new \InvalidArgumentException(
sprintf('Multiple values specified for column %s (index %d).', $column, $index),
1476049652
);
}
$this->parameters[] = $namedValue ? $values[$column] : $values[$index];
$valueSet[] = '?';
$namedType = isset($types[$column]);
$positionalType = isset($types[$index]);
if ($namedType && $positionalType && $types[$column] !== $types[$index]) {
throw new \InvalidArgumentException(
sprintf('Multiple types specified for column %s (index %d).', $column, $index),
1476049653
);
}
if ($namedType) {
$this->types[] = $types[$column];
continue;
}
if ($positionalType) {
$this->types[] = $types[$index];
continue;
}
$this->types[] = Typo3Connection::PARAM_STR;
}
$this->values[] = $valueSet;
}
/**
* Executes this INSERT query using the bound parameters and their types.
*
* @return int The number of affected rows.
*
* @throws \LogicException if this query contains more rows than acceptable
* for a single INSERT statement by the underlying platform.
*/
public function execute(): int
{
return $this->connection->executeStatement($this->getSQL(), $this->parameters, $this->types);
}
/**
* Returns the parameters for this INSERT query being constructed indexed by parameter index.
*/
public function getParameters(): array
{
return $this->parameters;
}
/**
* Returns the parameter types for this INSERT query being constructed indexed by parameter index.
*/
public function getParameterTypes(): array
{
return $this->types;
}
/**
* Returns the SQL formed by the current specifications of this INSERT query.
*
*
* @throws \LogicException if no values have been specified yet.
*/
public function getSQL(): string
{
if (empty($this->values)) {
throw new \LogicException(
'You need to add at least one set of values before generating the SQL.',
1476049702
);
}
$connection = $this->connection;
$columnList = '';
if (!empty($this->columns)) {
$columnList = sprintf(
' (%s)',
implode(
', ',
array_map(
static function (string $column) use ($connection): string {
return $connection->quoteIdentifier($column);
},
$this->columns
)
)
);
}
return sprintf(
'INSERT INTO %s%s VALUES (%s)',
$this->table,
$columnList,
implode(
'), (',
array_map(
static function (array $valueSet): string {
return implode(', ', $valueSet);
},
$this->values
)
)
);
}
}
@@ -0,0 +1,924 @@
<?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\Database\Query;
use Doctrine\DBAL\Platforms\MariaDBPlatform as DoctrineMariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQL80Platform as DoctrineMySQL80Platform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform as DoctrinePostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform as DoctrineSQLitePlatform;
use Doctrine\DBAL\Query\Expression\CompositeExpression;
use Doctrine\DBAL\Query\ForUpdate;
use Doctrine\DBAL\Query\ForUpdate\ConflictResolutionMode;
use Doctrine\DBAL\Query\From;
use Doctrine\DBAL\Query\Join;
use Doctrine\DBAL\Query\QueryBuilder as DoctrineQueryBuilder;
use Doctrine\DBAL\Query\QueryException;
use Doctrine\DBAL\Query\QueryType;
use Doctrine\DBAL\Query\Union;
use Doctrine\DBAL\Query\UnionType;
use TYPO3\CMS\Core\Database\Connection;
/**
* QueryBuilder class is responsible to dynamically create SQL queries.
*
* Important: Verify that every feature you use will work with your database vendor.
* SQL Query Builder does not attempt to validate the generated SQL at all.
*
* The query builder does no validation whatsoever if certain features even work with the
* underlying database vendor. Limit queries and joins are NOT applied to UPDATE and DELETE statements
* even if some vendors such as MySQL support it.
*
* @internal not part of public core API. Uses as intermediate decorator wrapper to keep track of state, which is
* considered internal and therefore by Doctrine DBAL but TYPO3 requires internal access.
*/
class ConcreteQueryBuilder extends DoctrineQueryBuilder
{
/**
* The complete SQL string for this query.
*/
protected ?string $sql = null;
/**
* The type of query this is. Can be select, update or delete.
*/
protected QueryType $type = QueryType::SELECT;
/**
* The SELECT parts of the query.
*
* @var string[]
*/
protected array $select = [];
/**
* Whether this is a SELECT DISTINCT query.
*/
protected bool $distinct = false;
/**
* The FROM parts of a SELECT query.
*
* @var From[]
*/
protected array $from = [];
protected ?ForUpdate $forUpdate = null;
/**
* The list of joins, indexed by from alias.
*
* @var array<string, Join[]>
*/
protected array $join = [];
/**
* The WHERE part of a SELECT, UPDATE or DELETE query.
*/
protected string|CompositeExpression|null $where = null;
/**
* The GROUP BY part of a SELECT query.
*
* @var string[]
*/
protected array $groupBy = [];
/**
* The HAVING part of a SELECT query.
*/
protected string|CompositeExpression|null $having = null;
/**
* The ORDER BY parts of a SELECT query.
*
* @var string[]
*/
protected array $orderBy = [];
/**
* The WITH query parts.
*/
protected WithCollection $typo3_with;
/**
* The QueryBuilder for the union parts.
*
* @var Union[]
*/
protected array $typo3_unionParts = [];
/**
* Initializes a new <tt>QueryBuilder</tt>.
*
* @param Connection $connection The DBAL Connection.
*/
public function __construct(protected readonly Connection $connection)
{
parent::__construct($this->connection);
$this->typo3_with = new WithCollection();
}
/**
* Deep clone of all expression objects in the SQL parts.
*/
public function __clone()
{
parent::__clone();
foreach ($this->from as $key => $from) {
$this->from[$key] = clone $from;
}
foreach ($this->join as $fromAlias => $joins) {
foreach ($joins as $key => $join) {
$this->join[$fromAlias][$key] = clone $join;
}
}
if (is_object($this->where)) {
$this->where = clone $this->where;
}
if (is_object($this->having)) {
$this->having = clone $this->having;
}
}
/**
* Specifies union parts to be used to build a UNION query.
* Replaces any previously specified parts.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->union('SELECT 1 AS field1', 'SELECT 2 AS field1');
* </code>
*
* @return $this
*/
public function union(string|ConcreteQueryBuilder|DoctrineQueryBuilder $part): self
{
parent::union($part);
$this->type = QueryType::UNION;
$this->typo3_unionParts = [new Union($part)];
return $this;
}
/**
* Add parts to be used to build a UNION query.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->union('SELECT 1 AS field1')
* ->addUnion('SELECT 2 AS field1', 'SELECT 3 AS field1')
* </code>
*
* @return $this
*/
public function addUnion(string|ConcreteQueryBuilder|DoctrineQueryBuilder $part, UnionType $type = UnionType::DISTINCT): self
{
parent::addUnion($part, $type);
$this->type = QueryType::UNION;
$this->typo3_unionParts[] = new Union($part, $type);
return $this;
}
/**
* Specifies an item that is to be returned in the query result.
* Replaces any previously specified selections, if any.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('u.id', 'p.id')
* ->from('users', 'u')
* ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
* </code>
*
* @param string ...$expressions The selection expressions.
*
* @return $this This QueryBuilder instance.
*/
public function select(string ...$expressions): self
{
parent::select(...$expressions);
$this->type = QueryType::SELECT;
if (count($expressions) < 1) {
return $this;
}
$this->select = $expressions;
$this->sql = null;
return $this;
}
/**
* Adds or removes DISTINCT to/from the query.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('u.id')
* ->distinct()
* ->from('users', 'u')
* </code>
*
* @return $this This QueryBuilder instance.
*/
public function distinct(bool $distinct = true): self
{
parent::distinct($distinct);
$this->distinct = $distinct;
$this->sql = null;
return $this;
}
/**
* Adds an item that is to be returned in the query result.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('u.id')
* ->addSelect('p.id')
* ->from('users', 'u')
* ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
* </code>
*
* @param string $expression The selection expression.
* @param string ...$expressions Additional selection expressions.
*
* @return $this This QueryBuilder instance.
*/
public function addSelect(string $expression, string ...$expressions): self
{
parent::addSelect($expression, ...$expressions);
$this->type = QueryType::SELECT;
$this->select = array_merge($this->select, [$expression], $expressions);
$this->sql = null;
return $this;
}
/**
* Turns the query being built into a bulk delete query that ranges over
* a certain table.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->delete('users', 'u')
* ->where('u.id = :user_id')
* ->setParameter(':user_id', 1);
* </code>
*
* @param string $table The table whose rows are subject to the deletion.
*
* @return $this This QueryBuilder instance.
*/
public function delete(string $table): self
{
parent::delete($table);
$this->type = QueryType::DELETE;
$this->sql = null;
return $this;
}
/**
* Turns the query being built into a bulk update query that ranges over
* a certain table
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->update('counters', 'c')
* ->set('c.value', 'c.value + 1')
* ->where('c.id = ?');
* </code>
*
* @param string $table The table whose rows are subject to the update.
*
* @return $this This QueryBuilder instance.
*/
public function update(string $table): self
{
parent::update($table);
$this->type = QueryType::UPDATE;
$this->sql = null;
return $this;
}
/**
* Turns the query being built into an insert query that inserts into
* a certain table
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->insert('users')
* ->values(
* array(
* 'name' => '?',
* 'password' => '?'
* )
* );
* </code>
*
* @param string $table The table into which the rows should be inserted.
*
* @return $this This QueryBuilder instance.
*/
public function insert(string $table): self
{
parent::insert($table);
$this->type = QueryType::INSERT;
$this->sql = null;
return $this;
}
/**
* Creates and adds a query root corresponding to the table identified by the
* given alias, forming a cartesian product with any existing query roots.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('u.id')
* ->from('users', 'u')
* </code>
*
* @param string $table The table.
* @param string|null $alias The alias of the table.
*
* @return $this This QueryBuilder instance.
*/
public function from(string $table, ?string $alias = null): self
{
parent::from($table, $alias);
$this->from[] = new From($table, $alias);
$this->sql = null;
return $this;
}
/**
* Creates and adds a join to the query.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('u.name')
* ->from('users', 'u')
* ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
* </code>
*
* @param string $fromAlias The alias that points to a from clause.
* @param string $join The table name to join.
* @param string $alias The alias of the join table.
* @param string $condition The condition for the join.
*
* @return $this This QueryBuilder instance.
*/
public function innerJoin(string $fromAlias, string $join, string $alias, ?string $condition = null): self
{
parent::innerJoin($fromAlias, $join, $alias, $condition);
$this->join[$fromAlias][] = Join::inner($join, $alias, $condition);
$this->sql = null;
return $this;
}
/**
* Creates and adds a left join to the query.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('u.name')
* ->from('users', 'u')
* ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
* </code>
*
* @param string $fromAlias The alias that points to a from clause.
* @param string $join The table name to join.
* @param string $alias The alias of the join table.
* @param string $condition The condition for the join.
*
* @return $this This QueryBuilder instance.
*/
public function leftJoin(string $fromAlias, string $join, string $alias, ?string $condition = null): self
{
parent::leftJoin($fromAlias, $join, $alias, $condition);
$this->join[$fromAlias][] = Join::left($join, $alias, $condition);
$this->sql = null;
return $this;
}
/**
* Creates and adds a right join to the query.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('u.name')
* ->from('users', 'u')
* ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
* </code>
*
* @param string $fromAlias The alias that points to a from clause.
* @param string $join The table name to join.
* @param string $alias The alias of the join table.
* @param string $condition The condition for the join.
*
* @return $this This QueryBuilder instance.
*/
public function rightJoin(string $fromAlias, string $join, string $alias, ?string $condition = null): self
{
parent::rightJoin($fromAlias, $join, $alias, $condition);
$this->join[$fromAlias][] = Join::right($join, $alias, $condition);
$this->sql = null;
return $this;
}
/**
* Specifies one or more restrictions to the query result.
* Replaces any previously specified restrictions, if any.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('c.value')
* ->from('counters', 'c')
* ->where('c.id = ?');
*
* // You can optionally programmatically build and/or expressions
* $qb = $conn->createQueryBuilder();
*
* $or = $qb->expr()->orx();
* $or->add($qb->expr()->eq('c.id', 1));
* $or->add($qb->expr()->eq('c.id', 2));
*
* $qb->update('counters', 'c')
* ->set('c.value', 'c.value + 1')
* ->where($or);
* </code>
*
* @param string|CompositeExpression $predicate The WHERE clause predicate.
* @param string|CompositeExpression ...$predicates Additional WHERE clause predicates.
*
* @return $this This QueryBuilder instance.
*/
public function where(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self
{
$where = $this->where = $this->createPredicate($predicate, ...$predicates);
parent::where($where);
$this->sql = null;
return $this;
}
/**
* Adds one or more restrictions to the query results, forming a logical
* conjunction with any previously specified restrictions.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('u')
* ->from('users', 'u')
* ->where('u.username LIKE ?')
* ->andWhere('u.is_active = 1');
* </code>
*
* @see where()
*
* @param string|CompositeExpression $predicate The predicate to append.
* @param string|CompositeExpression ...$predicates Additional predicates to append.
*
* @return $this This QueryBuilder instance.
*/
public function andWhere(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self
{
$where = $this->where = $this->appendToPredicate(
$this->where,
CompositeExpression::TYPE_AND,
$predicate,
...$predicates,
);
$this->where($where);
return $this;
}
/**
* Adds one or more restrictions to the query results, forming a logical
* disjunction with any previously specified restrictions.
*
* <code>
* $qb = $em->createQueryBuilder()
* ->select('u.name')
* ->from('users', 'u')
* ->where('u.id = 1')
* ->orWhere('u.id = 2');
* </code>
*
* @see where()
*
* @param string|CompositeExpression $predicate The predicate to append.
* @param string|CompositeExpression ...$predicates Additional predicates to append.
*
* @return $this This QueryBuilder instance.
*/
public function orWhere(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self
{
$where = $this->where = $this->appendToPredicate($this->where, CompositeExpression::TYPE_OR, $predicate, ...$predicates);
$this->where($where);
return $this;
}
/**
* Specifies one or more grouping expressions over the results of the query.
* Replaces any previously specified groupings, if any.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('u.name')
* ->from('users', 'u')
* ->groupBy('u.id');
* </code>
*
* @param string $expression The grouping expression
* @param string ...$expressions Additional grouping expressions
*
* @return $this This QueryBuilder instance.
*/
public function groupBy(string $expression, string ...$expressions): self
{
$groupBy = $this->groupBy = array_merge([$expression], $expressions);
parent::groupBy(...$groupBy);
$this->sql = null;
return $this;
}
/**
* Adds one or more grouping expressions to the query.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->select('u.name')
* ->from('users', 'u')
* ->groupBy('u.lastLogin')
* ->addGroupBy('u.createdAt');
* </code>
*
* @param string $expression The grouping expression
* @param string ...$expressions Additional grouping expressions
*
* @return $this This QueryBuilder instance.
*/
public function addGroupBy(string $expression, string ...$expressions): self
{
$groupBy = $this->groupBy = array_merge($this->groupBy, [$expression], $expressions);
$this->groupBy(...$groupBy);
return $this;
}
/**
* Specifies a restriction over the groups of the query.
* Replaces any previous having restrictions, if any.
*
* @param string|CompositeExpression $predicate The HAVING clause predicate.
* @param string|CompositeExpression ...$predicates Additional HAVING clause predicates.
*
* @return $this This QueryBuilder instance.
*/
public function having(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self
{
$having = $this->having = $this->createPredicate($predicate, ...$predicates);
parent::having($having);
$this->sql = null;
return $this;
}
/**
* Adds a restriction over the groups of the query, forming a logical
* conjunction with any existing having restrictions.
*
* @param string|CompositeExpression $predicate The predicate to append.
* @param string|CompositeExpression ...$predicates Additional predicates to append.
*
* @return $this This QueryBuilder instance.
*/
public function andHaving(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self
{
$having = $this->having = $this->appendToPredicate(
$this->having,
CompositeExpression::TYPE_AND,
$predicate,
...$predicates,
);
$this->having($having);
return $this;
}
/**
* Adds a restriction over the groups of the query, forming a logical
* disjunction with any existing having restrictions.
*
* @param string|CompositeExpression $predicate The predicate to append.
* @param string|CompositeExpression ...$predicates Additional predicates to append.
*
* @return $this This QueryBuilder instance.
*/
public function orHaving(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self
{
$having = $this->having = $this->appendToPredicate(
$this->having,
CompositeExpression::TYPE_OR,
$predicate,
...$predicates,
);
$this->having($having);
return $this;
}
/**
* Creates a CompositeExpression from one or more predicates combined by the AND logic.
*/
private function createPredicate(
string|CompositeExpression $predicate,
string|CompositeExpression ...$predicates,
): string|CompositeExpression {
if (count($predicates) === 0) {
return $predicate;
}
$predicates = array_filter($predicates, static fn(CompositeExpression|string|null $value): bool => !self::isEmptyPart($value));
return new CompositeExpression(CompositeExpression::TYPE_AND, $predicate, ...$predicates);
}
/**
* Appends the given predicates combined by the given type of logic to the current predicate.
*/
private function appendToPredicate(
string|CompositeExpression|null $currentPredicate,
string $type,
string|CompositeExpression ...$predicates,
): string|CompositeExpression {
$predicates = array_filter($predicates, static fn(CompositeExpression|string|null $value): bool => !self::isEmptyPart($value));
if ($currentPredicate instanceof CompositeExpression && $currentPredicate->getType() === $type) {
return $currentPredicate->with(...$predicates);
}
if ($currentPredicate !== null) {
array_unshift($predicates, $currentPredicate);
} elseif (count($predicates) === 1) {
return $predicates[0];
}
return new CompositeExpression($type, ...$predicates);
}
/**
* Specifies an ordering for the query results.
* Replaces any previously specified orderings, if any.
*
* @param string $sort The ordering expression.
* @param string $order The ordering direction.
*
* @return $this This QueryBuilder instance.
*/
public function orderBy(string $sort, ?string $order = null): self
{
parent::orderBy($sort, $order);
$orderBy = $sort;
if ($order !== null) {
$orderBy .= ' ' . $order;
}
$this->orderBy = [$orderBy];
$this->sql = null;
return $this;
}
/**
* Adds an ordering to the query results.
*
* @param string $sort The ordering expression.
* @param string $order The ordering direction.
*
* @return $this This QueryBuilder instance.
*/
public function addOrderBy(string $sort, ?string $order = null): self
{
parent::addOrderBy($sort, $order);
$orderBy = $sort;
if ($order !== null) {
$orderBy .= ' ' . $order;
}
$this->orderBy[] = $orderBy;
$this->sql = null;
return $this;
}
/**
* Resets the WHERE conditions for the query.
*
* @return $this This QueryBuilder instance.
*/
public function resetWhere(): self
{
parent::resetWhere();
$this->where = null;
$this->sql = null;
return $this;
}
/**
* Resets the grouping for the query.
*
* @return $this This QueryBuilder instance.
*/
public function resetGroupBy(): self
{
parent::resetGroupBy();
$this->groupBy = [];
$this->sql = null;
return $this;
}
/**
* Resets the HAVING conditions for the query.
*
* @return $this This QueryBuilder instance.
*/
public function resetHaving(): self
{
parent::resetHaving();
$this->having = null;
$this->sql = null;
return $this;
}
/**
* Resets the ordering for the query.
*
* @return $this This QueryBuilder instance.
*/
public function resetOrderBy(): self
{
parent::resetOrderBy();
$this->orderBy = [];
$this->sql = '';
return $this;
}
public function setMaxResults(?int $maxResults): ConcreteQueryBuilder
{
parent::setMaxResults($maxResults);
$this->sql = null;
return $this;
}
public function setFirstResult(int $firstResult): ConcreteQueryBuilder
{
parent::setFirstResult($firstResult);
$this->sql = null;
return $this;
}
public function forUpdate(ConflictResolutionMode $conflictResolutionMode = ConflictResolutionMode::ORDINARY): DoctrineQueryBuilder
{
parent::forUpdate($conflictResolutionMode);
$this->forUpdate = new ForUpdate($conflictResolutionMode);
$this->sql = null;
return $this;
}
public function getSQL(): string
{
if ($this->typo3_with->isEmpty()) {
return parent::getSQL();
}
return $this->sql ??= $this->prependWith(parent::getSQL());
}
//##################################################################################################################
// Below are added methods not originated from Doctrine DBAL QueryBuilder
//##################################################################################################################
/**
* @param string[] $fields
* @param string[] $dependsOn
*
* @internal not part of public API, experimental and may change at any given time.
*/
public function typo3_with(
string $name,
string|QueryBuilder $expression,
array $fields = [],
array $dependsOn = [],
): self {
$this->typo3_with->set(new With($name, $fields, $dependsOn, $expression, false));
return $this;
}
/**
* @param string[] $fields
* @param string[] $dependsOn
*
* @internal not part of public API, experimental and may change at any given time.
*/
public function typo3_addWith(
string $name,
string|QueryBuilder $expression,
array $fields = [],
array $dependsOn = [],
): self {
$this->typo3_with->add(new With($name, $fields, $dependsOn, $expression, false));
return $this;
}
/**
* @param string[] $fields
* @param string[] $dependsOn
*
* @internal not part of public API, experimental and may change at any given time.
*/
public function typo3_withRecursive(
string $name,
bool $uniqueRows,
string|QueryBuilder $initialExpression,
string|QueryBuilder $recursiveExpression,
array $fields = [],
array $dependsOn = [],
): self {
$unionExpression = $this->connection->createQueryBuilder()
->union($initialExpression)
->addUnion($recursiveExpression, $uniqueRows ? UnionType::DISTINCT : UnionType::ALL);
$this->typo3_with->set(new With($name, $fields, $dependsOn, $unionExpression, true));
return $this;
}
/**
* @param string[] $fields
* @param string[] $dependsOn
*
* @internal not part of public API, experimental and may change at any given time.
*/
public function typo3_addWithRecursive(
string $name,
bool $uniqueRows,
string|QueryBuilder $initialExpression,
string|QueryBuilder $recursiveExpression,
array $fields = [],
array $dependsOn = [],
): self {
$unionExpression = $this->connection->createQueryBuilder()
->union($initialExpression)
->addUnion($recursiveExpression, $uniqueRows ? UnionType::DISTINCT : UnionType::ALL);
$this->typo3_with->add(new With($name, $fields, $dependsOn, $unionExpression, true));
return $this;
}
/**
* Determine if a query part used for where or having is empty. Used as array_filter in ConcreteQueryBuilder
* methods. This is needed to avoid invalid sql syntax by empty parts, which can happen to relaxed custom
* CompositeExpression handling.
*
* For example used to avoid : (uid = 1) and () and (pid = 2).
*
* @see ConcreteQueryBuilder::createPredicate()
* @see ConcreteQueryBuilder::appendToPredicate()
* @see \TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression::isEmptyPart()
*
* @param CompositeExpression|string|null $value
* @return bool
*/
protected static function isEmptyPart(CompositeExpression|string|null $value): bool
{
return $value === null
|| ($value instanceof CompositeExpression && $value->count() === 0)
|| trim((string)$value, '() ') === ''
;
}
/**
* @todo Should be handled in {@see AbstractPlatform} class hierarchy directly in doctrine directly if support gets
* accepted or handled internally here to avoid the force to extend and replace platform classes.
*/
private function supportsCommonTableExpressions(): bool
{
$platform = $this->connection->getDatabasePlatform();
return $platform instanceof DoctrineMariaDBPlatform
|| $platform instanceof DoctrineMySQL80Platform
|| $platform instanceof DoctrineSQLitePlatform
|| $platform instanceof DoctrinePostgreSQLPlatform
;
}
private function prependWith(string $sql): string
{
if (!$this->typo3_with->isEmpty() && !$this->supportsCommonTableExpressions()) {
throw new QueryException(
'WITH not supported for current connection.',
1717762530,
);
}
return $this->typo3_with . $sql;
}
}
@@ -0,0 +1,165 @@
<?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\Database\Query\Expression;
use Doctrine\DBAL\Query\Expression\CompositeExpression as DoctrineCompositeExpression;
/**
* Facade of the Doctrine DBAL CompositeExpression to have
* all Query related classes with in TYPO3\CMS namespace.
*/
class CompositeExpression extends DoctrineCompositeExpression
{
/**
* Each expression part of the composite expression.
*
* @var self[]|string[]
*/
private array $parts;
/**
* The instance type of composite expression.
*/
private string $type;
private bool $isOuter;
/**
* @param list<self|DoctrineCompositeExpression|string|null> $parts
* @internal Use factory methods `and()` or `or()` methods instead. Signature will change along with doctrine/dbal 4.
*/
public function __construct(string $type, array $parts = [], bool $isOuter = false)
{
$this->isOuter = $isOuter;
// parent::__construct() call is left out by intention. doctrine/dbal works with private properties, which
// make it otherwise impossible to keep compat method signature and providing the features needed.
$this->type = $type;
if ($parts !== []) {
// doctrine/dbal solved the issue to avoid empty parts by making it mandatory to avoid instantiating this
// class without a part. As we allow this and handle empty parts later on, we apply the empty check here.
// @see https://github.com/doctrine/dbal/issues/2388
$parts = array_filter($parts, static fn(CompositeExpression|DoctrineCompositeExpression|string|null $value): bool => !self::isEmptyPart($value));
}
$this->parts = $parts;
}
/**
* Retrieves the string representation of this composite expression.
* If expression is empty, just return an empty string.
* Native Doctrine expression would return () instead.
*/
public function __toString(): string
{
$count = $this->count();
if ($count === 0) {
return '';
}
if ($count === 1) {
return (string)$this->parts[0];
}
if ($this->isOuter) {
return '(' . implode(') ' . $this->type . ' (', $this->parts) . ')';
}
return '((' . implode(') ' . $this->type . ' (', $this->parts) . '))';
}
/**
* @param self|string|null $part
* @param self|string|null ...$parts
*/
public static function and($part = null, ...$parts): self
{
return (new self(self::TYPE_AND, []))->with($part, ...$parts);
}
/**
* @param self|string|null $part
* @param self|string|null ...$parts
*/
public static function or($part = null, ...$parts): self
{
return (new self(self::TYPE_OR, []))->with($part, ...$parts);
}
/**
* Returns a new CompositeExpression with the given parts added.
*
* @param self|string|null $part
* @param self|string|null ...$parts
*/
public function with($part = null, ...$parts): self
{
$mergedParts = array_merge([$part], $parts);
$mergedParts = array_filter($mergedParts, static fn(CompositeExpression|DoctrineCompositeExpression|string|null $value): bool => !self::isEmptyPart($value));
$that = clone $this;
foreach ($mergedParts as $singlePart) {
$that->parts[] = $singlePart;
}
return $that;
}
/**
* Retrieves the amount of expressions on composite expression.
*/
public function count(): int
{
return count($this->parts);
}
/**
* Returns the type of this composite expression (AND/OR).
*/
public function getType(): string
{
return $this->type;
}
/**
* Determine if a part is considerable empty.
*
* doctrine/dbal solved the issue to avoid empty parts by making it mandatory to avoid instantiating this
* class without a part. As we allow this and handle empty parts later on, we apply the empty check here.
* @see https://github.com/doctrine/dbal/issues/2388
*/
private static function isEmptyPart(CompositeExpression|DoctrineCompositeExpression|string|null $value): bool
{
if ($value === null) {
return true;
}
if (is_string($value)) {
return trim($value, '() ') === '';
}
if ($value instanceof CompositeExpression) {
// TYPO3 implementation filters empty parts on setting and count is reliable in that case.
return $value->parts === [];
}
// We need to use the count method, because the property is private in Doctrine and cannot be checked
// against an empty array like it can be done for the own instance. Using Reflection would negate the
// benefit. That's life.
if ($value->count() === 0) {
// Note that this should not be possible with plain Doctrine DBAL
// composite expression, still lets ensure a fallback here.
return true;
}
// Doctrine DBAL CompositeExpression does not filter empty parts, so we need to build the string to
// evaluate if it is empty or not, which comes with some performance impact hitting only when TYPO3
// extension authors are using the Doctrine Composite Expression instead of the TYPO3 variant.
return trim((string)$value, '() ') === '';
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
<?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\Database\Query;
class NamedParameterNotSupportedForPreparedStatementException extends \InvalidArgumentException
{
public static function new(string $placeholderName): NamedParameterNotSupportedForPreparedStatementException
{
return new self(
sprintf(
"Cannot prepare statement for QueryBuilder because unsupported named placeholder '%s'",
$placeholderName
),
1639249867
);
}
}
File diff suppressed because it is too large Load Diff
+290
View File
@@ -0,0 +1,290 @@
<?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\Database\Query;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Contains misc helper methods to build syntactically valid SQL queries.
* Most helper functions are required to deal with legacy data where the
* format of the input is not strict enough to reliably use the SQL parts
* in queries directly.
*
* @internal
*/
readonly class QueryHelper
{
/**
* Takes an input, possibly prefixed with ORDER BY, and explodes it into
* and array of arrays where each item consists of a fieldName and an order
* direction.
*
* Each of the resulting fieldName/direction pairs can be used passed into
* QueryBuilder::orderBy() so sort a query result set.
*
* @param string $input eg . "ORDER BY title, uid
* @return array|array[] Array of arrays containing fieldName/direction pairs
*/
public static function parseOrderBy(string $input): array
{
if ($input === '') {
return [];
}
$input = preg_replace('/^(?:ORDER[[:space:]]*BY[[:space:]]*)+/i', '', trim($input)) ?: '';
$orderExpressions = GeneralUtility::trimExplode(',', $input, true);
return array_map(
static function (string $expression): array {
$fieldNameOrderArray = GeneralUtility::trimExplode(' ', $expression, true);
$fieldName = $fieldNameOrderArray[0] ?? null;
$order = $fieldNameOrderArray[1] ?? null;
return [$fieldName, $order];
},
$orderExpressions
);
}
/**
* Takes an input, possibly prefixed with FROM, and explodes it into
* and array of arrays where each item consists of a tableName and an
* optional alias name.
*
* Each of the resulting pairs can be used with QueryBuilder::from()
* to select from one or more tables.
*
* @param string $input eg . "FROM aTable, anotherTable AS b, aThirdTable c"
* @return array|array[] Array of arrays containing tableName/alias pairs
*/
public static function parseTableList(string $input): array
{
if ($input === '') {
return [];
}
$input = preg_replace('/^(?:FROM[[:space:]]+)+/i', '', trim($input)) ?: '';
$tableExpressions = GeneralUtility::trimExplode(',', $input, true);
return array_map(
static function (string $expression): array {
[$tableName, $as, $alias] = array_pad(GeneralUtility::trimExplode(' ', $expression, true), 3, null);
if (!empty($as) && strtolower($as) === 'as' && !empty($alias)) {
return [$tableName, $alias];
}
if (!empty($as) && empty($alias)) {
return [$tableName, $as];
}
return [$tableName, null];
},
$tableExpressions
);
}
/**
* Removes the prefix "GROUP BY" from the input string.
*
* This function should be used when you can't guarantee that the string
* that you want to use as a GROUP BY fragment is not prefixed.
*
* @param string $input eg. "GROUP BY title, uid
* @return array|string[] column names to group by
*/
public static function parseGroupBy(string $input): array
{
if ($input === '') {
return [];
}
$input = preg_replace('/^(?:GROUP[[:space:]]*BY[[:space:]]*)+/i', '', trim($input)) ?: '';
return GeneralUtility::trimExplode(',', $input, true);
}
/**
* Split a JOIN SQL fragment into table name, alias and join conditions.
*
* @param string $input eg. "JOIN tableName AS a ON a.uid = anotherTable.uid_foreign"
* @return array assoc array consisting of the keys tableName, tableAlias and joinCondition
*/
public static function parseJoin(string $input): array
{
$input = trim($input);
$quoteCharacter = ' ';
$matchQuotingStartCharacters = [
'`' => '`',
'"' => '"',
'[' => '[]',
];
// Check if the tableName is quoted
$firstCharOfInputValue = $input[0] ?? '';
if ($matchQuotingStartCharacters[$firstCharOfInputValue] ?? false) {
$quoteCharacter .= $matchQuotingStartCharacters[$firstCharOfInputValue];
$input = substr($input, 1);
$tableName = strtok($input, $quoteCharacter);
} else {
$tableName = strtok($input, $quoteCharacter);
}
$tableAlias = (string)strtok($quoteCharacter);
if (strtolower($tableAlias) === 'as') {
$tableAlias = (string)strtok($quoteCharacter);
// Skip the next token which must be ON
strtok(' ');
$joinCondition = strtok('');
} elseif (strtolower($tableAlias) === 'on') {
$tableAlias = null;
$joinCondition = strtok('');
} else {
// Skip the next token which must be ON
strtok(' ');
$joinCondition = strtok('');
}
// Catch the edge case that the table name is unquoted and the
// table alias is actually quoted. This will not work in the case
// that the quoted table alias contains whitespace.
$firstCharacterOfTableAlias = $tableAlias[0] ?? '';
if ($matchQuotingStartCharacters[$firstCharacterOfTableAlias] ?? false) {
$tableAlias = substr((string)$tableAlias, 1, -1);
}
$tableAlias = $tableAlias ?: $tableName;
return ['tableName' => $tableName, 'tableAlias' => $tableAlias, 'joinCondition' => $joinCondition];
}
/**
* Removes the prefixes AND/OR from the input string.
*
* This function should be used when you can't guarantee that the string
* that you want to use as a WHERE fragment is not prefixed.
*
* @param string $constraint The where part fragment with a possible leading AND or OR operator
* @return string The modified where part without leading operator
*/
public static function stripLogicalOperatorPrefix(string $constraint): string
{
return preg_replace('/^(?:(AND|OR)[[:space:]]*)+/i', '', trim($constraint)) ?: '';
}
/**
* Returns the date and time formats compatible with the given database.
* This simple method should probably be deprecated and removed later.
*/
public static function getDateTimeFormats(): array
{
return [
'date' => [
'empty' => '0000-00-00',
'format' => 'Y-m-d',
],
'datetime' => [
'empty' => '0000-00-00 00:00:00',
'format' => 'Y-m-d H:i:s',
],
'time' => [
'empty' => '00:00:00',
'format' => 'H:i:s',
],
];
}
/**
* Returns the date and time types compatible with the given database.
* This simple method should probably be deprecated and removed later.
*/
public static function getDateTimeTypes(): array
{
return [
'date',
'datetime',
'time',
];
}
public static function transformDateTimeToDatabaseValue(
?\DateTimeInterface $datetime,
bool $isNullable,
string $format,
?string $persistenceType,
): int|string|null {
if ($datetime === null) {
if ($isNullable) {
return null;
}
if ($persistenceType === null) {
return 0;
}
return self::getDateTimeFormats()[$persistenceType]['empty'] ?? null;
}
if (!$datetime instanceof \DateTimeImmutable) {
$datetime = \DateTimeImmutable::createFromInterface($datetime);
}
// Apply format-specific normalizations
if ($format === 'time') {
// time(sec) is stored as elapsed seconds in DB, hence we base the time on 1970-01-01
$datetime = $datetime->setDate(1970, 01, 01)->setTime((int)$datetime->format('H'), (int)$datetime->format('i'), 0);
} elseif ($format === 'timesec' || $persistenceType === 'time') {
$datetime = $datetime->setDate(1970, 01, 01);
} elseif ($format === 'date' || $persistenceType === 'date') {
$datetime = $datetime->setTime(0, 0, 0);
}
// datetimesec is a "normal" date and needs no removal/adjustment of seconds or date.
// Native DATETIME, DATE or TIME field
if (in_array($persistenceType, self::getDateTimeTypes(), true)) {
$dateTimeFormats = self::getDateTimeFormats();
$persistenceFormat = $dateTimeFormats[$persistenceType]['format'];
if ($persistenceType === 'datetime') {
// native DATETIME values are stored in server LOCALTIME. Force conversion to the servers current timezone.
$datetime = $datetime->setTimezone(new \DateTimeZone(date_default_timezone_get()));
}
return $datetime->format($persistenceFormat);
}
// Time is stored in seconds for integer fields
if ($format === 'timesec' || $format === 'time') {
return (int)$datetime->format('H') * 3600 + (int)$datetime->format('i') * 60 + (int)$datetime->format('s');
}
// Encode as unix timestamp (int) if no native field is used
return $datetime->getTimestamp();
}
/**
* Quote database table/column names indicated by {#identifier} markup in a SQL fragment string.
* This is an intermediate step to make SQL fragments in Typoscript and TCA database agnostic.
*/
public static function quoteDatabaseIdentifiers(Connection $connection, string $sql): string
{
if (str_contains($sql, '{#')) {
$sql = preg_replace_callback(
'/{#(?P<identifier>[^}]+)}/',
static function (array $matches) use ($connection) {
return $connection->quoteIdentifier($matches['identifier']);
},
$sql
);
}
return $sql;
}
}
@@ -0,0 +1,111 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Base class for query restriction collections
*/
abstract class AbstractRestrictionContainer implements QueryRestrictionContainerInterface
{
/**
* @var QueryRestrictionInterface[]
*/
protected $restrictions = [];
/**
* @var QueryRestrictionInterface[]
*/
protected $enforcedRestrictions = [];
/**
* Main method to build expressions for given tables.
* Iterating over all registered expressions and combine them with AND
*
* @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
{
$constraints = [];
foreach ($this->restrictions as $restriction) {
$constraints[] = $restriction->buildExpression($queriedTables, $expressionBuilder);
}
return $expressionBuilder->and(...$constraints);
}
/**
* Removes all restrictions stored within this container
*/
public function removeAll(): QueryRestrictionContainerInterface
{
$this->restrictions = $this->enforcedRestrictions;
return $this;
}
/**
* Remove restriction of a given type
*
* @param string $restrictionType Class name of the restriction to be removed
*/
public function removeByType(string $restrictionType): QueryRestrictionContainerInterface
{
foreach ($this->restrictions as $type => $instance) {
if ($instance instanceof $restrictionType) {
unset($this->restrictions[$type]);
break;
}
}
foreach ($this->enforcedRestrictions as $type => $instance) {
if ($instance instanceof $restrictionType) {
unset($this->enforcedRestrictions[$type]);
break;
}
}
return $this;
}
/**
* Add a new restriction instance to this collection
*/
public function add(QueryRestrictionInterface $restriction): QueryRestrictionContainerInterface
{
$this->restrictions[get_class($restriction)] = $restriction;
if ($restriction instanceof EnforceableQueryRestrictionInterface && $restriction->isEnforced()) {
$this->enforcedRestrictions[get_class($restriction)] = $restriction;
}
return $this;
}
/**
* Factory method for restrictions.
* Currently only instantiates the class.
*
* @param string $restrictionClass
*/
protected function createRestriction($restrictionClass): QueryRestrictionInterface
{
return GeneralUtility::makeInstance($restrictionClass);
}
}
@@ -0,0 +1,46 @@
<?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\Database\Query\Restriction;
/**
* This is the container with restrictions, that are added to any doctrine query
*/
class DefaultRestrictionContainer extends AbstractRestrictionContainer
{
/**
* Default restriction classes.
*
* @var string[]
*/
protected $defaultRestrictionTypes = [
DeletedRestriction::class,
HiddenRestriction::class,
StartTimeRestriction::class,
EndTimeRestriction::class,
];
/**
* Creates instances of the registered default restriction classes
*/
public function __construct()
{
foreach ($this->defaultRestrictionTypes as $restrictionType) {
$this->add($this->createRestriction($restrictionType));
}
}
}
@@ -0,0 +1,57 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
/**
* Restriction to respect the soft-delete functionality of TYPO3.
* Filters out records, that were marked as deleted.
*/
class DeletedRestriction implements QueryRestrictionInterface
{
/**
* Main method to build expressions for given tables
* Evaluates the ctrl/delete flag of the table and adds the according restriction if set
*
* @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
{
$tcaSchemaFactory = $expressionBuilder->getContainer()->get(TcaSchemaFactory::class);
$constraints = [];
foreach ($queriedTables as $tableAlias => $tableName) {
if (!$tcaSchemaFactory->has($tableName)) {
continue;
}
$schema = $tcaSchemaFactory->get($tableName);
if ($schema->hasCapability(TcaSchemaCapability::SoftDelete)) {
$constraints[] = $expressionBuilder->eq(
$tableAlias . '.' . $schema->getCapability(TcaSchemaCapability::SoftDelete)->getFieldName(),
0
);
}
}
return $expressionBuilder->and(...$constraints);
}
}
@@ -0,0 +1,66 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
/**
* Restriction to make queries for pages doktype-aware.
*/
class DocumentTypeExclusionRestriction implements QueryRestrictionInterface
{
/**
* @var int[]
*/
protected $doktypes;
/**
* @param int[]|int $doktype
*/
public function __construct($doktype)
{
if (is_array($doktype)) {
$this->doktypes = $doktype;
} else {
$this->doktypes = [$doktype];
}
}
/**
* Main method to build expressions for given tables
*
* @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
{
$constraints = [];
foreach ($queriedTables as $tableAlias => $tableName) {
if ($tableName !== 'pages') {
continue;
}
$constraints[] = $expressionBuilder->notIn($tableAlias . '.doktype', $this->doktypes);
}
return $expressionBuilder->and(...$constraints);
}
}
@@ -0,0 +1,74 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
/**
* Restriction to filter records with an end time set that has passed
*/
class EndTimeRestriction implements QueryRestrictionInterface
{
/**
* @var int
*/
protected $accessTimeStamp;
public function __construct(?int $accessTimeStamp = null)
{
$this->accessTimeStamp = $accessTimeStamp ?: ($GLOBALS['SIM_ACCESS_TIME'] ?? null);
}
/**
* Main method to build expressions for given tables
* Evaluates the ctrl/enablecolumns/endtime flag of the table and adds the according restriction if set
*
* @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)
* @throws \RuntimeException
*/
public function buildExpression(array $queriedTables, ExpressionBuilder $expressionBuilder): CompositeExpression
{
$tcaSchemaFactory = $expressionBuilder->getContainer()->get(TcaSchemaFactory::class);
$constraints = [];
foreach ($queriedTables as $tableAlias => $tableName) {
if (!$tcaSchemaFactory->has($tableName)) {
continue;
}
$schema = $tcaSchemaFactory->get($tableName);
if ($schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) {
if (empty($this->accessTimeStamp)) {
throw new \RuntimeException(
'accessTimeStamp needs to be set to an integer value, but is empty! Maybe $GLOBALS[\'SIM_ACCESS_TIME\'] has been overridden somewhere?',
1462821084
);
}
$fieldName = $tableAlias . '.' . $schema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName();
$constraints[] = $expressionBuilder->or(
$expressionBuilder->eq($fieldName, 0),
$expressionBuilder->gt($fieldName, (int)$this->accessTimeStamp)
);
}
}
return $expressionBuilder->and(...$constraints);
}
}
@@ -0,0 +1,33 @@
<?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\Database\Query\Restriction;
/**
* Can be added to QueryRestrictionInterface implementations.
* Restrictions implementing this interface will not be removed when removeAll()
* is called on the container and isEnforced() returns true.
* It can be removed though, when explicitly calling removeByType()
*/
interface EnforceableQueryRestrictionInterface
{
/**
* When returning false, restriction will be removed when removeAll()
* is called on the container
*/
public function isEnforced(): bool;
}
@@ -0,0 +1,85 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Restriction to filter records, which are limited to the given user groups
*/
class FrontendGroupRestriction implements QueryRestrictionInterface
{
protected array $frontendGroupIds;
/**
* @param array|null $frontendGroupIds Normalized array with user groups of currently logged in user (typically found in the Frontend Context)
*/
public function __construct(?array $frontendGroupIds = null)
{
if ($frontendGroupIds !== null) {
$this->frontendGroupIds = $frontendGroupIds;
} else {
/** @var UserAspect $frontendUserAspect */
$frontendUserAspect = GeneralUtility::makeInstance(Context::class)->getAspect('frontend.user');
$this->frontendGroupIds = $frontendUserAspect->getGroupIds();
}
}
/**
* Main method to build expressions for given tables
* Evaluates the ctrl/enablecolumns/fe_group flag of the table and adds the according restriction if set
*
* @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
{
$tcaSchemaFactory = $expressionBuilder->getContainer()->get(TcaSchemaFactory::class);
$constraints = [];
foreach ($queriedTables as $tableAlias => $tableName) {
if (!$tcaSchemaFactory->has($tableName)) {
continue;
}
$schema = $tcaSchemaFactory->get($tableName);
if ($schema->hasCapability(TcaSchemaCapability::RestrictionUserGroup)) {
$fieldName = $tableAlias . '.' . $schema->getCapability(TcaSchemaCapability::RestrictionUserGroup)->getFieldName();
// Allow records where no group access has been configured (field values NULL, 0 or empty string)
$tableConstraints = [
$expressionBuilder->isNull($fieldName),
$expressionBuilder->eq($fieldName, $expressionBuilder->literal('')),
$expressionBuilder->eq($fieldName, $expressionBuilder->literal('0')),
];
foreach ($this->frontendGroupIds as $frontendGroupId) {
$tableConstraints[] = $expressionBuilder->inSet(
$fieldName,
$expressionBuilder->literal((string)($frontendGroupId ?? ''))
);
}
$constraints[] = $expressionBuilder->or(...$tableConstraints);
}
}
return $expressionBuilder->and(...$constraints);
}
}
@@ -0,0 +1,104 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* A collection of restrictions to be used in frontend context.
* This is a replacement for PageRepository::enableFields()
*/
class FrontendRestrictionContainer extends AbstractRestrictionContainer
{
/**
* @var string[]
*/
protected array $defaultRestrictionTypes = [
DeletedRestriction::class,
WorkspaceRestriction::class,
HiddenRestriction::class,
StartTimeRestriction::class,
EndTimeRestriction::class,
FrontendGroupRestriction::class,
];
protected Context $context;
/**
* FrontendRestrictionContainer constructor.
* Initializes the default restrictions for frontend requests
*/
public function __construct(?Context $context = null)
{
$this->context = $context ?? GeneralUtility::makeInstance(Context::class);
foreach ($this->defaultRestrictionTypes as $restrictionType) {
$this->add($this->createRestriction($restrictionType));
}
}
/**
* Main method to build expressions for given tables
* Iterates over all registered restrictions and removes the hidden restriction if preview is requested
*
* @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
{
$constraints = [];
foreach ($this->restrictions as $restriction) {
foreach ($queriedTables as $tableAlias => $tableName) {
$disableRestriction = false;
if ($restriction instanceof HiddenRestriction || $restriction instanceof StartTimeRestriction || $restriction instanceof EndTimeRestriction) {
$visibilityAspect = $this->context->getAspect('visibility');
if ($restriction instanceof HiddenRestriction) {
// If display of hidden records is requested, we must disable the hidden restriction.
if ($tableName === 'pages') {
$disableRestriction = $visibilityAspect->includeHiddenPages();
} else {
$disableRestriction = $visibilityAspect->includeHiddenContent();
}
}
if ($restriction instanceof StartTimeRestriction || $restriction instanceof EndTimeRestriction) {
$disableRestriction = $visibilityAspect->includeScheduledRecords();
}
}
if (!$disableRestriction) {
$constraints[] = $restriction->buildExpression([$tableAlias => $tableName], $expressionBuilder);
}
}
}
return $expressionBuilder->and(...$constraints);
}
protected function createRestriction($restrictionClass): QueryRestrictionInterface
{
if ($restrictionClass === WorkspaceRestriction::class) {
return GeneralUtility::makeInstance($restrictionClass, (int)$this->context->getPropertyFromAspect('workspace', 'id', 0));
}
if ($restrictionClass === FrontendGroupRestriction::class) {
return GeneralUtility::makeInstance($restrictionClass, $this->context->getPropertyFromAspect('frontend.user', 'groupIds', []));
}
return parent::createRestriction($restrictionClass);
}
}
@@ -0,0 +1,56 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
/**
* Restriction to filter records that have been marked as hidden
*/
class HiddenRestriction implements QueryRestrictionInterface
{
/**
* Main method to build expressions for given tables
* Evaluates the ctrl/enablecolumns/disabled flag of the table and adds the according restriction if set
*
* @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
{
$tcaSchemaFactory = $expressionBuilder->getContainer()->get(TcaSchemaFactory::class);
$constraints = [];
foreach ($queriedTables as $tableAlias => $tableName) {
if (!$tcaSchemaFactory->has($tableName)) {
continue;
}
$schema = $tcaSchemaFactory->get($tableName);
if ($schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
$constraints[] = $expressionBuilder->eq(
$tableAlias . '.' . $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName(),
0
);
}
}
return $expressionBuilder->and(...$constraints);
}
}
@@ -0,0 +1,113 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
/**
* Restriction container that applies added restrictions only to the given table aliases.
* Enforced restrictions are treated equally to all other restrictions.
*/
class LimitToTablesRestrictionContainer implements QueryRestrictionContainerInterface
{
/**
* @var QueryRestrictionInterface[]
*/
private $restrictions = [];
/**
* @var QueryRestrictionContainerInterface[]
*/
private $restrictionContainer = [];
/**
* @var array
*/
private $applicableTableAliases;
public function removeAll(): QueryRestrictionContainerInterface
{
$this->applicableTableAliases = $this->restrictions = $this->restrictionContainer = [];
return $this;
}
public function removeByType(string $restrictionType): QueryRestrictionContainerInterface
{
unset($this->applicableTableAliases[$restrictionType], $this->restrictions[$restrictionType]);
foreach ($this->restrictionContainer as $restrictionContainer) {
$restrictionContainer->removeByType($restrictionType);
}
return $this;
}
public function add(QueryRestrictionInterface $restriction): QueryRestrictionContainerInterface
{
$this->restrictions[get_class($restriction)] = $restriction;
if ($restriction instanceof QueryRestrictionContainerInterface) {
$this->restrictionContainer[get_class($restriction)] = $restriction;
}
return $this;
}
/**
* Adds the restriction, but also remembers which table aliases it should be applied to
*
* @param array $tableAliases flat array of table aliases, not table names
*/
public function addForTables(QueryRestrictionInterface $restriction, array $tableAliases): QueryRestrictionContainerInterface
{
$this->applicableTableAliases[get_class($restriction)] = $tableAliases;
return $this->add($restriction);
}
/**
* Main method to build expressions for given tables, but respecting configured filters.
*
* @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
{
$constraints = [];
foreach ($this->restrictions as $name => $restriction) {
$constraints[] = $restriction->buildExpression(
$this->filterApplicableTableAliases($queriedTables, $name),
$expressionBuilder
);
}
return $expressionBuilder->and(...$constraints);
}
private function filterApplicableTableAliases(array $queriedTables, string $name): array
{
if (!isset($this->applicableTableAliases[$name])) {
return $queriedTables;
}
$filteredTables = [];
foreach ($this->applicableTableAliases[$name] as $tableAlias) {
if (isset($queriedTables[$tableAlias])) {
$filteredTables[$tableAlias] = $queriedTables[$tableAlias];
}
}
return $filteredTables;
}
}
@@ -0,0 +1,53 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
/**
* Restriction to filter records that only reside on a specific list of page IDs
*/
final readonly class PageIdListRestriction implements QueryRestrictionInterface
{
public function __construct(
private array $tableNames,
private array $pageIds
) {}
/**
* Main method to build expressions for given tables
*
* @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
{
$constraints = [];
foreach ($queriedTables as $tableAlias => $tableName) {
if (empty($this->tableNames) || in_array($tableAlias, $this->tableNames, true)) {
$constraints[] = $expressionBuilder->in(
$tableAlias . '.pid',
array_map(intval(...), $this->pageIds)
);
}
}
return $expressionBuilder->and(...$constraints);
}
}
@@ -0,0 +1,132 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
/**
* Restriction to make queries respect backend user rights for pages.
*
* Adds a WHERE-clause for the pages-table where user permissions according to input argument, $permissions, is validated.
* $permissions is the "mask" used to select - see Permission Bitset.
* E.g. if $perms is 1 then you'll get all pages that a user can actually see!
* 2^0 = show (1)
* 2^1 = edit (2)
* 2^2 = delete (4)
* 2^3 = new (8)
* If the user is 'admin' no validation is used.
*
* If the user is not set at all (->user is not an array), then "AND 1=0" is returned (will cause no selection results at all)
*
* The 95% use of this function is "->getPagePermsClause(1)" which will
* return WHERE clauses for *selecting* pages in backend listings - in other words this will check read permissions.
*/
class PagePermissionRestriction implements QueryRestrictionInterface
{
/**
* @var int
*/
protected $permissions;
/**
* @var UserAspect
*/
protected $userAspect;
public function __construct(UserAspect $userAspect, int $permissions)
{
$this->permissions = $permissions;
$this->userAspect = $userAspect;
}
/**
* Main method to build expressions for given tables
*
* @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
{
$constraints = [];
foreach ($queriedTables as $tableAlias => $tableName) {
if ($tableName !== 'pages') {
continue;
}
$constraint = $this->buildUserConstraints($expressionBuilder, $tableAlias);
if ($constraint) {
$constraints[] = $expressionBuilder->and($constraint);
}
}
return $expressionBuilder->and(...$constraints);
}
/**
* @return string|CompositeExpression|null
* @throws \TYPO3\CMS\Core\Context\Exception\AspectPropertyNotFoundException
*/
protected function buildUserConstraints(ExpressionBuilder $expressionBuilder, string $tableAlias)
{
if (!$this->userAspect->isLoggedIn()) {
return $expressionBuilder->comparison(1, ExpressionBuilder::EQ, 0);
}
if ($this->userAspect->isAdmin()) {
return null;
}
// User permissions
$constraint = $expressionBuilder->or(
$expressionBuilder->comparison(
$expressionBuilder->bitAnd($tableAlias . '.perms_everybody', $this->permissions),
ExpressionBuilder::EQ,
$this->permissions
),
$expressionBuilder->and(
$expressionBuilder->eq($tableAlias . '.perms_userid', $this->userAspect->get('id')),
$expressionBuilder->comparison(
$expressionBuilder->bitAnd($tableAlias . '.perms_user', $this->permissions),
ExpressionBuilder::EQ,
$this->permissions
)
)
);
// User groups (if any are set)
$groupIds = array_map(intval(...), $this->userAspect->getGroupIds());
if (!empty($groupIds)) {
$constraint = $constraint->with(
$expressionBuilder->and(
$expressionBuilder->in(
$tableAlias . '.perms_groupid',
$groupIds
),
$expressionBuilder->comparison(
$expressionBuilder->bitAnd($tableAlias . '.perms_group', $this->permissions),
ExpressionBuilder::EQ,
$this->permissions
)
)
);
}
return $constraint;
}
}
@@ -0,0 +1,47 @@
<?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\Database\Query\Restriction;
/**
* Interface that all restriction collections must implement.
* It is an extension of the QueryRestrictionInterface, so collections can be treated as single restriction
*/
interface QueryRestrictionContainerInterface extends QueryRestrictionInterface
{
/**
* Removes all restrictions stored within this container
*
* @return QueryRestrictionContainerInterface
*/
public function removeAll();
/**
* Remove restriction of a given type
*
* @param string $restrictionType Class name of the restriction to be removed
* @return QueryRestrictionContainerInterface
*/
public function removeByType(string $restrictionType);
/**
* Add a new restriction instance to this collection
*
* @return QueryRestrictionContainerInterface
*/
public function add(QueryRestrictionInterface $restriction);
}
@@ -0,0 +1,36 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
/**
* The main restriction interface. All restrictions (including the collections) must implement this.
*/
interface QueryRestrictionInterface
{
/**
* Main method to build expressions for given tables
*
* @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;
}
@@ -0,0 +1,58 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
/**
* Restriction to filter records which are not stored on the root page.
*/
class RootLevelRestriction implements QueryRestrictionInterface
{
/**
* @var array
*/
protected $tableNames;
public function __construct(array $tableNames = [])
{
$this->tableNames = $tableNames;
}
/**
* Main method to build expressions for given tables
*
* @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
{
$constraints = [];
foreach ($queriedTables as $tableAlias => $tableName) {
if (empty($this->tableNames) || in_array($tableAlias, $this->tableNames, true)) {
$constraints[] = $expressionBuilder->eq(
$tableAlias . '.pid',
0
);
}
}
return $expressionBuilder->and(...$constraints);
}
}
@@ -0,0 +1,73 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
/**
* Restriction to filter records, that should not be shown until the start time has been reached
*/
class StartTimeRestriction implements QueryRestrictionInterface
{
/**
* @var int
*/
protected $accessTimeStamp;
public function __construct(?int $accessTimeStamp = null)
{
$this->accessTimeStamp = $accessTimeStamp ?: ($GLOBALS['SIM_ACCESS_TIME'] ?? null);
}
/**
* Main method to build expressions for given tables
* Evaluates the ctrl/enablecolumns/starttime flag of the table and adds the according restriction if set
*
* @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)
* @throws \RuntimeException
*/
public function buildExpression(array $queriedTables, ExpressionBuilder $expressionBuilder): CompositeExpression
{
$tcaSchemaFactory = $expressionBuilder->getContainer()->get(TcaSchemaFactory::class);
$constraints = [];
foreach ($queriedTables as $tableAlias => $tableName) {
if (!$tcaSchemaFactory->has($tableName)) {
continue;
}
$schema = $tcaSchemaFactory->get($tableName);
if ($schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) {
if (empty($this->accessTimeStamp)) {
throw new \RuntimeException(
'accessTimeStamp needs to be set to an integer value, but is empty! Maybe $GLOBALS[\'SIM_ACCESS_TIME\'] has been overridden somewhere?',
1462820645
);
}
$constraints[] = $expressionBuilder->lte(
$tableAlias . '.' . $schema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName(),
(int)$this->accessTimeStamp
);
}
}
return $expressionBuilder->and(...$constraints);
}
}
@@ -0,0 +1,113 @@
<?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\Database\Query\Restriction;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Restriction to make queries workspace-aware. This restriction ALWAYS fetches the live version
* plus in current workspace the workspace records.
* It does not care about the state, as this should be done by overlays.
*
* As workspaces cannot be fully overlaid within ONE query, this query does the following:
* - In live context, only fetch published records
* - In a workspace, fetch all LIVE records and all workspace records which do not have "1" (= all new placeholders get fetched as well)
*
* This means, that all records which are fetched need to run through either
* - BackendUtility::getRecordWSOL() (when having one or a few records)
* - PageRepository->versionOL()
* - PlainDataResolver (when having lots of records)
*/
class WorkspaceRestriction implements QueryRestrictionInterface
{
protected int $workspaceId;
/**
* Used to also query records within a workspace, which is useful for DB queries
* that check for a specific field (e.g. "slug") which might have changed within a workspace.
* Please note that some duplicates might be shown and the "reduce" logic needs to be
* added after querying. Setting this flag might also be a problem when using the DB query
* with limit and offset settings.
*/
protected bool $includeAllVersionedRecords;
public function __construct(int $workspaceId = 0, bool $includeAllVersionedRecords = false)
{
$this->workspaceId = $workspaceId;
$this->includeAllVersionedRecords = $includeAllVersionedRecords;
}
/**
* Main method to build expressions for given tables
*
* @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
{
$tcaSchemaFactory = $expressionBuilder->getContainer()->get(TcaSchemaFactory::class);
$constraints = [];
foreach ($queriedTables as $tableAlias => $tableName) {
if (!$tcaSchemaFactory->has($tableName) || !$tcaSchemaFactory->get($tableName)->isWorkspaceAware()) {
continue;
}
if ($this->workspaceId === 0) {
// Only include records from live workspace
$workspaceIdExpression = $expressionBuilder->eq($tableAlias . '.t3ver_wsid', 0);
} else {
// Include live records PLUS records from the given workspace
$workspaceIdExpression = $expressionBuilder->in(
$tableAlias . '.t3ver_wsid',
[0, $this->workspaceId]
);
}
// Always filter out versioned records that have an "offline" record
// But include moved records AND newly created records (t3ver_oid=0)
if ($this->includeAllVersionedRecords === false) {
$constraints[] = $expressionBuilder->and(
$workspaceIdExpression,
$expressionBuilder->or(
$expressionBuilder->eq(
$tableAlias . '.t3ver_oid',
0
),
$expressionBuilder->eq(
$tableAlias . '.t3ver_state',
VersionState::MOVE_POINTER->value
)
)
);
} else {
// Include live records plus records from the given workspace
// but never include versioned records marked as deleted
$constraints[] = $expressionBuilder->and(
$workspaceIdExpression,
$expressionBuilder->neq(
$tableAlias . '.t3ver_state',
VersionState::DELETE_PLACEHOLDER->value
)
);
}
}
return $expressionBuilder->and(...$constraints);
}
}
@@ -0,0 +1,32 @@
<?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\Database\Query;
class UnsupportedPreparedStatementParameterTypeException extends \InvalidArgumentException
{
public static function new(string $parameterType): UnsupportedPreparedStatementParameterTypeException
{
return new self(
sprintf(
"Parameter type '%s' is not allowed for prepared statement retrieved from QueryBuilder. Use executeQuery() or executeStatement() directly.",
$parameterType
),
1639245170
);
}
}
+70
View File
@@ -0,0 +1,70 @@
<?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\Database\Query;
final readonly class With implements \Stringable
{
/**
* @param string[] $fields
* @param string[] $dependencies
*/
public function __construct(
private string $name,
private array $fields,
private array $dependencies,
private string|ConcreteQueryBuilder|QueryBuilder $expression,
private bool $recursive,
) {}
public function getName(): string
{
return $this->name;
}
public function isRecursive(): bool
{
return $this->recursive;
}
/** @return string[] */
public function getDependencies(): array
{
return $this->dependencies;
}
public function getSQL(): string
{
$fields = '';
if ($this->fields !== []) {
$fields = sprintf(' (%s)', implode(', ', $this->fields));
}
return sprintf(
'%s%s AS (%s)',
$this->getName(),
$fields,
$this->expression,
);
}
public function __toString(): string
{
return $this->getSQL();
}
}
+104
View File
@@ -0,0 +1,104 @@
<?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\Database\Query;
use TYPO3\CMS\Core\Service\DependencyOrderingService;
/**
* @internal experimental and not part of public core API.
*/
final class WithCollection implements \Stringable
{
/**
* @var With[]
*/
private array $with = [];
private bool $recursive = false;
public function set(With ...$with): WithCollection
{
return $this->reset()->add(...array_values($with));
}
public function add(With ...$with): WithCollection
{
foreach ($with as $singleWith) {
$this->with[] = $singleWith;
if ($singleWith->isRecursive()) {
$this->recursive = true;
}
}
return $this;
}
public function reset(): WithCollection
{
$this->recursive = false;
$this->with = [];
return $this;
}
public function isEmpty(): bool
{
return $this->with === [];
}
public function __toString(): string
{
if ($this->with === []) {
return '';
}
$parts = [];
foreach ($this->getSortedParts() as $part) {
$parts[] = (string)$part;
}
return sprintf(
'%s %s',
($this->recursive ? 'WITH RECURSIVE' : 'WITH'),
implode(', ', $parts)
);
}
/**
* @return With[]
*/
private function getSortedParts(): array
{
$parts = [];
foreach ($this->prepareParts() as $part) {
$parts[] = $part['instance'];
}
return $parts;
}
/**
* @return array<string, array{instance: With, before: string[], after: string[]}>
*/
private function prepareParts(): array
{
$parts = [];
foreach ($this->with as $part) {
$parts[$part->getName()] = [
'instance' => $part,
'before' => [],
'after' => $part->getDependencies(),
];
}
return (new DependencyOrderingService())->orderByDependencies($parts);
}
}