TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -0,0 +1,149 @@
<?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\DataHandling\Model;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* CorrelationId representation
*
* @todo Check internal state during v10 development
* @internal
*/
class CorrelationId implements \JsonSerializable
{
protected const DEFAULT_VERSION = 1;
protected const PATTERN_V1 = '#^(?P<flags>[[:xdigit:]]{4})\$(?:(?P<scope>[[:alnum:]_-]+):)?(?P<subject>[[:alnum:]_-]+)(?P<aspects>(?:\/[[:alnum:]._-]+)*)$#';
protected int $version = self::DEFAULT_VERSION;
protected ?string $scope = null;
protected int $capabilities = 0;
protected ?string $subject = null;
/**
* @var string[]
*/
protected array $aspects = [];
public static function forScope(string $scope): self
{
$target = static::create();
$target->scope = $scope;
return $target;
}
public static function forSubject(string $subject, string ...$aspects): self
{
return static::create()
->withSubject($subject)
->withAspects(...$aspects);
}
public static function fromString(string $correlationId): self
{
if (!preg_match(self::PATTERN_V1, $correlationId, $matches, PREG_UNMATCHED_AS_NULL)) {
throw new \InvalidArgumentException('Unknown format', 1569620858);
}
$flags = hexdec($matches['flags']);
$aspects = $matches['aspects'] === '' ? [] : explode('/', ltrim($matches['aspects'], '/'));
$target = static::create()
->withSubject($matches['subject'])
->withAspects(...$aspects);
$target->scope = $matches['scope'] ?? null;
$target->version = $flags >> 10;
$target->capabilities = $flags & ((1 << 10) - 1);
return $target;
}
protected static function create(): self
{
return GeneralUtility::makeInstance(static::class);
}
public function __toString(): string
{
if ($this->subject === null) {
throw new \LogicException('Cannot serialize for empty subject', 1569668681);
}
return $this->serialize();
}
public function jsonSerialize(): string
{
return (string)$this;
}
public function withSubject(string $subject): self
{
if ($this->subject === $subject) {
return $this;
}
$target = clone $this;
$target->subject = $subject;
return $target;
}
public function withAspects(string ...$aspects): self
{
if ($this->aspects === $aspects) {
return $this;
}
$target = clone $this;
$target->aspects = $aspects;
return $target;
}
public function getScope(): ?string
{
return $this->scope;
}
public function getSubject(): ?string
{
return $this->subject;
}
/**
* @return string[]
*/
public function getAspects(): array
{
return $this->aspects;
}
/**
* v1 specs (eBNF)
* + FLAGS "$" [ SCOPE ":" ] SUBJECT { "/" ASPECT }
* + FLAGS ::= XDIGIT (* 16-bit integer big-endian)
* + SCOPE ::= ALNUM { ALNUM }
* + SUBJECT ::= ALNUM { ALNUM }
* + ASPECT ::= ( ALNUM | '.' | '_' | '-' ) { ( ALNUM | '.' | '_' | '-' ) }
*/
protected function serialize(): string
{
// 6-bit version 10-bit capabilities
$flags = $this->version << 10 + $this->capabilities;
return sprintf(
'%s$%s%s%s',
bin2hex(pack('n', $flags)),
$this->scope ? $this->scope . ':' : '',
$this->subject,
$this->aspects ? '/' . implode('/', $this->aspects) : ''
);
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\DataHandling\Model;
/**
* Represents the context of an entity
*
* A context defines a "variant" of an entity, currently by its language and workspace assignment. The EntityContext
* is bound to a RecordState.
*/
class EntityContext
{
/**
* @var int
*/
protected $workspaceId = 0;
/**
* @var int
*/
protected $languageId = 0;
public function getWorkspaceId(): int
{
return $this->workspaceId;
}
/**
* @return static
*/
public function withWorkspaceId(int $workspaceId): self
{
if ($this->workspaceId === $workspaceId) {
return $this;
}
$target = clone $this;
$target->workspaceId = $workspaceId;
return $target;
}
public function getLanguageId(): int
{
return $this->languageId;
}
/**
* @return static
*/
public function withLanguageId(int $languageId): self
{
if ($this->languageId === $languageId) {
return $this;
}
$target = clone $this;
$target->languageId = $languageId;
return $target;
}
}
@@ -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\DataHandling\Model;
/**
* Interface describing pointers to an entity
*/
interface EntityPointer
{
public function getName(): string;
public function getIdentifier(): string;
public function isNode(): bool;
public function isEqualTo(EntityPointer $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\DataHandling\Model;
/**
* An EntityPointerLink is used to connect EntityPointer instances
*/
class EntityPointerLink
{
/**
* @var EntityPointer
*/
protected $subject;
/**
* @var EntityPointerLink|null
*/
protected $ancestor;
public function __construct(EntityPointer $subject)
{
$this->subject = $subject;
}
public function getSubject(): EntityPointer
{
return $this->subject;
}
public function getHead(): EntityPointerLink
{
$head = $this;
while ($head->ancestor !== null) {
$head = $head->ancestor;
}
return $head;
}
public function getAncestor(): ?EntityPointerLink
{
return $this->ancestor;
}
public function withAncestor(EntityPointerLink $ancestor): self
{
if ($this->ancestor === $ancestor) {
return $this;
}
$target = clone $this;
$target->ancestor = $ancestor;
return $target;
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\DataHandling\Model;
/**
* The EntityUidPointer represents the concrete origin of the entity
*/
class EntityUidPointer implements EntityPointer
{
/**
* @var string
*/
protected $name;
/**
* @var string
*/
protected $identifier;
public function __construct(string $name, string $identifier)
{
$this->name = $name;
$this->identifier = $identifier;
}
public function getName(): string
{
return $this->name;
}
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* @return static
*/
public function withUid(string $identifier): self
{
if ($this->identifier === $identifier) {
return $this;
}
$target = clone $this;
$target->identifier = $identifier;
return $target;
}
public function isNode(): bool
{
return $this->name === 'pages';
}
public function isEqualTo(EntityPointer $other): bool
{
return $this->identifier === $other->getIdentifier()
&& $this->name === $other->getName();
}
}
+178
View File
@@ -0,0 +1,178 @@
<?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\DataHandling\Model;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* A RecordState is an abstract description of a record that consists of
*
* - an EntityContext describing the "variant" of a record
* - an EntityPointer that describes the node where the record is stored
* - an EntityUidPointer of the record the RecordState instance represents
*
* Instances of this class are created by the RecordStateFactory.
*/
class RecordState
{
/**
* @var EntityContext
*/
protected $context;
/**
* @var EntityPointer
*/
protected $node;
/**
* @var EntityUidPointer
*/
protected $subject;
/**
* @var EntityPointerLink|null
*/
protected $languageLink;
/**
* @var EntityPointerLink|null
*/
protected $versionLink;
public function __construct(EntityContext $context, EntityPointer $node, EntityUidPointer $subject)
{
$this->context = $context;
$this->node = $node;
$this->subject = $subject;
}
public function getContext(): EntityContext
{
return $this->context;
}
public function getNode(): EntityPointer
{
return $this->node;
}
public function getSubject(): EntityUidPointer
{
return $this->subject;
}
/**
* @return EntityPointerLink
*/
public function getLanguageLink(): ?EntityPointerLink
{
return $this->languageLink;
}
/**
* @return static
*/
public function withLanguageLink(?EntityPointerLink $languageLink): self
{
if ($this->languageLink === $languageLink) {
return $this;
}
$target = clone $this;
$target->languageLink = $languageLink;
return $target;
}
/**
* @return EntityPointerLink
*/
public function getVersionLink(): ?EntityPointerLink
{
return $this->versionLink;
}
/**
* @return static
*/
public function withVersionLink(?EntityPointerLink $versionLink): self
{
if ($this->versionLink === $versionLink) {
return $this;
}
$target = clone $this;
$target->versionLink = $versionLink;
return $target;
}
public function isNew(): bool
{
return !MathUtility::canBeInterpretedAsInteger(
$this->subject->getIdentifier()
);
}
/**
* Resolves node identifier (`pid`) of current subject. For translated pages
* that would result in the `uid` of the outer-most language parent page
* otherwise it's the `pid` of the current subject.
*
* Example:
* + pages: uid: 10, pid: 5, sys_language_uid: 0, l10n_parent: 0 -> returns 5
* + pages: uid: 11, pid: 5, sys_language_uid: 1, l10n_parent: 10 -> returns 10
* + other: uid: 12, pid: 10 -> returns 10
*/
public function resolveNodeIdentifier(): string
{
if ($this->subject->isNode()
&& $this->context->getLanguageId() > 0
&& $this->languageLink !== null
) {
return $this->languageLink->getHead()->getSubject()->getIdentifier();
}
return $this->node->getIdentifier();
}
/**
* Resolves node identifier used as aggregate for current subject. For translated
* pages that would result in the `uid` of the outer-most language parent page,
* for pages it's the identifier of the current subject, otherwise it's
* the `pid` of the current subject.
*
* Example:
* + pages: uid: 10, pid: 5, sys_language_uid: 0, l10n_parent: 0 -> returns 10
* + pages: uid: 11, pid: 5, sys_language_uid: 1, l10n_parent: 10 -> returns 10
* + pages in version, return online page ID
* + other: uid: 12, pid: 10 -> returns 10
*/
public function resolveNodeAggregateIdentifier(): string
{
if ($this->subject->isNode()
&& $this->context->getLanguageId() > 0
&& $this->languageLink !== null
) {
return $this->languageLink->getHead()->getSubject()->getIdentifier();
}
if ($this->subject->isNode() && $this->versionLink) {
return $this->versionLink->getHead()->getSubject()->getIdentifier();
}
if ($this->subject->isNode()) {
return $this->subject->getIdentifier();
}
return $this->node->getIdentifier();
}
}
@@ -0,0 +1,159 @@
<?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\DataHandling\Model;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Factory class that creates a record state
*/
class RecordStateFactory
{
protected string $name;
public static function forName(string $name): self
{
return GeneralUtility::makeInstance(static::class, $name);
}
public function __construct(string $name)
{
$this->name = $name;
}
/**
* @param int|string|null $pageId
* @param int|string|null $recordId
*/
public function fromArray(array $data, $pageId = null, $recordId = null): RecordState
{
$pageId = $pageId ?? $data['pid'] ?? null;
$recordId = $recordId ?? $data['uid'] ?? null;
$aspectFieldValues = $this->resolveAspectFieldValues($data);
$context = GeneralUtility::makeInstance(EntityContext::class)
->withWorkspaceId($aspectFieldValues['workspace'])
->withLanguageId($aspectFieldValues['language']);
$node = $this->createEntityPointer($pageId, 'pages');
$subject = $this->createEntityPointer($recordId);
$target = GeneralUtility::makeInstance(
RecordState::class,
$context,
$node,
$subject
);
return $target
->withLanguageLink($this->resolveLanguageLink($aspectFieldValues))
->withVersionLink($this->resolveVersionLink($aspectFieldValues));
}
/**
* @return array<string, string|null>
*/
protected function resolveAspectFieldNames(): array
{
$schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($this->name);
$languageCapability = null;
if ($schema->isLanguageAware()) {
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
}
return [
'workspace' => 't3ver_wsid',
'versionParent' => 't3ver_oid',
'language' => $languageCapability?->getLanguageField()->getName(),
'languageParent' => $languageCapability?->getTranslationOriginPointerField()->getName(),
'languageSource' => $languageCapability?->getTranslationSourceField()?->getName(),
];
}
protected function resolveAspectFieldValues(array $data): array
{
return array_map(
static function (?string $aspectFieldName) use ($data): int {
return (int)($data[$aspectFieldName ?? ''] ?? 0);
},
$this->resolveAspectFieldNames()
);
}
protected function resolveLanguageLink(array $aspectFieldNames): ?EntityPointerLink
{
$languageSourceLink = null;
$languageParentLink = null;
if (!empty($aspectFieldNames['languageSource'])) {
$languageSourceLink = GeneralUtility::makeInstance(
EntityPointerLink::class,
$this->createEntityPointer($aspectFieldNames['languageSource'])
);
}
if (!empty($aspectFieldNames['languageParent'])) {
$languageParentLink = GeneralUtility::makeInstance(
EntityPointerLink::class,
$this->createEntityPointer($aspectFieldNames['languageParent'])
);
}
if (empty($languageSourceLink) || empty($languageParentLink)
|| $languageSourceLink->getSubject()->isEqualTo(
$languageParentLink->getSubject()
)
) {
return $languageSourceLink ?? $languageParentLink ?? null;
}
return $languageSourceLink->withAncestor($languageParentLink);
}
protected function resolveVersionLink(array $aspectFieldNames): ?EntityPointerLink
{
if (!empty($aspectFieldNames['versionParent'])) {
return GeneralUtility::makeInstance(
EntityPointerLink::class,
$this->createEntityPointer($aspectFieldNames['versionParent'])
);
}
return null;
}
/**
* @param string|int|null $identifier
* @param string|null $name
* @throws \LogicException
*/
protected function createEntityPointer($identifier, ?string $name = null): EntityPointer
{
if ($identifier === null) {
throw new \LogicException(
'Cannot create null pointer',
1536407967
);
}
$identifier = (string)$identifier;
return GeneralUtility::makeInstance(
EntityUidPointer::class,
$name ?? $this->name,
$identifier
);
}
}