Commit 3310c162 authored by Oliver Bartsch's avatar Oliver Bartsch
Browse files

[BUGFIX] Properly handle l10n_display=displayAsReadonly

TCA provides the "l10n_display" configuration,
which allows to define how TCA fields should
behave in localizations. One of the possible
values is "defaultAsReadonly", which just
renders the default records value in a
readonly field. This is non functional
and only used in FormEngine.

Common use cases are fields, which do not
change in a localization, e.g. the author
of a blog article.

How did this work?

The SingleFieldContainer, entry point of
each TCA field, checked whether the option
is set and if so, overrides the "itemFormElValue"
- containing the processed database field value -
with the raw field value of the default record
and automatically sets the field to "readOnly".

This already worked for basic types, such
as "input", "check" or "radio", since their
database values are usually not further
processed by any form data provider.

However, when it comes to types, dealing with
relations or enrichments, just using the default
records' raw database value did mostly not work.

Therefore, this patch removes the replacement
from SingleFieldContainer and adds a new
form data provider, dealing with this task.
This way, we ensure all following data providers
can process the correct value (either the
current database value or the default
records value).

This can be tested with a new styleguide record:
https://github.com/TYPO3/styleguide/pull/254

For some cases (mostly MM lookups and inline),
the "l10n_mode" needs to be set to "exclude",
when using "defaultAsReadonly", since otherwise
the DataHandler doesn't hold the relations in sync.

