Bug #91430
closedTask #86141: Remove superfluous database contraint in DataMapProcessor
Unrelated tt_content associated to a news after translating it
0%
Description
Context is the following:
- Context: TYPO3 v8, EXT:news v7.3.1
- tt_content allowed for news items
- Multilingual website
- No workspaces
How-to reproduce¶
#. Create a news record, fill-in "title" and create an empty CE (e.g. text & images), save and close
#. Click the button to translate to another language (in my case French, sys_language_uid = 2)
#. DO NOT TOUCH ANYTHING, within the "content elements" I have a relation to some completely other tt_content element
Analysis¶
I dumped the tables tt_content and tx_news_domain_model_news before creating the news record, so that I can rollback and start again. Here is what happens exactly:
- My news in default language has uid=4773
- My tt_content related element has uid=31995
Useful columns of this tt_content row:
- uid = 31995
- pid = 7715
- t3_origuid = 0
- CType = textmedia
- sys_language_uid = 0
- l18n_parent = 0
- tx_news_related_news = 4773
- l10n_source = 0
- l10n_state = NULL
The record that is referenced in my translated news is following:
- uid = 31842
- pid = 9074
- t3_origuid = 0
- CType = textmedia
- sys_language_uid = 2
- l18n_parent = 28440
- tx_news_related_news = 3525
- l10n_source = 0
- l10n_state = NULL
I tracked down the bug to method \TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor::resolveAncestorId()
.
This method is called from \TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor::synchronizeInlineRelations()
when preparing $dependentIdMap
with the call to \TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor::fetchDependentIdMap()
with following parameters:
$tableName = 'tt_content' $ids = [ 0 => 31995 ] $desiredLanguage = 2
The call to fetchTranslatedValues()
is done with
$tableName = 'tt_content' $fieldNames = [ 'l10n_state' => 'l10n_state', 'language' => 'sys_language_uid', 'parent' => 'l18n_parent', 'source' => 'l10n_source', 'uid' => 'uid' ] $ids = [ 0 => 31995 ]
Output of this method is:
$translationValues = [ 31995 => [ 'l10n_source' => 0, 'l10n_state' => null, 'l18n_parent' => 0, 'sys_language_uid' => 0, 'uid' => 31995 ] ]
Then there's a call to buildElementAncestorIdMap()
which will finally call resolveAncestorId
with following parameters:
$fieldNames = [ 'l10n_state' => 'l10n_state', 'language' => 'sys_language_uid', 'parent' => 'l18n_parent', 'source' => 'l10n_source', 'uid' => 'uid' ] $element = [ 'l10n_source' => 0, 'l10n_state' => null, 'l18n_parent' => 0, 'sys_language_uid' => 0, 'uid' => 31995 ]
First block of the method is not evaluated:
// implicit: having source value different to parent value, use source pointer if ( !empty($fieldNames['source']) && $element[$fieldNames['source']] !== $element[$fieldNames['parent']] ) { return (int)$fieldNames['source']; }
Thus next block is tested: the one that does:
if (!empty($fieldNames['parent'])) { // implicit: use parent pointer if defined return (int)$element[$fieldNames['parent']]; }
$fieldNames['parent'] = 'l18n_parent'
thus it is evaluated, it thus takes $element['l18n_parent']
which is not set (thus leads to null
) and casts it to int which means 0
.
This leads to method buildElementAncestorIdMap
to test 0 as not null and thus add this ancestor to the mapping:
$ancestorIdMap = [ 0 => [ 0 => 31995 ] ];
this leads to having the dependent elements being uids = 31995 and 0.
Now the call to fetchDependentElements
will build following query:
SELECT `uid`, `l10n_state`, `sys_language_uid`, `l18n_parent`, `l10n_source` FROM `tt_content` WHERE (`sys_language_uid` > 0) AND (`l18n_parent` > 0) AND ((`l18n_parent` IN (31995,0)) OR (`l10n_source` IN (31995,0))) AND ((`tt_content`.`deleted` = 0) AND ((`tt_content`.`t3ver_wsid` = 0) AND (`tt_content`.`pid` <> -1)));
which results in
uid l10n_state sys_language_uid l18n_parent l10n_source 31904 NULL 2 24951 0 25577 NULL 3 24951 0 25576 NULL 4 24951 0 26764 NULL 1 26761 0 26767 NULL 3 26761 0 26770 NULL 4 26761 0 26765 NULL 1 26762 0 26771 NULL 4 26762 0 26766 NULL 1 26763 0 26774 NULL 1 26773 0 31842 NULL 2 28440 0 31844 NULL 3 28440 0 31843 NULL 4 28440 0 31848 NULL 3 31845 0 31847 NULL 4 31845 0
In turn, every element in that array will be tested for the ancestorId and if that id is found in the dependant elements (31995 and 0) then it is kept. We clearly sees that this 0 is going to hurt us:
After the first loop, we have:
$dependantIdMap = [ 31995 => 31904 ]
A few records are skipped because not of the targeted language 2, then the map is updated to be
$dependantIdMap = [ 31995 => 31842 ]
and stays like that because there are no more matching records with targeted language.
And BANG! we have a totally wrong dependent object because of that "0" which was wrongly returned.
Solution¶
We must ensure we don't cast a null (non-existing source of information) to 0 in the first place. The patch is thus:
diff --git a/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php b/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php index 66ffd16d88..02deed2d33 100644 --- a/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php +++ b/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php @@ -1213,7 +1213,7 @@ class DataMapProcessor ) { return (int)$fieldNames['source']; } - if (!empty($fieldNames['parent'])) { + if (!empty($fieldNames['parent']) && isset($fieldNames[$fieldNames['parent']])) { // implicit: use parent pointer if defined return (int)$element[$fieldNames['parent']]; }