diff --git a/typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php b/typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php index b4e40b370200dab88c8c55a4f7eda7fc7d90498c..d684a70efdd372bd82ff501bcf0cea34f02ed71a 100644 --- a/typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php +++ b/typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php @@ -61,6 +61,15 @@ class InlineControlContainer extends AbstractContainer */ protected $requireJsModules = []; + /** + * @var array Default wizards + */ + protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], + ]; + /** * Container objects give $nodeFactory down to other containers. * @@ -295,6 +304,11 @@ class InlineControlContainer extends AbstractContainer $html .= '</div>'; + $fieldWizardResult = $this->renderfieldWizard(); + $fieldWizardHtml = $fieldWizardResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false); + $html .= $fieldWizardHtml; + // Add the level links after all child records: if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom') { $html .= $levelLinks . $localizationLinks; diff --git a/typo3/sysext/backend/Classes/Form/Element/CheckboxElement.php b/typo3/sysext/backend/Classes/Form/Element/CheckboxElement.php index 913ef29d74249133fd04950984d27bd732f1bf8a..b697f15984519a7dbd8582a76bc91c7d39443406 100644 --- a/typo3/sysext/backend/Classes/Form/Element/CheckboxElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/CheckboxElement.php @@ -27,8 +27,14 @@ class CheckboxElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/GroupElement.php b/typo3/sysext/backend/Classes/Form/Element/GroupElement.php index b5af2d5049f481f4871c6393e0623e608827e693..846396ecfb425af219b2600a025b5b2cec66f91a 100644 --- a/typo3/sysext/backend/Classes/Form/Element/GroupElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/GroupElement.php @@ -82,9 +82,13 @@ class GroupElement extends AbstractFormElement 'renderType' => 'fileUpload', 'after' => [ 'recordsOverview' ], ], + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + 'after' => [ 'fileUpload' ], + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', - 'after' => [ 'fileUpload' ], + 'after' => [ 'localizationStateSelector' ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php b/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php index ea07e6e95efc55b873681140e77369a0ae058eb5..bcf866a8fa98c71aa4770dfaffc677c1fcb7789c 100644 --- a/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php @@ -34,8 +34,14 @@ class ImageManipulationElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/InputColorPickerElement.php b/typo3/sysext/backend/Classes/Form/Element/InputColorPickerElement.php index f7c83a850fe25bb78896557b3d3b6c38332a09bd..eb7c8b69ff71d50e31debbc3ad4f49b803dea27a 100644 --- a/typo3/sysext/backend/Classes/Form/Element/InputColorPickerElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/InputColorPickerElement.php @@ -30,8 +30,14 @@ class InputColorPickerElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/InputDateTimeElement.php b/typo3/sysext/backend/Classes/Form/Element/InputDateTimeElement.php index 2002f1b7aa4923b618c7a850d3577a9788c669f7..6b291bd76aa551db73782219aba8f7c220745a59 100644 --- a/typo3/sysext/backend/Classes/Form/Element/InputDateTimeElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/InputDateTimeElement.php @@ -31,8 +31,14 @@ class InputDateTimeElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/InputLinkElement.php b/typo3/sysext/backend/Classes/Form/Element/InputLinkElement.php index e66bca8b7033ea893f1a558e9e46b975f01c5b99..f67b5b7b8644300842c64984e295ce0b067909bf 100644 --- a/typo3/sysext/backend/Classes/Form/Element/InputLinkElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/InputLinkElement.php @@ -52,13 +52,19 @@ class InputLinkElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ - OtherLanguageContent::class => [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], + 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], - DefaultLanguageDifferences::class => [ + 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', 'after' => [ - OtherLanguageContent::class + 'otherLanguageContent', ], ], ]; diff --git a/typo3/sysext/backend/Classes/Form/Element/InputTextElement.php b/typo3/sysext/backend/Classes/Form/Element/InputTextElement.php index c147ad68bcf06740ace16cdfed4f50e16d2534ab..08badc343301db274439dd20591ef1d213f3dbfa 100644 --- a/typo3/sysext/backend/Classes/Form/Element/InputTextElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/InputTextElement.php @@ -33,8 +33,14 @@ class InputTextElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/RadioElement.php b/typo3/sysext/backend/Classes/Form/Element/RadioElement.php index 3d75f71172442a13863cb39074e14b2ff128ccb1..2eb27c858b8c38a1c6169a778cd6d4699d0870ac 100644 --- a/typo3/sysext/backend/Classes/Form/Element/RadioElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/RadioElement.php @@ -25,8 +25,14 @@ class RadioElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php b/typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php index 64838ea667f5ad643a385aa1d5539b90663d95ee..48c0825d0cf6bbca4a30cc734471e84ca9ad9965 100644 --- a/typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php @@ -33,8 +33,14 @@ class SelectCheckBoxElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/SelectMultipleSideBySideElement.php b/typo3/sysext/backend/Classes/Form/Element/SelectMultipleSideBySideElement.php index 0954b77b4af30af5e079a6174f34b3bba268418c..9cfae06b710c3a0bd22f3ae5fe9dd9dd4dfb589b 100644 --- a/typo3/sysext/backend/Classes/Form/Element/SelectMultipleSideBySideElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/SelectMultipleSideBySideElement.php @@ -55,8 +55,14 @@ class SelectMultipleSideBySideElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/SelectSingleBoxElement.php b/typo3/sysext/backend/Classes/Form/Element/SelectSingleBoxElement.php index 94a6f69a60a46dded43c4e0d430f981714b83292..005f83dd0f16d37157c9fe3c66c10ea716df4498 100644 --- a/typo3/sysext/backend/Classes/Form/Element/SelectSingleBoxElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/SelectSingleBoxElement.php @@ -42,8 +42,14 @@ class SelectSingleBoxElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/SelectSingleElement.php b/typo3/sysext/backend/Classes/Form/Element/SelectSingleElement.php index 6539009db55fbf1ca792b7a3d974727fda24673c..63319c7dc3c257224978b199caa134fa6ee6f1b4 100644 --- a/typo3/sysext/backend/Classes/Form/Element/SelectSingleElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/SelectSingleElement.php @@ -37,9 +37,15 @@ class SelectSingleElement extends AbstractFormElement 'renderType' => 'selectIcons', 'disabled' => true, ], + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + 'after' => [ + 'selectIcons', + ], + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', - 'after' => [ 'selectIcons' ], + 'after' => [ 'localizationStateSelector' ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/TextElement.php b/typo3/sysext/backend/Classes/Form/Element/TextElement.php index 44093fe74d1a68bd74c50140c0f01df898a70915..a7d20fcc2741358ca2398c2b801d883acda0a508 100644 --- a/typo3/sysext/backend/Classes/Form/Element/TextElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/TextElement.php @@ -31,8 +31,14 @@ class TextElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/Element/TextTableElement.php b/typo3/sysext/backend/Classes/Form/Element/TextTableElement.php index c3a02508fd2328727ff843ef2d9c7f7b7bd963ab..d6d1c290a629de55cb3eef9a17287875377e7035 100644 --- a/typo3/sysext/backend/Classes/Form/Element/TextTableElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/TextTableElement.php @@ -30,8 +30,14 @@ class TextTableElement extends AbstractFormElement * @var array */ protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], 'otherLanguageContent' => [ 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector' + ], ], 'defaultLanguageDifferences' => [ 'renderType' => 'defaultLanguageDifferences', diff --git a/typo3/sysext/backend/Classes/Form/FieldWizard/LocalizationStateSelector.php b/typo3/sysext/backend/Classes/Form/FieldWizard/LocalizationStateSelector.php new file mode 100644 index 0000000000000000000000000000000000000000..a7da8bd81a42850a26aa9ba345a96d8157b68631 --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/FieldWizard/LocalizationStateSelector.php @@ -0,0 +1,141 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Backend\Form\FieldWizard; + +/* + * 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! + */ + +use TYPO3\CMS\Backend\Form\AbstractNode; +use TYPO3\CMS\Core\DataHandling\Localization\State; +use TYPO3\CMS\Lang\LanguageService; + +/** + * Allows to define the localization state per field. + */ +class LocalizationStateSelector extends AbstractNode +{ + /** + * Render the radio buttons if enabled + * + * @return array Result array + */ + public function render(): array + { + $languageService = $this->getLanguageService(); + $result = $this->initializeResultArray(); + + $fieldName = $this->data['fieldName']; + $l10nStateFieldName = ''; + if (isset($l10nStateFieldName)) { + $l10nStateFieldName = 'l10n_state'; + } + if ( + !$l10nStateFieldName + || !isset($this->data['defaultLanguageRow']) + || !isset($this->data['processedTca']['columns'][$fieldName]['config']['behaviour']['allowLanguageSynchronization']) + || !$this->data['processedTca']['columns'][$fieldName]['config']['behaviour']['allowLanguageSynchronization'] + ) { + return $result; + } + + $l10nParentFieldName = $this->data['processedTca']['ctrl']['transOrigPointerField'] ?? null; + $l10nSourceFieldName = $this->data['processedTca']['ctrl']['translationSource'] ?? null; + + $sourceLanguageTitle = ''; + $fieldValueInParentRow = ''; + $fieldValueInSourceRow = ''; + if ($l10nParentFieldName && $this->data['databaseRow'][$l10nParentFieldName] > 0) { + if ($l10nSourceFieldName && $this->data['databaseRow'][$l10nSourceFieldName] > 0) { + $languageField = $this->data['processedTca']['ctrl']['languageField'] ?? null; + if ($languageField + && isset($this->data['sourceLanguageRow'][$languageField]) + && $this->data['sourceLanguageRow'][$languageField] > 0 + ) { + $languageUidOfSourceRow = $this->data['sourceLanguageRow'][$languageField]; + $sourceLanguageTitle = $this->data['systemLanguageRows'][$languageUidOfSourceRow]['title'] ?? ''; + $fieldValueInSourceRow = $this->data['sourceLanguageRow'][$fieldName] ?? null; + } + } + $fieldValueInParentRow = (string)$this->data['defaultLanguageRow'][$fieldName]; + } + + $localizationState = State::fromJSON( + $this->data['tableName'], + $this->data['databaseRow'][$l10nStateFieldName] ?? null + ); + + $fieldElementName = 'data[' . htmlspecialchars($this->data['tableName']) . ']' + . '[' . (int)($this->data['databaseRow']['uid']) . ']' + . '[' . htmlspecialchars($l10nStateFieldName) . ']' + . '[' . htmlspecialchars($this->data['fieldName']) . ']'; + + $html = []; + $html[] = '<div class="t3js-l10n-state-container">'; + $html[] = '<div>'; + $html[] = '<strong>'; + $html[] = $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.header'); + $html[] = '</strong>'; + $html[] = '</div>'; + $html[] = '<div class="radio radio-inline">'; + $html[] = '<label>'; + $html[] = '<input'; + $html[] = ' type="radio"'; + $html[] = ' name="' . htmlspecialchars($fieldElementName) . '"'; + $html[] = ' class="t3js-l10n-state-custom"'; + $html[] = ' value="custom"'; + $html[] = $localizationState->isCustomState($fieldName) ? ' checked="checked"' : ''; + $html[] = ' data-original-language-value=""'; + $html[] = '>'; + $html[] = $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.customValue'); + $html[] = '</label>'; + $html[] = '</div>'; + $html[] = '<div class="radio radio-inline">'; + $html[] = '<label>'; + $html[] = '<input'; + $html[] = ' type="radio"'; + $html[] = ' name="' . htmlspecialchars($fieldElementName) . '"'; + $html[] = ' value="parent"'; + $html[] = $localizationState->isParentState($fieldName) ? ' checked="checked"' : ''; + $html[] = ' data-original-language-value="' . htmlspecialchars((string)$fieldValueInParentRow) . '"'; + $html[] = '>'; + $html[] = $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.defaultLanguageValue'); + $html[] = '</label>'; + $html[] = '</div>'; + if ($fieldValueInSourceRow) { + $html[] = '<div class="radio radio-inline">'; + $html[] = '<label>'; + $html[] = '<input'; + $html[] = ' type="radio"'; + $html[] = ' name="' . htmlspecialchars($fieldElementName) . '"'; + $html[] = ' value="source"'; + $html[] = $localizationState->isSourceState($fieldName) ? ' checked="checked"' : ''; + $html[] = ' data-original-language-value="' . htmlspecialchars((string)$fieldValueInSourceRow) . '"'; + $html[] = '>'; + $html[] = sprintf($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.sourceLanguageValue'), htmlspecialchars($sourceLanguageTitle)); + $html[] = '</label>'; + $html[] = '</div>'; + } + $html[] = '</div>'; + + $result['html'] = implode(LF, $html); + return $result; + } + + /** + * @return LanguageService + */ + protected function getLanguageService() + { + return $GLOBALS['LANG']; + } +} diff --git a/typo3/sysext/backend/Classes/Form/FormDataCompiler.php b/typo3/sysext/backend/Classes/Form/FormDataCompiler.php index 4bc0ad04cda92de26404c53931f6e90320232395..4e572fc1c9426638a1cede9dd3ed175571ecdc46 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataCompiler.php +++ b/typo3/sysext/backend/Classes/Form/FormDataCompiler.php @@ -187,6 +187,9 @@ class FormDataCompiler 'pageLanguageOverlayRows' => [], // If the handled row is a localized row, this entry hold the default language row array 'defaultLanguageRow' => null, + // If the handled row is a localived row and $TCA[<tableName>]['ctrl']['translationSource'] is configured, + // This entry holds the row of the language source record. + 'sourceLanguageRow' => null, // If the handled row is a localized row and a transOrigDiffSourceField is defined, this // is the unserialized version of it. The diff source field is basically a shadow version // of the default language record at the time when the language overlay record was created. diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseLanguageRows.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseLanguageRows.php index 74b4be8e8657726c883167029e6381274cc4708a..34f0ea48c4480ff78dc1287fe87aa2319c7c6319 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseLanguageRows.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseLanguageRows.php @@ -103,6 +103,22 @@ class DatabaseLanguageRows implements FormDataProviderInterface } } } + + // @todo do that only if l10n_parent > 0 (not in "free mode")? + if (!empty($result['processedTca']['ctrl']['translationSource']) + && is_string($result['processedTca']['ctrl']['translationSource']) + ) { + $translationSourceFieldName = $result['processedTca']['ctrl']['translationSource']; + if (isset($result['databaseRow'][$translationSourceFieldName]) + && $result['databaseRow'][$translationSourceFieldName] > 0 + ) { + $uidOfTranslationSource = $result['databaseRow'][$translationSourceFieldName]; + $result['sourceLanguageRow'] = $this->getRecordWorkspaceOverlay( + $result['tableName'], + $uidOfTranslationSource + ); + } + } } } diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRecordTypeValue.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRecordTypeValue.php index 90fdb5bde6f83edcb9fe7cc3151453814c0dd419..2e2f4c4f35f7a74bc177fae30815ef0196682337 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRecordTypeValue.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRecordTypeValue.php @@ -64,7 +64,7 @@ class DatabaseRecordTypeValue implements FormDataProviderInterface 1438183881 ); } - $recordTypeValue = $this->getValueFromDefaultLanguageRecordIfConfigured($result, $tcaTypeField); + $recordTypeValue = $result['databaseRow'][$tcaTypeField]; } else { // If type is configured as localField:foreignField, fetch the type value from // a foreign table. localField then point to a group or select field in the own table, @@ -81,7 +81,7 @@ class DatabaseRecordTypeValue implements FormDataProviderInterface ); } - $foreignUid = $this->getValueFromDefaultLanguageRecordIfConfigured($result, $pointerField); + $foreignUid = $result['databaseRow'][$pointerField]; // Resolve the foreign record only if there is a uid, otherwise fall back 0 if (!empty($foreignUid)) { // Determine table name to fetch record from @@ -149,29 +149,4 @@ class DatabaseRecordTypeValue implements FormDataProviderInterface return $row ?: []; } - - /** - * If a localized row is handled, the field value of the default language record - * is used instead if tca is configured as "exclude" with empty localized value. - * - * @param array $result Main "$result" data array - * @param string $field Field name to fetch value for - * @return string field value - */ - protected function getValueFromDefaultLanguageRecordIfConfigured($result, $field) - { - $value = $result['databaseRow'][$field]; - if ( - // is a localized record - !empty($result['processedTca']['ctrl']['languageField']) - && $result['databaseRow'][$result['processedTca']['ctrl']['languageField']] > 0 - // l10n_mode for field is configured - && !empty($result['processedTca']['columns'][$field]['l10n_mode']) - // is exclude -> fall back to value of default record - && $result['processedTca']['columns'][$field]['l10n_mode'] === 'exclude' - ) { - $value = $result['defaultLanguageRow'][$field]; - } - return $value; - } } diff --git a/typo3/sysext/backend/Classes/Form/NodeFactory.php b/typo3/sysext/backend/Classes/Form/NodeFactory.php index 422fcd537fc43ed18c3d312b1939dacbbad5a127..ef6b2661a812a53ce8cd33120353699ea0a38154 100644 --- a/typo3/sysext/backend/Classes/Form/NodeFactory.php +++ b/typo3/sysext/backend/Classes/Form/NodeFactory.php @@ -101,6 +101,7 @@ class NodeFactory 'fileThumbnails' => FieldWizard\FileThumbnails::class, 'fileTypeList' => FieldWizard\FileTypeList::class, 'fileUpload' => FieldWizard\FileUpload::class, + 'localizationStateSelector' => FieldWizard\LocalizationStateSelector::class, 'otherLanguageContent' => FieldWizard\OtherLanguageContent::class, 'recordsOverview' => FieldWizard\RecordsOverview::class, 'selectIcons' => FieldWizard\SelectIcons::class, diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js index 51b1f92e18402b75b4b51a90efe4c94c2f0362e9..ccc69525836672526a10de168fda7f40f1dcf775 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js @@ -701,6 +701,34 @@ define(['jquery', }).on('change', '.t3js-form-field-eval-null-placeholder-checkbox input[type="checkbox"]', function(e) { $(this).closest('.t3js-formengine-field-item').find('.t3js-formengine-placeholder-placeholder').toggle(); $(this).closest('.t3js-formengine-field-item').find('.t3js-formengine-placeholder-formfield').toggle(); + }).on('change', '.t3js-l10n-state-container input[type=radio]', function(event) { + // Change handler for "l10n_state" field changes + var $me = $(this); + var $input = $me.closest('.t3js-formengine-field-item').find('[data-formengine-input-name]'); + + if ($input.length > 0) { + var lastState = $input.data('last-l10n-state') || false, + currentState = $(this).val(); + + if (lastState && currentState === lastState) { + return; + } + + if (currentState === 'custom') { + if (lastState) { + $(this).attr('data-original-language-value', $input.val()); + } + $input.attr('disabled', false); + } else { + if (lastState === 'custom') { + $(this).closest('.t3js-l10n-state-container').find('.t3js-l10n-state-custom').attr('data-original-language-value', $input.val()); + } + $input.attr('disabled', 'disabled'); + } + + $input.val($(this).attr('data-original-language-value')).trigger('change'); + $input.data('last-l10n-state', $(this).val()); + } }); }; @@ -956,12 +984,26 @@ define(['jquery', FormEngine.initializeNullNoPlaceholderCheckboxes(); FormEngine.initializeNullWithPlaceholderCheckboxes(); FormEngine.initializeInputLinkToggle(); + FormEngine.initializeLocalizationStateSelector(); + }; + + /** + * Disable the input field on load if localization state selector is set to "parent" or "source" + */ + FormEngine.initializeLocalizationStateSelector = function() { + $('.t3js-l10n-state-container').each(function() { + var $input = $(this).closest('.t3js-formengine-field-item').find('[data-formengine-input-name]'); + var currentState = $(this).find('input[type="radio"]:checked').val(); + if (currentState === 'parent' || currentState === 'source') { + $input.attr('disabled', 'disabled'); + } + }); }; /** * Toggle for input link explanation */ - FormEngine.initializeInputLinkToggle = function () { + FormEngine.initializeInputLinkToggle = function() { $(document).on('click', '.t3js-form-field-inputlink-explanation-toggle', function(e) { e.preventDefault(); diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseLanguageRowsTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseLanguageRowsTest.php index df93e6a552d63f62267f912c1e2c70d497673447..2bc0d4d6d6d3ad0aacc0a2b32b41ba2c46c9c93f 100644 --- a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseLanguageRowsTest.php +++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseLanguageRowsTest.php @@ -367,4 +367,77 @@ class DatabaseLanguageRowsTest extends \TYPO3\Components\TestingFramework\Core\U $this->assertEquals($expected, $this->subject->addData($input)); } + + /** + * @test + */ + public function addDataSetsSourceLanguageRow() + { + $input = [ + 'tableName' => 'tt_content', + 'databaseRow' => [ + 'uid' => 42, + 'text' => 'localized text', + 'sys_language_uid' => 3, + 'l10n_parent' => 23, + 'l10n_source' => 24, + ], + 'processedTca' => [ + 'ctrl' => [ + 'languageField' => 'sys_language_uid', + 'transOrigPointerField' => 'l10n_parent', + 'translationSource' => 'l10n_source', + ], + ], + 'systemLanguageRows' => [ + 0 => [ + 'uid' => 0, + 'title' => 'Default Language', + 'iso' => 'DEV', + ], + 2 => [ + 'uid' => 2, + 'title' => 'dansk', + 'iso' => 'dk,' + ], + 3 => [ + 'uid' => 3, + 'title' => 'french', + 'iso' => 'fr', + ], + ], + 'defaultLanguageRow' => null, + 'sourceLanguageRow' => null, + 'additionalLanguageRows' => [], + ]; + + // For BackendUtility::getRecord() + $GLOBALS['TCA']['tt_content'] = ['foo']; + $sourceLanguageRow = [ + 'uid' => 24, + 'pid' => 32, + 'text' => 'localized text in dank', + 'sys_language_uid' => 2, + ]; + $defaultLanguageRow = [ + 'uid' => 23, + 'pid' => 32, + 'text' => 'default language text', + 'sys_language_uid' => 0, + ]; + $this->subject->expects($this->at(0)) + ->method('getRecordWorkspaceOverlay') + ->with('tt_content', 23) + ->willReturn($defaultLanguageRow); + $this->subject->expects($this->at(1)) + ->method('getRecordWorkspaceOverlay') + ->with('tt_content', 24) + ->willReturn($sourceLanguageRow); + + $expected = $input; + $expected['defaultLanguageRow'] = $defaultLanguageRow; + $expected['sourceLanguageRow'] = $sourceLanguageRow; + + $this->assertEquals($expected, $this->subject->addData($input)); + } } diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRecordTypeValueTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRecordTypeValueTest.php index 72b53a6e5a0f32086628b2068326468b27d2e7eb..772a72de7ea1fca5526c01fccbe2a2ca72bce222 100644 --- a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRecordTypeValueTest.php +++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRecordTypeValueTest.php @@ -259,42 +259,6 @@ class DatabaseRecordTypeValueTest extends \TYPO3\Components\TestingFramework\Cor $this->subject->addData($input); } - /** - * @test - */ - public function addDataSetsRecordTypeValueToValueOfDefaultLanguageRecordIfConfiguredAsExclude() - { - $input = [ - 'recordTypeValue' => '', - 'processedTca' => [ - 'ctrl' => [ - 'languageField' => 'sys_language_uid', - 'type' => 'aField', - ], - 'columns' => [ - 'aField' => [ - 'l10n_mode' => 'exclude', - ], - ], - 'types' => [ - '3' => 'foo', - ], - ], - 'databaseRow' => [ - 'sys_language_uid' => 2, - 'aField' => 4, - ], - 'defaultLanguageRow' => [ - 'aField' => 3, - ], - ]; - - $expected = $input; - $expected['recordTypeValue'] = '3'; - - $this->assertSame($expected, $this->subject->addData($input)); - } - /** * @test */ diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index 13eefbfd2191d1e795ed74ddd6c6390cb4d49a5f..708d3bfb0fade10642d90cc44350c0a0a2380244 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -31,6 +31,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface; use TYPO3\CMS\Core\Database\ReferenceIndex; use TYPO3\CMS\Core\Database\RelationHandler; +use TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor; use TYPO3\CMS\Core\Html\RteHtmlParser; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessageService; @@ -977,6 +978,8 @@ class DataHandler $hookObjectsArr[] = $hookObject; } } + // Pre-process data-map and synchronize localization states + $this->datamap = DataMapProcessor::instance($this->datamap, $this->BE_USER)->process(); // Organize tables so that the pages-table is always processed first. This is required if you want to make sure that content pointing to a new page will be created. $orderOfTables = []; // Set pages first. @@ -1500,6 +1503,9 @@ class DataHandler case 't3ver_tstamp': // t3ver_label is not here because it CAN be edited as a regular field! break; + case 'l10n_state': + $fieldArray[$field] = $fieldValue; + break; default: if (isset($GLOBALS['TCA'][$table]['columns'][$field])) { // Evaluating the value diff --git a/typo3/sysext/core/Classes/DataHandling/DatabaseSchemaService.php b/typo3/sysext/core/Classes/DataHandling/DatabaseSchemaService.php new file mode 100644 index 0000000000000000000000000000000000000000..f4886d3791db02e2166b67068a2cc940355c4c05 --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/DatabaseSchemaService.php @@ -0,0 +1,59 @@ +<?php +namespace TYPO3\CMS\Core\DataHandling; + +/* + * 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! + */ + +/** + * This service provides the sql schema database records. + */ +class DatabaseSchemaService +{ + const TABLE_TEMPLATE = 'CREATE TABLE %s (' . LF . '%s' . LF . ');'; + const FIELD_L10N_STATE_TEMPLATE = ' l10n_state text'; + + /** + * Add l10n_state field to tables that provide localization + * + * @return string Localization fields database schema + */ + public function getLocalizationRequiredDatabaseSchema(array $sqlString) + { + $tableSchemas = []; + + foreach ($GLOBALS['TCA'] as $tableName => $tableDefinition) { + if ( + empty($tableDefinition['columns']) + || empty($tableDefinition['ctrl']['languageField']) + || empty($tableDefinition['ctrl']['transOrigPointerField']) + ) { + continue; + } + + $fieldSchemas = []; + $fieldSchemas[] = static::FIELD_L10N_STATE_TEMPLATE; + + $tableSchemas[] = sprintf( + static::TABLE_TEMPLATE, + $tableName, + implode(',' . LF, $fieldSchemas) + ); + } + + if (!empty($tableSchemas)) { + $sqlString[] = implode(LF, $tableSchemas); + } + + return array('sqlString' => $sqlString); + } +} diff --git a/typo3/sysext/core/Classes/DataHandling/Localization/DataMapItem.php b/typo3/sysext/core/Classes/DataHandling/Localization/DataMapItem.php new file mode 100644 index 0000000000000000000000000000000000000000..6acc9a6a37cdc61530abdb1e321b26b72cd97ff4 --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/Localization/DataMapItem.php @@ -0,0 +1,430 @@ +<?php +namespace TYPO3\CMS\Core\DataHandling\Localization; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\MathUtility; + +/** + * Entity for data-map item. + */ +class DataMapItem +{ + const TYPE_PARENT = 'parent'; + const TYPE_DIRECT_CHILD = 'directChild'; + const TYPE_GRAND_CHILD = 'grandChild'; + + const SCOPE_PARENT = State::STATE_PARENT; + const SCOPE_SOURCE = State::STATE_SOURCE; + const SCOPE_EXCLUDE = 'exclude'; + + /** + * @var string + */ + protected $tableName; + + /** + * @var string|int + */ + protected $id; + + /** + * @var array + */ + protected $suggestedValues; + + /** + * @var array + */ + protected $persistedValues; + + /** + * @var array + */ + protected $configurationFieldNames; + + /** + * @var bool + */ + protected $new; + + /** + * @var string + */ + protected $type; + + /** + * @var State + */ + protected $state; + + /** + * @var string|int + */ + protected $language; + + /** + * @var string|int + */ + protected $parent; + + /** + * @var string|int + */ + protected $source; + + /** + * @var DataMapItem[][] + */ + protected $dependencies = []; + + /** + * Builds a data-map item. In addition to the constructor, the values + * for language, parent and source record pointers are assigned as well. + * + * @param string $tableName + * @param string|int $id + * @param array $suggestedValues + * @param array $persistedValues + * @param array $configurationFieldNames + * @return object|DataMapItem + */ + public static function build( + string $tableName, + $id, + array $suggestedValues, + array $persistedValues, + array $configurationFieldNames + ) { + $item = GeneralUtility::makeInstance( + static::class, + $tableName, + $id, + $suggestedValues, + $persistedValues, + $configurationFieldNames + ); + + $item->language = (int)($suggestedValues[$item->getLanguageFieldName()] ?? $persistedValues[$item->getLanguageFieldName()]); + $item->setParent($suggestedValues[$item->getParentFieldName()] ?? $persistedValues[$item->getParentFieldName()]); + if ($item->getSourceFieldName() !== null) { + $item->setSource($suggestedValues[$item->getSourceFieldName()] ?? $persistedValues[$item->getSourceFieldName()]); + } + + return $item; + } + + /** + * @param string $tableName + * @param string|int $id + * @param array $suggestedValues + * @param array $persistedValues + * @param array $configurationFieldNames + */ + public function __construct( + string $tableName, + $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. + * + * @return string + */ + public function getTableName(): string + { + return $this->tableName; + } + + /** + * Gets the table name used to resolve the language parent record. + * + * @return string + */ + public function getFromTableName(): string + { + if ($this->tableName === 'pages_language_overlay') { + return 'pages'; + } + return $this->tableName; + } + + /** + * Gets the table name used to resolve any kind of translations. + * + * @return string + */ + public function getForTableName(): string + { + if ($this->tableName === 'pages') { + return 'pages_language_overlay'; + } + return $this->tableName; + } + + /** + * Gets the id of this data-map item. + * + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * Gets the suggested values that were initially + * submitted as the whole data-map to the DataHandler. + * + * @return array + */ + 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. + * + * @return array + */ + public function getPersistedValues(): array + { + return $this->persistedValues; + } + + /** + * @return array + */ + public function getConfigurationFieldNames(): array + { + return $this->configurationFieldNames; + } + + /** + * @return string + */ + public function getLanguageFieldName(): string + { + return $this->configurationFieldNames['language']; + } + + /** + * @return string + */ + public function getParentFieldName(): string + { + return $this->configurationFieldNames['parent']; + } + + /** + * @return null|string + */ + public function getSourceFieldName() + { + return $this->configurationFieldNames['source']; + } + + /** + * @return bool + */ + public function isNew(): bool + { + return $this->new; + } + + /** + * @return string + */ + public function getType(): string + { + if ($this->type === null) { + // implicit: default language, it's a parent + if ($this->language === 0) { + $this->type = static::TYPE_PARENT; + // implicit: having source value different to parent value, it's a 2nd or higher level translation + } elseif ( + $this->source !== null + && $this->source !== $this->parent + ) { + $this->type = static::TYPE_GRAND_CHILD; + // implicit: otherwise, it's a 1st level translation + } else { + $this->type = static::TYPE_DIRECT_CHILD; + } + } + return $this->type; + } + + /** + * @return bool + */ + public function isParentType(): bool + { + return $this->getType() === static::TYPE_PARENT; + } + + /** + * @return bool + */ + public function isDirectChildType(): bool + { + return $this->getType() === static::TYPE_DIRECT_CHILD; + } + + /** + * @return bool + */ + public function isGrandChildType(): bool + { + return $this->getType() === static::TYPE_GRAND_CHILD; + } + + /** + * @return State + */ + public function getState(): State + { + if ($this->state === null && !$this->isParentType()) { + $this->state = State::fromJSON( + $this->tableName, + $this->persistedValues['l10n_state'] ?? null + ); + $this->state->update( + $this->suggestedValues['l10n_state'] ?? [] + ); + } + return $this->state; + } + + /** + * @return string|int + */ + public function getLanguage() + { + return $this->language; + } + + /** + * @param string|int $language + */ + public function setLanguage($language) + { + $this->language = $language; + } + + /** + * @return string|int + */ + public function getParent() + { + return $this->parent; + } + + /** + * @param string|int $parent + */ + public function setParent($parent) + { + $this->parent = $parent; + } + + /** + * @return string|int + */ + public function getSource() + { + return $this->source; + } + + /** + * @param string|int $source + */ + public function setSource($source) + { + $this->source = $source; + } + + /** + * @param string $scope + * @return int|string + */ + public function getIdForScope($scope) + { + if ( + $scope === static::SCOPE_PARENT + || $scope === static::SCOPE_EXCLUDE + ) { + return $this->getParent(); + } + if ($scope === static::SCOPE_SOURCE) { + return $this->getSource(); + } + throw new \RuntimeException('Invalid scope', 1486325248); + } + + /** + * @return DataMapItem[][] + */ + public function getDependencies(): array + { + return $this->dependencies; + } + + /** + * @param DataMapItem[][] $dependencies + */ + public function setDependencies(array $dependencies) + { + $this->dependencies = $dependencies; + } + + /** + * @param string $scope + * @return DataMapItem[] + */ + public function findDependencies(string $scope) + { + return ($this->dependencies[$scope] ?? []); + } + + /** + * @return string[] + */ + public function getApplicableScopes() + { + $scopes = []; + if (!empty($this->getSourceFieldName())) { + $scopes[] = static::SCOPE_SOURCE; + } + $scopes[] = static::SCOPE_PARENT; + $scopes[] = static::SCOPE_EXCLUDE; + return $scopes; + } +} \ No newline at end of file diff --git a/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php b/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php new file mode 100644 index 0000000000000000000000000000000000000000..d86f73bd7c773c4e09553d8856e6b1727e3adbf8 --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php @@ -0,0 +1,894 @@ +<?php +namespace TYPO3\CMS\Core\DataHandling\Localization; + +/* + * 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! + */ + +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Core\Database\RelationHandler; +use TYPO3\CMS\Core\DataHandling\DataHandler; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\MathUtility; +use TYPO3\CMS\Core\Utility\StringUtility; + +/** + * This processor analyses the provided data-map before actually being process + * in the calling DataHandler instance. Field names that are configured to have + * "allowLanguageSynchronization" enabled are either synchronized from there + * relative parent records (could be a default language record, or a l10n_source + * record) or to their dependent records (in case a default language record or + * nested records pointing upwards with l10n_source). + * + * Except inline relational record editing, all modifications are applied to + * the data-map directly, which ensures proper history entries as a side-effect. + * For inline relational record editing, this processor either triggers the copy + * or localize actions by instantiation a new local DataHandler instance. + */ +class DataMapProcessor +{ + /** + * @var array + */ + protected $dataMap = []; + + /** + * @var BackendUserAuthentication + */ + protected $backendUser; + + /** + * @var DataMapItem[] + */ + protected $items = []; + + /** + * Class generator + * + * @param array $dataMap The submitted data-map to be worked on + * @param BackendUserAuthentication $backendUser Forwared backend-user scope + * @return DataMapProcessor + */ + public static function instance(array $dataMap, BackendUserAuthentication $backendUser) + { + return GeneralUtility::makeInstance( + static::class, + $dataMap, + $backendUser + ); + } + + /** + * @param array $dataMap The submitted data-map to be worked on + * @param BackendUserAuthentication $backendUser Forwared backend-user scope + */ + public function __construct(array $dataMap, BackendUserAuthentication $backendUser) + { + $this->dataMap = $dataMap; + $this->backendUser = $backendUser; + } + + /** + * Processes the submitted data-map and returns the sanitized and enriched + * version depending on accordant localization states and dependencies. + * + * @return array + */ + public function process() + { + foreach ($this->dataMap as $tableName => $idValues) { + $this->collectItems($tableName, $idValues); + } + $this->sanitize(); + $this->enrich(); + return $this->dataMap; + } + + /** + * Create data map items of all affected rows + * + * @param string $tableName + * @param array $idValues + */ + protected function collectItems(string $tableName, array $idValues) + { + if (!$this->isApplicable($tableName)) { + return; + } + + $fieldNames = [ + 'uid' => 'uid', + 'l10n_state' => 'l10n_state', + 'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'], + 'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], + ]; + if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) { + $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource']; + } + + $translationValues = $this->fetchTranslationValues( + $tableName, + $fieldNames, + $this->filterNumericIds(array_keys($idValues)) + ); + + $dependencies = $this->fetchDependencies( + $tableName, + $this->filterNumericIds(array_keys($idValues)) + ); + + foreach ($idValues as $id => $values) + { + $recordValues = $translationValues[$id] ?? []; +// $values['l10n_state'] = json_decode($values['l10n_state'], true) ?? []; + $item = DataMapItem::build( + $tableName, + $id, + $values, + $recordValues, + $fieldNames + ); + + // must be any kind of localization and in connected mode + if ($item->getLanguage() > 0 && empty($item->getParent())) { + unset($item); + continue; + } + // add dependencies + if (!empty($dependencies[$id])) { + $item->setDependencies($dependencies[$id]); + } + $this->items[$tableName . ':' . $id] = $item; + } + } + + /** + * Sanitizes the submitted data-map and removes fields which are not + * defined as custom and thus rely on either parent or source values. + */ + protected function sanitize() + { + foreach (['grandChild', 'directChild'] as $type) { + foreach ($this->filterItemsByType($type) as $item) { + $this->sanitizeTranslationItem($item); + } + } + } + + /** + * Handle synchronization of an item list + */ + protected function enrich() + { + foreach (['grandChild', 'directChild'] as $type) { + foreach ($this->filterItemsByType($type) as $item) { + foreach ($item->getApplicableScopes() as $scope) { + $fromId = $item->getIdForScope($scope); + $fieldNames = $this->getFieldNamesForItemScope($item, $scope, !$item->isNew()); + $this->synchronizeTranslationItem($item, $fieldNames, $fromId); + } + $this->populateTranslationItem($item); + $this->finishTranslationItem($item); + } + } + foreach ($this->filterItemsByType('parent') as $item) { + $this->populateTranslationItem($item); + } + } + + /** + * Sanitizes the submitted data-map for a particular item and removes + * fields which are not defined as custom and thus rely on either parent + * or source values. + * + * @param DataMapItem $item + */ + protected function sanitizeTranslationItem(DataMapItem $item) + { + $fieldNames = array_merge( + $this->getFieldNamesForItemScope($item, DataMapItem::SCOPE_PARENT, !$item->isNew()), + $this->getFieldNamesForItemScope($item, DataMapItem::SCOPE_SOURCE, !$item->isNew()) + ); + // remove fields, that are submitted in data-map, but not defined as custom + $this->dataMap[$item->getTableName()][$item->getId()] = array_diff_key( + $this->dataMap[$item->getTableName()][$item->getId()], + array_combine($fieldNames, $fieldNames) + ); + } + + /** + * Synchronize a single item + * + * @param DataMapItem $item + * @param array $fieldNames + * @param int $fromId + */ + protected function synchronizeTranslationItem(DataMapItem $item, array $fieldNames, int $fromId) + { + if (empty($fieldNames)) { + return; + } + $fieldNameList = 'uid,' . implode(',', $fieldNames); + $fromRecord = BackendUtility::getRecordWSOL( + $item->getFromTableName(), + $fromId, + $fieldNameList + ); + $forRecord = []; + if (!$item->isNew()) { + $forRecord = BackendUtility::getRecordWSOL( + $item->getTableName(), + $item->getId(), + $fieldNameList + ); + } + foreach ($fieldNames as $fieldName) { + $this->synchronizeFieldValues( + $item, + $fieldName, + $fromRecord, + $forRecord + ); + } + } + + /** + * Populates values downwards, either from a parent language item or + * a source language item to an accordant dependent translation item. + * + * @param DataMapItem $item + */ + protected function populateTranslationItem(DataMapItem $item) + { + if ($item->isNew()) { + return; + } + + foreach ([State::STATE_PARENT, State::STATE_SOURCE] as $scope) { + foreach ($item->findDependencies($scope) as $dependentItem) { + // use suggested item, if it was submitted in data-map + $suggestedDependentItem = $this->findItem( + $dependentItem->getTableName(), + $dependentItem->getId() + ); + if ($suggestedDependentItem !== null) { + $dependentItem = $suggestedDependentItem; + } + $fieldNames = $this->getFieldNamesForItemScope( + $dependentItem, + $scope, + false + ); + $this->synchronizeTranslationItem( + $dependentItem, + $fieldNames, + $item->getId() + ); + } + } + } + + /** + * Finishes a translation item by updating states to be persisted. + * + * @param DataMapItem $item + */ + protected function finishTranslationItem(DataMapItem $item) + { + if ( + $item->isParentType() + || !State::isApplicable($item->getTableName()) + ) { + return; + } + + $this->dataMap[$item->getTableName()][$item->getId()]['l10n_state'] = $item->getState()->export(); + } + + /** + * Synchronize simple values like text and similar + * + * @param DataMapItem $item + * @param string $fieldName + * @param array $fromRecord + * @param array $forRecord + */ + protected function synchronizeFieldValues(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord) + { + // skip if this field has been processed already, assumed that proper sanitation happened + if (!empty($this->dataMap[$item->getTableName()][$item->getId()][$fieldName])) { + return; + } + + $fromId = $fromRecord['uid']; + $fromValue = $this->dataMap[$item->getFromTableName()][$fromId][$fieldName] ?? $fromRecord[$fieldName]; + + // plain values + if (!$this->isRelationField($item->getFromTableName(), $fieldName)) { + $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = $fromValue; + // direct relational values + } elseif (!$this->isInlineRelationField($item->getFromTableName(), $fieldName)) { + $this->synchronizeDirectRelations($item, $fieldName, $fromRecord); + // inline relational values + } else { + $this->synchronizeInlineRelations($item, $fieldName, $fromRecord, $forRecord); + } + } + + /** + * Synchronize select and group field localizations + * + * @param DataMapItem $item + * @param string $fieldName + * @param array $fromRecord + */ + protected function synchronizeDirectRelations(DataMapItem $item, string $fieldName, array $fromRecord) + { + $fromId = $fromRecord['uid']; + $fromValue = $this->dataMap[$item->getFromTableName()][$fromId][$fieldName] ?? $fromRecord[$fieldName]; + $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName]; + + // non-MM relations are stored as comma separated values, just use them + // if values are available in data-map already, just use them as well + if ( + empty($configuration['config']['MM']) + || isset($this->dataMap[$item->getFromTableName()][$fromId][$fieldName]) + || ($configuration['config']['special'] ?? null) === 'languages' + ) { + $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = $fromValue; + return; + } + + // fetch MM relations from storage + $type = $configuration['config']['type']; + $manyToManyTable = $configuration['config']['MM']; + if ($type === 'group' && $configuration['config']['internal_type'] === 'db') { + $tableNames = trim($configuration['config']['allowed'] ?? ''); + } elseif ($configuration['config']['type'] === 'select') { + $tableNames = ($configuration['foreign_table'] ?? ''); + } else { + return; + } + + $relationHandler = $this->createRelationHandler(); + $relationHandler->start( + '', + $tableNames, + $manyToManyTable, + $fromId, + $item->getFromTableName(), + $configuration['config'] + ); + + // provide list of relations, optionally prepended with table name + // e.g. "13,19,23" or "tt_content_27,tx_extension_items_28" + $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = implode( + ',', + $relationHandler->getValueArray() + ); + } + + /** + * Handle synchonization of inline relations + * + * @param DataMapItem $item + * @param string $fieldName + * @param array $fromRecord + * @param array $forRecord + */ + protected function synchronizeInlineRelations(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord) + { + $fromId = $fromRecord['uid']; + $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName]; + $foreignTableName = $configuration['config']['foreign_table']; + $manyToManyTable = ($configuration['config']['MM'] ?? ''); + + $languageFieldName = ($GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField'] ?? null); + $parentFieldName = ($GLOBALS['TCA'][$foreignTableName]['ctrl']['transOrigPointerField'] ?? null); + $sourceFieldName = ($GLOBALS['TCA'][$foreignTableName]['ctrl']['translationSource'] ?? null); + + // determine suggested elements of either translation parent or source record + // from data-map, in case the accordant language parent/source record was modified + if (isset($this->dataMap[$item->getFromTableName()][$fromId][$fieldName])) { + $suggestedAncestorIds = GeneralUtility::trimExplode( + ',', + $this->dataMap[$item->getFromTableName()][$fromId][$fieldName], + true + ); + // determine suggested elements of either translation parent or source record from storage + } else { + $relationHandler = $this->createRelationHandler(); + $relationHandler->start( + $fromRecord[$fieldName], + $foreignTableName, + $manyToManyTable, + $fromId, + $item->getFromTableName(), + $configuration['config'] + ); + $suggestedAncestorIds = $this->mapRelationItemId($relationHandler->itemArray); + } + // determine persisted elements for the current data-map item + $relationHandler = $this->createRelationHandler(); + $relationHandler->start( + $forRecord[$fieldName] ?? '', + $foreignTableName, + $manyToManyTable, + $item->getId(), + $item->getTableName(), + $configuration['config'] + ); + $persistedIds = $this->mapRelationItemId($relationHandler->itemArray); + // The dependent ID map points from language parent/source record to + // localization, thus keys: parents/sources & values: localizations + $dependentIdMap = $this->fetchDependentIdMap($foreignTableName, $suggestedAncestorIds); + // filter incomplete structures - this is a drawback of DataHandler's remap stack, since + // just created IRRE translations still belong to the language parent - filter them out + $suggestedAncestorIds = array_diff($suggestedAncestorIds, array_values($dependentIdMap)); + // compile element differences to be resolved + // remove elements that are persisted at the language translation, but not required anymore + $removeIds = array_diff($persistedIds, array_values($dependentIdMap)); + // remove elements that are persisted at the language parent/source, but not required anymore + $removeAncestorIds = array_diff(array_keys($dependentIdMap), $suggestedAncestorIds); + // missing elements that are persisted at the language parent/source, but not translated yet + $missingAncestorIds = array_diff($suggestedAncestorIds, array_keys($dependentIdMap)); + // persisted elements that should be copied or localized + $createAncestorIds = $this->filterNumericIds($missingAncestorIds, true); + // non-persisted elements that should be duplicated in data-map directly + $populateAncestorIds = $this->filterNumericIds($missingAncestorIds, false); + // this desired state map defines the final result of child elements of the translation + $desiredLocalizationIdMap = array_combine($suggestedAncestorIds, $suggestedAncestorIds); + // update existing translations in the desired state map + foreach ($dependentIdMap as $ancestorId => $translationId) { + if (isset($desiredLocalizationIdMap[$ancestorId])) { + $desiredLocalizationIdMap[$ancestorId] = $translationId; + } + } + // nothing to synchronize, but element order could have been changed + if (empty($removeAncestorIds) && empty($missingAncestorIds)) { + $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = implode( + ',', + array_values($desiredLocalizationIdMap) + ); + return; + } + + $localCommandMap = []; + foreach ($removeIds as $removeId) { + $localCommandMap[$foreignTableName][$removeId]['delete'] = true; + } + foreach ($removeAncestorIds as $removeAncestorId) { + $removeId = $dependentIdMap[$removeAncestorId]; + $localCommandMap[$foreignTableName][$removeId]['delete'] = true; + } + foreach ($createAncestorIds as $createAncestorId) { + // if child table is not aware of localization, just copy + if (empty($languageFieldName) || empty($parentFieldName)) { + $localCommandMap[$foreignTableName][$createAncestorId]['copy'] = true; + // otherwise, trigger the localization process + } else { + $localCommandMap[$foreignTableName][$createAncestorId]['localize'] = $item->getLanguage(); + } + } + // execute copy, localize and delete actions on persisted child records + if (!empty($localCommandMap)) { + $localDataHandler = GeneralUtility::makeInstance(DataHandler::class); + $localDataHandler->start([], $localCommandMap, $this->backendUser); + $localDataHandler->process_cmdmap(); + // update copied or localized ids + foreach ($createAncestorIds as $createAncestorId) { + if (empty($localDataHandler->copyMappingArray[$foreignTableName][$createAncestorId])) { + throw new \RuntimeException('Child record was not processed', 1486233164); + } + $newLocalizationId = $localDataHandler->copyMappingArray[$foreignTableName][$createAncestorId]; + $newLocalizationId = $localDataHandler->getAutoVersionId($foreignTableName, $newLocalizationId) ?? $newLocalizationId; + $desiredLocalizationIdMap[$createAncestorId] = $newLocalizationId; + } + } + // populate new child records in data-map + if (!empty($populateAncestorIds)) { + foreach ($populateAncestorIds as $populateId) { + $newLocalizationId = StringUtility::getUniqueId('NEW'); + $desiredLocalizationIdMap[$populateId] = $newLocalizationId; + // @todo l10n_mode=prefixLangTitle is not applied to this "in-memory translation" + $this->dataMap[$foreignTableName][$newLocalizationId] = $this->dataMap[$foreignTableName][$populateId]; + $this->dataMap[$foreignTableName][$newLocalizationId][$languageFieldName] = $item->getLanguage(); + // @todo Only $populatedIs used in TCA type 'select' is resolved in DataHandler's remapStack + $this->dataMap[$foreignTableName][$newLocalizationId][$parentFieldName] = $populateId; + if ($sourceFieldName !== null) { + // @todo Not sure, whether $populateId is resolved in DataHandler's remapStack + $this->dataMap[$foreignTableName][$newLocalizationId][$sourceFieldName] = $populateId; + } + } + } + // update inline parent field references - required to update pointer fields + $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = implode( + ',', + array_values($desiredLocalizationIdMap) + ); + } + + /** + * Fetches translation related field values for the items submitted in + * the data-map. That's why further adjustment for the tables pages vs. + * pages_language_overlay is not required. + * + * @param string $tableName + * @param array $fieldNames + * @param array $ids + * @return array + */ + protected function fetchTranslationValues(string $tableName, array $fieldNames, array $ids) + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable($tableName); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $statement = $queryBuilder + ->select(...array_values($fieldNames)) + ->from($tableName) + ->where( + $queryBuilder->expr()->in( + 'uid', + $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY) + ) + ) + ->execute(); + + $translationValues = []; + foreach ($statement as $record) { + $translationValues[$record['uid']] = $record; + } + return $translationValues; + } + + /** + * Create arary of dependent records + * + * @param string $tableName + * @param array $ids + * @return DataMapItem[][] + */ + protected function fetchDependencies(string $tableName, array $ids) + { + if ($tableName === 'pages') { + $tableName = 'pages_language_overlay'; + } + + $fieldNames = [ + 'uid' => 'uid', + 'l10n_state' => 'l10n_state', + 'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'], + 'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], + ]; + if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) { + $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource']; + } + + $dependentElements = $this->fetchDependentElements($tableName, $ids, $fieldNames); + + $dependencyMap = []; + foreach ($dependentElements as $dependentElement) { + $dependentItem = DataMapItem::build( + $tableName, + $dependentElement['uid'], + [], + $dependentElement, + $fieldNames + ); + + if ($dependentItem->isDirectChildType()) { + $dependencyMap[$dependentItem->getParent()][State::STATE_PARENT][] = $dependentItem; + } + if ($dependentItem->isGrandChildType()) { + $dependencyMap[$dependentItem->getSource()][State::STATE_SOURCE][] = $dependentItem; + } + } + return $dependencyMap; + } + + /** + * Fetch dependent records that depend on given record id's in their parent or source field and + * create an id map as further lookup array + * + * @param string $tableName + * @param array $ids + * @return array + */ + protected function fetchDependentIdMap(string $tableName, array $ids) + { + if ($tableName === 'pages') { + $tableName = 'pages_language_overlay'; + } + + $fieldNames = [ + 'uid' => 'uid', + 'l10n_state' => 'l10n_state', + 'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'], + 'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], + ]; + if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) { + $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource']; + } + + $dependentElements = $this->fetchDependentElements($tableName, $ids, $fieldNames); + + $dependentIdMap = []; + foreach ($dependentElements as $dependentElement) { + // implicit: having source value different to parent value, use source pointer + if ( + !empty($fieldNames['source']) + && $dependentElement[$fieldNames['source']] !== $dependentElement[$fieldNames['parent']] + ) { + $dependentIdMap[$dependentElement[$fieldNames['source']]] = $dependentElement['uid']; + // implicit: otherwise, use parent pointer + } else { + $dependentIdMap[$dependentElement[$fieldNames['parent']]] = $dependentElement['uid']; + } + } + return $dependentIdMap; + } + + + /** + * Fetch all elements that depend on given record id's in their parent or source field + * + * @param string $tableName + * @param array $ids + * @param array|null $fieldNames + * @return array + */ + protected function fetchDependentElements(string $tableName, array $ids, array $fieldNames) + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable($tableName); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + + $predicates = [ + $queryBuilder->expr()->in( + $fieldNames['parent'], + $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY) + ) + ]; + + if (!empty($fieldNames['source'])) { + $predicates = [ + $queryBuilder->expr()->in( + $fieldNames['source'], + $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY) + ) + ]; + } + + $statement = $queryBuilder + ->select(...array_values($fieldNames)) + ->from($tableName) + ->andWhere( + // must be any kind of localization + $queryBuilder->expr()->gt( + $fieldNames['language'], + $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) + ), + // must be in connected mode + $queryBuilder->expr()->gt( + $fieldNames['parent'], + $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) + ), + // any parent or source pointers + $queryBuilder->expr()->orX(...$predicates) + ) + ->execute(); + + $dependentElements = []; + foreach ($statement as $record) { + $dependentElements[] = $record; + } + return $dependentElements; + } + + /** + * Return array of data map items that are of given type + * + * @param string $type + * @return DataMapItem[] + */ + protected function filterItemsByType(string $type) + { + return array_filter( + $this->items, + function(DataMapItem $item) use ($type) { + return $item->getType() === $type; + } + ); + } + + /** + * Return only id's that are integer - so no NEW... + * + * @param array $ids + * @param bool $numeric + * @return array + */ + protected function filterNumericIds(array $ids, bool $numeric = true) + { + return array_filter( + $ids, + function($id) use ($numeric) { + return MathUtility::canBeInterpretedAsInteger($id) === $numeric; + } + ); + } + + /** + * Flatten array + * + * @param array $relationItems + * @return string[] + */ + protected function mapRelationItemId(array $relationItems) + { + return array_map( + function(array $relationItem) { + return (string)$relationItem['id']; + }, + $relationItems + ); + } + + /** + * See if an items is in item list and return it + * + * @param string $tableName + * @param string|int $id + * @return null|DataMapItem + */ + protected function findItem(string $tableName, $id) + { + return $this->items[$tableName . ':' . $id] ?? null; + } + + /** + * Field names we have to deal with + * + * @param DataMapItem $item + * @param string $scope + * @param null|bool $modified + * @return string[] + */ + protected function getFieldNamesForItemScope( + DataMapItem $item, + string $scope, + bool $modified + ) { + if ( + $scope === DataMapItem::SCOPE_PARENT + || $scope === DataMapItem::SCOPE_SOURCE + ) { + if (!State::isApplicable($item->getTableName())) { + return []; + } + return $item->getState()->filterFieldNames($scope, $modified); + } + if ($scope === DataMapItem::SCOPE_EXCLUDE) { + return $this->getLocalizationModeExcludeFieldNames( + $item->getTableName() + ); + } + return []; + } + + /** + * Field names of TCA table with columns having l10n_mode=exclude + * + * @param string $tableName + * @return string[] + */ + protected function getLocalizationModeExcludeFieldNames(string $tableName) + { + $localizationExcludeFieldNames = []; + if (empty($GLOBALS['TCA'][$tableName]['columns'])) { + return $localizationExcludeFieldNames; + } + + foreach ($GLOBALS['TCA'][$tableName]['columns'] as $fieldName => $configuration) { + if (($configuration['l10n_mode'] ?? null) === 'exclude') { + $localizationExcludeFieldNames[] = $fieldName; + } + } + + return $localizationExcludeFieldNames; + } + + /** + * True if we're dealing with a field that has foreign db relations + * + * @param string $tableName + * @param string $fieldName + * @return bool True if field is type=group with internalType === db or select with foreign_table + */ + protected function isRelationField(string $tableName, string $fieldName): bool + { + if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'])) { + return false; + } + + $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']; + + return ( + $configuration['type'] === 'group' + && ($configuration['internal_type'] ?? null) === 'db' + && !empty($configuration['allowed']) + || $configuration['type'] === 'select' + && ( + !empty($configuration['foreign_table']) + && !empty($GLOBALS['TCA'][$configuration['foreign_table']]) + || ($configuration['special'] ?? null) === 'languages' + ) + || $this->isInlineRelationField($tableName, $fieldName) + ); + } + + /** + * True if we're dealing with an inline field + * + * @param string $tableName + * @param string $fieldName + * @return bool TRUE if field is of type inline with foreign_table set + */ + protected function isInlineRelationField(string $tableName, string $fieldName): bool + { + if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'])) { + return false; + } + + $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']; + + return ( + $configuration['type'] === 'inline' + && !empty($configuration['foreign_table']) + && !empty($GLOBALS['TCA'][$configuration['foreign_table']]) + ); + } + + /** + * Determines whether the table can be localized and either has fields + * with allowLanguageSynchronization enabled or l10n_mode set to exclude. + * + * @param string $tableName + * @return bool + */ + protected function isApplicable(string $tableName): bool + { + return ( + State::isApplicable($tableName) + || BackendUtility::isTableLocalizable($tableName) + && count($this->getLocalizationModeExcludeFieldNames($tableName)) > 0 + ); + } + + /** + * @return RelationHandler + */ + protected function createRelationHandler() + { + $relationHandler = GeneralUtility::makeInstance(RelationHandler::class); + $relationHandler->setWorkspaceId($this->backendUser->workspace); + return $relationHandler; + } +} \ No newline at end of file diff --git a/typo3/sysext/core/Classes/DataHandling/Localization/State.php b/typo3/sysext/core/Classes/DataHandling/Localization/State.php new file mode 100644 index 0000000000000000000000000000000000000000..93ed768676f5ccb20fa8bd374e79f9c7648acbdd --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/Localization/State.php @@ -0,0 +1,296 @@ +<?php +namespace TYPO3\CMS\Core\DataHandling\Localization; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Value object for l10n_state field value. + */ +class State +{ + const STATE_CUSTOM = 'custom'; + const STATE_PARENT = 'parent'; + const STATE_SOURCE = 'source'; + + /** + * @param string $tableName + * @return null|State + */ + public static function create(string $tableName) + { + if (!static::isApplicable($tableName)) { + return null; + } + + return GeneralUtility::makeInstance( + static::class, + $tableName + ); + } + + /** + * @param string $tableName + * @param string|null $json + * @return null|State + */ + public static function fromJSON(string $tableName, string $json = null) + { + if (!static::isApplicable($tableName)) { + return null; + } + + $states = json_decode($json ?? '', true); + return GeneralUtility::makeInstance( + static::class, + $tableName, + $states ?? [] + ); + } + + /** + * @param string $tableName + * @return bool + */ + public static function isApplicable(string $tableName) + { + return ( + static::hasColumns($tableName) + && static::hasLanguageFieldName($tableName) + && static::hasTranslationParentFieldName($tableName) + && count(static::getFieldNames($tableName)) > 0 + ); + } + + /** + * @param string $tableName + * @return bool + */ + protected static function hasColumns(string $tableName) + { + return ( + !empty($GLOBALS['TCA'][$tableName]['columns']) + && is_array($GLOBALS['TCA'][$tableName]['columns']) + ); + } + + /** + * @param string $tableName + * @return bool + */ + protected static function hasLanguageFieldName(string $tableName) + { + return !empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField']); + } + + /** + * @param string $tableName + * @return bool + */ + protected static function hasTranslationParentFieldName(string $tableName) + { + return !empty($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']); + } + + /** + * @param string $tableName + * @return array + */ + protected static function getFieldNames(string $tableName) + { + return array_keys( + array_filter( + $GLOBALS['TCA'][$tableName]['columns'], + function(array $fieldConfiguration) { + return !empty( + $fieldConfiguration['config'] + ['behaviour']['allowLanguageSynchronization'] + ); + } + ) + ); + } + + /** + * @var string + */ + protected $tableName; + + /** + * @var array + */ + protected $states; + + /** + * @var array + */ + protected $originalStates; + + /** + * @param string $tableName + * @param array $states + */ + public function __construct(string $tableName, array $states = array()) + { + $this->tableName = $tableName; + $this->states = $states; + $this->originalStates = $states; + + $this->states = $this->sanitize($states); + $this->states = $this->enrich($states); + } + + /** + * @param array $states + */ + public function update(array $states) + { + $this->states = array_merge( + $this->states, + $this->sanitize($states) + ); + } + + /** + * @return string|null + */ + public function export() + { + if (empty($this->states)) { + return null; + } + return json_encode($this->states); + } + + /** + * @return string[] + */ + public function getModifiedFieldNames() + { + return array_keys( + array_diff_assoc( + $this->states, + $this->originalStates + ) + ); + } + + /** + * @return bool + */ + public function isModified() + { + return !empty($this->getModifiedFieldNames()); + } + + /** + * @param string $fieldName + * @return bool + */ + public function isUndefined(string $fieldName) + { + return !isset($this->states[$fieldName]); + } + + /** + * @param string $fieldName + * @return bool + */ + public function isCustomState(string $fieldName) + { + return ($this->states[$fieldName] ?? null) === static::STATE_CUSTOM; + } + + /** + * @param string $fieldName + * @return bool + */ + public function isParentState(string $fieldName) + { + return ($this->states[$fieldName] ?? null) === static::STATE_PARENT; + } + + /** + * @param string $fieldName + * @return bool + */ + public function isSourceState(string $fieldName) + { + return ($this->states[$fieldName] ?? null) === static::STATE_SOURCE; + } + + /** + * @param string $fieldName + * @return null|string + */ + public function getState(string $fieldName) + { + return ($this->states[$fieldName] ?? null); + } + + /** + * Filters field names having a desired state. + * + * @param string $desiredState + * @param bool $modified + * @return string[] + */ + public function filterFieldNames(string $desiredState, bool $modified = false) + { + if (!$modified) { + $fieldNames = array_keys($this->states); + } else { + $fieldNames = $this->getModifiedFieldNames(); + } + return array_filter( + $fieldNames, + function($fieldName) use ($desiredState) { + return $this->states[$fieldName] === $desiredState; + } + ); + } + + /** + * Filter out field names that don't exist in TCA. + * + * @param array $states + * @return array + */ + protected function sanitize(array $states) + { + $fieldNames = static::getFieldNames($this->tableName); + return array_intersect_key( + $states, + array_combine($fieldNames, $fieldNames) + ); + } + + /** + * Add missing states for field names. + * + * @param array $states + * @return array + */ + protected function enrich(array $states) + { + foreach (static::getFieldNames($this->tableName) as $fieldName) { + if (!empty($states[$fieldName])) { + continue; + } + $states[$fieldName] = static::STATE_PARENT; + } + return $states; + } +} \ No newline at end of file diff --git a/typo3/sysext/core/Classes/Migrations/TcaMigration.php b/typo3/sysext/core/Classes/Migrations/TcaMigration.php index 0a66f0273fc18880853e45e9c2d52becab43f1db..115f7c86fad964d7285232fb8828fa4bc63ad4f5 100644 --- a/typo3/sysext/core/Classes/Migrations/TcaMigration.php +++ b/typo3/sysext/core/Classes/Migrations/TcaMigration.php @@ -928,8 +928,13 @@ class TcaMigration } if ($fieldConfig['l10n_mode'] === 'mergeIfNotBlank') { unset($fieldConfig['l10n_mode']); + if (empty($fieldConfig['config']['behaviour']['allowLanguageSynchronization'])) { + $fieldConfig['config']['behaviour']['allowLanguageSynchronization'] = true; + } $this->messages[] = 'The TCA setting \'mergeIfNotBlank\' was removed ' - . 'in TCA ' . $table . '[\'columns\'][\'' . $fieldName . '\'][\'l10n_mode\']'; + . 'in TCA ' . $table . '[\'columns\'][\'' . $fieldName . '\'][\'l10n_mode\']' + . ' and changed to ' . $table . '[\'columns\'][\'' . $fieldName . '\'][\'behaviour\']' + . '[\'allowLanguageSynchronization\'] = true'; } } } diff --git a/typo3/sysext/core/Configuration/TCA/sys_category.php b/typo3/sysext/core/Configuration/TCA/sys_category.php index 721fbbbd4daec1fb6107f8120014925a373832e2..a3a159947bc383328661f816a3be372590ebfda3 100644 --- a/typo3/sysext/core/Configuration/TCA/sys_category.php +++ b/typo3/sysext/core/Configuration/TCA/sys_category.php @@ -116,7 +116,10 @@ return [ 'type' => 'input', 'renderType' => 'inputDateTime', 'eval' => 'datetime', - 'default' => 0 + 'default' => 0, + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ] ] ], 'endtime' => [ @@ -129,6 +132,9 @@ return [ 'default' => 0, 'range' => [ 'upper' => mktime(0, 0, 0, 1, 1, 2038), + ], + 'behaviour' => [ + 'allowLanguageSynchronization' => true, ] ] ], diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-51291-PageRepositoryShouldFieldBeOverlaid.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-51291-PageRepositoryShouldFieldBeOverlaid.rst new file mode 100644 index 0000000000000000000000000000000000000000..1bcfd1b37a3cb506815b3aaf28d43d8c436f1eb4 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-51291-PageRepositoryShouldFieldBeOverlaid.rst @@ -0,0 +1,34 @@ +.. include:: ../../Includes.txt + +============================================================ +Deprecation: #51291 - PageRepository shouldFieldBeOverlaid() +============================================================ + +See :issue:`51291` + +Description +=========== + +The following method has been deprecated: + +* :code:`TYPO3\CMS\Frontend\Page\PageRepository->shouldFieldBeOverlaid()` + + +Impact +====== + +Localized record fields are always "overlaid", the method returns true in all cases. + + +Affected Installations +====================== + +Instances with extensions calling this method + + +Migration +========= + +The deprecated method returns TRUE in all cases, the call can be omitted. + +.. index:: Frontend, PHP-API \ No newline at end of file diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-51291-SynchronizedFieldValuesInLocalizedRecords.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-51291-SynchronizedFieldValuesInLocalizedRecords.rst new file mode 100644 index 0000000000000000000000000000000000000000..f938d286a0c4adf56214c696c686990089cf1fac --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-51291-SynchronizedFieldValuesInLocalizedRecords.rst @@ -0,0 +1,46 @@ +.. include:: ../../Includes.txt + +================================================================ +Feature: #51291 - Synchronized field values in localized records +================================================================ + +See :issue:`51291` + +Description +=========== + +The localized record overlay behaviour has been changed to make localization rows standalone. + +Previously, if fields in :code:`TCA` columns were set to :code:`l10n_mode` :code:`exclude` +or :code:`mergeIfNotBlank`, the localized record overlay did not contain values, and those +values were "pulled up" from the underlying default language records. + +This has been changed, the :code:`DataHandler` now copies those values over to the localized +record and synchronizes them if the default language record is changed. + +As a substitution of the :code:`mergeIfNotBlank` feature, the new configuration :code:`allowLanguageSynchronization` +has been added. Setting this adds a wizard to single fields and an editor can select if a field of a localized record +should be kept in sync with the default language record, or the localized record it was derived from. + +A typical configuration looks like that: + +.. code-block:: php + + 'columns' => [ + ... + 'header' => [ + 'label' => 'My header', + 'config' => [ + 'type' => 'input', + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ], + ], + ], + ], + +:code:`TCA` tables that configure the language localization get field :code:`l10n_state` added by the schema analyzer +which stores an json array with field names and the values :code:`custom`, :code:`parent` or :code:`source` to +specify if and from which record a single field gets its value. + +.. index:: Backend, Database, Frontend, PHP-API, TCA \ No newline at end of file diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php b/typo3/sysext/core/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php index b0950f8fc0e181a9c29debf561a4c9534afa93a1..b091ed9b401865734467798490983fc87a9598f2 100644 --- a/typo3/sysext/core/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php @@ -217,6 +217,16 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast]; } + public function localizeContentOfRelationWithLanguageSynchronization() + { + $GLOBALS['TCA']['tt_content']['columns']['tx_testdatahandler_group']['config']['behaviour']['allowLanguageSynchronization'] = true; + $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdLast, self::VALUE_LanguageId); + $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast]; + $this->actionService->modifyReferences( + self::TABLE_Content, self::VALUE_ContentIdLast, self::FIELD_ContentElement, [self::VALUE_ElementIdFirst, self::VALUE_ElementIdSecond] + ); + } + /** * @test * @see DataSet/localizeElementOfRelation.csv diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/ActionTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/ActionTest.php index 658b4074d91fffe083dda993cba2ec86466f51d8..98d197a585e196399a4ee55ae92e3fad578ea21c 100644 --- a/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/ActionTest.php +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/ActionTest.php @@ -286,6 +286,21 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\Group\Abs ->setTable(self::TABLE_Element)->setField('title')->setValues('Element #2', 'Element #3')); } + /** + * @test + * @see DataSet/localizeContentOfRelationWSynchronization.csv + */ + public function localizeContentOfRelationWithLanguageSynchronization() + { + parent::localizeContentOfRelationWithLanguageSynchronization(); + $this->assertAssertionDataSet('localizeContentOfRelationWSynchronization'); + + $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections(); + $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint() + ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField(self::FIELD_ContentElement) + ->setTable(self::TABLE_Element)->setField('title')->setValues('Element #1', 'Element #2')); + } + /** * @test * @see DataSet/localizeElementOfRelation.csv diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/DataSet/localizeContentOfRelationWSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/DataSet/localizeContentOfRelationWSynchronization.csv new file mode 100644 index 0000000000000000000000000000000000000000..f52954fb72a72ab2184bf577246e3067493f9301 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/DataSet/localizeContentOfRelationWSynchronization.csv @@ -0,0 +1,16 @@ +"pages",,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title",,, +,1,0,256,0,0,0,0,0,0,0,"FunctionalTest",,, +,88,1,256,0,0,0,0,0,0,0,"DataHandlerTest",,, +,89,88,256,0,0,0,0,0,0,0,"Relations",,, +,90,88,512,0,0,0,0,0,0,0,"Target",,, +"tt_content",,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","header","tx_testdatahandler_group" +,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1","1,2" +,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2","1,2" +,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2","1,2" +"tx_testdatahandler_element",,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","sys_language_uid","l10n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title", +,1,89,256,0,0,0,0,0,0,0,0,0,"Element #1", +,2,89,512,0,0,0,0,0,0,0,0,0,"Element #2", +,3,89,768,0,0,0,0,0,0,0,0,0,"Element #3", diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/AbstractActionTestCase.php b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/AbstractActionTestCase.php index 586ee679074e7550dc57b5edbac16bfcae141b94..4d75bc68727f9f058cc5d5ada06418f93f5f12dc 100644 --- a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/AbstractActionTestCase.php +++ b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/AbstractActionTestCase.php @@ -194,6 +194,23 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast]; } + public function localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization() + { + $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizationMode'] = 'select'; + $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['allowLanguageSynchronization'] = true; + $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizeChildrenAtParentLocalization'] = true; + $GLOBALS['TCA'][self::TABLE_Hotel]['columns'][self::FIELD_HotelOffer]['config']['behaviour']['localizeChildrenAtParentLocalization'] = true; + $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdLast, self::VALUE_LanguageId); + $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast]; + $this->actionService->modifyRecords( + self::VALUE_PageId, + [ + self::TABLE_Content => ['uid' => self::VALUE_ContentIdLast, self::FIELD_ContentHotel => '5,__nextUid'], + self::TABLE_Hotel => ['uid' => '__NEW', 'title' => 'Hotel #2'], + ] + ); + } + /** * @see DataSet/changeParentContentRecordSorting.csv */ diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/ActionTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/ActionTest.php index e1e215b4fa105427df83ee3dfbf292b239671fbe..02255f6ef570f12b11bbd753a320411a3f7b08bb 100644 --- a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/ActionTest.php +++ b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/ActionTest.php @@ -227,6 +227,22 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\IRRE\CSV\ ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Dansk:] Hotel #1')); } + /** + * @test + * @see DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv + */ + public function localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization() + { + parent::localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization(); + $this->assertAssertionDataSet('localizeParentContentWAllChildrenSelectNLanguageSynchronization'); + + $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections(); + $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint() + ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField(self::FIELD_ContentHotel) + // @todo Actually Hotel #2 should be prefixed as well + ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Dansk:] Hotel #1', 'Hotel #2')); + } + /** * @test * @see DataSet/changeParentContentRecordSorting.csv diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv new file mode 100644 index 0000000000000000000000000000000000000000..4c3d1c047cd8c342ed922320bc43813f084a003f --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv @@ -0,0 +1,30 @@ +"tt_content",,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","header","tx_irretutorial_1ncsv_hotels" +,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1","3,4" +,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2","5,7" +,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2","6,8" +"tx_irretutorial_1ncsv_hotel",,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","offers" +,3,89,256,0,0,0,0,0,0,0,0,0,"Hotel #1","5,6" +,4,89,128,0,0,0,0,0,0,0,0,0,"Hotel #2",7 +,5,89,64,0,0,0,0,0,0,0,0,0,"Hotel #1",8 +,6,89,96,0,1,5,5,0,0,0,0,0,"[Translate to Dansk:] Hotel #1",9 +,7,89,32,0,0,0,0,0,0,0,0,0,"Hotel #2","" +,8,89,16,0,1,7,0,0,0,0,0,0,"Hotel #2","" +"tx_irretutorial_1ncsv_offer",,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","prices" +,5,89,256,0,0,0,0,0,0,0,0,0,"Offer #1.1","7,8,9" +,6,89,128,0,0,0,0,0,0,0,0,0,"Offer #1.2","10,11" +,7,89,64,0,0,0,0,0,0,0,0,0,"Offer #2.1",12 +,8,89,32,0,0,0,0,0,0,0,0,0,"Offer #1.1",13 +,9,89,48,0,1,8,8,0,0,0,0,0,"[Translate to Dansk:] Offer #1.1",14 +"tx_irretutorial_1ncsv_price",,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title", +,7,89,256,0,0,0,0,0,0,0,0,0,"Price #1.1.1", +,8,89,128,0,0,0,0,0,0,0,0,0,"Price #1.1.2", +,9,89,64,0,0,0,0,0,0,0,0,0,"Price #1.1.3", +,10,89,32,0,0,0,0,0,0,0,0,0,"Price #1.2.1", +,11,89,16,0,0,0,0,0,0,0,0,0,"Price #1.2.2", +,12,89,8,0,0,0,0,0,0,0,0,0,"Price #2.1.1", +,13,89,4,0,0,0,0,0,0,0,0,0,"Price #1.1.1", +,14,89,6,0,1,13,13,0,0,0,0,0,"[Translate to Dansk:] Price #1.1.1", diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php index 9ebcea4560e68c9a3fdb4f747300df229d6e695c..81b52e050c947722800f3447fb844a8dfbd33a39 100644 --- a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php +++ b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php @@ -200,6 +200,26 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast]; } + /** + * @see DataSet/localizeParentContentWAllChildrenSelect.csv + */ + public function localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization() + { + $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizationMode'] = 'select'; + $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['allowLanguageSynchronization'] = true; + $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizeChildrenAtParentLocalization'] = true; + $GLOBALS['TCA'][self::TABLE_Hotel]['columns'][self::FIELD_HotelOffer]['config']['behaviour']['localizeChildrenAtParentLocalization'] = true; + $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdLast, self::VALUE_LanguageId); + $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast]; + $this->actionService->modifyRecords( + self::VALUE_PageId, + [ + self::TABLE_Content => ['uid' => self::VALUE_ContentIdLast, self::FIELD_ContentHotel => '5,__nextUid'], + self::TABLE_Hotel => ['uid' => '__NEW', 'title' => 'Hotel #2'], + ] + ); + } + /** * @see DataSet/changeParentContentRecordSorting.csv */ diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/ActionTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/ActionTest.php index 0b199202bd4f5faa142e8c4f899e5f2f9d9e3587..ff28c1a5a1dccc1372ce1caa37f4061b3106fa98 100644 --- a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/ActionTest.php +++ b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/ActionTest.php @@ -228,6 +228,22 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\IRRE\Fore ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Dansk:] Hotel #1')); } + /** + * @test + * @see DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv + */ + public function localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization() + { + parent::localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization(); + $this->assertAssertionDataSet('localizeParentContentWAllChildrenSelectNLanguageSynchronization'); + + $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections('Default', 'Extbase:list()'); + $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint() + ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField(self::FIELD_ContentHotel) + // @todo Actually Hotel #2 should be prefixed as well + ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Dansk:] Hotel #1', 'Hotel #2')); + } + /** * @test * @see DataSet/changeParentContentRecordSorting.csv diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv new file mode 100644 index 0000000000000000000000000000000000000000..39054713a0235d5a09b40c8f03a010a3ed0f0540 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv @@ -0,0 +1,30 @@ +"tt_content",,,,,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","header","tx_irretutorial_1nff_hotels",,, +,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1",2,,, +,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2",2,,, +,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2",2,,, +"tx_irretutorial_1nff_hotel",,,,,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parentid","parenttable","parentidentifier","offers" +,3,89,1024,0,0,0,0,0,0,0,0,0,"Hotel #1",297,"tt_content",,2 +,4,89,1536,0,0,0,0,0,0,0,0,0,"Hotel #2",297,"tt_content",,1 +,5,89,1,0,0,0,0,0,0,0,0,0,"Hotel #1",298,"tt_content",,1 +,6,89,1,0,1,5,5,0,0,0,0,0,"[Translate to Dansk:] Hotel #1",299,"tt_content",,1 +,7,89,2,0,0,0,0,0,0,0,0,0,"Hotel #2",298,"tt_content",,0 +,8,89,2,0,1,7,0,0,0,0,0,0,"Hotel #2",299,"tt_content",,0 +"tx_irretutorial_1nff_offer",,,,,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parentid","parenttable","parentidentifier","prices" +,5,89,512,0,0,0,0,0,0,0,0,0,"Offer #1.1",3,"tx_irretutorial_1nff_hotel",,3 +,6,89,1536,0,0,0,0,0,0,0,0,0,"Offer #1.2",3,"tx_irretutorial_1nff_hotel",,2 +,7,89,768,0,0,0,0,0,0,0,0,0,"Offer #2.1",4,"tx_irretutorial_1nff_hotel",,1 +,8,89,1024,0,0,0,0,0,0,0,0,0,"Offer #1.1",5,"tx_irretutorial_1nff_hotel",,1 +,9,89,1,0,1,8,8,0,0,0,0,0,"[Translate to Dansk:] Offer #1.1",6,"tx_irretutorial_1nff_hotel",,1 +"tx_irretutorial_1nff_price",,,,,,,,,,,,,,,,, +,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parentid","parenttable","parentidentifier", +,7,89,512,0,0,0,0,0,0,0,0,0,"Price #1.1.1",5,"tx_irretutorial_1nff_offer",, +,8,89,1792,0,0,0,0,0,0,0,0,0,"Price #1.1.2",5,"tx_irretutorial_1nff_offer",, +,9,89,2304,0,0,0,0,0,0,0,0,0,"Price #1.1.3",5,"tx_irretutorial_1nff_offer",, +,10,89,768,0,0,0,0,0,0,0,0,0,"Price #1.2.1",6,"tx_irretutorial_1nff_offer",, +,11,89,2048,0,0,0,0,0,0,0,0,0,"Price #1.2.2",6,"tx_irretutorial_1nff_offer",, +,12,89,1024,0,0,0,0,0,0,0,0,0,"Price #2.1.1",7,"tx_irretutorial_1nff_offer",, +,13,89,1280,0,0,0,0,0,0,0,0,0,"Price #1.1.1",8,"tx_irretutorial_1nff_offer",, +,14,89,1,0,1,13,13,0,0,0,0,0,"[Translate to Dansk:] Price #1.1.1",9,"tx_irretutorial_1nff_offer",, diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php index 51837c519402c42e7cbcbbc845033ea45b29d54e..f487ff37a4d0fa397da6a8b4de28e8418b9dc251 100644 --- a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php @@ -137,6 +137,13 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdSecond, self::VALUE_LanguageId); } + public function localizeContentWithLanguageSynchronization() + { + $GLOBALS['TCA']['tt_content']['columns']['header']['config']['behaviour']['allowLanguageSynchronization'] = true; + $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdSecond, self::VALUE_LanguageId); + $this->actionService->modifyRecord(self::TABLE_Content, self::VALUE_ContentIdSecond, ['header' => 'Testing #1']); + } + /** * @test * @see DataSet/localizeContentFromNonDefaultLanguage.csv diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php index 37a38e0c1a07d63b5f0b6d434504e2a069678ccf..25e13f974f8c84cd23d77a06335f7a0675be165e 100644 --- a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php @@ -164,6 +164,20 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\Regular\A ->setTable(self::TABLE_Content)->setField('header')->setValues('[Translate to Dansk:] Regular Element #1', '[Translate to Dansk:] Regular Element #2')); } + /** + * @test + * @see DataSet/localizeContentWSynchronization.csv + */ + public function localizeContentWithLanguageSynchronization() + { + parent::localizeContentWithLanguageSynchronization(); + $this->assertAssertionDataSet('localizeContentWSynchronization'); + + $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections(); + $this->assertThat($responseSections, $this->getRequestSectionHasRecordConstraint() + ->setTable(self::TABLE_Content)->setField('header')->setValues('[Translate to Dansk:] Regular Element #1', 'Testing #1')); + } + /** * @test * @see DataSet/localizeContentFromNonDefaultLanguage.csv diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/localizeContentWSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/localizeContentWSynchronization.csv new file mode 100644 index 0000000000000000000000000000000000000000..741cfaa28b39c5d7c72a4e9dd400b3871ea55ace --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/localizeContentWSynchronization.csv @@ -0,0 +1,9 @@ +tt_content,,,,,,,,,,,,,, +,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,l10n_source,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,header +,297,89,256,0,0,0,0,0,0,0,0,0,0,Regular Element #1 +,298,89,512,0,0,0,0,0,0,0,0,0,0,Testing #1 +,299,89,768,0,0,0,0,0,0,0,0,0,0,Regular Element #3 +,300,89,1024,0,1,299,299,299,0,0,0,0,0,[Translate to Dansk:] Regular Element #3 +,301,89,384,0,1,297,297,297,0,0,0,0,0,[Translate to Dansk:] Regular Element #1 +,302,89,448,0,2,297,301,301,0,0,0,0,0,[Translate to Deutsch:] [Translate to Dansk:] Regular Element #1 +,303,89,416,0,1,298,298,298,0,0,0,0,0,Testing #1 diff --git a/typo3/sysext/core/Tests/Unit/Migrations/TcaMigrationTest.php b/typo3/sysext/core/Tests/Unit/Migrations/TcaMigrationTest.php index 306ed0e61f362cd4655b9f40e381acc2229fc1eb..5d58d72cdf16277c423455632267d98182cba459 100644 --- a/typo3/sysext/core/Tests/Unit/Migrations/TcaMigrationTest.php +++ b/typo3/sysext/core/Tests/Unit/Migrations/TcaMigrationTest.php @@ -2121,6 +2121,11 @@ class TcaMigrationTest extends \TYPO3\Components\TestingFramework\Core\Unit\Unit 'aTable' => [ 'columns' => [ 'aColumn' => [ + 'config' => [ + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ] + ] ], ], ], diff --git a/typo3/sysext/core/ext_localconf.php b/typo3/sysext/core/ext_localconf.php index 1f8091c45ba0991ab6dd6dfce8e42224ddaf87c4..22ce307faaef9bf2df51da732de71a69220d8402 100644 --- a/typo3/sysext/core/ext_localconf.php +++ b/typo3/sysext/core/ext_localconf.php @@ -69,6 +69,14 @@ $signalSlotDispatcher->connect( 'processFile' ); +$signalSlotDispatcher->connect( + \TYPO3\CMS\Install\Service\SqlExpectedSchemaService::class, + 'tablesDefinitionIsBeingBuilt', + \TYPO3\CMS\Core\DataHandling\DatabaseSchemaService::class, + 'getLocalizationRequiredDatabaseSchema' +); + + unset($signalSlotDispatcher); $GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['dumpFile'] = \TYPO3\CMS\Core\Controller\FileDumpController::class . '::dumpAction'; diff --git a/typo3/sysext/filemetadata/Configuration/TCA/Overrides/sys_file_metadata.php b/typo3/sysext/filemetadata/Configuration/TCA/Overrides/sys_file_metadata.php index b3310007cade4b2db0963e3125a449a2fea68cfd..3b15749da90404ae288832fa470759e8ef18e6f8 100644 --- a/typo3/sysext/filemetadata/Configuration/TCA/Overrides/sys_file_metadata.php +++ b/typo3/sysext/filemetadata/Configuration/TCA/Overrides/sys_file_metadata.php @@ -279,7 +279,10 @@ $tca = [ 'config' => [ 'type' => 'input', 'size' => 20, - 'eval' => 'trim' + 'eval' => 'trim', + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ] ], ], 'location_region' => [ @@ -289,7 +292,10 @@ $tca = [ 'config' => [ 'type' => 'input', 'size' => 20, - 'eval' => 'trim' + 'eval' => 'trim', + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ] ], ], 'location_city' => [ @@ -299,7 +305,10 @@ $tca = [ 'config' => [ 'type' => 'input', 'size' => 20, - 'eval' => 'trim' + 'eval' => 'trim', + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ] ], ], 'latitude' => [ diff --git a/typo3/sysext/frontend/Classes/Page/PageRepository.php b/typo3/sysext/frontend/Classes/Page/PageRepository.php index 1ff9ef510731695c90b4ca7ad185ad6b6b62afa1..0098bf758c2fc0ebbcf3c20823c618f94964d9a5 100644 --- a/typo3/sysext/frontend/Classes/Page/PageRepository.php +++ b/typo3/sysext/frontend/Classes/Page/PageRepository.php @@ -506,9 +506,7 @@ class PageRepository // Overwrite the original field with the overlay foreach ($overlays[$origPage['uid']] as $fieldName => $fieldValue) { if ($fieldName !== 'uid' && $fieldName !== 'pid') { - if ($this->shouldFieldBeOverlaid('pages_language_overlay', $fieldName, $fieldValue)) { - $pagesOutput[$key][$fieldName] = $fieldValue; - } + $pagesOutput[$key][$fieldName] = $fieldValue; } } } @@ -594,9 +592,7 @@ class PageRepository } foreach ($row as $fN => $fV) { if ($fN !== 'uid' && $fN !== 'pid' && isset($olrow[$fN])) { - if ($this->shouldFieldBeOverlaid($table, $fN, $olrow[$fN])) { - $row[$fN] = $olrow[$fN]; - } + $row[$fN] = $olrow[$fN]; } elseif ($fN === 'uid') { $row['_LOCALIZED_UID'] = $olrow['uid']; } @@ -1889,10 +1885,7 @@ class PageRepository ); if ($isTableLocalizable && $localizedId !== null) { $localizedReferences = $fileRepository->findByRelation($tableName, $fieldName, $localizedId); - $localizedReferencesValue = $localizedReferences ?: ''; - if ($this->shouldFieldBeOverlaid($tableName, $fieldName, $localizedReferencesValue)) { - $references = $localizedReferences; - } + $references = $localizedReferences; } return $references; @@ -1922,20 +1915,12 @@ class PageRepository * @param string $field TCA fieldname * @param mixed $value Current value of the field * @return bool Returns TRUE if a given record field needs to be overlaid + * @deprecated since TYPO3 v8, will be removed in TYPO3 v9 */ protected function shouldFieldBeOverlaid($table, $field, $value) { - $l10n_mode = isset($GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode']) - ? $GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode'] - : ''; - - $shouldFieldBeOverlaid = true; - - if ($l10n_mode === 'exclude') { - $shouldFieldBeOverlaid = false; - } - - return $shouldFieldBeOverlaid; + GeneralUtility::logDeprecatedFunction(); + return true; } /** diff --git a/typo3/sysext/frontend/Configuration/TCA/tt_content.php b/typo3/sysext/frontend/Configuration/TCA/tt_content.php index cd31a8520723c1a7c479ab9ca17d9551f727c818..33d74cd7c9b181a75737c74f7f3d57646d340fa4 100644 --- a/typo3/sysext/frontend/Configuration/TCA/tt_content.php +++ b/typo3/sysext/frontend/Configuration/TCA/tt_content.php @@ -466,8 +466,8 @@ return [ 'config' => [ 'type' => 'input', 'size' => 50, - 'max' => 255 - ] + 'max' => 255, + ], ], 'header_layout' => [ 'exclude' => true, @@ -971,7 +971,7 @@ return [ 'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig('media', [ 'appearance' => [ 'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:media.addFileReference' - ] + ], ]) ], 'filelink_size' => [ diff --git a/typo3/sysext/frontend/Tests/Unit/Page/PageRepositoryTest.php b/typo3/sysext/frontend/Tests/Unit/Page/PageRepositoryTest.php index 28770c99562d6162c5c9e3b713b59e5a50067d51..6ff7ad639b3c115e94700840a4f2a7f71fc1d99a 100644 --- a/typo3/sysext/frontend/Tests/Unit/Page/PageRepositoryTest.php +++ b/typo3/sysext/frontend/Tests/Unit/Page/PageRepositoryTest.php @@ -126,11 +126,11 @@ class PageRepositoryTest extends \TYPO3\Components\TestingFramework\Core\Unit\Un public function getShouldFieldBeOverlaidData() { return [ - ['default', 'fake_table', 'foobar', true, 'default is to merge non-empty string'], - ['default', 'fake_table', '', true, 'default is to merge empty string'], + ['default', 'fake_table', 'foobar', true, 'default is to overlay non-empty string'], + ['default', 'fake_table', '', true, 'default is to overlay empty string'], - ['exclude', 'fake_table', '', false, 'exclude field with empty string'], - ['exclude', 'fake_table', 'foobar', false, 'exclude field with non-empty string'], + ['exclude', 'fake_table', '', true, 'exclude field with empty string'], + ['exclude', 'fake_table', 'foobar', true, 'exclude field with non-empty string'], ['prefixLangTitle', 'fake_table', 'foobar', true, 'prefixLangTitle is merged with non-empty string'], ['prefixLangTitle', 'fake_table', '', true, 'prefixLangTitle is merged with empty string'], diff --git a/typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php b/typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php index d922717459e511fd19952ed9895b2f179c039043..eaa9da20ea60bbe6b43b30df77cd65944fb28f5c 100644 --- a/typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php +++ b/typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php @@ -48,7 +48,7 @@ class DatabaseRowsUpdateWizard extends AbstractUpdate * @var array Single classes that may update rows */ protected $rowUpdater = [ - L10nModeUpdater::class, +// L10nModeUpdater::class, ]; /** diff --git a/typo3/sysext/install/Classes/Updates/RowUpdater/L10nModeUpdater.php b/typo3/sysext/install/Classes/Updates/RowUpdater/L10nModeUpdater.php index 7383b587f6e364b37116a571a2c150200d68c421..ee99575cad61857f54d5c31ef12ad1c6ed924fa5 100644 --- a/typo3/sysext/install/Classes/Updates/RowUpdater/L10nModeUpdater.php +++ b/typo3/sysext/install/Classes/Updates/RowUpdater/L10nModeUpdater.php @@ -17,7 +17,6 @@ namespace TYPO3\CMS\Install\Updates\RowUpdater; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; use TYPO3\CMS\Core\DataHandling\DataHandler; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Install\Service\LoadTcaService; @@ -25,26 +24,11 @@ use TYPO3\CMS\Install\Service\LoadTcaService; /** * Migrate values for database records having columns * using "l10n_mode" set to "mergeIfNotBlank". + * + * @todo: This needs a review and finish */ class L10nModeUpdater implements RowUpdaterInterface { - /** - * Field names that previously had a migrated l10n_mode setting in TCA. - * - * @var array - */ - protected $migratedL10nCoreFieldNames = [ - 'sys_category' => [ - 'starttime' => 'mergeIfNotBlank', - 'endtime' => 'mergeIfNotBlank', - ], - 'sys_file_metadata' => [ - 'location_country' => 'mergeIfNotBlank', - 'location_region' => 'mergeIfNotBlank', - 'location_city' => 'mergeIfNotBlank', - ], - ]; - /** * List of tables with information about to migrate fields. * Created during hasPotentialUpdateForTable(), used in updateTableRow() @@ -60,7 +44,8 @@ class L10nModeUpdater implements RowUpdaterInterface */ public function getTitle(): string { - return 'Migrate values in database records having "l10n_mode" set to "mergeIfNotBlank'; + return 'Migrate values in database records having "l10n_mode"' + . ' either set to "exclude" or "mergeIfNotBlank"'; } /** @@ -102,6 +87,7 @@ class L10nModeUpdater implements RowUpdaterInterface $fakeAdminUser = GeneralUtility::makeInstance(BackendUserAuthentication::class); $fakeAdminUser->user = ['admin' => 1]; + // disable DataHandler hooks for processing this update if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'])) { $dataHandlerHooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']; unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']); @@ -119,6 +105,7 @@ class L10nModeUpdater implements RowUpdaterInterface $sourceRow = $this->getRow($sourceTableName, $source); $updateValues = []; + $l10nState = []; $row = $this->getRow($tableName, $uid); foreach ($row as $fieldName => $fieldValue) { @@ -126,6 +113,8 @@ class L10nModeUpdater implements RowUpdaterInterface continue; } + $l10nState[$fieldName] = 'custom'; + if ( // default empty($fieldTypes[$fieldName]) @@ -139,6 +128,7 @@ class L10nModeUpdater implements RowUpdaterInterface ) ) { $updateValues[$fieldName] = $sourceRow[$fieldName]; + $l10nState[$fieldName] = 'parent'; } // inline types, but only file references if ( @@ -161,12 +151,11 @@ class L10nModeUpdater implements RowUpdaterInterface $dataHandler = GeneralUtility::makeInstance(DataHandler::class); $dataHandler->start([], $commandMap, $fakeAdminUser); $dataHandler->process_cmdmap(); + $l10nState[$fieldName] = 'parent'; } } - if (empty($updateValues)) { - return $inputRow; - } + $updateValues['l10n_state'] = json_encode($l10nState); $queryBuilder = $connectionPool->getQueryBuilderForTable($tableName); foreach ($updateValues as $updateFieldName => $updateValue) { @@ -231,20 +220,16 @@ class L10nModeUpdater implements RowUpdaterInterface $fields = []; $fieldTypes = []; foreach ($tableDefinition['columns'] as $fieldName => $fieldConfiguration) { - if ( - empty($fieldConfiguration['l10n_mode']) - && !empty($this->migratedL10nCoreFieldNames[$tableName][$fieldName]) - ) { - $fieldConfiguration['l10n_mode'] = $this->migratedL10nCoreFieldNames[$tableName][$fieldName]; - } - if ( empty($fieldConfiguration['l10n_mode']) || empty($fieldConfiguration['config']['type']) ) { continue; } - if ($fieldConfiguration['l10n_mode'] === 'mergeIfNotBlank') { + if ( + $fieldConfiguration['l10n_mode'] === 'exclude' + || $fieldConfiguration['l10n_mode'] === 'mergeIfNotBlank' + ) { $fields[$fieldName] = $fieldConfiguration; } } @@ -253,35 +238,17 @@ class L10nModeUpdater implements RowUpdaterInterface return $payload; } - $parentQueryBuilder = $connectionPool->getQueryBuilderForTable($tableName); - $parentQueryBuilder->getRestrictions()->removeAll(); - $parentQueryBuilder->from($tableName); + $queryBuilder = $connectionPool->getQueryBuilderForTable($tableName); + $queryBuilder->getRestrictions()->removeAll(); + $queryBuilder->from($tableName); - $predicates = []; foreach ($fields as $fieldName => $fieldConfiguration) { - $predicates[] = $parentQueryBuilder->expr()->comparison( - $parentQueryBuilder->expr()->trim($fieldName), - ExpressionBuilder::EQ, - $parentQueryBuilder->createNamedParameter('', \PDO::PARAM_STR) - ); - $predicates[] = $parentQueryBuilder->expr()->eq( - $fieldName, - $parentQueryBuilder->createNamedParameter('', \PDO::PARAM_STR) - ); - if (empty($fieldConfiguration['config']['type'])) { continue; } if ($fieldConfiguration['config']['type'] === 'group') { $fieldTypes[$fieldName] = 'group'; - $predicates[] = $parentQueryBuilder->expr()->isNull( - $fieldName - ); - $predicates[] = $parentQueryBuilder->expr()->eq( - $fieldName, - $parentQueryBuilder->createNamedParameter('0', \PDO::PARAM_STR) - ); } if ( $fieldConfiguration['config']['type'] === 'inline' @@ -291,32 +258,6 @@ class L10nModeUpdater implements RowUpdaterInterface && $fieldConfiguration['config']['foreign_table'] === 'sys_file_reference' ) { $fieldTypes[$fieldName] = 'inline/FAL'; - - $childQueryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_reference'); - $childQueryBuilder->getRestrictions()->removeAll(); - $childExpression = $childQueryBuilder - ->count('uid') - ->from('sys_file_reference') - ->andWhere( - $childQueryBuilder->expr()->eq( - 'sys_file_reference.uid_foreign', - $parentQueryBuilder->getConnection()->quoteIdentifier($tableName . '.uid') - ), - $childQueryBuilder->expr()->eq( - 'sys_file_reference.tablenames', - $parentQueryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR) - ), - $childQueryBuilder->expr()->eq( - 'sys_file_reference.fieldname', - $parentQueryBuilder->createNamedParameter($fieldName, \PDO::PARAM_STR) - ) - ); - - $predicates[] = $parentQueryBuilder->expr()->comparison( - '(' . $childExpression->getSQL() . ')', - ExpressionBuilder::GT, - $parentQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT) - ); } } @@ -330,18 +271,17 @@ class L10nModeUpdater implements RowUpdaterInterface ); } - $statement = $parentQueryBuilder + $statement = $queryBuilder ->select(...$selectFieldNames) ->andWhere( - $parentQueryBuilder->expr()->gt( + $queryBuilder->expr()->gt( $tableDefinition['ctrl']['languageField'], - $parentQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT) + $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) ), - $parentQueryBuilder->expr()->gt( + $queryBuilder->expr()->gt( $sourceFieldName, - $parentQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT) - ), - $parentQueryBuilder->expr()->orX(...$predicates) + $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) + ) ) ->execute(); diff --git a/typo3/sysext/lang/Resources/Private/Language/locallang_wizards.xlf b/typo3/sysext/lang/Resources/Private/Language/locallang_wizards.xlf index 02d04ae9af72af3a035c24014f763fb0c3db0c76..d4a6b3eb8ceab2766b108dc6a184689f0a130752 100644 --- a/typo3/sysext/lang/Resources/Private/Language/locallang_wizards.xlf +++ b/typo3/sysext/lang/Resources/Private/Language/locallang_wizards.xlf @@ -330,6 +330,18 @@ <trans-unit id="imwizard.crop-height"> <source>height:</source> </trans-unit> + <trans-unit id="localizationStateSelector.header"> + <source>Translation behavior</source> + </trans-unit> + <trans-unit id="localizationStateSelector.customValue"> + <source>Custom value</source> + </trans-unit> + <trans-unit id="localizationStateSelector.defaultLanguageValue"> + <source>Value of default language</source> + </trans-unit> + <trans-unit id="localizationStateSelector.sourceLanguageValue"> + <source>Value of %1s language</source> + </trans-unit> </body> </file> </xliff>