Note: TCA type "slug" and TCA renderType
"belayoutwizard" do not yet handle readOnly
at all, because this option is not supported
as "columns config". This is fixed in two
separate patches (#96096 and #96095).

Note: TCA type "flex" does still not work,
since the corresponding containers do not
implement readOnly handling at all. Fixing
this will be done in a separate patch.

Resolves: #89152
Related: #96095
Related: #96096
Releases: main, 11.5
Change-Id: Ic5346606c0309784a689c83dcefd1888bf31fc89
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72275


Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
parent 414d4159
......@@ -99,12 +99,12 @@ class SingleFieldContainer extends AbstractContainer
// The value to show in the form field.
$parameterArray['itemFormElValue'] = $row[$fieldName];
// Set field to read-only if configured for translated records to show default language content as readonly
// Note: In such case, the database value of this field was already overridden by DatabaseRowDefaultAsReadonly.
if (($parameterArray['fieldConf']['l10n_display'] ?? false)
&& GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly')
&& $isOverlay
) {
$parameterArray['fieldConf']['config']['readOnly'] = true;
$parameterArray['itemFormElValue'] = $this->data['defaultLanguageRow'][$fieldName];
}
$processedTcaType = $this->data['processedTca']['ctrl']['type'] ?? '';
......
......@@ -86,14 +86,8 @@ class SelectCheckBoxElement extends AbstractFormElement
return $resultArray;
}
// Get values in an array (and make unique, which is fine because there can be no duplicates anyway)
// In case e.g. "l10n_display" is set to "defaultAsReadonly" only one value (as string) could be handed in
if (is_array($parameterArray['itemFormElValue'])) {
$itemArray = $parameterArray['itemFormElValue'];
} else {
$itemArray = [(string)$parameterArray['itemFormElValue']];
}
$itemArray = array_flip($itemArray);
// Get item value as array and make unique, which is fine because there can be no duplicates anyway.
$itemArray = array_flip($parameterArray['itemFormElValue']);
// Initialize variables and traverse the items
$groups = [];
......@@ -245,7 +239,7 @@ class SelectCheckBoxElement extends AbstractFormElement
$html[] = '<button type="button" class="btn btn-default btn-sm t3js-revert-selection">';
$html[] = $this->iconFactory->getIcon('actions-edit-undo', Icon::SIZE_SMALL)->render() . ' ';
$html[] = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.revertSelection'));
$html[] = '</buttn>';
$html[] = '</button>';
$html[] = '</th>';
$html[] = '</tr>';
$html[] = '</thead>';
......
......@@ -90,7 +90,7 @@ class SelectSingleBoxElement extends AbstractFormElement
$selectItems = $parameterArray['fieldConf']['config']['items'];
$disabled = !empty($config['readOnly']);
// Get values in an array (and make unique, which is fine because there can be no duplicates anyway):
// Get item value as array and make unique, which is fine because there can be no duplicates anyway.
$itemArray = array_flip($parameterArray['itemFormElValue']);
$width = $this->formMaxWidth($this->defaultInputWidth);
......
......@@ -211,7 +211,7 @@ class TextTableElement extends AbstractFormElement
$html[] = '<div class="form-wizards-wrap">';
$html[] = $this->getTableWizard($attributes['id']);
$html[] = '<div>';
$html[] = '<textarea " ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . htmlspecialchars($itemValue) . '</textarea>';
$html[] = '<textarea ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . htmlspecialchars($itemValue) . '</textarea>';
$html[] = '</div>';
if (!empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-items-aside form-wizards-items-aside--field-control">';
......
......@@ -35,9 +35,12 @@ class ResetSelection extends AbstractNode
public function render()
{
$parameterArray = $this->data['parameterArray'];
$itemName = $parameterArray['itemFormElName'];
$selectItems = $parameterArray['fieldConf']['config']['items'];
if (($parameterArray['fieldConf']['config']['readOnly'] ?? false) || empty($selectItems)) {
// Early return if the field is readOnly or no items exist
return [];
}
$itemName = $parameterArray['itemFormElName'];
$itemArray = array_flip($parameterArray['itemFormElValue']);
$initiallySelectedIndices = [];
foreach ($selectItems as $i => $item) {
......
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Special data provider for replacing a database field with the value of
* the default record in case "l10n_display" is set to "defaultAsReadonly".
*/
class DatabaseRowDefaultAsReadonly implements FormDataProviderInterface
{
/**
* Check each field for being an overlay, having l10n_display set to defaultAsReadonly
* and whether the field exists in the default language row. If so, the current
* database value will be replaced by the one from the default language row.
*/
public function addData(array $result): array
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (!isset($result['defaultLanguageRow'][$fieldName])) {
// No default value available for this field
continue;
}
if (!GeneralUtility::inList(($result['processedTca']['columns'][$fieldName]['l10n_display'] ?? ''), 'defaultAsReadonly')) {
// defaultAsReadonly is not set for this field
continue;
}
if (!($result['databaseRow'][$result['processedTca']['ctrl']['languageField'] ?? null] ?? false)
|| !($result['databaseRow'][$result['processedTca']['ctrl']['transOrigPointerField'] ?? null] ?? false)
) {
// The current record is not an overlay. Note: This check might have already took place
// while creating the default language row. However, since this field might be set by
// other data providers unintentional, we check this here again to be sure.
continue;
}
if ((int)$result['databaseRow'][$result['processedTca']['ctrl']['transOrigPointerField']] !== (int)$result['defaultLanguageRow']['uid']) {
// The current records "transOrigPointerField" doesn't point to the current default language row
continue;
}
// Override the current database field with the one from the default language
$result['databaseRow'][$fieldName] = $result['defaultLanguageRow'][$fieldName];
}
return $result;
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataProvider;
use TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDefaultAsReadonly;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
/**
* Test case
*/
class DatabaseRowDefaultAsReadonlyTest extends UnitTestCase
{
/**
* @test
*/
public function addDataReplacesCurrentDatabaseValue(): void
{
$input = [
'databaseRow' => [
'uid' => 10,
'l10n_parent' => 5,
'sys_language_uid' => 2,
'aField' => '',
],
'defaultLanguageRow' => [
'uid' => 5,
'l10n_parent' => 0,
'sys_language_uid' => 0,
'aField' => 'some-default-value',
],
'processedTca' => [
'ctrl' => [
'transOrigPointerField' => 'l10n_parent',
'languageField' => 'sys_language_uid',
],
'columns' => [
'aField' => [
'label' => 'aField',
'l10n_display' => 'defaultAsReadonly',
],
],
],
];
$expected = $input;
$expected['databaseRow']['aField'] = $expected['defaultLanguageRow']['aField'];
self::assertEquals($expected, (new DatabaseRowDefaultAsReadonly())->addData($input));
}
/**
* @test
* @dataProvider addDataDoesNotReplaceCurrentDatabaseValueDataProvider
*/
public function addDataDoesNotReplaceCurrentDatabaseValue(array $input): void
{
self::assertEquals(
$input['databaseRow']['aField'],
(new DatabaseRowDefaultAsReadonly())->addData($input)['databaseRow']['aField']
);
}
public function addDataDoesNotReplaceCurrentDatabaseValueDataProvider(): \Generator
{
yield 'No default language row available' => [
[
'databaseRow' => [
'uid' => 10,
'l10n_parent' => 5,
'sys_language_uid' => 2,
'aField' => 'wont-be-overridden',
],
'processedTca' => [
'ctrl' => [
'transOrigPointerField' => 'l10n_parent',
'languageField' => 'sys_language_uid',
],
'columns' => [
'aField' => [
'label' => 'aField',
'l10n_display' => 'defaultAsReadonly',
],
],
],
],
];
yield 'defaultAsReadonly is not set' => [
[
'databaseRow' => [
'uid' => 10,
'l10n_parent' => 5,
'sys_language_uid' => 2,
'aField' => 'wont-be-overridden',
],
'defaultLanguageRow' => [
'uid' => 5,
'l10n_parent' => 0,
'sys_language_uid' => 0,
'aField' => 'some-default-value',
],
'processedTca' => [
'ctrl' => [
'transOrigPointerField' => 'l10n_parent',
'languageField' => 'sys_language_uid',
],
'columns' => [
'aField' => [
'label' => 'aField',
],
],
],
],
];
yield 'current record is no overlay' => [
[
'databaseRow' => [
'uid' => 10,
'l10n_parent' => 0,
'sys_language_uid' => 2,
'aField' => 'wont-be-overridden',
],
'defaultLanguageRow' => [
// This case usually can not occure, however since it's possible
// for 3rd party to hook in we have to check this case as well.
'aField' => 'some-default-value',
],
'processedTca' => [
'ctrl' => [
'transOrigPointerField' => 'l10n_parent',
'languageField' => 'sys_language_uid',
],
'columns' => [
'aField' => [
'label' => 'aField',
'l10n_display' => 'defaultAsReadonly',
],
],
],
],
];
yield 'default row is not the localization parent of the current record' => [
[
'databaseRow' => [
'uid' => 10,
'l10n_parent' => 7,
'sys_language_uid' => 2,
'aField' => 'wont-be-overridden',
],
'defaultLanguageRow' => [
'uid' => 5,
'l10n_parent' => 0,
'sys_language_uid' => 0,
'aField' => 'some-default-value',
],
'processedTca' => [
'ctrl' => [
'transOrigPointerField' => 'l10n_parent',
'languageField' => 'sys_language_uid',
],
'columns' => [
'aField' => [
'label' => 'aField',
'l10n_display' => 'defaultAsReadonly',
],
],
],
],
];
}
}
......@@ -503,11 +503,16 @@ return [
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabasePageLanguageOverlayRows::class,
],
],
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRecordTypeValue::class => [
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDefaultAsReadonly::class => [
'depends' => [
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseLanguageRows::class,
],
],
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRecordTypeValue::class => [
'depends' => [
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDefaultAsReadonly::class,
],
],
\TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfigMerged::class => [
'depends' => [
\TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfig::class,
......@@ -764,10 +769,15 @@ return [
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabasePageLanguageOverlayRows::class,
],
],
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDefaultAsReadonly::class => [
'depends' => [
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseLanguageRows::class,
],
],
\TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfigMerged::class => [
'depends' => [
\TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfig::class,
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseLanguageRows::class,
\TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDefaultAsReadonly::class,
],
],
\TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsOverrides::class => [
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment