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,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\DataHandling\Localization;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Entity for data-map item.
*
* @internal
*/
class DataMapItem
{
public const TYPE_PARENT = 'parent';
public const TYPE_DIRECT_CHILD = 'directChild';
public const TYPE_GRAND_CHILD = 'grandChild';
public const SCOPE_PARENT = State::STATE_PARENT;
public const SCOPE_SOURCE = State::STATE_SOURCE;
public const SCOPE_EXCLUDE = 'exclude';
protected string $tableName;
protected string|int $id;
protected int $liveId;
protected array $suggestedValues;
protected array $persistedValues;
protected array $configurationFieldNames;
protected bool $new;
protected ?string $type = null;
protected ?State $state = null;
protected string|int $language = 0;
protected string|int $parent = '';
protected string|int|null $source = null;
/** @var DataMapItem[][] */
protected array $dependencies = [];
/**
* Builds a data-map item. In addition to the constructor, the values
* for language, parent and source record pointers are assigned as well.
*/
public static function build(
string $tableName,
string|int $id,
array $suggestedValues,
array $persistedValues,
array $configurationFieldNames
): static {
$item = GeneralUtility::makeInstance(
static::class,
$tableName,
$id,
$suggestedValues,
$persistedValues,
$configurationFieldNames
);
// assign language specific settings of item
$item->language = (int)($suggestedValues[$item->getLanguageFieldName()] ?? $persistedValues[$item->getLanguageFieldName()] ?? 0);
$item->setParent($suggestedValues[$item->getParentFieldName()] ?? $persistedValues[$item->getParentFieldName()] ?? '');
if ($item->getSourceFieldName() !== null) {
$item->setSource($suggestedValues[$item->getSourceFieldName()] ?? $persistedValues[$item->getSourceFieldName()] ?? '');
}
// assign live-id of item if available
$versionSourceFieldName = $item->getVersionSourceFieldName();
if ($versionSourceFieldName !== null && !empty($persistedValues[$versionSourceFieldName])) {
$item->liveId = $persistedValues[$versionSourceFieldName];
}
return $item;
}
public function __construct(
string $tableName,
string|int $id,
array $suggestedValues,
array $persistedValues,
array $configurationFieldNames
) {
$this->tableName = $tableName;
$this->id = $id;
$this->suggestedValues = $suggestedValues;
$this->persistedValues = $persistedValues;
$this->configurationFieldNames = $configurationFieldNames;
$this->new = !MathUtility::canBeInterpretedAsInteger($id);
}
/**
* Gets the current table name of this data-map item.
*/
public function getTableName(): string
{
return $this->tableName;
}
/**
* Gets the id of this data-map item.
*/
public function getId(): string|int
{
return $this->id;
}
public function getLiveId(): int|string
{
return $this->liveId ?? $this->id;
}
/**
* Gets the suggested values that were initially
* submitted as the whole data-map to the DataHandler.
*/
public function getSuggestedValues(): array
{
return $this->suggestedValues;
}
/**
* Gets the persisted values that represent the persisted state
* of the record this data-map item is a surrogate for - does only
* contain relevant field values.
*/
public function getPersistedValues(): array
{
return $this->persistedValues;
}
public function getConfigurationFieldNames(): array
{
return $this->configurationFieldNames;
}
public function getLanguageFieldName(): string
{
return $this->configurationFieldNames['language'];
}
public function getParentFieldName(): string
{
return $this->configurationFieldNames['parent'];
}
public function getSourceFieldName(): ?string
{
return $this->configurationFieldNames['source'] ?? null;
}
protected function getVersionSourceFieldName(): ?string
{
return $this->configurationFieldNames['versionSource'] ?? null;
}
public function isNew(): bool
{
return $this->new;
}
public function getType(): string
{
if ($this->type === null) {
// implicit: default language, it's a parent
if ($this->language === 0) {
$this->type = static::TYPE_PARENT;
} elseif (
// implicit: having source value different to parent value, it's a 2nd or higher level translation
$this->source !== 0
&& $this->source !== null
&& $this->source !== $this->parent
) {
$this->type = static::TYPE_GRAND_CHILD;
} else {
// implicit: otherwise, it's a 1st level translation
$this->type = static::TYPE_DIRECT_CHILD;
}
}
return $this->type;
}
public function isParentType(): bool
{
return $this->getType() === static::TYPE_PARENT;
}
public function isDirectChildType(): bool
{
return $this->getType() === static::TYPE_DIRECT_CHILD;
}
public function isGrandChildType(): bool
{
return $this->getType() === static::TYPE_GRAND_CHILD;
}
public function getState(): ?State
{
if ($this->state === null && !$this->isParentType()) {
$this->state = $this->buildState();
}
return $this->state;
}
public function getLanguage(): string|int
{
return $this->language;
}
public function setLanguage(string|int $language): void
{
$this->language = $language;
}
public function getParent(): string|int
{
return $this->parent;
}
public function setParent(string|int $parent): void
{
$this->parent = $this->extractId($parent);
}
public function getSource(): string|int|null
{
return $this->source;
}
public function setSource(string|int $source): void
{
$this->source = $this->extractId($source);
}
public function getIdForScope(string $scope): string|int
{
if (
$scope === static::SCOPE_PARENT
|| $scope === static::SCOPE_EXCLUDE
) {
return $this->getParent();
}
if ($scope === static::SCOPE_SOURCE) {
// $source is guaranteed to be non-null when SCOPE_SOURCE is applicable:
// getApplicableScopes() only includes it when getSourceFieldName() !== null,
// which means setSource() was called during build().
return $this->source;
}
throw new \RuntimeException('Invalid scope', 1486325248);
}
/**
* @return DataMapItem[][]
*/
public function getDependencies(): array
{
return $this->dependencies;
}
/**
* @param DataMapItem[][] $dependencies
*/
public function setDependencies(array $dependencies): void
{
$this->dependencies = $dependencies;
}
/**
* @return DataMapItem[]
*/
public function findDependencies(string $scope): array
{
return $this->dependencies[$scope] ?? [];
}
/**
* @return string[]
*/
public function getApplicableScopes(): array
{
$scopes = [];
if (!empty($this->getSourceFieldName())) {
$scopes[] = static::SCOPE_SOURCE;
}
$scopes[] = static::SCOPE_PARENT;
$scopes[] = static::SCOPE_EXCLUDE;
return $scopes;
}
/**
* Extracts real id from provided id-value, which can either be a real
* integer value, a 'NEW...' id, or a combined identifier 'tt_content_13'.
*/
protected function extractId(int|string $idValue): int|string
{
if (MathUtility::canBeInterpretedAsInteger($idValue)) {
return $idValue;
}
$idValue = (string)$idValue;
if (str_starts_with($idValue, 'NEW')) {
return $idValue;
}
// @todo Handle if $tableName does not match $this->tableName
$id = BackendUtility::splitTable_Uid($idValue)[1];
return $id;
}
protected function buildState(): ?State
{
// build from persisted states
if (!$this->isNew()) {
$state = State::fromJSON(
$this->tableName,
$this->persistedValues['l10n_state'] ?? null
);
} elseif (is_string($this->suggestedValues['l10n_state'] ?? null)) {
// use provided states for a new and copied element
$state = State::fromJSON(
$this->tableName,
$this->suggestedValues['l10n_state']
);
} else {
// provide the default states
$state = State::create($this->tableName);
}
// switch "custom" to "source" state for 2nd level translations
if ($this->isNew() && $this->isGrandChildType()) {
$state->updateStates(State::STATE_CUSTOM, State::STATE_SOURCE);
}
// apply any provided updates to the states
if (is_array($this->suggestedValues['l10n_state'] ?? null)) {
$state->update($this->suggestedValues['l10n_state']);
}
return $state;
}
}
File diff suppressed because it is too large Load Diff
+240
View File
@@ -0,0 +1,240 @@
<?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\Localization;
use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Value object for l10n_state field value.
*
* @internal
*/
class State
{
public const STATE_CUSTOM = 'custom';
public const STATE_PARENT = 'parent';
public const STATE_SOURCE = 'source';
protected const VALID_STATES = [
self::STATE_CUSTOM,
self::STATE_SOURCE,
self::STATE_PARENT,
];
protected string $tableName;
protected array $states;
protected array $originalStates;
public static function create(string $tableName): ?State
{
if (!static::isApplicable($tableName)) {
return null;
}
return GeneralUtility::makeInstance(
static::class,
$tableName
);
}
public static function fromJSON(string $tableName, ?string $json = null): ?State
{
if (!static::isApplicable($tableName)) {
return null;
}
$states = json_decode($json ?? '', true);
return GeneralUtility::makeInstance(
static::class,
$tableName,
$states ?? []
);
}
public static function isApplicable(string $tableName): bool
{
$schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
return $schemaFactory->has($tableName)
&& $schemaFactory->get($tableName)->isLanguageAware()
&& count(static::getFieldNames($tableName)) > 0;
}
/**
* @return string[]
*/
public static function getFieldNames(string $tableName): array
{
$schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
if (!$schemaFactory->has($tableName)) {
return [];
}
return array_map(
static fn(FieldTypeInterface $field) => $field->getName(),
iterator_to_array(
$schemaFactory->get($tableName)->getFields(
static fn(FieldTypeInterface $field): bool => !empty($field->getConfiguration()['behaviour']['allowLanguageSynchronization'])
)
)
);
}
public function __construct(string $tableName, array $states = [])
{
$this->tableName = $tableName;
$this->states = $states;
$this->originalStates = $states;
$this->states = $this->enrich(
$this->sanitize($states)
);
}
public function update(array $states): void
{
$this->states = array_merge(
$this->states,
$this->sanitize($states)
);
}
/**
* Updates field names having a particular state to a target state.
*/
public function updateStates(string $currentState, string $targetState): void
{
$states = [];
foreach ($this->filterFieldNames($currentState) as $fieldName) {
$states[$fieldName] = $targetState;
}
if (!empty($states)) {
$this->update($states);
}
}
public function export(): string|false|null
{
if (empty($this->states)) {
return null;
}
return json_encode($this->states);
}
public function toArray(): array
{
return $this->states;
}
/**
* @return string[]
*/
public function getModifiedFieldNames(): array
{
return array_keys(
array_diff_assoc(
$this->states,
$this->originalStates
)
);
}
public function isModified(): bool
{
return !empty($this->getModifiedFieldNames());
}
public function isUndefined(string $fieldName): bool
{
return !isset($this->states[$fieldName]);
}
public function isCustomState(string $fieldName): bool
{
return ($this->states[$fieldName] ?? null) === static::STATE_CUSTOM;
}
public function isParentState(string $fieldName): bool
{
return ($this->states[$fieldName] ?? null) === static::STATE_PARENT;
}
public function isSourceState(string $fieldName): bool
{
return ($this->states[$fieldName] ?? null) === static::STATE_SOURCE;
}
public function getState(string $fieldName): ?string
{
return $this->states[$fieldName] ?? null;
}
/**
* Filters field names having a desired state.
*
* @return string[]
*/
public function filterFieldNames(string $desiredState, bool $modified = false): array
{
if (!$modified) {
$fieldNames = array_keys($this->states);
} else {
$fieldNames = $this->getModifiedFieldNames();
}
return array_filter(
$fieldNames,
function (string $fieldName) use ($desiredState): bool {
return $this->states[$fieldName] === $desiredState;
}
);
}
/**
* Filter out field names that don't exist in TCA.
*
* @return string[]
*/
protected function sanitize(array $states): array
{
$fieldNames = static::getFieldNames($this->tableName);
return array_intersect_key(
$states,
array_combine($fieldNames, $fieldNames) ?: []
);
}
/**
* Add missing states for field names.
*/
protected function enrich(array $states): array
{
foreach (static::getFieldNames($this->tableName) as $fieldName) {
$isValid = in_array(
$states[$fieldName] ?? null,
static::VALID_STATES,
true
);
if ($isValid) {
continue;
}
$states[$fieldName] = static::STATE_PARENT;
}
return $states;
}
}
@@ -0,0 +1,112 @@
<?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\Localization;
use TYPO3\CMS\Core\DataHandling\PlainDataResolver;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Version to live id map.
*
* This is on a per-workspace and per-table setup, so it is recommended
* to use a runtime cache to hold these instances.
*
* @internal
*/
class VersionToLiveIdMap
{
protected string $tableName;
protected int $workspaceId;
/**
* @var int[]
*/
protected array $map = [];
public function __construct(string $tableName, int $workspaceId)
{
$this->tableName = $tableName;
$this->workspaceId = $workspaceId;
}
/**
* @param int[] $ids
*/
public function update(array $ids): self
{
$ids = array_map(intval(...), $ids);
$candidateIds = array_diff(
$ids,
array_keys($this->map),
array_values($this->map)
);
if (empty($candidateIds)) {
return $this;
}
$candidateIdMap = array_combine($candidateIds, $candidateIds);
$schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
if (
!$schemaFactory->has($this->tableName)
|| $this->workspaceId === 0
|| !$schemaFactory->get($this->tableName)->isWorkspaceAware()
) {
$this->map += $candidateIdMap;
return $this;
}
$plainDataResolver = GeneralUtility::makeInstance(
PlainDataResolver::class,
$this->tableName,
[]
);
$plainDataResolver->setWorkspaceId($this->workspaceId);
$plainDataResolver->setKeepLiveIds(true);
$versionLiveIdMap = $plainDataResolver->applyLiveIds($candidateIdMap);
$this->map += $versionLiveIdMap;
return $this;
}
public function getVersionId(int $liveId): int
{
$versionId = array_search($liveId, $this->map, true);
return $versionId ?: $liveId;
}
public function getLiveId(int $versionId): string|int
{
return $this->map[$versionId] ?? $versionId;
}
/**
* @param int[] $versionIds
* @return int[]
*/
public function getLiveIds(array $versionIds): array
{
return array_map(
function (int $versionId) {
return $this->getLiveId($versionId);
},
$versionIds
);
}
}