TYPO3 CMS  TYPO3_8-7
DataMapProcessor.php
Go to the documentation of this file.
1 <?php
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
29 
48 {
52  protected $allDataMap = [];
53 
57  protected $modifiedDataMap = [];
58 
62  protected $sanitizationMap = [];
63 
67  protected $backendUser;
68 
72  protected $allItems = [];
73 
77  protected $nextItems = [];
78 
86  public static function instance(array $dataMap, BackendUserAuthentication $backendUser)
87  {
89  static::class,
90  $dataMap,
91  $backendUser
92  );
93  }
94 
99  public function __construct(array $dataMap, BackendUserAuthentication $backendUser)
100  {
101  $this->allDataMap = $dataMap;
102  $this->modifiedDataMap = $dataMap;
103  $this->backendUser = $backendUser;
104  }
105 
112  public function process()
113  {
114  $iterations = 0;
115 
116  while (!empty($this->modifiedDataMap)) {
117  $this->nextItems = [];
118  foreach ($this->modifiedDataMap as $tableName => $idValues) {
119  $this->collectItems($tableName, $idValues);
120  }
121 
122  $this->modifiedDataMap = [];
123  if (empty($this->nextItems)) {
124  break;
125  }
126 
127  if ($iterations++ === 0) {
128  $this->sanitize($this->allItems);
129  }
130  $this->enrich($this->nextItems);
131  }
132 
133  $this->allDataMap = $this->purgeDataMap($this->allDataMap);
134  return $this->allDataMap;
135  }
136 
143  protected function purgeDataMap(array $dataMap): array
144  {
145  foreach ($dataMap as $tableName => $idValues) {
146  foreach ($idValues as $id => $values) {
147  if (empty($values)) {
148  unset($dataMap[$tableName][$id]);
149  }
150  }
151  if (empty($dataMap[$tableName])) {
152  unset($dataMap[$tableName]);
153  }
154  }
155  return $dataMap;
156  }
157 
164  protected function collectItems(string $tableName, array $idValues)
165  {
166  $forTableName = $tableName;
167  if ($forTableName === 'pages') {
168  $forTableName = 'pages_language_overlay';
169  }
170 
171  if (!$this->isApplicable($forTableName)) {
172  return;
173  }
174 
175  $fieldNames = [
176  'uid' => 'uid',
177  'l10n_state' => 'l10n_state',
178  'language' => $GLOBALS['TCA'][$forTableName]['ctrl']['languageField'],
179  'parent' => $GLOBALS['TCA'][$forTableName]['ctrl']['transOrigPointerField'],
180  ];
181  if (!empty($GLOBALS['TCA'][$forTableName]['ctrl']['translationSource'])) {
182  $fieldNames['source'] = $GLOBALS['TCA'][$forTableName]['ctrl']['translationSource'];
183  }
184 
185  $translationValues = [];
186  // Fetching parent/source pointer values does not make sense for pages
187  if ($tableName !== 'pages') {
188  $translationValues = $this->fetchTranslationValues(
189  $tableName,
190  $fieldNames,
191  $this->filterNewItemIds(
192  $tableName,
193  $this->filterNumericIds(array_keys($idValues))
194  )
195  );
196  }
197 
198  $dependencies = [];
199  // Fetching dependent localizations does not make sense for pages_language_overlay
200  // (pages_language_overlay records depend on one page and cannot be localized further)
201  if ($tableName !== 'pages_language_overlay') {
202  $dependencies = $this->fetchDependencies(
203  $forTableName,
204  $this->filterNewItemIds($forTableName, array_keys($idValues))
205  );
206  }
207 
208  foreach ($idValues as $id => $values) {
209  $item = $this->findItem($tableName, $id);
210  // build item if it has not been created in a previous iteration
211  if ($item === null) {
212  $recordValues = $translationValues[$id] ?? [];
213  $item = DataMapItem::build(
214  $tableName,
215  $id,
216  $values,
217  $recordValues,
218  $fieldNames
219  );
220 
221  // elements using "all language" cannot be localized
222  if ($item->getLanguage() === -1) {
223  unset($item);
224  continue;
225  }
226  // must be any kind of localization and in connected mode
227  if ($item->getLanguage() > 0 && empty($item->getParent())) {
228  unset($item);
229  continue;
230  }
231  // add dependencies
232  if (!empty($dependencies[$id])) {
233  $item->setDependencies($dependencies[$id]);
234  }
235  }
236  // add item to $this->allItems and $this->nextItems
237  $this->addNextItem($item);
238  }
239  }
240 
247  protected function sanitize(array $items)
248  {
249  foreach (['directChild', 'grandChild'] as $type) {
250  foreach ($this->filterItemsByType($type, $items) as $item) {
251  $this->sanitizeTranslationItem($item);
252  }
253  }
254  }
255 
261  protected function enrich(array $items)
262  {
263  foreach (['directChild', 'grandChild'] as $type) {
264  foreach ($this->filterItemsByType($type, $items) as $item) {
265  foreach ($item->getApplicableScopes() as $scope) {
266  $fromId = $item->getIdForScope($scope);
267  $fieldNames = $this->getFieldNamesForItemScope($item, $scope, !$item->isNew());
268  $this->synchronizeTranslationItem($item, $fieldNames, $fromId);
269  }
270  $this->populateTranslationItem($item);
271  $this->finishTranslationItem($item);
272  }
273  }
274  foreach ($this->filterItemsByType('parent', $items) as $item) {
275  $this->populateTranslationItem($item);
276  }
277  }
278 
286  protected function sanitizeTranslationItem(DataMapItem $item)
287  {
288  $fieldNames = [];
289  foreach ($item->getApplicableScopes() as $scope) {
290  $fieldNames = array_merge(
291  $fieldNames,
292  $this->getFieldNamesForItemScope($item, $scope, false)
293  );
294  }
295 
296  $fieldNameMap = array_combine($fieldNames, $fieldNames);
297  // separate fields, that are submitted in data-map, but not defined as custom
298  $this->sanitizationMap[$item->getTableName()][$item->getId()] = array_intersect_key(
299  $this->allDataMap[$item->getTableName()][$item->getId()],
300  $fieldNameMap
301  );
302  // remove fields, that are submitted in data-map, but not defined as custom
303  $this->allDataMap[$item->getTableName()][$item->getId()] = array_diff_key(
304  $this->allDataMap[$item->getTableName()][$item->getId()],
305  $fieldNameMap
306  );
307  }
308 
316  protected function synchronizeTranslationItem(DataMapItem $item, array $fieldNames, $fromId)
317  {
318  if (empty($fieldNames)) {
319  return;
320  }
321 
322  $fieldNameList = 'uid,' . implode(',', $fieldNames);
323 
324  $fromRecord = ['uid' => $fromId];
326  $fromRecord = BackendUtility::getRecordWSOL(
327  $item->getFromTableName(),
328  $fromId,
329  $fieldNameList
330  );
331  }
332 
333  $forRecord = [];
334  if (!$item->isNew()) {
335  $forRecord = BackendUtility::getRecordWSOL(
336  $item->getTableName(),
337  $item->getId(),
338  $fieldNameList
339  );
340  }
341 
342  if (is_array($fromRecord) && is_array($forRecord)) {
343  foreach ($fieldNames as $fieldName) {
344  $this->synchronizeFieldValues(
345  $item,
346  $fieldName,
347  $fromRecord,
348  $forRecord
349  );
350  }
351  }
352  }
353 
360  protected function populateTranslationItem(DataMapItem $item)
361  {
363  foreach ($item->findDependencies($scope) as $dependentItem) {
364  // use suggested item, if it was submitted in data-map
365  $suggestedDependentItem = $this->findItem(
366  $dependentItem->getTableName(),
367  $dependentItem->getId()
368  );
369  if ($suggestedDependentItem !== null) {
370  $dependentItem = $suggestedDependentItem;
371  }
372  foreach ([$scope, DataMapItem::SCOPE_EXCLUDE] as $dependentScope) {
373  $fieldNames = $this->getFieldNamesForItemScope(
374  $dependentItem,
375  $dependentScope,
376  false
377  );
379  $dependentItem,
380  $fieldNames,
381  $item->getId()
382  );
383  }
384  }
385  }
386  }
387 
393  protected function finishTranslationItem(DataMapItem $item)
394  {
395  if (
396  $item->isParentType()
397  || !State::isApplicable($item->getTableName())
398  ) {
399  return;
400  }
401 
402  $this->allDataMap[$item->getTableName()][$item->getId()]['l10n_state'] = $item->getState()->export();
403  }
404 
413  protected function synchronizeFieldValues(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord)
414  {
415  // skip if this field has been processed already, assumed that proper sanitation happened
416  if ($this->isSetInDataMap($item->getTableName(), $item->getId(), $fieldName)) {
417  return;
418  }
419 
420  $fromId = $fromRecord['uid'];
421  // retrieve value from in-memory data-map
422  if ($this->isSetInDataMap($item->getFromTableName(), $fromId, $fieldName)) {
423  $fromValue = $this->allDataMap[$item->getFromTableName()][$fromId][$fieldName];
424  } elseif (array_key_exists($fieldName, $fromRecord)) {
425  // retrieve value from record
426  $fromValue = $fromRecord[$fieldName];
427  } else {
428  // otherwise abort synchronization
429  return;
430  }
431 
432  // plain values
433  if (!$this->isRelationField($item->getFromTableName(), $fieldName)) {
434  $this->modifyDataMap(
435  $item->getTableName(),
436  $item->getId(),
437  [$fieldName => $fromValue]
438  );
439  } elseif (!$this->isInlineRelationField($item->getFromTableName(), $fieldName)) {
440  // direct relational values
441  $this->synchronizeDirectRelations($item, $fieldName, $fromRecord);
442  } else {
443  // inline relational values
444  $this->synchronizeInlineRelations($item, $fieldName, $fromRecord, $forRecord);
445  }
446  }
447 
455  protected function synchronizeDirectRelations(DataMapItem $item, string $fieldName, array $fromRecord)
456  {
457  $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName];
458  $isSpecialLanguageField = ($configuration['config']['special'] ?? null) === 'languages';
459 
460  $fromId = $fromRecord['uid'];
461  if ($this->isSetInDataMap($item->getFromTableName(), $fromId, $fieldName)) {
462  $fromValue = $this->allDataMap[$item->getFromTableName()][$fromId][$fieldName];
463  } else {
464  $fromValue = $fromRecord[$fieldName];
465  }
466 
467  // non-MM relations are stored as comma separated values, just use them
468  // if values are available in data-map already, just use them as well
469  if (
470  empty($configuration['config']['MM'])
471  || $this->isSetInDataMap($item->getFromTableName(), $fromId, $fieldName)
472  || $isSpecialLanguageField
473  ) {
474  $this->modifyDataMap(
475  $item->getTableName(),
476  $item->getId(),
477  [$fieldName => $fromValue]
478  );
479  return;
480  }
481  // resolve the language special table name
482  if ($isSpecialLanguageField) {
483  $specialTableName = 'sys_language';
484  }
485  // fetch MM relations from storage
486  $type = $configuration['config']['type'];
487  $manyToManyTable = $configuration['config']['MM'];
488  if ($type === 'group' && $configuration['config']['internal_type'] === 'db') {
489  $tableNames = trim($configuration['config']['allowed'] ?? '');
490  } elseif ($configuration['config']['type'] === 'select') {
491  $tableNames = ($specialTableName ?? $configuration['config']['foreign_table'] ?? '');
492  } else {
493  return;
494  }
495 
496  $relationHandler = $this->createRelationHandler();
497  $relationHandler->start(
498  '',
499  $tableNames,
500  $manyToManyTable,
501  $fromId,
502  $item->getFromTableName(),
503  $configuration['config']
504  );
505 
506  // provide list of relations, optionally prepended with table name
507  // e.g. "13,19,23" or "tt_content_27,tx_extension_items_28"
508  $this->modifyDataMap(
509  $item->getTableName(),
510  $item->getId(),
511  [$fieldName => implode(',', $relationHandler->getValueArray())]
512  );
513  }
514 
524  protected function synchronizeInlineRelations(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord)
525  {
526  $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName];
527  $isLocalizationModeExclude = ($configuration['l10n_mode'] ?? null) === 'exclude';
528  $foreignTableName = $configuration['config']['foreign_table'];
529 
530  $fieldNames = [
531  'language' => ($GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField'] ?? null),
532  'parent' => ($GLOBALS['TCA'][$foreignTableName]['ctrl']['transOrigPointerField'] ?? null),
533  'source' => ($GLOBALS['TCA'][$foreignTableName]['ctrl']['translationSource'] ?? null),
534  ];
535  $isTranslatable = (!empty($fieldNames['language']) && !empty($fieldNames['parent']));
536 
537  $suggestedAncestorIds = $this->resolveSuggestedInlineRelations(
538  $item,
539  $fieldName,
540  $fromRecord
541  );
542  $persistedIds = $this->resolvePersistedInlineRelations(
543  $item,
544  $fieldName,
545  $forRecord
546  );
547 
548  // The dependent ID map points from language parent/source record to
549  // localization, thus keys: parents/sources & values: localizations
550  $dependentIdMap = $this->fetchDependentIdMap($foreignTableName, $suggestedAncestorIds, $item->getLanguage());
551  // filter incomplete structures - this is a drawback of DataHandler's remap stack, since
552  // just created IRRE translations still belong to the language parent - filter them out
553  $suggestedAncestorIds = array_diff($suggestedAncestorIds, array_values($dependentIdMap));
554  // compile element differences to be resolved
555  // remove elements that are persisted at the language translation, but not required anymore
556  $removeIds = array_diff($persistedIds, array_values($dependentIdMap));
557  // remove elements that are persisted at the language parent/source, but not required anymore
558  $removeAncestorIds = array_diff(array_keys($dependentIdMap), $suggestedAncestorIds);
559  // missing elements that are persisted at the language parent/source, but not translated yet
560  $missingAncestorIds = array_diff($suggestedAncestorIds, array_keys($dependentIdMap));
561  // persisted elements that should be copied or localized
562  $createAncestorIds = $this->filterNumericIds($missingAncestorIds, true);
563  // non-persisted elements that should be duplicated in data-map directly
564  $populateAncestorIds = $this->filterNumericIds($missingAncestorIds, false);
565  // this desired state map defines the final result of child elements in their parent translation
566  $desiredIdMap = array_combine($suggestedAncestorIds, $suggestedAncestorIds);
567  // update existing translations in the desired state map
568  foreach ($dependentIdMap as $ancestorId => $translationId) {
569  if (isset($desiredIdMap[$ancestorId])) {
570  $desiredIdMap[$ancestorId] = $translationId;
571  }
572  }
573  // no children to be synchronized, but element order could have been changed
574  if (empty($removeAncestorIds) && empty($missingAncestorIds)) {
575  $this->modifyDataMap(
576  $item->getTableName(),
577  $item->getId(),
578  [$fieldName => implode(',', array_values($desiredIdMap))]
579  );
580  return;
581  }
582  // In case only missing elements shall be created, re-use previously sanitized
583  // values IF the relation parent item is new and the count of missing relations
584  // equals the count of previously sanitized relations.
585  // This is caused during copy processes, when the child relations
586  // already have been cloned in DataHandler::copyRecord_procBasedOnFieldType()
587  // without the possibility to resolve the initial connections at this point.
588  // Otherwise child relations would superfluously be duplicated again here.
589  // @todo Invalid manually injected child relations cannot be determined here
590  $sanitizedValue = $this->sanitizationMap[$item->getTableName()][$item->getId()][$fieldName] ?? null;
591  if (
592  !empty($missingAncestorIds) && $item->isNew() && $sanitizedValue !== null
593  && count(GeneralUtility::trimExplode(',', $sanitizedValue, true)) === count($missingAncestorIds)
594  ) {
595  $this->modifyDataMap(
596  $item->getTableName(),
597  $item->getId(),
598  [$fieldName => $sanitizedValue]
599  );
600  return;
601  }
602 
603  $localCommandMap = [];
604  foreach ($removeIds as $removeId) {
605  $localCommandMap[$foreignTableName][$removeId]['delete'] = true;
606  }
607  foreach ($removeAncestorIds as $removeAncestorId) {
608  $removeId = $dependentIdMap[$removeAncestorId];
609  $localCommandMap[$foreignTableName][$removeId]['delete'] = true;
610  }
611  foreach ($createAncestorIds as $createAncestorId) {
612  // if child table is not aware of localization, just copy
613  if ($isLocalizationModeExclude || !$isTranslatable) {
614  $localCommandMap[$foreignTableName][$createAncestorId]['copy'] = -$createAncestorId;
615  } else {
616  // otherwise, trigger the localization process
617  $localCommandMap[$foreignTableName][$createAncestorId]['localize'] = $item->getLanguage();
618  }
619  }
620  // execute copy, localize and delete actions on persisted child records
621  if (!empty($localCommandMap)) {
622  $localDataHandler = GeneralUtility::makeInstance(DataHandler::class);
623  $localDataHandler->start([], $localCommandMap, $this->backendUser);
624  $localDataHandler->process_cmdmap();
625  // update copied or localized ids
626  foreach ($createAncestorIds as $createAncestorId) {
627  if (empty($localDataHandler->copyMappingArray_merged[$foreignTableName][$createAncestorId])) {
628  $additionalInformation = '';
629  if (!empty($localDataHandler->errorLog)) {
630  $additionalInformation = ', reason "'
631  . implode(', ', $localDataHandler->errorLog) . '"';
632  }
633  throw new \RuntimeException(
634  'Child record was not processed' . $additionalInformation,
635  1486233164
636  );
637  }
638  $newLocalizationId = $localDataHandler->copyMappingArray_merged[$foreignTableName][$createAncestorId];
639  $newLocalizationId = $localDataHandler->getAutoVersionId($foreignTableName, $newLocalizationId) ?? $newLocalizationId;
640  $desiredIdMap[$createAncestorId] = $newLocalizationId;
641  }
642  }
643  // populate new child records in data-map
644  if (!empty($populateAncestorIds)) {
645  foreach ($populateAncestorIds as $populateAncestorId) {
646  $newLocalizationId = StringUtility::getUniqueId('NEW');
647  $desiredIdMap[$populateAncestorId] = $newLocalizationId;
648  $duplicatedValues = $this->duplicateFromDataMap(
649  $foreignTableName,
650  $populateAncestorId,
651  $item->getLanguage(),
652  $fieldNames,
653  !$isLocalizationModeExclude && $isTranslatable
654  );
655  $this->modifyDataMap(
656  $foreignTableName,
657  $newLocalizationId,
658  $duplicatedValues
659  );
660  }
661  }
662  // update inline parent field references - required to update pointer fields
663  $this->modifyDataMap(
664  $item->getTableName(),
665  $item->getId(),
666  [$fieldName => implode(',', array_values($desiredIdMap))]
667  );
668  }
669 
680  protected function resolveSuggestedInlineRelations(DataMapItem $item, string $fieldName, array $fromRecord): array
681  {
682  $suggestedAncestorIds = [];
683  $fromId = $fromRecord['uid'];
684  $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName];
685  $foreignTableName = $configuration['config']['foreign_table'];
686  $manyToManyTable = ($configuration['config']['MM'] ?? '');
687 
688  // determine suggested elements of either translation parent or source record
689  // from data-map, in case the accordant language parent/source record was modified
690  if ($this->isSetInDataMap($item->getFromTableName(), $fromId, $fieldName)) {
691  $suggestedAncestorIds = GeneralUtility::trimExplode(
692  ',',
693  $this->allDataMap[$item->getFromTableName()][$fromId][$fieldName],
694  true
695  );
696  } elseif (MathUtility::canBeInterpretedAsInteger($fromId)) {
697  // determine suggested elements of either translation parent or source record from storage
698  $relationHandler = $this->createRelationHandler();
699  $relationHandler->start(
700  $fromRecord[$fieldName],
701  $foreignTableName,
702  $manyToManyTable,
703  $fromId,
704  $item->getFromTableName(),
705  $configuration['config']
706  );
707  $suggestedAncestorIds = $this->mapRelationItemId($relationHandler->itemArray);
708  }
709 
710  return array_filter($suggestedAncestorIds);
711  }
712 
721  private function resolvePersistedInlineRelations(DataMapItem $item, string $fieldName, array $forRecord): array
722  {
723  $persistedIds = [];
724  $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName];
725  $foreignTableName = $configuration['config']['foreign_table'];
726  $manyToManyTable = ($configuration['config']['MM'] ?? '');
727 
728  // determine persisted elements for the current data-map item
729  if (!$item->isNew()) {
730  $relationHandler = $this->createRelationHandler();
731  $relationHandler->start(
732  $forRecord[$fieldName] ?? '',
733  $foreignTableName,
734  $manyToManyTable,
735  $item->getId(),
736  $item->getTableName(),
737  $configuration['config']
738  );
739  $persistedIds = $this->mapRelationItemId($relationHandler->itemArray);
740  }
741 
742  return array_filter($persistedIds);
743  }
744 
755  protected function isSetInDataMap(string $tableName, $id, string $fieldName)
756  {
757  return
758  // directly look-up field name
759  isset($this->allDataMap[$tableName][$id][$fieldName])
760  // check existence of field name as key for null values
761  || isset($this->allDataMap[$tableName][$id])
762  && is_array($this->allDataMap[$tableName][$id])
763  && array_key_exists($fieldName, $this->allDataMap[$tableName][$id]);
764  }
765 
776  protected function modifyDataMap(string $tableName, $id, array $values)
777  {
778  // avoid superfluous iterations by data-map changes with values
779  // that actually have not been changed and were available already
780  $sameValues = array_intersect_assoc(
781  $this->allDataMap[$tableName][$id] ?? [],
782  $values
783  );
784  if (!empty($sameValues)) {
785  $fieldNames = implode(', ', array_keys($sameValues));
786  throw new \RuntimeException(
787  sprintf(
788  'Issued data-map change for table %s with same values '
789  . 'for these fields names %s',
790  $tableName,
791  $fieldNames
792  ),
793  1488634845
794  );
795  }
796 
797  $this->modifiedDataMap[$tableName][$id] = array_merge(
798  $this->modifiedDataMap[$tableName][$id] ?? [],
799  $values
800  );
801  $this->allDataMap[$tableName][$id] = array_merge(
802  $this->allDataMap[$tableName][$id] ?? [],
803  $values
804  );
805  }
806 
810  protected function addNextItem(DataMapItem $item)
811  {
812  $identifier = $item->getTableName() . ':' . $item->getId();
813  if (!isset($this->allItems[$identifier])) {
814  $this->allItems[$identifier] = $item;
815  }
816  $this->nextItems[$identifier] = $item;
817  }
818 
829  protected function fetchTranslationValues(string $tableName, array $fieldNames, array $ids)
830  {
831  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
832  ->getQueryBuilderForTable($tableName);
833  $queryBuilder->getRestrictions()
834  ->removeAll()
835  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
836  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $this->backendUser->workspace, false));
837  $statement = $queryBuilder
838  ->select(...array_values($fieldNames))
839  ->from($tableName)
840  ->where(
841  $queryBuilder->expr()->in(
842  'uid',
843  $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
844  )
845  )
846  ->execute();
847 
848  $translationValues = [];
849  foreach ($statement as $record) {
850  $translationValues[$record['uid']] = $record;
851  }
852  return $translationValues;
853  }
854 
872  protected function fetchDependencies(string $tableName, array $ids)
873  {
874  if ($tableName === 'pages') {
875  $tableName = 'pages_language_overlay';
876  }
877 
878  if (!BackendUtility::isTableLocalizable($tableName)) {
879  return [];
880  }
881 
882  $fieldNames = [
883  'uid' => 'uid',
884  'l10n_state' => 'l10n_state',
885  'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
886  'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'],
887  ];
888  if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) {
889  $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource'];
890  }
891  $fieldNamesMap = array_combine($fieldNames, $fieldNames);
892 
893  $persistedIds = $this->filterNumericIds($ids, true);
894  $createdIds = $this->filterNumericIds($ids, false);
895  $dependentElements = $this->fetchDependentElements($tableName, $persistedIds, $fieldNames);
896 
897  foreach ($createdIds as $createdId) {
898  $data = $this->allDataMap[$tableName][$createdId] ?? null;
899  if ($data === null) {
900  continue;
901  }
902  $dependentElements[] = array_merge(
903  ['uid' => $createdId],
904  array_intersect_key($data, $fieldNamesMap)
905  );
906  }
907 
908  $dependencyMap = [];
909  foreach ($dependentElements as $dependentElement) {
910  $dependentItem = DataMapItem::build(
911  $tableName,
912  $dependentElement['uid'],
913  [],
914  $dependentElement,
915  $fieldNames
916  );
917 
918  if ($dependentItem->isDirectChildType()) {
919  $dependencyMap[$dependentItem->getParent()][State::STATE_PARENT][] = $dependentItem;
920  }
921  if ($dependentItem->isGrandChildType()) {
922  $dependencyMap[$dependentItem->getParent()][State::STATE_PARENT][] = $dependentItem;
923  $dependencyMap[$dependentItem->getSource()][State::STATE_SOURCE][] = $dependentItem;
924  }
925  }
926  return $dependencyMap;
927  }
928 
953  protected function fetchDependentIdMap(string $tableName, array $ids, int $desiredLanguage)
954  {
955  if ($tableName === 'pages') {
956  $tableName = 'pages_language_overlay';
957  }
958 
959  $ids = $this->filterNumericIds($ids, true);
960  $isTranslatable = BackendUtility::isTableLocalizable($tableName);
961  $originFieldName = ($GLOBALS['TCA'][$tableName]['ctrl']['origUid'] ?? null);
962 
963  if (!$isTranslatable && $originFieldName === null) {
964  return [];
965  }
966 
967  if ($isTranslatable) {
968  $fieldNames = [
969  'uid' => 'uid',
970  'l10n_state' => 'l10n_state',
971  'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
972  'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'],
973  ];
974  if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) {
975  $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource'];
976  }
977  } else {
978  $fieldNames = [
979  'uid' => 'uid',
980  'origin' => $originFieldName,
981  ];
982  }
983 
984  $fetchIds = $ids;
985  if ($isTranslatable) {
986  // expand search criteria via parent and source elements
987  $translationValues = $this->fetchTranslationValues($tableName, $fieldNames, $ids);
988  $ancestorIdMap = $this->buildElementAncestorIdMap($fieldNames, $translationValues);
989  $fetchIds = array_unique(array_merge($ids, array_keys($ancestorIdMap)));
990  }
991 
992  $dependentElements = $this->fetchDependentElements($tableName, $fetchIds, $fieldNames);
993 
994  $dependentIdMap = [];
995  foreach ($dependentElements as $dependentElement) {
996  $dependentId = $dependentElement['uid'];
997  // implicit: use origin pointer if table cannot be translated
998  if (!$isTranslatable) {
999  $ancestorId = (int)$dependentElement[$fieldNames['origin']];
1000  // only consider element if it reflects the desired language
1001  } elseif ((int)$dependentElement[$fieldNames['language']] === $desiredLanguage) {
1002  $ancestorId = $this->resolveAncestorId($fieldNames, $dependentElement);
1003  } else {
1004  // otherwise skip the element completely
1005  continue;
1006  }
1007  // only keep ancestors that were initially requested before expanding
1008  if (in_array($ancestorId, $ids)) {
1009  $dependentIdMap[$ancestorId] = $dependentId;
1010  } elseif (!empty($ancestorIdMap[$ancestorId])) {
1011  // resolve from previously expanded search criteria
1012  $possibleChainedIds = array_intersect(
1013  $ids,
1014  $ancestorIdMap[$ancestorId]
1015  );
1016  if (!empty($possibleChainedIds)) {
1017  $ancestorId = $possibleChainedIds[0];
1018  $dependentIdMap[$ancestorId] = $dependentId;
1019  }
1020  }
1021  }
1022  return $dependentIdMap;
1023  }
1024 
1036  protected function fetchDependentElements(string $tableName, array $ids, array $fieldNames)
1037  {
1038  $ids = $this->filterNumericIds($ids, true);
1039 
1040  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1041  ->getQueryBuilderForTable($tableName);
1042  $queryBuilder->getRestrictions()
1043  ->removeAll()
1044  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1045  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $this->backendUser->workspace, false));
1046 
1047  $zeroParameter = $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT);
1048  $ids = array_filter($ids, [MathUtility::class, 'canBeInterpretedAsInteger']);
1049  $idsParameter = $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY);
1050 
1051  // fetch by language dependency
1052  if (!empty($fieldNames['language']) && !empty($fieldNames['parent'])) {
1053  $ancestorPredicates = [
1054  $queryBuilder->expr()->in(
1055  $fieldNames['parent'],
1056  $idsParameter
1057  )
1058  ];
1059  if (!empty($fieldNames['source'])) {
1060  $ancestorPredicates[] = $queryBuilder->expr()->in(
1061  $fieldNames['source'],
1062  $idsParameter
1063  );
1064  }
1065  $predicates = [
1066  // must be any kind of localization
1067  $queryBuilder->expr()->gt(
1068  $fieldNames['language'],
1069  $zeroParameter
1070  ),
1071  // must be in connected mode
1072  $queryBuilder->expr()->gt(
1073  $fieldNames['parent'],
1074  $zeroParameter
1075  ),
1076  // any parent or source pointers
1077  $queryBuilder->expr()->orX(...$ancestorPredicates),
1078  ];
1079  } elseif (!empty($fieldNames['origin'])) {
1080  // fetch by origin dependency ("copied from")
1081  $predicates = [
1082  $queryBuilder->expr()->in(
1083  $fieldNames['origin'],
1084  $idsParameter
1085  )
1086  ];
1087  } else {
1088  // otherwise: stop execution
1089  throw new \InvalidArgumentException(
1090  'Invalid combination of query field names given',
1091  1487192370
1092  );
1093  }
1094 
1095  $statement = $queryBuilder
1096  ->select(...array_values($fieldNames))
1097  ->from($tableName)
1098  ->andWhere(...$predicates)
1099  ->execute();
1100 
1101  $dependentElements = [];
1102  foreach ($statement as $record) {
1103  $dependentElements[] = $record;
1104  }
1105  return $dependentElements;
1106  }
1107 
1115  protected function filterItemsByType(string $type, array $items)
1116  {
1117  return array_filter(
1118  $items,
1119  function (DataMapItem $item) use ($type) {
1120  return $item->getType() === $type;
1121  }
1122  );
1123  }
1124 
1132  protected function filterNumericIds(array $ids, bool $numeric = true)
1133  {
1134  return array_filter(
1135  $ids,
1136  function ($id) use ($numeric) {
1137  return MathUtility::canBeInterpretedAsInteger($id) === $numeric;
1138  }
1139  );
1140  }
1141 
1149  protected function filterNewItemIds(string $tableName, array $ids)
1150  {
1151  return array_filter(
1152  $ids,
1153  function ($id) use ($tableName) {
1154  return $this->findItem($tableName, $id) === null;
1155  }
1156  );
1157  }
1158 
1165  protected function mapRelationItemId(array $relationItems)
1166  {
1167  return array_map(
1168  function (array $relationItem) {
1169  return (int)$relationItem['id'];
1170  },
1171  $relationItems
1172  );
1173  }
1174 
1180  protected function resolveAncestorId(array $fieldNames, array $element)
1181  {
1182  // implicit: having source value different to parent value, use source pointer
1183  if (
1184  !empty($fieldNames['source'])
1185  && $element[$fieldNames['source']] !== $element[$fieldNames['parent']]
1186  ) {
1187  return (int)$fieldNames['source'];
1188  }
1189  if (!empty($fieldNames['parent'])) {
1190  // implicit: use parent pointer if defined
1191  return (int)$element[$fieldNames['parent']];
1192  }
1193  return null;
1194  }
1195 
1206  protected function buildElementAncestorIdMap(array $fieldNames, array $elements)
1207  {
1208  $ancestorIdMap = [];
1209  foreach ($elements as $element) {
1210  $ancestorId = $this->resolveAncestorId($fieldNames, $element);
1211  if ($ancestorId !== null) {
1212  $ancestorIdMap[$ancestorId][] = (int)$element['uid'];
1213  }
1214  }
1215  return $ancestorIdMap;
1216  }
1217 
1225  protected function findItem(string $tableName, $id)
1226  {
1227  return $this->allItems[$tableName . ':' . $id] ?? null;
1228  }
1229 
1241  protected function duplicateFromDataMap(string $tableName, $fromId, int $language, array $fieldNames, bool $localize)
1242  {
1243  $data = $this->allDataMap[$tableName][$fromId];
1244  // just return duplicated item if localization cannot be applied
1245  if (empty($language) || !$localize) {
1246  return $data;
1247  }
1248 
1249  $data[$fieldNames['language']] = $language;
1250  if (empty($data[$fieldNames['parent']])) {
1251  // @todo Only $id used in TCA type 'select' is resolved in DataHandler's remapStack
1252  $data[$fieldNames['parent']] = $fromId;
1253  }
1254  if (!empty($fieldNames['source'])) {
1255  // @todo Not sure, whether $id is resolved in DataHandler's remapStack
1256  $data[$fieldNames['source']] = $fromId;
1257  }
1258  // unset field names that are expected to be handled in this processor
1259  foreach ($this->getFieldNamesToBeHandled($tableName) as $fieldName) {
1260  unset($data[$fieldName]);
1261  }
1262 
1263  $prefixFieldNames = array_intersect(
1264  array_keys($data),
1265  $this->getPrefixLanguageTitleFieldNames($tableName)
1266  );
1267  if (empty($prefixFieldNames)) {
1268  return $data;
1269  }
1270 
1271  $languageService = $this->getLanguageService();
1272  $languageRecord = BackendUtility::getRecord('sys_language', $language, 'title');
1273  list($pageId) = BackendUtility::getTSCpid($tableName, $fromId, $data['pid'] ?? null);
1274 
1275  $TSconfig = $this->backendUser->getTSConfig(
1276  'TCEMAIN',
1278  );
1279  if (!empty($TSconfig['translateToMessage'])) {
1280  $prefix = $TSconfig['translateToMessage'];
1281  if ($languageService !== null) {
1282  $prefix = $languageService->sL($prefix);
1283  }
1284  $prefix = sprintf($prefix, $languageRecord['title']);
1285  }
1286  if (empty($prefix)) {
1287  $prefix = 'Translate to ' . $languageRecord['title'] . ':';
1288  }
1289 
1290  foreach ($prefixFieldNames as $prefixFieldName) {
1291  // @todo The hook in DataHandler is not applied here
1292  $data[$prefixFieldName] = '[' . $prefix . '] ' . $data[$prefixFieldName];
1293  }
1294 
1295  return $data;
1296  }
1297 
1306  protected function getFieldNamesForItemScope(
1307  DataMapItem $item,
1308  string $scope,
1309  bool $modified
1310  ) {
1311  if (
1312  $scope === DataMapItem::SCOPE_PARENT
1313  || $scope === DataMapItem::SCOPE_SOURCE
1314  ) {
1315  if (!State::isApplicable($item->getTableName())) {
1316  return [];
1317  }
1318  return $item->getState()->filterFieldNames($scope, $modified);
1319  }
1320  if ($scope === DataMapItem::SCOPE_EXCLUDE) {
1322  $item->getTableName()
1323  );
1324  }
1325  return [];
1326  }
1327 
1334  protected function getLocalizationModeExcludeFieldNames(string $tableName)
1335  {
1336  $localizationExcludeFieldNames = [];
1337  if (empty($GLOBALS['TCA'][$tableName]['columns'])) {
1338  return $localizationExcludeFieldNames;
1339  }
1340 
1341  foreach ($GLOBALS['TCA'][$tableName]['columns'] as $fieldName => $configuration) {
1342  if (($configuration['l10n_mode'] ?? null) === 'exclude') {
1343  $localizationExcludeFieldNames[] = $fieldName;
1344  }
1345  }
1346 
1347  return $localizationExcludeFieldNames;
1348  }
1349 
1357  protected function getFieldNamesToBeHandled(string $tableName)
1358  {
1359  return array_merge(
1360  State::getFieldNames($tableName),
1361  $this->getLocalizationModeExcludeFieldNames($tableName)
1362  );
1363  }
1364 
1371  protected function getPrefixLanguageTitleFieldNames(string $tableName)
1372  {
1373  if ($tableName === 'pages') {
1374  $tableName = 'pages_language_overlay';
1375  }
1376 
1377  $prefixLanguageTitleFieldNames = [];
1378  if (empty($GLOBALS['TCA'][$tableName]['columns'])) {
1379  return $prefixLanguageTitleFieldNames;
1380  }
1381 
1382  foreach ($GLOBALS['TCA'][$tableName]['columns'] as $fieldName => $configuration) {
1383  $type = $configuration['config']['type'] ?? null;
1384  if (
1385  ($configuration['l10n_mode'] ?? null) === 'prefixLangTitle'
1386  && ($type === 'input' || $type === 'text')
1387  ) {
1388  $prefixLanguageTitleFieldNames[] = $fieldName;
1389  }
1390  }
1391 
1392  return $prefixLanguageTitleFieldNames;
1393  }
1394 
1402  protected function isRelationField(string $tableName, string $fieldName): bool
1403  {
1404  if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'])) {
1405  return false;
1406  }
1407 
1408  $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1409 
1410  return
1411  $configuration['type'] === 'group'
1412  && ($configuration['internal_type'] ?? null) === 'db'
1413  && !empty($configuration['allowed'])
1414  || $configuration['type'] === 'select'
1415  && (
1416  !empty($configuration['foreign_table'])
1417  && !empty($GLOBALS['TCA'][$configuration['foreign_table']])
1418  || ($configuration['special'] ?? null) === 'languages'
1419  )
1420  || $this->isInlineRelationField($tableName, $fieldName)
1421  ;
1422  }
1423 
1431  protected function isInlineRelationField(string $tableName, string $fieldName): bool
1432  {
1433  if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'])) {
1434  return false;
1435  }
1436 
1437  $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1438 
1439  return
1440  $configuration['type'] === 'inline'
1441  && !empty($configuration['foreign_table'])
1442  && !empty($GLOBALS['TCA'][$configuration['foreign_table']])
1443  ;
1444  }
1445 
1453  protected function isApplicable(string $tableName): bool
1454  {
1455  return
1456  State::isApplicable($tableName)
1457  || BackendUtility::isTableLocalizable($tableName)
1458  && count($this->getLocalizationModeExcludeFieldNames($tableName)) > 0
1459  ;
1460  }
1461 
1465  protected function createRelationHandler()
1466  {
1467  $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
1468  $relationHandler->setWorkspaceId($this->backendUser->workspace);
1469  return $relationHandler;
1470  }
1471 
1475  protected function getLanguageService()
1476  {
1477  return $GLOBALS['LANG'] ?? null;
1478  }
1479 }
static getPagesTSconfig($id, $rootLine=null, $returnPartArray=false)
static getRecordWSOL( $table, $uid, $fields=' *', $where='', $useDeleteClause=true, $unsetMovePointers=false)
__construct(array $dataMap, BackendUserAuthentication $backendUser)
synchronizeDirectRelations(DataMapItem $item, string $fieldName, array $fromRecord)
static getFieldNames(string $tableName)
Definition: State.php:81
synchronizeFieldValues(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord)
fetchTranslationValues(string $tableName, array $fieldNames, array $ids)
resolvePersistedInlineRelations(DataMapItem $item, string $fieldName, array $forRecord)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static makeInstance($className,... $constructorArguments)
synchronizeTranslationItem(DataMapItem $item, array $fieldNames, $fromId)
buildElementAncestorIdMap(array $fieldNames, array $elements)
getFieldNamesForItemScope(DataMapItem $item, string $scope, bool $modified)
static instance(array $dataMap, BackendUserAuthentication $backendUser)
fetchDependentElements(string $tableName, array $ids, array $fieldNames)
isSetInDataMap(string $tableName, $id, string $fieldName)
fetchDependentIdMap(string $tableName, array $ids, int $desiredLanguage)
synchronizeInlineRelations(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord)
isInlineRelationField(string $tableName, string $fieldName)
static build(string $tableName, $id, array $suggestedValues, array $persistedValues, array $configurationFieldNames)
resolveSuggestedInlineRelations(DataMapItem $item, string $fieldName, array $fromRecord)
modifyDataMap(string $tableName, $id, array $values)
static getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static isApplicable(string $tableName)
Definition: State.php:67
duplicateFromDataMap(string $tableName, $fromId, int $language, array $fieldNames, bool $localize)