TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
<?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\Domain\Access;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
|
||||
/**
|
||||
* Event to modify records to be checked against "enableFields".
|
||||
* Listeners are able to grant access or to modify the record itself to
|
||||
* continue to use the native access check functionality with a modified dataset.
|
||||
*/
|
||||
final class RecordAccessGrantedEvent implements StoppableEventInterface
|
||||
{
|
||||
private ?bool $accessGranted = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $tableName,
|
||||
private array $record,
|
||||
private readonly Context $context
|
||||
) {}
|
||||
|
||||
public function isPropagationStopped(): bool
|
||||
{
|
||||
return $this->accessGranted !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function accessGranted(): bool
|
||||
{
|
||||
if ($this->accessGranted === null) {
|
||||
throw new \RuntimeException('Access was not yet defined.', 1645506529);
|
||||
}
|
||||
|
||||
return $this->accessGranted;
|
||||
}
|
||||
|
||||
public function setAccessGranted(bool $accessGranted): void
|
||||
{
|
||||
$this->accessGranted = $accessGranted;
|
||||
}
|
||||
|
||||
public function getTable(): string
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
public function getRecord(): array
|
||||
{
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
public function updateRecord(array $record): void
|
||||
{
|
||||
$this->record = $record;
|
||||
}
|
||||
|
||||
public function getContext(): Context
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
}
|
||||
@@ -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\Domain\Access;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
|
||||
/**
|
||||
* Checks if a record can be accessed (usually in TYPO3 Frontend) due to various "enableFields" or group access checks.
|
||||
*
|
||||
* Not related to "write permissions" etc.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class RecordAccessVoter
|
||||
{
|
||||
public function __construct(
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected TcaSchemaFactory $tcaSchemaFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Checks page record for enableFields
|
||||
* Returns TRUE if enableFields does not disable the page record.
|
||||
* Takes notice of the includeHiddenPages visibility aspect flag and uses SIM_ACCESS_TIME for start/endtime evaluation
|
||||
*
|
||||
* @param string $table the TCA table to check for
|
||||
* @param array $record The record to evaluate (needs fields: hidden, starttime, endtime, fe_group)
|
||||
* @param Context $context Context API to check against
|
||||
* @return bool TRUE, if record is viewable.
|
||||
*/
|
||||
public function accessGranted(string $table, array $record, Context $context): bool
|
||||
{
|
||||
$event = new RecordAccessGrantedEvent($table, $record, $context);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
if ($event->isPropagationStopped()) {
|
||||
return $event->accessGranted();
|
||||
}
|
||||
$record = $event->getRecord();
|
||||
|
||||
$schema = $this->tcaSchemaFactory->get($table);
|
||||
$visibilityAspect = $context->getAspect('visibility');
|
||||
$includeHidden = $table === 'pages'
|
||||
? $visibilityAspect->includeHiddenPages()
|
||||
: $visibilityAspect->includeHiddenContent();
|
||||
|
||||
// Hidden field is active and hidden records should not be included
|
||||
if ($schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
|
||||
$fieldName = $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
|
||||
if (($record[$fieldName] ?? false) && !$includeHidden) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Records' starttime set AND is HIGHER than the current access time
|
||||
if ($schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) {
|
||||
$fieldName = $schema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName();
|
||||
if (isset($record[$fieldName])
|
||||
&& (int)$record[$fieldName] > $GLOBALS['SIM_ACCESS_TIME']
|
||||
&& !$visibilityAspect->includeScheduledRecords()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Records' endtime is set AND NOT "0" AND LOWER than the current access time
|
||||
if ($schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) {
|
||||
$fieldName = $schema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName();
|
||||
if (isset($record[$fieldName])
|
||||
&& ((int)$record[$fieldName] !== 0)
|
||||
&& ((int)$record[$fieldName] < $GLOBALS['SIM_ACCESS_TIME'])
|
||||
&& !$visibilityAspect->includeScheduledRecords()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Insufficient group access
|
||||
if ($this->groupAccessGranted($table, $record, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
// Record is available
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check group access against a record, if the current users' groups match the fe_group values of the record.
|
||||
*
|
||||
* @param string $table the TCA table to check for
|
||||
* @param array $record The record to evaluate (needs enableField: fe_group)
|
||||
* @param Context $context Context API to check against
|
||||
* @return bool TRUE, if group access is granted.
|
||||
*/
|
||||
public function groupAccessGranted(string $table, array $record, Context $context): bool
|
||||
{
|
||||
if (!$this->tcaSchemaFactory->has($table)) {
|
||||
return true;
|
||||
}
|
||||
$schema = $this->tcaSchemaFactory->get($table);
|
||||
// No fe_group field in TCA, so no group access check
|
||||
if (!$schema->hasCapability(TcaSchemaCapability::RestrictionUserGroup)) {
|
||||
return true;
|
||||
}
|
||||
$fieldName = $schema->getCapability(TcaSchemaCapability::RestrictionUserGroup)->getFieldName();
|
||||
// Field not given, so no group access check
|
||||
if (!($record[$fieldName] ?? false)) {
|
||||
return true;
|
||||
}
|
||||
// No frontend user, but 'fe_group' is not empty, so shut this down.
|
||||
if (!$context->hasAspect('frontend.user')) {
|
||||
return false;
|
||||
}
|
||||
$pageGroupList = explode(',', (string)$record[$fieldName]);
|
||||
return count(array_intersect($context->getAspect('frontend.user')->getGroupIds(), $pageGroupList)) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current page of the root line is visible.
|
||||
*
|
||||
* If the field extendToSubpages is 0, access is granted,
|
||||
* else the fields hidden, starttime, endtime, fe_group are evaluated.
|
||||
*
|
||||
* @internal this is a special use case and should only be used with care, not part of TYPO3's Public API.
|
||||
*/
|
||||
public function accessGrantedForPageInRootLine(array $pageRecord, Context $context): bool
|
||||
{
|
||||
return !($pageRecord['extendToSubpages'] ?? false) || $this->accessGranted('pages', $pageRecord, $context);
|
||||
}
|
||||
}
|
||||
@@ -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\Domain;
|
||||
|
||||
/**
|
||||
* String wrapper that keeps track of how often the value was consumed.
|
||||
* This can be used to make decisions during runtime, depending on whether
|
||||
* a provided value actually has been used (e.g. in rendered content).
|
||||
*/
|
||||
class ConsumableString implements \Countable, \Stringable
|
||||
{
|
||||
/**
|
||||
* @internal use the `consume()` method instead
|
||||
*/
|
||||
public readonly string $value;
|
||||
private int $counter = 0;
|
||||
|
||||
public function __construct(string $value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->consume();
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return $this->counter;
|
||||
}
|
||||
|
||||
public function consume(): string
|
||||
{
|
||||
$this->counter++;
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?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\Domain;
|
||||
|
||||
use TYPO3\CMS\Core\Database\Query\QueryHelper;
|
||||
use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final readonly class DateTimeFactory
|
||||
{
|
||||
public static function createFromDatabaseValue(int|string|null $value, DateTimeFieldType $fieldInformation): ?\DateTimeImmutable
|
||||
{
|
||||
return self::fromDatabase(
|
||||
$value,
|
||||
$fieldInformation->isNullable(),
|
||||
$fieldInformation->getFormat(),
|
||||
$fieldInformation->getPersistenceType(),
|
||||
);
|
||||
}
|
||||
|
||||
public static function createFromDatabaseValueAndTCAConfig(int|string|null $value, array $fieldConfig): ?\DateTimeImmutable
|
||||
{
|
||||
$persistenceType = in_array($fieldConfig['dbType'] ?? null, QueryHelper::getDateTimeTypes(), true) ? $fieldConfig['dbType'] : null;
|
||||
$isNative = $persistenceType !== null;
|
||||
$isNullable = (bool)($fieldConfig['nullable'] ?? $isNative);
|
||||
$format = self::getFormatFromTCAConfig($fieldConfig);
|
||||
return self::fromDatabase(
|
||||
$value,
|
||||
$isNullable,
|
||||
$format,
|
||||
$persistenceType
|
||||
);
|
||||
}
|
||||
|
||||
public static function getFormatFromTCAConfig(array $fieldConfig): string
|
||||
{
|
||||
$format = $fieldConfig['format'] ?? null;
|
||||
$persistenceType = in_array($fieldConfig['dbType'] ?? null, QueryHelper::getDateTimeTypes(), true) ? $fieldConfig['dbType'] : null;
|
||||
// A native time field must not be formatted as date
|
||||
if (($format === 'datetime' || $format === 'datetimesec' || $format === 'date') && $persistenceType === 'time') {
|
||||
return 'timesec';
|
||||
}
|
||||
// A native date field must not be formatted as time
|
||||
if (($format === 'time' || $format === 'timesec' || $format === 'datetime' || $format === 'datetimesec') && $persistenceType === 'date') {
|
||||
return 'date';
|
||||
}
|
||||
if (in_array($format, ['datetime', 'date', 'time', 'timesec', 'datetimesec'], true)) {
|
||||
return $format;
|
||||
}
|
||||
if ($persistenceType !== null) {
|
||||
return $persistenceType === 'time' ? 'timesec' : $persistenceType;
|
||||
}
|
||||
return 'datetime';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a DateTimeImmutable object from a unix timestamp in server localtime
|
||||
*
|
||||
* Alternative to \DateTimeImmutable('@…') which forces UTC timezone
|
||||
*/
|
||||
public static function createFromTimestamp(int $timestamp): \DateTimeImmutable
|
||||
{
|
||||
// Create a new DateTime object in current timezone
|
||||
//
|
||||
// Note: As documented by PHP, `\DateTime` or `\DateTimeImmutable`
|
||||
// objects created from timestamps (e.g., '@12345678') as the
|
||||
// first constructor argument will use UTC as timezone instead of localtime,
|
||||
// therefore we must not initialize with a timestamp directly.
|
||||
$datetime = new \DateTimeImmutable();
|
||||
|
||||
// Apply timestamp (which will not change the objects timezone)
|
||||
return $datetime->setTimestamp($timestamp);
|
||||
}
|
||||
|
||||
private static function fromDatabase(
|
||||
int|string|null $value,
|
||||
bool $isNullable,
|
||||
string $format,
|
||||
?string $persistenceType
|
||||
): ?\DateTimeImmutable {
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$emptyFormat = QueryHelper::getDateTimeFormats()[$persistenceType ?? '']['empty'] ?? null;
|
||||
// A regular empty value is null for nullable fields
|
||||
$emptyValue = $isNullable ? null : ($emptyFormat ?? 0);
|
||||
// A legacy empty value is "0000-00-00" or "0000-00-00 00:00:00" stored
|
||||
// in a nullable native DATE or DATETIME field (which should already use
|
||||
// a proper `null` value, but still has a legacy empty value set).
|
||||
$legacyEmptyValue = $persistenceType === 'date' || $persistenceType === 'datetime' ? $emptyFormat : null;
|
||||
|
||||
if (MathUtility::canBeInterpretedAsInteger($value)) {
|
||||
$value = (int)$value;
|
||||
}
|
||||
if ($value === $emptyValue || $value === $legacyEmptyValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$datetime = match (true) {
|
||||
is_int($value) && ($format === 'time' || $format === 'timesec') => new \DateTimeImmutable(
|
||||
// time(sec) is stored as elapsed seconds in DB and has no defined date associated.
|
||||
// Per convention we map to 1970-01-01 for the sake of a reliable date.
|
||||
// We still want a PHP localtime timezone in the DateTime object set,
|
||||
// therefore we interpret the second as UTC time on 1970-01-01T00:00:00
|
||||
// and map the resulting value to PHP localtime
|
||||
gmdate(DateTimeFormat::ISO8601_LOCALTIME, $value)
|
||||
),
|
||||
// Unix timestamp
|
||||
is_int($value) => self::createFromTimestamp($value),
|
||||
// The database always contains server localtime in native fields.
|
||||
// The field value is something like "2016-01-01" or "2016-01-01 10:11:12.
|
||||
default => new \DateTimeImmutable($value),
|
||||
};
|
||||
} catch (\DateMalformedStringException $e) {
|
||||
throw new \InvalidArgumentException('Invalid date provided', 1743159490, $e);
|
||||
}
|
||||
|
||||
return match ($format) {
|
||||
// time(sec) is stored as elapsed seconds in DB, hence we normalize it as time on 1970-01-01 for consistency
|
||||
'time' => $datetime->setDate(1970, 1, 1)->setTime((int)$datetime->format('H'), (int)$datetime->format('i'), 0),
|
||||
'timesec' => $datetime->setDate(1970, 1, 1),
|
||||
'date' => $datetime->setTime(0, 0, 0),
|
||||
// default case also for 'datetimesec'
|
||||
default => $datetime,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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\Domain;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final readonly class DateTimeFormat
|
||||
{
|
||||
// Like \DateTimeInterface::ATOM but without timezone offset,
|
||||
// e.g. 2005-08-15T15:52:01
|
||||
// Links:
|
||||
// * https://en.wikipedia.org/wiki/ISO_8601#Local_time_(unqualified)
|
||||
// * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#local-dates-and-times
|
||||
// * https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local)
|
||||
public const string ISO8601_LOCALTIME = 'Y-m-d\\TH:i:s';
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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\Domain;
|
||||
|
||||
/**
|
||||
* Interface that indicated an object can be compared for equality with other instances.
|
||||
*
|
||||
* @internal not part of public API
|
||||
*/
|
||||
interface EqualityInterface
|
||||
{
|
||||
public function equals(EqualityInterface $other): bool;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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\Domain\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Context\LanguageAspect;
|
||||
|
||||
/**
|
||||
* Event which is fired after a record was translated (or tried to be localized).
|
||||
*/
|
||||
final class AfterRecordLanguageOverlayEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $table,
|
||||
private readonly array $record,
|
||||
private ?array $localizedRecord,
|
||||
private bool $overlayingWasAttempted,
|
||||
private readonly LanguageAspect $languageAspect
|
||||
) {}
|
||||
|
||||
public function getTable(): string
|
||||
{
|
||||
return $this->table;
|
||||
}
|
||||
|
||||
public function getRecord(): array
|
||||
{
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
public function getLanguageAspect(): LanguageAspect
|
||||
{
|
||||
return $this->languageAspect;
|
||||
}
|
||||
|
||||
public function setLocalizedRecord(?array $localizedRecord): void
|
||||
{
|
||||
$this->overlayingWasAttempted = true;
|
||||
$this->localizedRecord = $localizedRecord;
|
||||
}
|
||||
|
||||
public function getLocalizedRecord(): ?array
|
||||
{
|
||||
return $this->localizedRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the overlay functionality happened, thus, returning the lo
|
||||
*/
|
||||
public function overlayingWasAttempted(): bool
|
||||
{
|
||||
return $this->overlayingWasAttempted;
|
||||
}
|
||||
}
|
||||
@@ -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\Domain\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Domain\Page;
|
||||
|
||||
/**
|
||||
* Event which is fired before a page (id) is being resolved from PageRepository.
|
||||
*
|
||||
* Allows to change the corresponding page ID, e.g. to resolve a different page
|
||||
* with custom overlaying, or to fully resolve the page on your own.
|
||||
*/
|
||||
final class BeforePageIsRetrievedEvent
|
||||
{
|
||||
private ?Page $page = null;
|
||||
|
||||
public function __construct(
|
||||
private int $pageId,
|
||||
private bool $skipGroupAccessCheck,
|
||||
private readonly Context $context,
|
||||
) {}
|
||||
|
||||
public function getPage(): ?Page
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
public function setPage(Page $page): void
|
||||
{
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
public function hasPage(): bool
|
||||
{
|
||||
return $this->page !== null;
|
||||
}
|
||||
|
||||
public function getPageId(): int
|
||||
{
|
||||
return $this->pageId;
|
||||
}
|
||||
|
||||
public function setPageId(int $pageId): void
|
||||
{
|
||||
$this->pageId = $pageId;
|
||||
}
|
||||
|
||||
public function skipGroupAccessCheck(): void
|
||||
{
|
||||
$this->skipGroupAccessCheck = true;
|
||||
}
|
||||
|
||||
public function respectGroupAccessCheck(): void
|
||||
{
|
||||
$this->skipGroupAccessCheck = false;
|
||||
}
|
||||
|
||||
public function isGroupAccessCheckSkipped(): bool
|
||||
{
|
||||
return $this->skipGroupAccessCheck;
|
||||
}
|
||||
|
||||
public function getContext(): Context
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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\Domain\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Context\LanguageAspect;
|
||||
|
||||
/**
|
||||
* Event which is fired before a single page or a list of
|
||||
* pages are about to be translated (or tried to be localized).
|
||||
*/
|
||||
final class BeforePageLanguageOverlayEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $pageInput,
|
||||
private array $pageIds,
|
||||
private LanguageAspect $languageAspect
|
||||
) {}
|
||||
|
||||
public function getPageInput(): array
|
||||
{
|
||||
return $this->pageInput;
|
||||
}
|
||||
|
||||
public function setPageInput(array $pageInput): void
|
||||
{
|
||||
$this->pageInput = $pageInput;
|
||||
}
|
||||
|
||||
public function getPageIds(): array
|
||||
{
|
||||
return $this->pageIds;
|
||||
}
|
||||
|
||||
public function setPageIds(array $pageIds): void
|
||||
{
|
||||
$this->pageIds = array_map(intval(...), $pageIds);
|
||||
}
|
||||
|
||||
public function getLanguageAspect(): LanguageAspect
|
||||
{
|
||||
return $this->languageAspect;
|
||||
}
|
||||
|
||||
public function setLanguageAspect(LanguageAspect $languageAspect): void
|
||||
{
|
||||
$this->languageAspect = $languageAspect;
|
||||
}
|
||||
}
|
||||
@@ -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\Domain\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Context\LanguageAspect;
|
||||
|
||||
/**
|
||||
* Event which is fired before a record in a language should be "language overlaid",
|
||||
* that is: Finding a translation for a given record.
|
||||
*/
|
||||
final class BeforeRecordLanguageOverlayEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $table,
|
||||
private array $record,
|
||||
private LanguageAspect $languageAspect
|
||||
) {}
|
||||
|
||||
public function getTable(): string
|
||||
{
|
||||
return $this->table;
|
||||
}
|
||||
|
||||
public function getRecord(): array
|
||||
{
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
public function setRecord(array $record): void
|
||||
{
|
||||
$this->record = $record;
|
||||
}
|
||||
|
||||
public function getLanguageAspect(): LanguageAspect
|
||||
{
|
||||
return $this->languageAspect;
|
||||
}
|
||||
|
||||
public function setLanguageAspect(LanguageAspect $languageAspect): void
|
||||
{
|
||||
$this->languageAspect = $languageAspect;
|
||||
}
|
||||
}
|
||||
@@ -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\Domain\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
|
||||
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
|
||||
|
||||
/**
|
||||
* Event which is fired when compiling the list of constraints such as "deleted" and "starttime",
|
||||
* "endtime" etc.
|
||||
*
|
||||
* This Event allows for additional enableColumns to be added or removed to the list of constraints.
|
||||
*
|
||||
* An example: The extension ingmar_accessctrl enables assigning more
|
||||
* than one usergroup to content and page records
|
||||
*/
|
||||
final class ModifyDefaultConstraintsForDatabaseQueryEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $table,
|
||||
private readonly string $tableAlias,
|
||||
private readonly ExpressionBuilder $expressionBuilder,
|
||||
/** @var array<string, CompositeExpression|string> */
|
||||
private array $constraints,
|
||||
/** @var array<string, bool> */
|
||||
private readonly array $enableFieldsToIgnore,
|
||||
private readonly Context $context
|
||||
) {}
|
||||
|
||||
public function getTable(): string
|
||||
{
|
||||
return $this->table;
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return $this->tableAlias;
|
||||
}
|
||||
|
||||
public function getExpressionBuilder(): ExpressionBuilder
|
||||
{
|
||||
return $this->expressionBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CompositeExpression|string>
|
||||
*/
|
||||
public function getConstraints(): array
|
||||
{
|
||||
return $this->constraints;
|
||||
}
|
||||
|
||||
public function setConstraints(array $constraints): void
|
||||
{
|
||||
$this->constraints = $constraints;
|
||||
}
|
||||
|
||||
public function getEnableFieldsToIgnore(): array
|
||||
{
|
||||
return array_keys(array_filter($this->enableFieldsToIgnore));
|
||||
}
|
||||
|
||||
public function getContext(): Context
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?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\Domain\Event;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap;
|
||||
use TYPO3\CMS\Core\Domain\RawRecord;
|
||||
use TYPO3\CMS\Core\Domain\Record\SystemProperties;
|
||||
use TYPO3\CMS\Core\Domain\RecordInterface;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchema;
|
||||
|
||||
/**
|
||||
* Event which allows to manipulate the properties to be used for a new Record.
|
||||
* With this event, it's even possible to create a new Record manually.
|
||||
*/
|
||||
final class RecordCreationEvent implements StoppableEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
private array $properties,
|
||||
private readonly RawRecord $rawRecord,
|
||||
private readonly SystemProperties $systemProperties,
|
||||
private readonly Context $context,
|
||||
private readonly RecordIdentityMap $recordIdentityMap,
|
||||
private readonly TcaSchema $schema,
|
||||
private ?RecordInterface $record = null,
|
||||
) {}
|
||||
|
||||
public function setRecord(RecordInterface $record): void
|
||||
{
|
||||
$this->record = $record;
|
||||
}
|
||||
|
||||
public function isPropagationStopped(): bool
|
||||
{
|
||||
return $this->record !== null;
|
||||
}
|
||||
|
||||
public function hasProperty(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->properties);
|
||||
}
|
||||
|
||||
public function setProperty(string $name, mixed $propertyValue): void
|
||||
{
|
||||
$this->properties[$name] = $propertyValue;
|
||||
}
|
||||
|
||||
public function setProperties(array $properties): void
|
||||
{
|
||||
$this->properties = $properties;
|
||||
}
|
||||
|
||||
public function unsetProperty(string $name): bool
|
||||
{
|
||||
if (!$this->hasProperty($name)) {
|
||||
return false;
|
||||
}
|
||||
unset($this->properties[$name]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getProperty(string $name): mixed
|
||||
{
|
||||
return $this->properties[$name] ?? null;
|
||||
}
|
||||
|
||||
public function getProperties(): array
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
public function getRawRecord(): RawRecord
|
||||
{
|
||||
return $this->rawRecord;
|
||||
}
|
||||
|
||||
public function getSystemProperties(): SystemProperties
|
||||
{
|
||||
return $this->systemProperties;
|
||||
}
|
||||
|
||||
public function getContext(): Context
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
public function getRecordIdentityMap(): RecordIdentityMap
|
||||
{
|
||||
return $this->recordIdentityMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* If available this is the subSchema for the current record type
|
||||
*/
|
||||
public function getSchema(): TcaSchema
|
||||
{
|
||||
return $this->schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getRecord(): ?RecordInterface
|
||||
{
|
||||
return $this->record;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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\Domain\Exception;
|
||||
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class FlexFieldPropertyException extends Exception implements ContainerExceptionInterface {}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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\Domain\Exception;
|
||||
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class FlexFieldPropertyNotFoundException extends Exception implements NotFoundExceptionInterface {}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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\Domain\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class IncompleteRecordException extends Exception {}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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\Domain\Exception;
|
||||
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RecordPropertyException extends Exception implements ContainerExceptionInterface {}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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\Domain\Exception;
|
||||
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RecordPropertyNotFoundException extends Exception implements NotFoundExceptionInterface {}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?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\Domain;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use TYPO3\CMS\Core\Domain\Exception\FlexFieldPropertyException;
|
||||
use TYPO3\CMS\Core\Domain\Exception\FlexFieldPropertyNotFoundException;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
|
||||
/**
|
||||
* Represents a record's flex form field values.
|
||||
*
|
||||
* @internal not part of public API, as this needs to be streamlined and proven
|
||||
*/
|
||||
class FlexFormFieldValues implements ContainerInterface, \ArrayAccess
|
||||
{
|
||||
public function __construct(
|
||||
protected array $sheets
|
||||
) {}
|
||||
|
||||
public function get(string $id)
|
||||
{
|
||||
if (!$this->has($id)) {
|
||||
throw new FlexFieldPropertyNotFoundException('Flex property "' . $id . '" is not available.', 1731962637);
|
||||
}
|
||||
|
||||
[$sheetName, $propertyPath] = $this->processId($id);
|
||||
|
||||
if ($sheetName === '' && $this->hasMultipleSheets()) {
|
||||
// Get the sheet name for the requested property path - There is one, since has() returned true.
|
||||
foreach ($this->sheets as $name => $sheet) {
|
||||
if (ArrayUtility::isValidPath($sheet, $propertyPath, '.')) {
|
||||
$sheetName = $name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$propertyValue = ArrayUtility::getValueByPath($this->sheets[$sheetName], $propertyPath, '.');
|
||||
if (is_array($propertyValue)) {
|
||||
array_walk_recursive($propertyValue, fn(mixed &$value): mixed => $value = $this->resolveRecordPropertyClosure($propertyPath, $value));
|
||||
} else {
|
||||
$propertyValue = $this->resolveRecordPropertyClosure($propertyPath, $propertyValue);
|
||||
}
|
||||
ArrayUtility::setValueByPath($this->sheets[$sheetName], $propertyPath, $propertyValue, '.');
|
||||
return $propertyValue;
|
||||
}
|
||||
|
||||
public function has(string $id): bool
|
||||
{
|
||||
// If there are no sheets, no value can be determined.
|
||||
if ($this->sheets === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[$sheetName, $propertyPath] = $this->processId($id);
|
||||
|
||||
if ($sheetName !== '' && !isset($this->sheets[$sheetName])) {
|
||||
// Given sheet name does not exist
|
||||
return false;
|
||||
}
|
||||
if ($this->hasMultipleSheets()) {
|
||||
if ($sheetName !== '') {
|
||||
return ArrayUtility::isValidPath($this->sheets[$sheetName], $propertyPath, '.');
|
||||
}
|
||||
// In case no sheet name is given, we try to execute fallback handling
|
||||
// by searching for the requested $propertyPath in all sheets.
|
||||
$occurences = [];
|
||||
foreach ($this->sheets as $sheetName => $sheet) {
|
||||
if (ArrayUtility::isValidPath($sheet, $propertyPath, '.')) {
|
||||
$occurences[$sheetName] = $propertyPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($occurences) > 1) {
|
||||
// This is a special case, which we handle with an exception to create awareness for the error.
|
||||
throw new FlexFieldPropertyException('Given id is ambigious since the field exists in multiple sheets and no sheet is defined.', 1731962638);
|
||||
}
|
||||
// Whether the requested $propertyPath name exist in a sheet
|
||||
return count($occurences) === 1;
|
||||
}
|
||||
|
||||
// Standard case, whether the $propertyPath exists in the given sheet name
|
||||
return ArrayUtility::isValidPath($this->sheets[$sheetName], $propertyPath, '.');
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return $this->has($offset);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->get($offset);
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter to be used in fluid for accessing field values
|
||||
*/
|
||||
public function getSheets(): array
|
||||
{
|
||||
return $this->sheets;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->getSheets();
|
||||
}
|
||||
|
||||
protected function hasMultipleSheets(): bool
|
||||
{
|
||||
return count($this->sheets) > 1;
|
||||
}
|
||||
|
||||
protected function processId(string $id): array
|
||||
{
|
||||
if (str_contains($id, '/')) {
|
||||
// $id contains a sheet name
|
||||
return explode('/', $id, 2);
|
||||
}
|
||||
if ($this->hasMultipleSheets()) {
|
||||
// $id does not contain a sheet name while there are multiple sheets. Therefore, we
|
||||
// return an empty sheet name. This allows executing fallback handling in has() and get().
|
||||
return ['', $id];
|
||||
}
|
||||
|
||||
// In case the $id does not contain a sheet name, but we have a
|
||||
// single sheet flex form, we fall back to this name automatically.
|
||||
return [key($this->sheets), $id];
|
||||
}
|
||||
|
||||
protected function resolveRecordPropertyClosure(string $id, mixed $propertyValue): mixed
|
||||
{
|
||||
if ($propertyValue instanceof RecordPropertyClosure) {
|
||||
try {
|
||||
$propertyValue = $propertyValue->instantiate();
|
||||
} catch (\Exception $e) {
|
||||
// Consumers of this method can rely on catching ContainerExceptionInterface
|
||||
throw new FlexFieldPropertyException(
|
||||
'An exception occurred while instantiating flex field property "' . $id . '"',
|
||||
1731962735,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
return $propertyValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<?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\Domain;
|
||||
|
||||
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyNotFoundException;
|
||||
use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
|
||||
use TYPO3\CMS\Core\Domain\Record\SystemProperties;
|
||||
|
||||
/**
|
||||
* @internal not part of public API, as this needs to be streamlined and proven
|
||||
*/
|
||||
class Page extends Record implements \ArrayAccess
|
||||
{
|
||||
protected array $specialPropertyNames = [
|
||||
'_language',
|
||||
'_LOCALIZED_UID',
|
||||
'_REQUESTED_OVERLAY_LANGUAGE',
|
||||
'_MP_PARAM',
|
||||
'_ORIG_uid',
|
||||
'_ORIG_pid',
|
||||
'_SHORTCUT_ORIGINAL_PAGE_UID',
|
||||
'_TRANSLATION_SOURCE',
|
||||
];
|
||||
|
||||
protected array $specialProperties = [];
|
||||
|
||||
/**
|
||||
* @param RawRecord|array $rawRecordOrProperties RawRecord when created via RecordFactory, array for legacy usage
|
||||
*/
|
||||
public function __construct(RawRecord|array $rawRecordOrProperties, array $properties = [], ?SystemProperties $systemProperties = null)
|
||||
{
|
||||
if ($rawRecordOrProperties instanceof RawRecord) {
|
||||
parent::__construct($rawRecordOrProperties, $properties, $systemProperties);
|
||||
$this->extractSpecialPropertiesFromComputed($rawRecordOrProperties);
|
||||
} else {
|
||||
$this->initFromArray($rawRecordOrProperties);
|
||||
}
|
||||
}
|
||||
|
||||
public function has(string $id): bool
|
||||
{
|
||||
if (parent::has($id)) {
|
||||
return true;
|
||||
}
|
||||
return array_key_exists($id, $this->specialProperties);
|
||||
}
|
||||
|
||||
public function get(string $id): mixed
|
||||
{
|
||||
if (parent::has($id)) {
|
||||
return parent::get($id);
|
||||
}
|
||||
if (array_key_exists($id, $this->specialProperties)) {
|
||||
return $this->specialProperties[$id];
|
||||
}
|
||||
throw new RecordPropertyNotFoundException('Record property "' . $id . '" is not available.', 1725892141);
|
||||
}
|
||||
|
||||
public function getLanguageId(): int
|
||||
{
|
||||
if ($this->systemProperties?->getLanguage() !== null) {
|
||||
return $this->systemProperties->getLanguage()->getLanguageId();
|
||||
}
|
||||
return (int)($this->specialProperties['_language'] ?? $this->properties['language_tag'] ?? 0);
|
||||
}
|
||||
|
||||
public function getPageId(): int
|
||||
{
|
||||
if ($this->systemProperties?->getLanguage() !== null) {
|
||||
$translationParent = $this->systemProperties->getLanguage()->getTranslationParent();
|
||||
return $translationParent > 0 ? $translationParent : $this->getUid();
|
||||
}
|
||||
$pageId = isset($this->properties['l10n_parent']) && $this->properties['l10n_parent'] > 0 ? $this->properties['l10n_parent'] : $this->getUid();
|
||||
return (int)$pageId;
|
||||
}
|
||||
|
||||
public function getTranslationSource(): ?Page
|
||||
{
|
||||
return $this->specialProperties['_TRANSLATION_SOURCE'] ?? null;
|
||||
}
|
||||
|
||||
public function getRequestedLanguage(): ?int
|
||||
{
|
||||
return $this->specialProperties['_REQUESTED_OVERLAY_LANGUAGE'] ?? null;
|
||||
}
|
||||
|
||||
public function toArray(bool $includeSystemProperties = false): array
|
||||
{
|
||||
if ($includeSystemProperties) {
|
||||
// When including system properties, return the full raw record overlaid
|
||||
// with resolved properties and special properties for backward compatibility.
|
||||
$result = $this->rawRecord->toArray();
|
||||
foreach ($this->properties as $key => $property) {
|
||||
if ($property instanceof RecordPropertyClosure) {
|
||||
$this->properties[$key] = $property->instantiate();
|
||||
}
|
||||
$result[$key] = $this->properties[$key];
|
||||
}
|
||||
$result += ['_system' => $this->systemProperties?->toArray() ?? []];
|
||||
$result += $this->specialProperties;
|
||||
return $result;
|
||||
}
|
||||
return parent::toArray();
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return $this->has((string)$offset);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->has((string)$offset) ? $this->get((string)$offset) : null;
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
$this->properties[$offset] = $value;
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
unset($this->properties[$offset]);
|
||||
}
|
||||
|
||||
private function extractSpecialPropertiesFromComputed(RawRecord $rawRecord): void
|
||||
{
|
||||
$computedProperties = $rawRecord->getComputedProperties();
|
||||
if ($computedProperties->getLocalizedUid() !== null) {
|
||||
$this->specialProperties['_LOCALIZED_UID'] = $computedProperties->getLocalizedUid();
|
||||
}
|
||||
if ($computedProperties->getRequestedOverlayLanguageId() !== null) {
|
||||
$this->specialProperties['_REQUESTED_OVERLAY_LANGUAGE'] = $computedProperties->getRequestedOverlayLanguageId();
|
||||
}
|
||||
if ($computedProperties->getTranslationSource() !== null) {
|
||||
$this->specialProperties['_TRANSLATION_SOURCE'] = $computedProperties->getTranslationSource();
|
||||
}
|
||||
if ($computedProperties->getVersionedUid() !== null) {
|
||||
$this->specialProperties['_ORIG_uid'] = $computedProperties->getVersionedUid();
|
||||
}
|
||||
// Extract remaining special properties from the raw record
|
||||
$rawProperties = $rawRecord->toArray();
|
||||
foreach ($this->specialPropertyNames as $name) {
|
||||
if (isset($rawProperties[$name]) && !isset($this->specialProperties[$name])) {
|
||||
$this->specialProperties[$name] = $rawProperties[$name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function initFromArray(array $properties): void
|
||||
{
|
||||
$regularProperties = [];
|
||||
$translationSource = null;
|
||||
$localizedUid = null;
|
||||
$versionedUid = null;
|
||||
$requestedOverlayLanguageId = null;
|
||||
|
||||
foreach ($properties as $propertyName => $propertyValue) {
|
||||
if (in_array($propertyName, $this->specialPropertyNames)) {
|
||||
if ($propertyName === '_TRANSLATION_SOURCE' && !$propertyValue instanceof Page) {
|
||||
$translationSource = new Page($propertyValue);
|
||||
$this->specialProperties[$propertyName] = $translationSource;
|
||||
} elseif ($propertyName === '_TRANSLATION_SOURCE') {
|
||||
$translationSource = $propertyValue;
|
||||
$this->specialProperties[$propertyName] = $propertyValue;
|
||||
} elseif ($propertyName === '_LOCALIZED_UID') {
|
||||
$localizedUid = $propertyValue;
|
||||
$this->specialProperties[$propertyName] = $propertyValue;
|
||||
} elseif ($propertyName === '_ORIG_uid') {
|
||||
$versionedUid = $propertyValue;
|
||||
$this->specialProperties[$propertyName] = $propertyValue;
|
||||
} elseif ($propertyName === '_REQUESTED_OVERLAY_LANGUAGE') {
|
||||
$requestedOverlayLanguageId = $propertyValue;
|
||||
$this->specialProperties[$propertyName] = $propertyValue;
|
||||
} else {
|
||||
$this->specialProperties[$propertyName] = $propertyValue;
|
||||
}
|
||||
} else {
|
||||
$regularProperties[$propertyName] = $propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
$computedProperties = new ComputedProperties(
|
||||
versionedUid: $versionedUid,
|
||||
localizedUid: $localizedUid,
|
||||
requestedOverlayLanguageId: $requestedOverlayLanguageId,
|
||||
translationSource: $translationSource
|
||||
);
|
||||
|
||||
$recordType = isset($regularProperties['doktype']) ? (string)$regularProperties['doktype'] : null;
|
||||
$fullType = $recordType !== null ? 'pages.' . $recordType : 'pages';
|
||||
|
||||
$rawRecord = new RawRecord(
|
||||
uid: (int)($regularProperties['uid'] ?? 0),
|
||||
pid: (int)($regularProperties['pid'] ?? 0),
|
||||
properties: $regularProperties,
|
||||
computedProperties: $computedProperties,
|
||||
fullType: $fullType
|
||||
);
|
||||
|
||||
parent::__construct($rawRecord, $regularProperties, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?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\Domain\Persistence;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\DateTimeAspect;
|
||||
use TYPO3\CMS\Core\Context\LanguageAspect;
|
||||
use TYPO3\CMS\Core\Context\UserAspect;
|
||||
use TYPO3\CMS\Core\Context\VisibilityAspect;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
|
||||
use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Fetches all records of a single table of one / multiple PID(s)
|
||||
* in one DB query, and stores them in a runtime cache.
|
||||
*
|
||||
* The records are neither grouped nor do we care about
|
||||
* - sorting
|
||||
* - limit / offset
|
||||
* - or BE User permissions.
|
||||
*
|
||||
* It is "greedy" because it is meant to do simple query
|
||||
* in a fast way, to reduce overlays on a "per record" basis.
|
||||
*
|
||||
* The records are fetched depending on
|
||||
* - fe_group permissions
|
||||
* - language (incl. overlays etc)
|
||||
* - workspace
|
||||
* - visibility restrictions (starttime / endtime / hidden / deleted)
|
||||
*
|
||||
* The class returns an array and does not handle objects,
|
||||
* as it is very "lowlevel" and thus: internal.
|
||||
*
|
||||
* @internal not part of public API, as this needs to be streamlined and proven
|
||||
*/
|
||||
readonly class GreedyDatabaseBackend
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
protected FrontendInterface $runtimeCache,
|
||||
protected RecordAccessVoter $recordAccessVoter,
|
||||
protected ConnectionPool $connectionPool
|
||||
) {}
|
||||
|
||||
public function getRows(string $tableName, array $uids, Context $context): array
|
||||
{
|
||||
$cacheIdentifier = $this->createRuntimeCacheIdentifier($tableName, $uids, $context);
|
||||
$allRows = $this->getRowsFromCache($cacheIdentifier, $tableName, $uids, $context);
|
||||
if ($allRows === null) {
|
||||
$allRows = $this->getRowsFromDatabase($tableName, $uids, $context);
|
||||
$this->setCache($cacheIdentifier, $tableName, $context, $allRows);
|
||||
}
|
||||
return $this->handleOverlays(
|
||||
// Only use the records from the given UIDs
|
||||
array_filter($allRows, static fn(array $row) => in_array((int)$row['uid'], $uids, true)),
|
||||
$tableName,
|
||||
$context
|
||||
);
|
||||
}
|
||||
|
||||
protected function setCache(string $cacheIdentifier, string $tableName, Context $context, array $allRows): void
|
||||
{
|
||||
$resultUids = array_map(fn(array $row): int => (int)$row['uid'], $allRows);
|
||||
foreach ($resultUids as $resultUid) {
|
||||
$resultUidCacheIdentifier = $this->createRuntimeCacheIdentifier($tableName, [$resultUid], $context, 'pointer');
|
||||
// Set pointer to actual rows cache entry.
|
||||
$this->runtimeCache->set($resultUidCacheIdentifier, $cacheIdentifier);
|
||||
}
|
||||
$this->runtimeCache->set($cacheIdentifier, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates cache identifier pointers for each provided uid.
|
||||
* These uids come from the same pid, so they will have the same result set.
|
||||
* If at a later point in time one of these uids is requested again,
|
||||
* the pointer will be used to retrieve the actual cache entry of the db row.
|
||||
* Without this mechanism, the runtime cache would be filled quickly with the
|
||||
* same database rows over and over again.
|
||||
*
|
||||
* Example: Having 1000 records on the same pid with each having a relation to
|
||||
* a file reference. When RecordFactory is used to resolve all of these 1000 records
|
||||
* at a time, each of these 1000 relations would produce a cache entry with 1000
|
||||
* file reference database rows (1000*1000 = 1.000.000 database rows).
|
||||
*
|
||||
* Instead, only 1 cache entry is created with 1000 database rows and in addition
|
||||
* 1000 lightweight cache identifier pointers, pointing to the actual value of the
|
||||
* cache identifier.
|
||||
*/
|
||||
protected function getRowsFromCache(string $cacheIdentifier, string $tableName, array $uids, Context $context): ?array
|
||||
{
|
||||
if ($this->runtimeCache->has($cacheIdentifier)) {
|
||||
return $this->runtimeCache->get($cacheIdentifier);
|
||||
}
|
||||
foreach ($uids as $uid) {
|
||||
$cacheIdentifierPointer = $this->createRuntimeCacheIdentifier($tableName, [$uid], $context, 'pointer');
|
||||
if ($this->runtimeCache->has($cacheIdentifierPointer)) {
|
||||
$cacheIdentifier = $this->runtimeCache->get($cacheIdentifierPointer);
|
||||
if ($this->runtimeCache->has($cacheIdentifier)) {
|
||||
return $this->runtimeCache->get($cacheIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getRowsFromDatabase(string $tableName, array $uids, Context $context): array
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
|
||||
$queryBuilder
|
||||
->select('*')
|
||||
->from($tableName);
|
||||
// @todo: consider a context-based query restriction container here!
|
||||
// @todo: we should not remove the restrictions but rather add them based on the given Context
|
||||
/** @var DefaultRestrictionContainer $restrictions */
|
||||
$restrictions = $queryBuilder->getRestrictions();
|
||||
$visibilityAspect = $context->getAspect('visibility');
|
||||
if ($visibilityAspect->includeHidden()) {
|
||||
$restrictions->removeByType(HiddenRestriction::class);
|
||||
}
|
||||
if ($visibilityAspect->includeDeletedRecords()) {
|
||||
$restrictions->removeByType(DeletedRestriction::class);
|
||||
}
|
||||
if ($visibilityAspect->includeScheduledRecords()) {
|
||||
$restrictions->removeByType(StartTimeRestriction::class);
|
||||
$restrictions->removeByType(EndTimeRestriction::class);
|
||||
}
|
||||
if ($context->hasAspect('frontend.user')) {
|
||||
$groupIds = $context->getAspect('frontend.user')->getGroupIds();
|
||||
$restrictions->add(GeneralUtility::makeInstance(FrontendGroupRestriction::class, $groupIds));
|
||||
}
|
||||
// Workspace Restriction is never added
|
||||
$restrictions->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $context->getAspect('workspace')->getId()));
|
||||
|
||||
// Subselect is doing: give me the PID of the given UIDs
|
||||
// So we can get a greedy query for all records of these PIDs
|
||||
$queryBuilderForSubselect = $queryBuilder->getConnection()->createQueryBuilder();
|
||||
// Subselect must use same restrictions as main query
|
||||
$queryBuilderForSubselect->setRestrictions($restrictions);
|
||||
$queryBuilderForSubselect
|
||||
->select('pid')
|
||||
->from($tableName)
|
||||
->where(
|
||||
$queryBuilderForSubselect->expr()->in(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($uids, Connection::PARAM_INT_ARRAY)
|
||||
)
|
||||
);
|
||||
|
||||
// Inject the subselect in the WHERE part of the main query
|
||||
$queryBuilder->where(
|
||||
$queryBuilder->expr()->comparison(
|
||||
$queryBuilder->quoteIdentifier('pid'),
|
||||
'IN',
|
||||
'(' . $queryBuilderForSubselect->getSQL() . ')'
|
||||
)
|
||||
);
|
||||
|
||||
$allRows = $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
return $allRows;
|
||||
}
|
||||
|
||||
protected function handleOverlays(array $rows, string $dbTable, Context $context): array
|
||||
{
|
||||
$pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
|
||||
$finalRows = [];
|
||||
foreach ($rows as $row) {
|
||||
$pageRepository->versionOL($dbTable, $row);
|
||||
if ($row === false) {
|
||||
continue;
|
||||
}
|
||||
$row = $pageRepository->getLanguageOverlay($dbTable, $row);
|
||||
if ($row === null) {
|
||||
continue;
|
||||
}
|
||||
// Check fe_group, hidden, starttime, endtime etc.
|
||||
if (!$this->recordAccessVoter->accessGranted($dbTable, $row, $context)) {
|
||||
continue;
|
||||
}
|
||||
$finalRows[] = $row;
|
||||
}
|
||||
return $finalRows;
|
||||
}
|
||||
|
||||
protected function createRuntimeCacheIdentifier(string $tableName, array $uids, Context $context, string $suffix = ''): string
|
||||
{
|
||||
sort($uids);
|
||||
$cacheIdentifier = $tableName . '-' . md5(implode('_', $uids)) . '-';
|
||||
$cacheIdentifier .= $context->getAspect('workspace')->getId() . '-';
|
||||
/** @var LanguageAspect $languageAspect */
|
||||
$languageAspect = $context->getAspect('language');
|
||||
$cacheIdentifier .= $languageAspect->getId() . '-' . $languageAspect->getOverlayType() . '-' . md5(implode('_', $languageAspect->getFallbackChain())) . '-';
|
||||
/** @var VisibilityAspect $visibilityAspect */
|
||||
$visibilityAspect = $context->getAspect('visibility');
|
||||
$cacheIdentifier .= $visibilityAspect->includeHiddenPages() ? '1' : '0';
|
||||
$cacheIdentifier .= $visibilityAspect->includeHiddenContent() ? '1' : '0';
|
||||
$cacheIdentifier .= $visibilityAspect->includeScheduledRecords() ? '1' : '0';
|
||||
$cacheIdentifier .= $visibilityAspect->includeDeletedRecords() ? '1' : '0';
|
||||
|
||||
/** @var DateTimeAspect $dateAspect */
|
||||
$dateAspect = $context->getAspect('date');
|
||||
$cacheIdentifier .= '-' . $dateAspect->get('timestamp');
|
||||
|
||||
/** @var UserAspect $userAspect */
|
||||
$userAspect = $context->getAspect('frontend.user');
|
||||
$groupIds = $userAspect->getGroupIds();
|
||||
$cacheIdentifier .= '-' . implode('_', $groupIds);
|
||||
$cacheIdentifier .= '-' . $suffix;
|
||||
|
||||
return 'greedy_database_backend_' . hash('xxh3', $cacheIdentifier);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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\Domain\Persistence;
|
||||
|
||||
use TYPO3\CMS\Core\Domain\RecordInterface;
|
||||
|
||||
/**
|
||||
* The identity map for records is a database access design pattern used to improve
|
||||
* performance by providing a context-specific, in-memory cache to prevent duplicate retrieval
|
||||
* of the same object data from the database.
|
||||
*
|
||||
* In TYPO3 Context, an instance of the RecordIdentityMap is also shared in Frontend
|
||||
* (e.g. RecordFactory) and Backend (Page Module) to know which records have been created
|
||||
* already.
|
||||
*
|
||||
* For this reason, the identity map is shared, but needs to be explicitly shared, and
|
||||
* is thus, NOT, marked as a singleton.
|
||||
*
|
||||
* Why? Since we deal with "overlays", we also need to keep track of
|
||||
* - Language (Chain)
|
||||
* - Workspace
|
||||
*
|
||||
* In TYPO3 Context, the identity map is especially important to avoid infinite recursions
|
||||
* when resolving relations in records but allows to re-use existing objects used by this
|
||||
* map.
|
||||
*
|
||||
* The purpose for the Identity Map for Records is currently only for reading record objects
|
||||
* within one records (not writing, and not shared between requests).
|
||||
*
|
||||
* @internal not part of TYPO3 Core API as it is used on a low-level basis to keep state of created record objects.
|
||||
*/
|
||||
class RecordIdentityMap
|
||||
{
|
||||
/**
|
||||
* @var array<int|string, RecordInterface>
|
||||
*/
|
||||
protected array $recordMap = [];
|
||||
|
||||
public function add(RecordInterface $record): void
|
||||
{
|
||||
$this->recordMap[$record->getMainType()][$record->getUid()] = $record;
|
||||
}
|
||||
|
||||
public function has(RecordInterface $record): bool
|
||||
{
|
||||
return isset($this->recordMap[$record->getMainType()][$record->getUid()]);
|
||||
}
|
||||
|
||||
public function findByIdentifier(string $mainType, int $identifier): RecordInterface
|
||||
{
|
||||
if ($this->hasIdentifier($mainType, $identifier)) {
|
||||
return $this->recordMap[$mainType][$identifier];
|
||||
}
|
||||
throw new \InvalidArgumentException(
|
||||
'Record with type "' . $mainType . '" and identifier "' . $identifier . '" not found in the Identity Map.',
|
||||
1720730774
|
||||
);
|
||||
}
|
||||
|
||||
public function hasIdentifier(string $mainType, int $identifier): bool
|
||||
{
|
||||
return isset($this->recordMap[$mainType][$identifier]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?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\Domain;
|
||||
|
||||
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyNotFoundException;
|
||||
use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
|
||||
|
||||
/**
|
||||
* Holds all properties of a raw database row with unfiltered and unprocessed values.
|
||||
*
|
||||
* @internal not part of public API, as this needs to be streamlined and proven
|
||||
*/
|
||||
readonly class RawRecord implements RecordInterface
|
||||
{
|
||||
protected string $mainType;
|
||||
protected ?string $recordType;
|
||||
|
||||
public function __construct(
|
||||
protected int $uid,
|
||||
protected int $pid,
|
||||
protected array $properties,
|
||||
protected ComputedProperties $computedProperties,
|
||||
protected string $fullType
|
||||
) {
|
||||
$parts = $this->normalizeTypeParts($this->fullType);
|
||||
$this->mainType = $parts[0] ?? '';
|
||||
$this->recordType = $parts[1] ?? null;
|
||||
}
|
||||
|
||||
public function getUid(): int
|
||||
{
|
||||
return $this->uid;
|
||||
}
|
||||
|
||||
public function getPid(): int
|
||||
{
|
||||
return $this->pid;
|
||||
}
|
||||
|
||||
public function getFullType(): string
|
||||
{
|
||||
return $this->fullType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string|null
|
||||
*/
|
||||
public function getRecordType(): ?string
|
||||
{
|
||||
return $this->recordType;
|
||||
}
|
||||
|
||||
public function getMainType(): string
|
||||
{
|
||||
return $this->mainType;
|
||||
}
|
||||
|
||||
public function toArray(bool $includeComputedProperties = false): array
|
||||
{
|
||||
$properties = ['uid' => $this->uid, 'pid' => $this->pid] + $this->properties;
|
||||
if ($includeComputedProperties) {
|
||||
$properties += ['_computed' => $this->computedProperties->toArray()];
|
||||
}
|
||||
return $properties;
|
||||
}
|
||||
|
||||
public function has(string $id): bool
|
||||
{
|
||||
return array_key_exists($id, $this->properties);
|
||||
}
|
||||
|
||||
public function get(string $id): mixed
|
||||
{
|
||||
if (!$this->has($id)) {
|
||||
throw new RecordPropertyNotFoundException(
|
||||
'Record property "' . $id . '" is not available.',
|
||||
1725892140
|
||||
);
|
||||
}
|
||||
|
||||
return $this->properties[$id] ?? null;
|
||||
}
|
||||
|
||||
public function getComputedProperties(): ComputedProperties
|
||||
{
|
||||
return $this->computedProperties;
|
||||
}
|
||||
|
||||
public function getRawRecord(): RawRecord
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0?: string, 1?: string}
|
||||
*/
|
||||
protected function normalizeTypeParts(string $type): array
|
||||
{
|
||||
return array_filter(
|
||||
array_map(trim(...), explode('.', $type, 2)),
|
||||
static fn(string $part): bool => $part !== ''
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?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\Domain;
|
||||
|
||||
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyException;
|
||||
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyNotFoundException;
|
||||
use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
|
||||
use TYPO3\CMS\Core\Domain\Record\LanguageInfo;
|
||||
use TYPO3\CMS\Core\Domain\Record\SystemProperties;
|
||||
use TYPO3\CMS\Core\Domain\Record\VersionInfo;
|
||||
|
||||
/**
|
||||
* Represents a record with all properties valid for this record type.
|
||||
*
|
||||
* @internal not part of public API, as this needs to be streamlined and proven
|
||||
*/
|
||||
class Record implements RecordInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly RawRecord $rawRecord,
|
||||
protected array $properties,
|
||||
protected readonly ?SystemProperties $systemProperties = null,
|
||||
) {}
|
||||
|
||||
public function getUid(): int
|
||||
{
|
||||
return $this->rawRecord->getUid();
|
||||
}
|
||||
|
||||
public function getPid(): int
|
||||
{
|
||||
return $this->rawRecord->getPid();
|
||||
}
|
||||
|
||||
public function getFullType(): string
|
||||
{
|
||||
return $this->rawRecord->getFullType();
|
||||
}
|
||||
|
||||
public function getRecordType(): ?string
|
||||
{
|
||||
return $this->rawRecord->getRecordType();
|
||||
}
|
||||
|
||||
public function getMainType(): string
|
||||
{
|
||||
return $this->rawRecord->getMainType();
|
||||
}
|
||||
|
||||
public function toArray(bool $includeSystemProperties = false): array
|
||||
{
|
||||
$properties = ['uid' => $this->getUid(), 'pid' => $this->getPid()];
|
||||
foreach ($this->properties as $key => $property) {
|
||||
if ($property instanceof RecordPropertyClosure) {
|
||||
$this->properties[$key] = $property->instantiate();
|
||||
}
|
||||
}
|
||||
$properties += $this->properties;
|
||||
if ($includeSystemProperties) {
|
||||
$properties += ['_system' => $this->systemProperties?->toArray() ?? []];
|
||||
}
|
||||
return $properties;
|
||||
}
|
||||
|
||||
public function has(string $id): bool
|
||||
{
|
||||
if (array_key_exists($id, $this->properties)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (in_array($id, ['uid', 'pid'], true)) {
|
||||
// Enable access of uid and pid via array access
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->getRecordType() === null && $this->rawRecord->has($id)) {
|
||||
// Only fall back to the raw record in case no record type is defined.
|
||||
// This allows to properly check for only record type specific fields.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get(string $id): mixed
|
||||
{
|
||||
if (array_key_exists($id, $this->properties)) {
|
||||
$property = $this->properties[$id];
|
||||
if ($property instanceof RecordPropertyClosure) {
|
||||
try {
|
||||
$property = $property->instantiate();
|
||||
} catch (\Exception $e) {
|
||||
// Consumers of this method can rely on catching ContainerExceptionInterface
|
||||
throw new RecordPropertyException(
|
||||
'An exception occurred while instantiating record property "' . $id . '"',
|
||||
1725892139,
|
||||
$e
|
||||
);
|
||||
}
|
||||
$this->properties[$id] = $property;
|
||||
}
|
||||
return $property;
|
||||
}
|
||||
|
||||
if (in_array($id, ['uid', 'pid'], true)) {
|
||||
// Enable access of uid and pid via array access
|
||||
return $this->rawRecord->get($id);
|
||||
}
|
||||
|
||||
if ($this->getRecordType() === null && $this->rawRecord->has($id)) {
|
||||
// Only fall back to the raw record in case no record type is defined.
|
||||
// This ensures that only record type specific fields are being returned.
|
||||
return $this->rawRecord->get($id);
|
||||
}
|
||||
|
||||
throw new RecordPropertyNotFoundException('Record property "' . $id . '" is not available.', 1725892138);
|
||||
}
|
||||
|
||||
public function getVersionInfo(): ?VersionInfo
|
||||
{
|
||||
return $this->systemProperties?->getVersion();
|
||||
}
|
||||
|
||||
public function getLanguageInfo(): ?LanguageInfo
|
||||
{
|
||||
return $this->systemProperties?->getLanguage();
|
||||
}
|
||||
|
||||
public function getLanguageId(): ?int
|
||||
{
|
||||
return $this->systemProperties?->getLanguage()?->getLanguageId();
|
||||
}
|
||||
|
||||
public function getSystemProperties(): ?SystemProperties
|
||||
{
|
||||
return $this->systemProperties;
|
||||
}
|
||||
|
||||
public function getComputedProperties(): ComputedProperties
|
||||
{
|
||||
return $this->rawRecord->getComputedProperties();
|
||||
}
|
||||
|
||||
public function getRawRecord(): RawRecord
|
||||
{
|
||||
return $this->rawRecord;
|
||||
}
|
||||
|
||||
public function getOverlaidUid(): int
|
||||
{
|
||||
$computedProperties = $this->getComputedProperties();
|
||||
if ($computedProperties->getLocalizedUid() !== null) {
|
||||
return $computedProperties->getLocalizedUid();
|
||||
}
|
||||
if ($computedProperties->getVersionedUid() !== null) {
|
||||
return $computedProperties->getVersionedUid();
|
||||
}
|
||||
return $this->getUid();
|
||||
}
|
||||
}
|
||||
@@ -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\Domain\Record;
|
||||
|
||||
use TYPO3\CMS\Core\Domain\Page;
|
||||
|
||||
/**
|
||||
* Contains all information about computed properties for the current context.
|
||||
*
|
||||
* @internal not part of public API, as this needs to be streamlined and proven
|
||||
*/
|
||||
readonly class ComputedProperties
|
||||
{
|
||||
public function __construct(
|
||||
protected ?int $versionedUid = null,
|
||||
protected ?int $localizedUid = null,
|
||||
protected ?int $requestedOverlayLanguageId = null,
|
||||
protected ?Page $translationSource = null,
|
||||
) {}
|
||||
|
||||
public function getVersionedUid(): ?int
|
||||
{
|
||||
return $this->versionedUid;
|
||||
}
|
||||
|
||||
public function getLocalizedUid(): ?int
|
||||
{
|
||||
return $this->localizedUid;
|
||||
}
|
||||
|
||||
public function getRequestedOverlayLanguageId(): ?int
|
||||
{
|
||||
return $this->requestedOverlayLanguageId;
|
||||
}
|
||||
|
||||
public function getTranslationSource(): ?Page
|
||||
{
|
||||
return $this->translationSource;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'versionedUid' => $this->versionedUid,
|
||||
'localizedUid' => $this->localizedUid,
|
||||
'requestedOverlayLanguageId' => $this->requestedOverlayLanguageId,
|
||||
'translationSource' => $this->translationSource,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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\Domain\Record;
|
||||
|
||||
/**
|
||||
* Contains all information about language for a language-aware record.
|
||||
*
|
||||
* @internal not part of public API, as this needs to be streamlined and proven
|
||||
*/
|
||||
readonly class LanguageInfo
|
||||
{
|
||||
public function __construct(
|
||||
protected int $languageId,
|
||||
protected ?int $translationParent,
|
||||
protected ?int $translationSource
|
||||
) {}
|
||||
|
||||
public function getLanguageId(): int
|
||||
{
|
||||
return $this->languageId;
|
||||
}
|
||||
|
||||
public function getTranslationParent(): ?int
|
||||
{
|
||||
return $this->translationParent;
|
||||
}
|
||||
|
||||
public function getTranslationSource(): ?int
|
||||
{
|
||||
return $this->translationSource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?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\Domain\Record;
|
||||
|
||||
/**
|
||||
* Contains all information about system-related properties for the current record.
|
||||
*
|
||||
* @internal not part of public API, as this needs to be streamlined and proven
|
||||
*/
|
||||
readonly class SystemProperties
|
||||
{
|
||||
public function __construct(
|
||||
protected ?LanguageInfo $languageInfo,
|
||||
protected ?VersionInfo $versionInfo,
|
||||
protected ?bool $isDeleted,
|
||||
protected ?bool $isDisabled,
|
||||
protected ?bool $isLockedForEditing,
|
||||
protected ?\DateTimeInterface $createdAt,
|
||||
protected ?\DateTimeInterface $lastUpdatedAt,
|
||||
protected ?\DateTimeInterface $publishAt,
|
||||
protected ?\DateTimeInterface $publishUntil,
|
||||
protected ?array $userGroupRestriction,
|
||||
protected ?int $sorting,
|
||||
protected ?string $description,
|
||||
) {}
|
||||
|
||||
public function getLanguage(): ?LanguageInfo
|
||||
{
|
||||
return $this->languageInfo;
|
||||
}
|
||||
|
||||
public function getVersion(): ?VersionInfo
|
||||
{
|
||||
return $this->versionInfo;
|
||||
}
|
||||
|
||||
public function isDeleted(): ?bool
|
||||
{
|
||||
return $this->isDeleted;
|
||||
}
|
||||
|
||||
public function isDisabled(): ?bool
|
||||
{
|
||||
return $this->isDisabled;
|
||||
}
|
||||
|
||||
public function isLockedForEditing(): ?bool
|
||||
{
|
||||
return $this->isLockedForEditing;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getLastUpdatedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->lastUpdatedAt;
|
||||
}
|
||||
|
||||
public function getPublishAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->publishAt;
|
||||
}
|
||||
|
||||
public function getPublishUntil(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->publishUntil;
|
||||
}
|
||||
|
||||
public function getUserGroupRestriction(): ?array
|
||||
{
|
||||
return $this->userGroupRestriction;
|
||||
}
|
||||
|
||||
public function getSorting(): ?int
|
||||
{
|
||||
return $this->sorting;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'language' => $this->languageInfo,
|
||||
'version' => $this->versionInfo,
|
||||
'isDeleted' => $this->isDeleted,
|
||||
'isDisabled' => $this->isDisabled,
|
||||
'isLockedForEditing' => $this->isLockedForEditing,
|
||||
'createdAt' => $this->createdAt,
|
||||
'lastUpdatedAt' => $this->lastUpdatedAt,
|
||||
'publishAt' => $this->publishAt,
|
||||
'publishUntil' => $this->publishUntil,
|
||||
'userGroupRestriction' => $this->userGroupRestriction,
|
||||
'sorting' => $this->sorting,
|
||||
'description' => $this->description,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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\Domain\Record;
|
||||
|
||||
use TYPO3\CMS\Core\Versioning\VersionState;
|
||||
|
||||
/**
|
||||
* Contains all information about versioning for a workspace-aware record.
|
||||
*
|
||||
* @internal not part of public API, as this needs to be streamlined and proven
|
||||
*/
|
||||
readonly class VersionInfo
|
||||
{
|
||||
public function __construct(
|
||||
protected int $workspaceId,
|
||||
protected int $liveId,
|
||||
protected VersionState $state,
|
||||
protected int $stage,
|
||||
) {}
|
||||
|
||||
public function getWorkspaceId(): int
|
||||
{
|
||||
return $this->workspaceId;
|
||||
}
|
||||
|
||||
public function getLiveId(): int
|
||||
{
|
||||
return $this->liveId;
|
||||
}
|
||||
|
||||
public function getState(): VersionState
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function getStageId(): int
|
||||
{
|
||||
return $this->stage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
<?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\Domain;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\DataHandling\RecordFieldTransformer;
|
||||
use TYPO3\CMS\Core\Domain\Event\RecordCreationEvent;
|
||||
use TYPO3\CMS\Core\Domain\Exception\IncompleteRecordException;
|
||||
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyNotFoundException;
|
||||
use TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap;
|
||||
use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
|
||||
use TYPO3\CMS\Core\Domain\Record\LanguageInfo;
|
||||
use TYPO3\CMS\Core\Domain\Record\SystemProperties;
|
||||
use TYPO3\CMS\Core\Domain\Record\VersionInfo;
|
||||
use TYPO3\CMS\Core\Schema\Capability\FieldCapability;
|
||||
use TYPO3\CMS\Core\Schema\Capability\LanguageAwareSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\Capability\SystemInternalFieldCapability;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchema;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Versioning\VersionState;
|
||||
|
||||
/**
|
||||
* Creates record objects out of TCA-based database rows by evaluating the TCA columns and splitting
|
||||
* everything which is not a declared column for a TCA type. This is usually the case when a TCA table
|
||||
* has a 'typeField' defined, such as "pages", "be_users" and "tt_content".
|
||||
*
|
||||
* In addition, the RecordFactory can create "Resolved records" by utilizing the RecordFieldTransformer.
|
||||
* A "Resolved record" is checked for the actual type (TCA field column type) and is then resolved to
|
||||
* - a relation (records, files or folders - wrapped in collections)
|
||||
* - an exploded list (e.g. static select)
|
||||
* - a FlexForm field
|
||||
* - a DateTime field.
|
||||
*
|
||||
* This means that the field value of a "Resolved Record" is expanded to the actual types (Date objects etc.)
|
||||
*
|
||||
* @internal not part of TYPO3 Core API yet.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class RecordFactory
|
||||
{
|
||||
public function __construct(
|
||||
protected TcaSchemaFactory $schemaFactory,
|
||||
protected RecordFieldTransformer $fieldTransformer,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Takes a full database record (the whole row), and creates a Record object out of it,
|
||||
* based on the type of the record.
|
||||
*
|
||||
* This method does not handle special expansion of fields.
|
||||
* @todo Now unused - we might want to remove this again
|
||||
*/
|
||||
public function createFromDatabaseRow(string $table, array $record): RecordInterface
|
||||
{
|
||||
$rawRecord = $this->createRawRecord($table, $record);
|
||||
$schema = $this->schemaFactory->get($table);
|
||||
$subSchema = null;
|
||||
if ($schema->hasSubSchema($rawRecord->getRecordType() ?? '')) {
|
||||
$subSchema = $schema->getSubSchema($rawRecord->getRecordType());
|
||||
}
|
||||
|
||||
// Only use the fields that are defined in the schema
|
||||
$properties = [];
|
||||
foreach ($record as $fieldName => $fieldValue) {
|
||||
if ($subSchema) {
|
||||
if (!$subSchema->hasField($fieldName)) {
|
||||
continue;
|
||||
}
|
||||
$schema = $subSchema;
|
||||
} elseif (!$schema->hasField($fieldName)) {
|
||||
continue;
|
||||
}
|
||||
$properties[$fieldName] = $fieldValue;
|
||||
}
|
||||
return $this->createRecord($rawRecord, $properties, $schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a "resolved" record. Resolved means that the fields will have
|
||||
* their values resolved and extended. A typical use-case is resolving
|
||||
* of related records, or using \DateTimeImmutable objects for datetime fields.
|
||||
*/
|
||||
public function createResolvedRecordFromDatabaseRow(string $table, array $record, ?Context $context = null, ?RecordIdentityMap $recordIdentityMap = null): RecordInterface
|
||||
{
|
||||
$context = $context ?? GeneralUtility::makeInstance(Context::class);
|
||||
/** @var RecordIdentityMap $recordIdentityMap */
|
||||
$recordIdentityMap = $recordIdentityMap ?? GeneralUtility::makeInstance(RecordIdentityMap::class);
|
||||
if ($recordIdentityMap->hasIdentifier($table, (int)($record['uid'] ?? 0))) {
|
||||
return $recordIdentityMap->findByIdentifier($table, (int)$record['uid']);
|
||||
}
|
||||
$properties = [];
|
||||
$rawRecord = $this->createRawRecord($table, $record);
|
||||
$schema = $this->schemaFactory->get($table);
|
||||
$subSchema = null;
|
||||
if ($schema->hasSubSchema($rawRecord->getRecordType() ?? '')) {
|
||||
$subSchema = $schema->getSubSchema($rawRecord->getRecordType());
|
||||
}
|
||||
|
||||
// Only use the fields that are defined in the schema
|
||||
foreach ($record as $fieldName => $fieldValue) {
|
||||
if ($subSchema) {
|
||||
if (!$subSchema->hasField($fieldName)) {
|
||||
continue;
|
||||
}
|
||||
$schema = $subSchema;
|
||||
} elseif (!$schema->hasField($fieldName)) {
|
||||
continue;
|
||||
}
|
||||
$fieldInformation = $schema->getField($fieldName);
|
||||
$properties[$fieldName] = $this->fieldTransformer->transformField(
|
||||
$fieldInformation,
|
||||
$rawRecord,
|
||||
$context,
|
||||
$recordIdentityMap
|
||||
);
|
||||
}
|
||||
$resolvedRecord = $this->createRecord($rawRecord, $properties, $schema, $context, $recordIdentityMap);
|
||||
$recordIdentityMap->add($resolvedRecord);
|
||||
return $resolvedRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a raw record object from a table and a record array.
|
||||
*/
|
||||
public function createRawRecord(string $table, array $record): RawRecord
|
||||
{
|
||||
if (!$this->schemaFactory->has($table)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Unable to create Record from non-TCA table "' . $table . '".',
|
||||
1715266929
|
||||
);
|
||||
}
|
||||
$schema = $this->schemaFactory->get($table);
|
||||
$fullType = $table;
|
||||
if ($schema->supportsSubSchema() && ($subSchemaTypeInformation = $schema->getSubSchemaTypeInformation())->isPointerToForeignFieldInForeignSchema() === false) {
|
||||
// @todo Limitation to local SubSchemaDivisorField, because the actual record type is defined in foreign record.
|
||||
$subSchemaDivisorFieldName = $subSchemaTypeInformation->getFieldName();
|
||||
if (!isset($record[$subSchemaDivisorFieldName])) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Missing typeField "' . $subSchemaDivisorFieldName . '" in record of requested table "' . $table . '".',
|
||||
1715267513,
|
||||
);
|
||||
}
|
||||
$recordType = (string)$record[$subSchemaDivisorFieldName];
|
||||
$fullType .= '.' . $recordType;
|
||||
}
|
||||
$computedProperties = $this->extractComputedProperties($record);
|
||||
// @todo We might want to throw an exception in case uid / pid are not defined.
|
||||
return new RawRecord((int)($record['uid'] ?? 0), (int)($record['pid'] ?? 0), $record, $computedProperties, $fullType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick helper function in order to avoid duplicate code.
|
||||
*/
|
||||
protected function createRecord(RawRecord $rawRecord, array $properties, TcaSchema $schema, ?Context $context = null, ?RecordIdentityMap $recordIdentityMap = null): RecordInterface
|
||||
{
|
||||
$context = $context ?? GeneralUtility::makeInstance(Context::class);
|
||||
$mainSchema = $this->schemaFactory->get($rawRecord->getMainType());
|
||||
$recordIdentityMap = $recordIdentityMap ?? GeneralUtility::makeInstance(RecordIdentityMap::class);
|
||||
[$properties, $systemProperties] = $this->extractSystemInformation(
|
||||
$mainSchema,
|
||||
$rawRecord,
|
||||
$properties,
|
||||
);
|
||||
$event = new RecordCreationEvent($properties, $rawRecord, $systemProperties, $context, $recordIdentityMap, $schema);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
if ($event->isPropagationStopped()) {
|
||||
return $event->getRecord();
|
||||
}
|
||||
if ($event->getRawRecord()->getMainType() === 'pages') {
|
||||
return new Page($event->getRawRecord(), $event->getProperties(), $event->getSystemProperties());
|
||||
}
|
||||
return new Record($event->getRawRecord(), $event->getProperties(), $event->getSystemProperties());
|
||||
}
|
||||
|
||||
protected function extractComputedProperties(array &$record): ComputedProperties
|
||||
{
|
||||
$computed = $record['_computed'] ?? null;
|
||||
if (is_array($computed)) {
|
||||
$computedProperties = new ComputedProperties(
|
||||
$computed['versionedUid'] ?? null,
|
||||
$computed['localizedUid'] ?? null,
|
||||
$computed['requestedOverlayLanguageId'] ?? null,
|
||||
$computed['translationSource'] ?? null
|
||||
);
|
||||
unset($record['_computed']);
|
||||
return $computedProperties;
|
||||
}
|
||||
$computedProperties = new ComputedProperties(
|
||||
$record['_ORIG_uid'] ?? null,
|
||||
$record['_LOCALIZED_UID'] ?? null,
|
||||
$record['_REQUESTED_OVERLAY_LANGUAGE'] ?? null,
|
||||
$record['_TRANSLATION_SOURCE'] ?? null
|
||||
);
|
||||
unset(
|
||||
$record['_ORIG_uid'],
|
||||
$record['_LOCALIZED_UID'],
|
||||
$record['_REQUESTED_OVERLAY_LANGUAGE'],
|
||||
$record['_TRANSLATION_SOURCE']
|
||||
);
|
||||
return $computedProperties;
|
||||
}
|
||||
|
||||
protected function extractSystemInformation(TcaSchema $schema, RawRecord $rawRecord, array $properties): array
|
||||
{
|
||||
// Language information.
|
||||
$systemProperties = [];
|
||||
if ($schema->isLanguageAware()) {
|
||||
/** @var LanguageAwareSchemaCapability $languageCapability */
|
||||
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
||||
$languageField = $languageCapability->getLanguageField()->getName();
|
||||
$transOrigPointerField = $languageCapability->getTranslationOriginPointerField()->getName();
|
||||
$translationSourceField = $languageCapability->getTranslationSourceField()?->getName() ?? '';
|
||||
try {
|
||||
$systemProperties['language'] = new LanguageInfo(
|
||||
(int)$rawRecord->get($languageField),
|
||||
(int)$rawRecord->get($transOrigPointerField),
|
||||
$rawRecord->has($translationSourceField) ? (int)$rawRecord->get($translationSourceField) : null,
|
||||
);
|
||||
} catch (RecordPropertyNotFoundException $e) {
|
||||
throw new IncompleteRecordException(
|
||||
'Table "' . $schema->getName() . '" is defined as language aware but the record misses necessary fields: ' . $e->getMessage(),
|
||||
1726046917
|
||||
);
|
||||
}
|
||||
unset($properties[$languageField]);
|
||||
unset($properties[$transOrigPointerField]);
|
||||
if ($translationSourceField !== '') {
|
||||
unset($properties[$translationSourceField]);
|
||||
}
|
||||
if ($languageCapability->hasDiffSourceField()) {
|
||||
unset($properties[$languageCapability->getDiffSourceField()?->getName()]);
|
||||
}
|
||||
unset($properties['l10n_state']);
|
||||
}
|
||||
|
||||
// Workspaces.
|
||||
if ($schema->isWorkspaceAware()) {
|
||||
try {
|
||||
$systemProperties['version'] = new VersionInfo(
|
||||
(int)$rawRecord->get('t3ver_wsid'),
|
||||
(int)$rawRecord->get('t3ver_oid'),
|
||||
VersionState::tryFrom((int)$rawRecord->get('t3ver_state')),
|
||||
(int)$rawRecord->get('t3ver_stage'),
|
||||
);
|
||||
} catch (RecordPropertyNotFoundException $e) {
|
||||
throw new IncompleteRecordException(
|
||||
'Table "' . $schema->getName() . '" is defined as workspace aware but the record misses necessary fields: ' . $e->getMessage(),
|
||||
1726046918
|
||||
);
|
||||
}
|
||||
unset(
|
||||
$properties['t3ver_wsid'],
|
||||
$properties['t3ver_oid'],
|
||||
$properties['t3ver_state'],
|
||||
$properties['t3ver_stage']
|
||||
);
|
||||
}
|
||||
|
||||
// Date-related fields
|
||||
foreach (TcaSchemaCapability::getSystemCapabilities() as $capability) {
|
||||
if (!$schema->hasCapability($capability)) {
|
||||
continue;
|
||||
}
|
||||
/** @var SystemInternalFieldCapability|FieldCapability $capabilityInstance */
|
||||
$capabilityInstance = $schema->getCapability($capability);
|
||||
$fieldName = $capabilityInstance->getFieldName();
|
||||
if (!$rawRecord->has($fieldName)) {
|
||||
throw new IncompleteRecordException(
|
||||
'Table "' . $schema->getName() . '" has capability "' . $capability->name . '" set but the record misses the corresponding field "' . $fieldName . '"',
|
||||
1726046919
|
||||
);
|
||||
}
|
||||
switch ($capability) {
|
||||
case TcaSchemaCapability::CreatedAt:
|
||||
$systemProperties['createdAt'] = DateTimeFactory::createFromTimestamp($rawRecord->get($fieldName));
|
||||
break;
|
||||
case TcaSchemaCapability::UpdatedAt:
|
||||
$systemProperties['lastUpdatedAt'] = DateTimeFactory::createFromTimestamp($rawRecord->get($fieldName));
|
||||
break;
|
||||
case TcaSchemaCapability::RestrictionStartTime:
|
||||
$systemProperties['publishAt'] = DateTimeFactory::createFromTimestamp($rawRecord->get($fieldName));
|
||||
break;
|
||||
case TcaSchemaCapability::RestrictionEndTime:
|
||||
$systemProperties['publishUntil'] = DateTimeFactory::createFromTimestamp($rawRecord->get($fieldName));
|
||||
break;
|
||||
|
||||
case TcaSchemaCapability::SoftDelete:
|
||||
$systemProperties['isDeleted'] = (bool)($rawRecord->get($fieldName));
|
||||
break;
|
||||
case TcaSchemaCapability::EditLock:
|
||||
$systemProperties['isLockedForEditing'] = (bool)($rawRecord->get($fieldName));
|
||||
break;
|
||||
case TcaSchemaCapability::RestrictionDisabledField:
|
||||
$systemProperties['isDisabled'] = (bool)($rawRecord->get($fieldName));
|
||||
break;
|
||||
case TcaSchemaCapability::InternalDescription:
|
||||
$systemProperties['description'] = $rawRecord->get($fieldName);
|
||||
break;
|
||||
case TcaSchemaCapability::SortByField:
|
||||
$systemProperties['sorting'] = (int)($rawRecord->get($fieldName));
|
||||
break;
|
||||
case TcaSchemaCapability::RestrictionUserGroup:
|
||||
$systemProperties['userGroupRestriction'] = GeneralUtility::intExplode(
|
||||
',',
|
||||
$rawRecord->get($fieldName),
|
||||
true
|
||||
);
|
||||
break;
|
||||
}
|
||||
unset($properties[$fieldName]);
|
||||
}
|
||||
|
||||
$systemProperties = new SystemProperties(
|
||||
$systemProperties['language'] ?? null,
|
||||
$systemProperties['version'] ?? null,
|
||||
$systemProperties['isDeleted'] ?? null,
|
||||
$systemProperties['isDisabled'] ?? null,
|
||||
$systemProperties['isLockedForEditing'] ?? null,
|
||||
$systemProperties['createdAt'] ?? null,
|
||||
$systemProperties['lastUpdatedAt'] ?? null,
|
||||
$systemProperties['publishAt'] ?? null,
|
||||
$systemProperties['publishUntil'] ?? null,
|
||||
$systemProperties['userGroupRestriction'] ?? null,
|
||||
$systemProperties['sorting'] ?? null,
|
||||
$systemProperties['description'] ?? null,
|
||||
);
|
||||
return [$properties, $systemProperties];
|
||||
}
|
||||
}
|
||||
@@ -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\Domain;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
|
||||
|
||||
/**
|
||||
* An interface for database / TCA records.
|
||||
*/
|
||||
interface RecordInterface extends ContainerInterface
|
||||
{
|
||||
public function getUid(): int;
|
||||
public function getPid(): int;
|
||||
|
||||
/**
|
||||
* The full type contains the type of the record (e.g. "be_users", which is usually the TCA table)
|
||||
* and the subtype of the record (such as "textpic" in tt_content records) separated by a ".".
|
||||
*/
|
||||
public function getFullType(): string;
|
||||
|
||||
/**
|
||||
* The type contains the subtype of the record (such as "textpic"). Returns null if there is
|
||||
* no "subtype".
|
||||
*/
|
||||
public function getRecordType(): ?string;
|
||||
|
||||
/**
|
||||
* This is the TCA table for the record, all in lowercase.
|
||||
*/
|
||||
public function getMainType(): string;
|
||||
|
||||
public function toArray(): array;
|
||||
|
||||
public function getRawRecord(): ?RawRecord;
|
||||
|
||||
public function getComputedProperties(): ComputedProperties;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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\Domain;
|
||||
|
||||
/**
|
||||
* Used to initialize a record once a property is accessed
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class RecordPropertyClosure
|
||||
{
|
||||
public function __construct(
|
||||
private \Closure $instantiator
|
||||
) {}
|
||||
|
||||
public function instantiate(): mixed
|
||||
{
|
||||
return ($this->instantiator)();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user