‪TYPO3CMS  ‪main
DataHandler.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
18 use Doctrine\DBAL\Exception as DBALException;
19 use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
20 use Doctrine\DBAL\Types\IntegerType;
21 use Doctrine\DBAL\Types\JsonType;
22 use Psr\Container\ContainerInterface;
23 use Psr\EventDispatcher\EventDispatcherInterface;
24 use Psr\Log\LoggerAwareInterface;
25 use Psr\Log\LoggerAwareTrait;
26 use Symfony\Component\Uid\Uuid;
27 use TYPO3\CMS\Backend\Utility\BackendUtility;
40 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
54 use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
67 use ‪TYPO3\CMS\Core\SysLog\Action\Cache as SystemLogCacheAction;
68 use ‪TYPO3\CMS\Core\SysLog\Action\Database as SystemLogDatabaseAction;
69 use ‪TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
70 use ‪TYPO3\CMS\Core\SysLog\Type as SystemLogType;
79 
93 class ‪DataHandler implements LoggerAwareInterface
94 {
95  use ‪LogDataTrait;
96  use LoggerAwareTrait;
97 
98  // *********************
99  // Public variables you can configure before using the class:
100  // *********************
105  public bool ‪$storeLogMessages = true;
106 
110  public bool ‪$enableLogging = true;
111 
116  public bool ‪$reverseOrder = false;
117 
119  public ‪$checkStoredRecords = true;
122 
126  public bool ‪$neverHideAtCopy = false;
127 
131  public bool ‪$isImporting = false;
132 
136  public bool ‪$dontProcessTransformations = false;
137 
143  protected bool ‪$useTransOrigPointerField = true;
144 
151  public bool ‪$bypassWorkspaceRestrictions = false;
152 
157  public bool ‪$bypassAccessCheckForRecords = false;
158 
165  public string ‪$copyWhichTables = '*';
166 
174  public ‪$copyTree = 0;
175 
184  public array ‪$defaultValues = [];
185 
193  public array ‪$suggestedInsertUids = [];
194 
201  public ?object ‪$callBackObj = null;
202 
207  protected ?‪CorrelationId ‪$correlationId = null;
208 
209  // *********************
210  // Internal variables (mapping arrays) which can be used (read-only) from outside
211  // *********************
218  public array ‪$autoVersionIdMap = [];
219 
225  public array ‪$substNEWwithIDs = [];
226 
232  public array ‪$substNEWwithIDs_table = [];
233 
239  public array ‪$newRelatedIDs = [];
240 
246  public array ‪$copyMappingArray_merged = [];
247 
251  protected array ‪$deletedRecords = [];
252 
258  public array ‪$errorLog = [];
259 
263  public array ‪$pagetreeRefreshFieldsFromPages = ['pid', 'sorting', 'deleted', 'hidden', 'title', 'doktype', 'is_siteroot', 'fe_group', 'nav_hide', 'nav_title', 'module', 'starttime', 'endtime', 'content_from_pid', 'extendToSubpages'];
264 
270  public bool ‪$pagetreeNeedsRefresh = false;
271 
272  // *********************
273  // Internal Variables, do not touch.
274  // *********************
275 
276  // Variables set in init() function:
277 
282 
288  public int ‪$userid;
289 
295  public bool ‪$admin;
296 
298 
302  protected array ‪$excludedTablesAndFields = [];
303 
308  protected array ‪$control = [];
309 
318  public array ‪$datamap = [];
319 
328  public array ‪$cmdmap = [];
329 
333  protected array ‪$mmHistoryRecords = [];
334 
338  protected array ‪$historyRecords = [];
339 
340  // Internal static:
341 
349  public int ‪$sortIntervals = 256;
350 
351  // Internal caching arrays
355  protected array ‪$recInsertAccessCache = [];
356 
360  protected array ‪$isRecordInWebMount_Cache = [];
361 
365  protected array ‪$isInWebMount_Cache = [];
366 
372  protected array ‪$pageCache = [];
373 
374  // Other arrays:
380  public array ‪$dbAnalysisStore = [];
381 
388  public array ‪$registerDBList = [];
389 
395  public array ‪$registerDBPids = [];
396 
408  public array ‪$copyMappingArray = [];
409 
415  public array ‪$remapStack = [];
416 
423  public array ‪$remapStackRecords = [];
424 
428  protected array ‪$remapStackActions = [];
429 
438 
439  // Various
440 
447  public ‪$checkValue_currentRecord = [];
448 
452  protected bool ‪$disableDeleteClause = false;
453 
454  protected ?array ‪$checkModifyAccessListHookObjects = null;
455 
460  protected ?self ‪$outerMostInstance = null;
461 
465  protected static array ‪$recordsToClearCacheFor = [];
466 
471  protected static array ‪$recordPidsForDeletedRecords = [];
472 
474  private readonly ‪FrontendInterface ‪$runtimeCache;
475  private readonly ‪ConnectionPool ‪$connectionPool;
476 
480  protected const ‪CACHE_IDENTIFIER_NESTED_ELEMENT_CALLS_PREFIX = 'core-datahandler-nestedElementCalls-';
481  protected const ‪CACHE_IDENTIFIER_ELEMENTS_TO_BE_DELETED = 'core-datahandler-elementsToBeDeleted';
482 
489  {
490  $this->cacheManager = GeneralUtility::makeInstance(CacheManager::class);
491  $this->runtimeCache = $this->cacheManager->getCache('runtime');
492  $this->connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
493  $this->pagePermissionAssembler = GeneralUtility::makeInstance(PagePermissionAssembler::class, ‪$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions']);
494  if (‪$referenceIndexUpdater === null) {
495  // Create ReferenceIndexUpdater object. This should only happen on outermost instance,
496  // sub instances should receive the reference index updater from a parent.
497  ‪$referenceIndexUpdater = GeneralUtility::makeInstance(ReferenceIndexUpdater::class);
498  }
499  $this->referenceIndexUpdater = ‪$referenceIndexUpdater;
500  }
501 
505  public function ‪setControl(array ‪$control): void
506  {
507  $this->control = ‪$control;
508  }
509 
519  public function ‪start(array $dataMap, array $commandMap, ?BackendUserAuthentication $backendUser = null): void
520  {
521  // Initializing BE_USER
522  $this->BE_USER = $backendUser ?: ‪$GLOBALS['BE_USER'];
523  $this->userid = (int)($this->BE_USER->user['uid'] ?? 0);
524  $this->admin = $this->BE_USER->user['admin'] ?? false;
525 
526  // set correlation id for each new set of data or commands
527  $this->correlationId = ‪CorrelationId::forScope(
528  md5(‪StringUtility::getUniqueId(self::class))
529  );
530 
531  // Get default values from user TSconfig
532  $tcaDefaultOverride = $this->BE_USER->getTSConfig()['TCAdefaults.'] ?? null;
533  if (is_array($tcaDefaultOverride)) {
534  $this->‪setDefaultsFromUserTS($tcaDefaultOverride);
535  }
536 
537  // generates the excludelist, based on TCA/exclude-flag and non_exclude_fields for the user:
538  if (!$this->admin) {
539  $this->excludedTablesAndFields = array_flip($this->‪getExcludeListArray());
540  }
541 
542  foreach ($dataMap as $tableName => $tableRecordArray) {
543  // @todo: Move this to a public setter and call it here. Then protect the property.
544  if (!is_string($tableName) || !is_array($tableRecordArray)) {
545  throw new \UnexpectedValueException('Data array must be shaped ["tableName" => [uid/"NEW.." => ["fieldName" => value]]]', 1709035799);
546  }
547  }
548  $this->datamap = $dataMap;
549 
550  foreach ($commandMap as $idCommandArray) {
551  // @todo: Move this to a public setter and call it here. Then protect the property.
552  if (!is_array($idCommandArray)) {
553  throw new \UnexpectedValueException('Command array must be shaped ["table" => [uid => ["command" => value]]]', 1708586415);
554  }
555  foreach ($idCommandArray as $id => $commandValueArray) {
556  if (!‪MathUtility::canBeInterpretedAsInteger($id) || !is_array($commandValueArray)) {
557  throw new \UnexpectedValueException('Single record commands must be shaped [uid => ["command" => value]]', 1708586979);
558  }
559  }
560  }
561  $this->cmdmap = $commandMap;
562  }
563 
571  public function ‪setMirror($mirror): void
572  {
573  if (!is_array($mirror)) {
574  return;
575  }
576  foreach ($mirror as $table => $uid_array) {
577  if (!isset($this->datamap[$table])) {
578  continue;
579  }
580  foreach ($uid_array as $id => $uidList) {
581  if (!isset($this->datamap[$table][$id])) {
582  continue;
583  }
584  $theIdsInArray = ‪GeneralUtility::trimExplode(',', $uidList, true);
585  foreach ($theIdsInArray as $copyToUid) {
586  $this->datamap[$table][$copyToUid] = $this->datamap[$table][$id];
587  }
588  }
589  }
590  }
591 
598  public function ‪setDefaultsFromUserTS($userTS): void
599  {
600  if (!is_array($userTS)) {
601  return;
602  }
603  foreach ($userTS as $k => $v) {
604  $k = mb_substr($k, 0, -1);
605  if (!$k || !is_array($v) || !isset(‪$GLOBALS['TCA'][$k])) {
606  continue;
607  }
608  if (is_array($this->defaultValues[$k] ?? false)) {
609  $this->defaultValues[$k] = array_merge($this->defaultValues[$k], $v);
610  } else {
611  $this->defaultValues[$k] = $v;
612  }
613  }
614  }
615 
623  protected function ‪applyDefaultsForFieldArray(string $table, int $pageId, array $prepopulatedFieldArray): array
624  {
625  // First set TCAdefaults respecting the given PageID
626  $tcaDefaults = BackendUtility::getPagesTSconfig($pageId)['TCAdefaults.'] ?? null;
627  // Re-apply $this->defaultValues settings
628  $this->‪setDefaultsFromUserTS($tcaDefaults);
629  $cleanFieldArray = $this->‪newFieldArray($table);
630  if (isset($prepopulatedFieldArray['pid'])) {
631  $cleanFieldArray['pid'] = $prepopulatedFieldArray['pid'];
632  }
633  $sortColumn = ‪$GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? null;
634  if ($sortColumn !== null && isset($prepopulatedFieldArray[$sortColumn])) {
635  $cleanFieldArray[$sortColumn] = $prepopulatedFieldArray[$sortColumn];
636  }
637  return $cleanFieldArray;
638  }
639 
640  /*********************************************
641  *
642  * HOOKS
643  *
644  *********************************************/
659  public function ‪hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray): void
660  {
661  // Process hook directly:
662  if (!isset($this->remapStackRecords[$table][$id])) {
663  foreach ($hookObjectsArr as $hookObj) {
664  if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
665  $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);
666  }
667  }
668  } else {
669  $this->remapStackRecords[$table][$id]['processDatamap_afterDatabaseOperations'] = [
670  'status' => $status,
671  'fieldArray' => $fieldArray,
672  'hookObjectsArr' => $hookObjectsArr,
673  ];
674  }
675  }
676 
684  protected function ‪getCheckModifyAccessListHookObjects(): array
685  {
686  if ($this->checkModifyAccessListHookObjects === null) {
687  $this->checkModifyAccessListHookObjects = [];
688  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] ?? [] as $className) {
689  $hookObject = GeneralUtility::makeInstance($className);
690  if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
691  throw new \UnexpectedValueException($className . ' must implement interface ' . DataHandlerCheckModifyAccessListHookInterface::class, 1251892472);
692  }
693  $this->checkModifyAccessListHookObjects[] = $hookObject;
694  }
695  }
697  }
698 
699  /*********************************************
700  *
701  * PROCESSING DATA
702  *
703  *********************************************/
710  public function ‪process_datamap()
711  {
712  $this->‪controlActiveElements();
713 
714  // Keep versionized(!) relations here locally:
715  $registerDBList = [];
717  $this->datamap = $this->‪unsetElementsToBeDeleted($this->datamap);
718  // Editing frozen:
719  if ($this->BE_USER->workspace !== 0 && ($this->BE_USER->workspaceRec['freeze'] ?? false)) {
720  $this->‪log('sys_workspace', $this->BE_USER->workspace, SystemLogDatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'All editing in this workspace has been frozen');
721  return false;
722  }
723  // First prepare user defined objects (if any) for hooks which extend this function:
724  $hookObjectsArr = [];
725  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] ?? [] as $className) {
726  $hookObject = GeneralUtility::makeInstance($className);
727  if (method_exists($hookObject, 'processDatamap_beforeStart')) {
728  $hookObject->processDatamap_beforeStart($this);
729  }
730  $hookObjectsArr[] = $hookObject;
731  }
732 
733  foreach ($this->datamap as $tableName => $tableDataMap) {
734  foreach ($tableDataMap as ‪$identifier => $fieldValues) {
736  $this->datamap[$tableName][‪$identifier] = $this->‪initializeSlugFieldsToEmptyString($tableName, $fieldValues);
737  }
738  }
739  }
740 
741  $this->datamap = DataMapProcessor::instance($this->datamap, $this->BE_USER, $this->referenceIndexUpdater)->process();
742  // 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.
743  $orderOfTables = [];
744  // Set pages first.
745  if (isset($this->datamap['pages'])) {
746  $orderOfTables[] = 'pages';
747  }
748  $orderOfTables = array_unique(array_merge($orderOfTables, array_keys($this->datamap)));
749  // Process the tables...
750  foreach ($orderOfTables as $table) {
751  // Check if
752  // - table is set in $GLOBALS['TCA'],
753  // - table is NOT readOnly
754  // - the table is set with content in the data-array (if not, there's nothing to process...)
755  // - permissions for tableaccess OK
756  $modifyAccessList = $this->‪checkModifyAccessList($table);
757  if (!$modifyAccessList) {
758  $this->‪log($table, 0, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify table "{table}" without permission', 1, ['table' => $table]);
759  }
760  if (!isset(‪$GLOBALS['TCA'][$table]) || $this->‪tableReadOnly($table) || !is_array($this->datamap[$table]) || !$modifyAccessList) {
761  continue;
762  }
763 
764  if ($this->reverseOrder) {
765  $this->datamap[$table] = array_reverse($this->datamap[$table], true);
766  }
767  // For each record from the table, do:
768  // $id is the record uid, may be a string if new records...
769  // $incomingFieldArray is the array of fields
770  foreach ($this->datamap[$table] as $id => $incomingFieldArray) {
771  if (!is_array($incomingFieldArray)) {
772  continue;
773  }
774  $theRealPid = null;
775 
776  // Hook: processDatamap_preProcessFieldArray
777  foreach ($hookObjectsArr as $hookObj) {
778  if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
779  $hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
780  // in case hook invalidated `$incomingFieldArray`, skip the record completely
781  if (!is_array($incomingFieldArray)) {
782  continue 2;
783  }
784  }
785  }
786  // ******************************
787  // Checking access to the record
788  // ******************************
789  $createNewVersion = false;
790  $old_pid_value = '';
791  // Is it a new record? (Then Id is a string)
793  // Get a fieldArray with tca default values
794  $fieldArray = $this->‪newFieldArray($table);
795  // A pid must be set for new records.
796  if (isset($incomingFieldArray['pid'])) {
797  $pid_value = $incomingFieldArray['pid'];
798  // Checking and finding numerical pid, it may be a string-reference to another value
799  $canProceed = true;
800  // If a NEW... id
801  if (str_contains($pid_value, 'NEW')) {
802  if ($pid_value[0] === '-') {
803  $negFlag = -1;
804  $pid_value = substr($pid_value, 1);
805  } else {
806  $negFlag = 1;
807  }
808  // Trying to find the correct numerical value as it should be mapped by earlier processing of another new record.
809  if (isset($this->substNEWwithIDs[$pid_value])) {
810  if ($negFlag === 1) {
811  $old_pid_value = $this->substNEWwithIDs[$pid_value];
812  }
813  $pid_value = (int)($negFlag * $this->substNEWwithIDs[$pid_value]);
814  } else {
815  $canProceed = false;
816  }
817  }
818  $pid_value = (int)$pid_value;
819  if ($canProceed) {
820  $fieldArray = $this->‪resolveSortingAndPidForNewRecord($table, $pid_value, $fieldArray);
821  }
822  }
823  $theRealPid = $fieldArray['pid'];
824  // Checks if records can be inserted on this $pid.
825  // If this is a page translation, the check needs to be done for the l10n_parent record
826  $languageField = ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? null;
827  $transOrigPointerField = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null;
828  if ($table === 'pages'
829  && $languageField && isset($incomingFieldArray[$languageField]) && $incomingFieldArray[$languageField] > 0
830  && $transOrigPointerField && isset($incomingFieldArray[$transOrigPointerField]) && $incomingFieldArray[$transOrigPointerField] > 0
831  ) {
832  $recordAccess = $this->‪checkRecordInsertAccess($table, $incomingFieldArray[$transOrigPointerField]);
833  } else {
834  $recordAccess = $this->‪checkRecordInsertAccess($table, $theRealPid);
835  }
836  if ($recordAccess) {
837  $incomingFieldArray = $this->‪addDefaultPermittedLanguageIfNotSet($table, $incomingFieldArray, $theRealPid);
838  $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $incomingFieldArray, true);
839  if (!$recordAccess) {
840  $this->‪log($table, 0, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::USER_ERROR, 'recordEditAccessInternals() check failed [{reason}]', -1, ['reason' => $this->BE_USER->errorMsg]);
841  } elseif (!$this->bypassWorkspaceRestrictions && !$this->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
842  // If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
843  // So, if no live records were allowed in the current workspace, we have to create a new version of this record
844  if (BackendUtility::isTableWorkspaceEnabled($table)) {
845  $createNewVersion = true;
846  } else {
847  $recordAccess = false;
848  $this->‪log(
849  $table,
850  0,
851  SystemLogDatabaseAction::VERSIONIZE,
852  0,
853  SystemLogErrorClassification::USER_ERROR,
854  'Attempt to insert version record "{table}:{uid}" to this workspace failed. "Live" edit permissions of records from tables without versioning required',
855  -1,
856  [
857  'table' => $table,
858  'uid' => $id,
859  ]
860  );
861  }
862  }
863  }
864  // Yes new record, change $record_status to 'insert'
865  $status = 'new';
866  } else {
867  // Nope... $id is a number
868  $id = (int)$id;
869  $fieldArray = [];
870 
871  $recordAccess = null;
872  if (is_array($hookObjectsArr)) {
873  foreach ($hookObjectsArr as $hookObj) {
874  if (method_exists($hookObj, 'checkRecordUpdateAccess')) {
875  $recordAccess = $hookObj->checkRecordUpdateAccess($table, $id, $incomingFieldArray, $recordAccess, $this);
876  }
877  }
878  }
879  if ($recordAccess !== null) {
880  $recordAccess = (bool)$recordAccess;
881  } else {
882  $recordAccess = $this->‪checkRecordUpdateAccess($table, $id);
883  }
884  if (!$recordAccess) {
885  if ($this->enableLogging) {
886  $propArr = $this->‪getRecordProperties($table, $id);
887  $this->‪log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify record "{title}" ({table}:{uid}) without permission or non-existing page', 2, ['title' => $propArr['header'], 'table' => $table, 'uid' => $id], $propArr['event_pid']);
888  }
889  continue;
890  }
891 
892  // Next check of the record permissions (internals)
893  $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $id);
894  if (!$recordAccess) {
895  $this->‪log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'recordEditAccessInternals() check failed [{reason}]', -1, ['reason' => $this->BE_USER->errorMsg]);
896  } else {
897  // Here we fetch the PID of the record that we point to...
898  $tempdata = $this->‪recordInfo($table, $id);
899  $theRealPid = $tempdata['pid'] ?? null;
900  // Use the new id of the versionized record we're trying to write to:
901  // (This record is a child record of a parent and has already been versionized.)
902  if (!empty($this->autoVersionIdMap[$table][$id])) {
903  // For the reason that creating a new version of this record, automatically
904  // created related child records (e.g. "IRRE"), update the accordant field:
905  $this->‪getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, ‪$registerDBList);
906  // Use the new id of the copied/versionized record:
907  $id = $this->autoVersionIdMap[$table][$id];
908  $recordAccess = true;
909  } elseif (!$this->bypassWorkspaceRestrictions && $tempdata && ($errorCode = $this->‪workspaceCannotEditRecord($table, $tempdata))) {
910  $recordAccess = false;
911  // Versioning is required and it must be offline version!
912  // Check if there already is a workspace version
913  $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid,t3ver_oid');
914  if ($workspaceVersion) {
915  $id = $workspaceVersion['uid'];
916  $recordAccess = true;
917  } elseif ($this->‪workspaceAllowAutoCreation($table, $id, $theRealPid)) {
918  // new version of a record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
919  $this->pagetreeNeedsRefresh = true;
920 
921  $tce = GeneralUtility::makeInstance(self::class, $this->referenceIndexUpdater);
922  $tce->enableLogging = ‪$this->enableLogging;
923  // Setting up command for creating a new version of the record:
924  $cmd = [];
925  $cmd[$table][$id]['version'] = [
926  'action' => 'new',
927  // Default is to create a version of the individual records
928  'label' => 'Auto-created for WS #' . $this->BE_USER->workspace,
929  ];
930  $tce->start([], $cmd, $this->BE_USER);
931  $tce->process_cmdmap();
932  $this->errorLog = array_merge($this->errorLog, $tce->errorLog);
933  // If copying was successful, share the new uids (also of related children):
934  if (!empty($tce->copyMappingArray[$table][$id])) {
935  foreach ($tce->copyMappingArray as $origTable => $origIdArray) {
936  foreach ($origIdArray as $origId => $newId) {
937  $this->autoVersionIdMap[$origTable][$origId] = $newId;
938  }
939  }
940  // Update registerDBList, that holds the copied relations to child records:
941  ‪$registerDBList = array_merge(‪$registerDBList, $tce->registerDBList);
942  // For the reason that creating a new version of this record, automatically
943  // created related child records (e.g. "IRRE"), update the accordant field:
944  $this->‪getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, ‪$registerDBList);
945  // Use the new id of the copied/versionized record:
946  $id = $this->autoVersionIdMap[$table][$id];
947  $recordAccess = true;
948  } else {
949  $this->‪log(
950  $table,
951  $id,
952  SystemLogDatabaseAction::VERSIONIZE,
953  0,
954  SystemLogErrorClassification::USER_ERROR,
955  'Attempt to version record "{table}:{uid}" failed [{reason}]',
956  -1,
957  [
958  'reason' => $errorCode,
959  'table' => $table,
960  'uid' => $id,
961  ]
962  );
963  }
964  } else {
965  $this->‪log(
966  $table,
967  $id,
968  SystemLogDatabaseAction::VERSIONIZE,
969  0,
970  SystemLogErrorClassification::USER_ERROR,
971  'Attempt to version record "{table}:{uid}" failed [{reason}]. "Live" edit permissions of records from tables without versioning required',
972  -1,
973  [
974  'reason' => $errorCode,
975  'table' => $table,
976  'uid' => $id,
977  ]
978  );
979  }
980  }
981  }
982  // The default is 'update'
983  $status = 'update';
984  }
985  // If access was granted above, proceed to create or update record:
986  if (!$recordAccess) {
987  continue;
988  }
989 
990  // Here the "pid" is set IF NOT the old pid was a string pointing to a place in the subst-id array.
991  [$tscPID] = BackendUtility::getTSCpid($table, $id, $old_pid_value ?: ($fieldArray['pid'] ?? 0));
992  if ($status === 'new') {
993  // Apply TCAdefaults from pageTS
994  $fieldArray = $this->‪applyDefaultsForFieldArray($table, (int)$tscPID, $fieldArray);
995  // Apply page permissions as well
996  if ($table === 'pages') {
997  $fieldArray = $this->pagePermissionAssembler->applyDefaults(
998  $fieldArray,
999  (int)$tscPID,
1000  (int)$this->userid,
1001  (int)$this->BE_USER->firstMainGroup
1002  );
1003  }
1004  // Ensure that the default values, that are stored in the $fieldArray (built from internal default values)
1005  // Are also placed inside the incomingFieldArray, so this is checked in "fillInFieldArray" and
1006  // all default values are also checked for validity
1007  // This allows to set TCAdefaults (for example) without having to use FormEngine to have the fields available first.
1008  $incomingFieldArray = array_replace_recursive($fieldArray, $incomingFieldArray);
1009  }
1010  // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1011  $fieldArray = $this->‪fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1012  // Setting system fields
1013  if ($status === 'new') {
1014  if (‪$GLOBALS['TCA'][$table]['ctrl']['crdate'] ?? false) {
1015  $fieldArray[‪$GLOBALS['TCA'][$table]['ctrl']['crdate']] = ‪$GLOBALS['EXEC_TIME'];
1016  }
1017  }
1018  // Set stage to "Editing" to make sure we restart the workflow
1019  if (BackendUtility::isTableWorkspaceEnabled($table)) {
1020  $fieldArray['t3ver_stage'] = 0;
1021  }
1022  if ($status !== 'new') {
1023  // Removing fields which are equal to the current value:
1024  $fieldArray = $this->‪compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1025  }
1026  if ((‪$GLOBALS['TCA'][$table]['ctrl']['tstamp'] ?? false) && !empty($fieldArray)) {
1027  $fieldArray[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = ‪$GLOBALS['EXEC_TIME'];
1028  }
1029  // Hook: processDatamap_postProcessFieldArray
1030  foreach ($hookObjectsArr as $hookObj) {
1031  if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1032  $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1033  }
1034  }
1035  // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1036  // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already
1037  if (is_array($fieldArray)) {
1038  if ($status === 'new') {
1039  if ($table === 'pages') {
1040  // for new pages always a refresh is needed
1041  $this->pagetreeNeedsRefresh = true;
1042  }
1043 
1044  // This creates a version of the record, instead of adding it to the live workspace
1045  if ($createNewVersion) {
1046  // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1047  $this->pagetreeNeedsRefresh = true;
1048  $fieldArray['pid'] = $theRealPid;
1049  $fieldArray['t3ver_oid'] = 0;
1050  // Setting state for version (so it can know it is currently a new version...)
1051  $fieldArray['t3ver_state'] = VersionState::NEW_PLACEHOLDER->value;
1052  $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1053  $this->‪insertDB($table, $id, $fieldArray, true, (int)($incomingFieldArray['uid'] ?? 0));
1054  // Hold auto-versionized ids of placeholders
1055  $this->autoVersionIdMap[$table][$this->substNEWwithIDs[$id]] = $this->substNEWwithIDs[$id];
1056  } else {
1057  $this->‪insertDB($table, $id, $fieldArray, false, (int)($incomingFieldArray['uid'] ?? 0));
1058  }
1059  } else {
1060  if ($table === 'pages') {
1061  // Only a certain number of fields needs to be checked for updates,
1062  // fields with unchanged values are already removed here.
1063  $fieldsToCheck = array_intersect($this->pagetreeRefreshFieldsFromPages, array_keys($fieldArray));
1064  if (!empty($fieldsToCheck)) {
1065  $this->pagetreeNeedsRefresh = true;
1066  }
1067  }
1068  $this->‪updateDB($table, $id, $fieldArray);
1069  }
1070  }
1071  // Hook: processDatamap_afterDatabaseOperations
1072  // Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
1073  // but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
1074  $this->‪hook_processDatamap_afterDatabaseOperations($hookObjectsArr, $status, $table, $id, $fieldArray);
1075  }
1076  }
1077  // Process the stack of relations to remap/correct
1078  $this->‪processRemapStack();
1079  $this->‪dbAnalysisStoreExec();
1080  // Hook: processDatamap_afterAllOperations
1081  // Note: When this hook gets called, all operations on the submitted data have been finished.
1082  foreach ($hookObjectsArr as $hookObj) {
1083  if (method_exists($hookObj, 'processDatamap_afterAllOperations')) {
1084  $hookObj->processDatamap_afterAllOperations($this);
1085  }
1086  }
1087 
1088  if ($this->‪isOuterMostInstance()) {
1089  $this->referenceIndexUpdater->update();
1090  $this->‪processClearCacheQueue();
1091  $this->‪resetElementsToBeDeleted();
1092  }
1093  }
1094 
1101  protected function ‪initializeSlugFieldsToEmptyString(string $tableName, array $fieldValues): array
1102  {
1103  foreach ((‪$GLOBALS['TCA'][$tableName]['columns'] ?? []) as $columnName => $columnConfig) {
1104  if (($columnConfig['config']['type'] ?? '') === 'slug' && !isset($fieldValues[$columnName])) {
1105  $fieldValues[$columnName] = '';
1106  }
1107  }
1108  return $fieldValues;
1109  }
1110 
1122  protected function ‪resolveSortingAndPidForNewRecord(string $table, int $pid, array $fieldArray): array
1123  {
1124  $sortColumn = ‪$GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
1125  // Points to a page on which to insert the element, possibly in the top of the page
1126  if ($pid >= 0) {
1127  // Ensure that the "pid" is not a translated page ID, but the default page ID
1128  $pid = $this->‪getDefaultLanguagePageId($pid);
1129  // The numerical pid is inserted in the data array
1130  $fieldArray['pid'] = $pid;
1131  // If this table is sorted we better find the top sorting number
1132  if ($sortColumn) {
1133  $fieldArray[$sortColumn] = $this->‪getSortNumber($table, 0, $pid);
1134  }
1135  } elseif ($sortColumn) {
1136  // Points to another record before itself
1137  // If this table is sorted we better find the top sorting number
1138  // Because $pid is < 0, getSortNumber() returns an array
1139  $sortingInfo = $this->‪getSortNumber($table, 0, $pid);
1140  $fieldArray['pid'] = $sortingInfo['pid'];
1141  $fieldArray[$sortColumn] = $sortingInfo['sortNumber'];
1142  } else {
1143  // Here we fetch the PID of the record that we point to
1144  ‪$record = $this->‪recordInfo($table, abs($pid));
1145  // Ensure that the "pid" is not a translated page ID, but the default page ID
1146  $fieldArray['pid'] = $this->‪getDefaultLanguagePageId(‪$record['pid']);
1147  }
1148  return $fieldArray;
1149  }
1150 
1165  public function ‪fillInFieldArray($table, $id, array $fieldArray, array $incomingFieldArray, $realPid, $status, $tscPID)
1166  {
1167  // Initialize:
1168  $originalLanguageRecord = null;
1169  $originalLanguage_diffStorage = null;
1170  $diffStorageFlag = false;
1171  $isNewRecord = str_contains((string)$id, 'NEW');
1172  // Setting 'currentRecord' and 'checkValueRecord':
1173  if ($isNewRecord) {
1174  // Overlay default values with incoming values.
1175  $checkValueRecord = $fieldArray;
1176  ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);
1177  $currentRecord = $checkValueRecord;
1178  } else {
1179  $id = (int)$id;
1180  // We must use the current values as basis for this!
1181  $currentRecord = ($checkValueRecord = $this->‪recordInfo($table, $id));
1182  }
1183 
1184  // Get original language record if available:
1185  if (is_array($currentRecord)
1186  && (‪$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'] ?? false)
1187  && !empty(‪$GLOBALS['TCA'][$table]['ctrl']['languageField'])
1188  && (int)($currentRecord[‪$GLOBALS['TCA'][$table]['ctrl']['languageField']] ?? 0) > 0
1189  && (‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? false)
1190  && (int)($currentRecord[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] ?? 0) > 0
1191  ) {
1192  $originalLanguageRecord = $this->‪recordInfo($table, $currentRecord[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]);
1193  BackendUtility::workspaceOL($table, $originalLanguageRecord);
1194  $originalLanguage_diffStorage = json_decode(
1195  (string)($currentRecord[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] ?? ''),
1196  true
1197  );
1198  }
1199 
1200  $this->checkValue_currentRecord = $checkValueRecord;
1201  // In the following all incoming value-fields are tested:
1202  // - Are the user allowed to change the field?
1203  // - Is the field uid/pid (which are already set)
1204  // - perms-fields for pages-table, then do special things...
1205  // - If the field is nothing of the above and the field is configured in TCA, the fieldvalues are evaluated by ->checkValue
1206  // If everything is OK, the field is entered into $fieldArray[]
1207  foreach ($incomingFieldArray as $field => $fieldValue) {
1208  if (isset($this->excludedTablesAndFields[$table . '-' . $field])) {
1209  continue;
1210  }
1211 
1212  // The field must be editable.
1213  // Checking if a value for language can be changed:
1214  if ((‪$GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? false)
1215  && (string)‪$GLOBALS['TCA'][$table]['ctrl']['languageField'] === (string)$field
1216  && !$this->BE_USER->checkLanguageAccess($fieldValue)
1217  ) {
1218  continue;
1219  }
1220 
1221  switch ($field) {
1222  case 'uid':
1223  case 'pid':
1224  // Nothing happens, already set
1225  break;
1226  case 'perms_userid':
1227  case 'perms_groupid':
1228  case 'perms_user':
1229  case 'perms_group':
1230  case 'perms_everybody':
1231  // Permissions can be edited by the owner or the administrator
1232  if ($table === 'pages' && ($this->admin || $status === 'new' || $this->‪pageInfo((int)$id, 'perms_userid') == $this->userid)) {
1233  $value = (int)$fieldValue;
1234  switch ($field) {
1235  case 'perms_userid':
1236  case 'perms_groupid':
1237  $fieldArray[$field] = $value;
1238  break;
1239  default:
1240  if ($value >= 0 && $value < (2 ** 5)) {
1241  $fieldArray[$field] = $value;
1242  }
1243  }
1244  }
1245  break;
1246  case 't3ver_oid':
1247  case 't3ver_wsid':
1248  case 't3ver_state':
1249  case 't3ver_stage':
1250  break;
1251  case 'l10n_state':
1252  $fieldArray[$field] = $fieldValue;
1253  break;
1254  default:
1255  if (isset(‪$GLOBALS['TCA'][$table]['columns'][$field])) {
1256  // Evaluating the value
1257  $res = $this->‪checkValue($table, $field, $fieldValue, $id, $status, $realPid, $tscPID, $incomingFieldArray);
1258  if (array_key_exists('value', $res)) {
1259  $fieldArray[$field] = $res['value'];
1260  }
1261  // Add the value of the original record to the diff-storage content:
1262  if (‪$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'] ?? false) {
1263  $originalLanguage_diffStorage[$field] = (string)($originalLanguageRecord[$field] ?? '');
1264  $diffStorageFlag = true;
1265  }
1266  } elseif (isset(‪$GLOBALS['TCA'][$table]['ctrl']['origUid']) && ‪$GLOBALS['TCA'][$table]['ctrl']['origUid'] === $field) {
1267  // Allow value for original UID to pass by...
1268  $fieldArray[$field] = $fieldValue;
1269  }
1270  }
1271  }
1272 
1273  // Dealing with a page translation, setting "sorting", "pid", "perms_*" to the same values as the original record
1274  if ($table === 'pages' && is_array($originalLanguageRecord)) {
1275  $fieldArray['sorting'] = $originalLanguageRecord['sorting'];
1276  $fieldArray['perms_userid'] = $originalLanguageRecord['perms_userid'];
1277  $fieldArray['perms_groupid'] = $originalLanguageRecord['perms_groupid'];
1278  $fieldArray['perms_user'] = $originalLanguageRecord['perms_user'];
1279  $fieldArray['perms_group'] = $originalLanguageRecord['perms_group'];
1280  $fieldArray['perms_everybody'] = $originalLanguageRecord['perms_everybody'];
1281  }
1282 
1283  // Add diff-storage information
1284  if ($diffStorageFlag
1285  && (
1286  !array_key_exists(‪$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'], $fieldArray)
1287  || ($isNewRecord && $originalLanguageRecord !== null)
1288  )
1289  ) {
1290  // If the field is set it would probably be because of an undo-operation - in which case we should not
1291  // update the field of course. On the other hand, e.g. for record localization, we need to update the field.
1292  $fieldArray[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = json_encode($originalLanguage_diffStorage);
1293  }
1294  return $fieldArray;
1295  }
1296 
1297  /*********************************************
1298  *
1299  * Evaluation of input values
1300  *
1301  ********************************************/
1318  public function ‪checkValue($table, $field, $value, $id, $status, $realPid, $tscPID, $incomingFieldArray = []): array
1319  {
1320  $curValueRec = null;
1321  // Result array
1322  $res = [];
1323 
1324  // Processing special case of field pages.doktype
1325  if ($table === 'pages' && $field === 'doktype') {
1326  // If the user may not use this specific doktype, we issue a warning
1327  if (!($this->admin || ‪GeneralUtility::inList($this->BE_USER->groupData['pagetypes_select'], $value))) {
1328  if ($this->enableLogging) {
1329  $propArr = $this->‪getRecordProperties($table, $id);
1330  $this->‪log($table, (int)$id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'You cannot change the "doktype" of page "{title}" to the desired value', 1, ['title' => $propArr['header']], $propArr['event_pid']);
1331  }
1332  return $res;
1333  }
1334  if ($status === 'update') {
1335  // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1336  $onlyAllowedTables = GeneralUtility::makeInstance(PageDoktypeRegistry::class)->doesDoktypeOnlyAllowSpecifiedRecordTypes((int)$value);
1337  if ($onlyAllowedTables) {
1338  // use the real page id (default language)
1339  $recordId = $this->‪getDefaultLanguagePageId((int)$id);
1340  $theWrongTables = $this->‪doesPageHaveUnallowedTables($recordId, (int)$value);
1341  if ($theWrongTables !== []) {
1342  if ($this->enableLogging) {
1343  $propArr = $this->‪getRecordProperties($table, $id);
1344  $this->‪log($table, (int)$id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, '"doktype" of page "{title}" could not be changed because the page contains records from disallowed tables; {disallowedTables}', 2, ['title' => $propArr['header'], 'disallowedTables' => implode(', ', $theWrongTables)], $propArr['event_pid']);
1345  }
1346  return $res;
1347  }
1348  }
1349  }
1350  }
1351 
1352  $curValue = null;
1353  if ((int)$id !== 0) {
1354  // Get current value:
1355  $curValueRec = $this->‪recordInfo($table, (int)$id);
1356  // isset() won't work here, since values can be NULL
1357  if ($curValueRec !== null && array_key_exists($field, $curValueRec)) {
1358  $curValue = $curValueRec[$field];
1359  }
1360  }
1361 
1362  if ($table === 'be_users'
1363  && ($field === 'admin' || $field === 'password')
1364  && $status === 'update'
1365  ) {
1366  // Do not allow a non system maintainer admin to change admin flag and password of system maintainers
1367  $systemMaintainers = array_map(intval(...), ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []);
1368  // False if current user is not in system maintainer list or if switch to user mode is active
1369  $isCurrentUserSystemMaintainer = $this->BE_USER->isSystemMaintainer();
1370  $isTargetUserInSystemMaintainerList = in_array((int)$id, $systemMaintainers, true);
1371  if ($field === 'admin') {
1372  $isFieldChanged = (int)$curValueRec[$field] !== (int)$value;
1373  } else {
1374  $isFieldChanged = $curValueRec[$field] !== $value;
1375  }
1376  if (!$isCurrentUserSystemMaintainer && $isTargetUserInSystemMaintainerList && $isFieldChanged) {
1377  $value = $curValueRec[$field];
1378  $this->‪log(
1379  $table,
1380  (int)$id,
1381  SystemLogDatabaseAction::UPDATE,
1382  0,
1383  SystemLogErrorClassification::SECURITY_NOTICE,
1384  'Only system maintainers can change the admin flag and password of other system maintainers. The value has not been updated'
1385  );
1386  }
1387  }
1388 
1389  // Getting config for the field
1390  $tcaFieldConf = $this->‪resolveFieldConfigurationAndRespectColumnsOverrides($table, $field);
1391 
1392  // Create $recFID only for those types that need it
1393  if ($tcaFieldConf['type'] === 'flex') {
1394  $recFID = $table . ':' . $id . ':' . $field;
1395  } else {
1396  $recFID = '';
1397  }
1398 
1399  // Perform processing:
1400  $res = $this->‪checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $tscPID, ['incomingFieldArray' => $incomingFieldArray]);
1401  return $res;
1402  }
1403 
1413  protected function ‪resolveFieldConfigurationAndRespectColumnsOverrides(string $table, string $field): array
1414  {
1415  $tcaFieldConf = ‪$GLOBALS['TCA'][$table]['columns'][$field]['config'];
1416  $recordType = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
1417  $columnsOverridesConfigOfField = ‪$GLOBALS['TCA'][$table]['types'][$recordType]['columnsOverrides'][$field]['config'] ?? null;
1418  if ($columnsOverridesConfigOfField) {
1419  ArrayUtility::mergeRecursiveWithOverrule($tcaFieldConf, $columnsOverridesConfigOfField);
1420  }
1421  return $tcaFieldConf;
1422  }
1423 
1444  public function ‪checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $tscPID, array $additionalData = null): array
1445  {
1446  // Convert to NULL value if defined in TCA
1447  if ($value === null && ($tcaFieldConf['nullable'] ?? false)) {
1448  return ['value' => null];
1449  }
1450 
1451  // This is either a normal field or a FlexForm field.
1452  // Used to enrich the (potential) error log with contextual information.
1453  $checkField = $recFID !== '' ? explode(':', $recFID)[2] : $field;
1454 
1455  $res = (array)match ((string)$tcaFieldConf['type']) {
1456  'category' => $this->‪checkValueForCategory($res, (string)$value, $tcaFieldConf, (string)$table, $id, (string)$status, (string)$field),
1457  'check' => $this->‪checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field),
1458  'color' => $this->‪checkValueForColor((string)$value, $tcaFieldConf),
1459  'datetime' => $this->‪checkValueForDatetime($value, $tcaFieldConf),
1460  'email' => $this->‪checkValueForEmail((string)$value, $tcaFieldConf, $table, $id, (int)$realPid, $checkField),
1461  'flex' => $field ? $this->‪checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $field) : [],
1462  'inline' => $this->‪checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData) ?: [],
1463  'file' => $this->‪checkValueForFile($res, (string)$value, $tcaFieldConf, $table, $id, $field, $additionalData),
1464  'input' => $this->‪checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field),
1465  'language' => $this->‪checkValueForLanguage((int)$value, $table, $field),
1466  'link' => $this->‪checkValueForLink((string)$value, $tcaFieldConf, $table, $id, $checkField),
1467  'number' => $this->‪checkValueForNumber($value, $tcaFieldConf),
1468  'password' => $this->‪checkValueForPassword((string)$value, $tcaFieldConf, $table, $id, (int)$realPid, $additionalData['incomingFieldArray'] ?? []),
1469  'radio' => $this->‪checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $realPid, $field),
1470  'slug' => $this->‪checkValueForSlug((string)$value, $tcaFieldConf, $table, $id, (int)$realPid, $field, $additionalData['incomingFieldArray'] ?? []),
1471  'text' => $this->‪checkValueForText($value, $tcaFieldConf, $table, $realPid, $field),
1472  'group', 'folder', 'select' => $this->‪checkValueForGroupFolderSelect($res, $value, $tcaFieldConf, $table, $id, $status, $field),
1473  'json' => $this->‪checkValueForJson($value, $tcaFieldConf),
1474  'uuid' => $this->‪checkValueForUuid((string)$value, $tcaFieldConf),
1475  'passthrough', 'imageManipulation', 'user' => ['value' => $value],
1476  default => [],
1477  };
1478 
1479  return $this->‪checkValueForInternalReferences($res, $value, $tcaFieldConf, $table, $id, $field);
1480  }
1481 
1500  protected function ‪checkValueForInternalReferences(array $res, $value, $tcaFieldConf, $table, $id, $field): array
1501  {
1502  $relevantFieldNames = [
1503  ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null,
1504  ‪$GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null,
1505  ];
1506 
1507  if (
1508  // in case field is empty
1509  empty($field)
1510  // in case the field is not relevant
1511  || !in_array($field, $relevantFieldNames)
1512  // in case the 'value' index has been unset already
1513  || !array_key_exists('value', $res)
1514  // in case it's not a NEW-identifier
1515  || !str_contains($value, 'NEW')
1516  ) {
1517  return $res;
1518  }
1519 
1520  $valueArray = [$value];
1521  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
1522  $this->remapStack[] = [
1523  'args' => [$valueArray, $tcaFieldConf, $id, $table, $field],
1524  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 3],
1525  'field' => $field,
1526  ];
1527  unset($res['value']);
1528 
1529  return $res;
1530  }
1531 
1542  protected function ‪checkValueForText($value, $tcaFieldConf, $table, $realPid, $field)
1543  {
1544  $richtextEnabled = (bool)($tcaFieldConf['enableRichtext'] ?? false);
1545 
1546  // Reset value to empty string, if less than "min" characters.
1547  $min = $tcaFieldConf['min'] ?? 0;
1548  if (!$richtextEnabled && $min > 0 && mb_strlen((string)$value) < $min) {
1549  $value = '';
1550  }
1551 
1552  if (!$this->‪validateValueForRequired($tcaFieldConf, $value)) {
1553  $valueArray = [];
1554  } elseif (isset($tcaFieldConf['eval']) && $tcaFieldConf['eval'] !== '') {
1555  $evalCodesArray = ‪GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1556  $valueArray = $this->‪checkValue_text_Eval($value, $evalCodesArray, $tcaFieldConf['is_in'] ?? '');
1557  } else {
1558  $valueArray = ['value' => $value];
1559  }
1560 
1561  // Handle richtext transformations
1562  if ($this->dontProcessTransformations) {
1563  return $valueArray;
1564  }
1565  // Keep null as value
1566  if ($value === null) {
1567  return $valueArray;
1568  }
1569  if ($richtextEnabled) {
1570  $recordType = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
1571  $richtextConfigurationProvider = GeneralUtility::makeInstance(Richtext::class);
1572  $richtextConfiguration = $richtextConfigurationProvider->getConfiguration($table, $field, $realPid, $recordType, $tcaFieldConf);
1573  $rteParser = GeneralUtility::makeInstance(RteHtmlParser::class);
1574  $valueArray['value'] = $rteParser->transformTextForPersistence((string)$value, $richtextConfiguration['proc.'] ?? []);
1575  }
1576 
1577  return $valueArray;
1578  }
1579 
1591  protected function ‪checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field): array
1592  {
1593  // Secures the string-length to be less than max.
1594  if (isset($tcaFieldConf['max']) && (int)$tcaFieldConf['max'] > 0) {
1595  $value = mb_substr((string)$value, 0, (int)$tcaFieldConf['max'], 'utf-8');
1596  }
1597 
1598  // Reset value to empty string, if less than "min" characters.
1599  $min = $tcaFieldConf['min'] ?? 0;
1600  if ($min > 0 && mb_strlen((string)$value) < $min) {
1601  $value = '';
1602  }
1603 
1604  if (!$this->‪validateValueForRequired($tcaFieldConf, (string)$value)) {
1605  $res = [];
1606  } elseif (empty($tcaFieldConf['eval'])) {
1607  $res = ['value' => $value];
1608  } else {
1609  // Process evaluation settings:
1610  $evalCodesArray = ‪GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1611  $res = $this->‪checkValue_input_Eval((string)$value, $evalCodesArray, $tcaFieldConf['is_in'] ?? '', $table, $id);
1612  // Process UNIQUE settings:
1613  // Field is NOT set for flexForms - which also means that uniqueInPid and unique is NOT available for flexForm fields! Also getUnique should not be done for versioning
1614  if ($field && !empty($res['value'])) {
1615  if (in_array('uniqueInPid', $evalCodesArray, true)) {
1616  $res['value'] = $this->‪getUnique($table, $field, $res['value'], $id, $realPid);
1617  }
1618  if ($res['value'] && in_array('unique', $evalCodesArray, true)) {
1619  $res['value'] = $this->‪getUnique($table, $field, $res['value'], $id);
1620  }
1621  }
1622  }
1623 
1624  return $res;
1625  }
1626 
1633  protected function ‪checkValueForNumber(mixed $value, array $tcaFieldConf): array
1634  {
1635  $format = $tcaFieldConf['format'] ?? 'integer';
1636  if ($format !== 'integer' && $format !== 'decimal') {
1637  // Early return if format is not valid
1638  return [];
1639  }
1640 
1641  if (!$this->‪validateValueForRequired($tcaFieldConf, (string)$value)) {
1642  return [];
1643  }
1644 
1645  if ($format === 'decimal') {
1646  // @todo Make precision configurable
1647  $precision = 2;
1648  $value = preg_replace('/[^0-9,\\.-]/', '', $value);
1649  $negative = substr($value, 0, 1) === '-';
1650  $value = strtr($value, [',' => '.', '-' => '']);
1651  if (!str_contains($value, '.')) {
1652  $value .= '.0';
1653  }
1654  $valueArray = explode('.', $value);
1655  $dec = array_pop($valueArray);
1656  $value = (float)(implode('', $valueArray) . '.' . $dec);
1657  if ($negative) {
1658  $value = $value * -1;
1659  }
1660  $result['value'] = number_format($value, $precision, '.', '');
1661  } else {
1662  $result['value'] = (int)$value;
1663  }
1664 
1665  // Checking range of value:
1666  if (is_array($tcaFieldConf['range'] ?? false)) {
1667  if (isset($tcaFieldConf['range']['upper']) && ceil($result['value']) > (int)$tcaFieldConf['range']['upper']) {
1668  $result['value'] = (int)$tcaFieldConf['range']['upper'];
1669  }
1670  if (isset($tcaFieldConf['range']['lower']) && floor($result['value']) < (int)$tcaFieldConf['range']['lower']) {
1671  $result['value'] = (int)$tcaFieldConf['range']['lower'];
1672  }
1673  }
1674 
1675  return $result;
1676  }
1677 
1685  protected function ‪checkValueForColor(string $value, array $tcaFieldConf): array
1686  {
1687  // Always trim the value
1688  $value = trim($value);
1689  // Secures the string-length to be <= 7.
1690  $value = mb_substr($value, 0, 7, 'utf-8');
1691  // Early return if required validation fails
1692  if (!$this->‪validateValueForRequired($tcaFieldConf, $value)) {
1693  return [];
1694  }
1695  return [
1696  'value' => $value,
1697  ];
1698  }
1699 
1711  protected function ‪checkValueForEmail(
1712  string $value,
1713  array $tcaFieldConf,
1714  string $table,
1715  int|string $id,
1716  int $realPid,
1717  string $field
1718  ): array {
1719  // Always trim the value
1720  $value = trim($value);
1721 
1722  // Early return if required validation fails
1723  // Note: The "required" check is evaluated but does not yet lead to an error, see
1724  // the comment in the DataHandler::validateValueForRequired() for more information.
1725  if (!$this->‪validateValueForRequired($tcaFieldConf, $value)) {
1726  return [];
1727  }
1728 
1729  if ($value !== '' && !GeneralUtility::validEmail($value)) {
1730  // A non-empty value is given, which however is no valid email. Log this and unset the value afterwards.
1731  $this->‪log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, '"{email}" is not a valid e-mail address for the field "{field}" of the table "{table}"', -1, ['email' => $value, 'field' => $field, 'table' => $table]);
1732  $value = '';
1733  }
1734 
1735  $res = [
1736  'value' => $value,
1737  ];
1738 
1739  // Early return if no evaluation is configured
1740  if (!isset($tcaFieldConf['eval'])) {
1741  return $res;
1742  }
1743  $evalCodesArray = ‪GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1744 
1745  // Process UNIQUE settings:
1746  // Field is NOT set for flexForms - which also means that uniqueInPid and unique is NOT available for flexForm fields! Also getUnique should not be done for versioning
1747  if ($field && !empty($res['value'])) {
1748  if (in_array('uniqueInPid', $evalCodesArray, true)) {
1749  $res['value'] = $this->‪getUnique($table, $field, $res['value'], $id, $realPid);
1750  }
1751  if ($res['value'] && in_array('unique', $evalCodesArray, true)) {
1752  $res['value'] = $this->‪getUnique($table, $field, $res['value'], $id);
1753  }
1754  }
1755 
1756  return $res;
1757  }
1758 
1770  protected function ‪checkValueForPassword(
1771  string $value,
1772  array $tcaFieldConf,
1773  string $table,
1774  int|string $id,
1775  int $realPid,
1776  array $incomingFieldArray = []
1777  ): array {
1778  // Always trim the value
1779  $value = trim($value);
1780 
1781  // Early return if required validation fails
1782  // Note: The "required" check is evaluated but does not yet lead to an error, see
1783  // the comment in the DataHandler::validateValueForRequired() for more information.
1784  if (!$this->‪validateValueForRequired($tcaFieldConf, $value)) {
1785  return [];
1786  }
1787 
1788  // Early return, if password hashing is disabled and the table is not fe_users or be_users
1789  if (!($tcaFieldConf['hashed'] ?? true) && !in_array($table, ['fe_users', 'be_users'], true)) {
1790  return [
1791  'value' => $value,
1792  ];
1793  }
1794 
1795  // An incoming value is either the salted password if the user did not change existing password
1796  // when submitting the form, or a plaintext new password that needs to be turned into a salted password now.
1797  // The strategy is to see if a salt instance can be created from the incoming value. If so,
1798  // no new password was submitted and we keep the value. If no salting instance can be created,
1799  // incoming value must be a new plain text value that needs to be hashed.
1800  $hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
1801  $mode = $table === 'fe_users' ? 'FE' : 'BE';
1802  $isNewUser = str_contains((string)$id, 'NEW');
1803  $newHashInstance = $hashFactory->getDefaultHashInstance($mode);
1804 
1805  try {
1806  $hashFactory->get($value, $mode);
1807  } catch (InvalidPasswordHashException $e) {
1808  // We got no salted password instance, incoming value must be a new plaintext password
1809  // Validate new password against password policy for field
1810  $passwordPolicy = $tcaFieldConf['passwordPolicy'] ?? '';
1811  $passwordPolicyValidator = GeneralUtility::makeInstance(
1812  PasswordPolicyValidator::class,
1814  is_string($passwordPolicy) ? $passwordPolicy : ''
1815  );
1816 
1817  $contextData = new ContextData(
1818  loginMode: $mode,
1819  newUsername: $incomingFieldArray['username'] ?? '',
1820  newUserFirstName: $incomingFieldArray['first_name'] ?? '',
1821  newUserLastName: $incomingFieldArray['last_name'] ?? '',
1822  newUserFullName: $incomingFieldArray['realName'] ?? '',
1823  );
1824  $event = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch(
1825  new EnrichPasswordValidationContextDataEvent(
1826  $contextData,
1827  $incomingFieldArray,
1828  self::class
1829  )
1830  );
1831  $contextData = $event->getContextData();
1832 
1833  $isValidPassword = $passwordPolicyValidator->isValidPassword($value, $contextData);
1834  if (!$isValidPassword) {
1835  $message = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:dataHandler.passwordNotSaved');
1836  $this->‪log(
1837  $table,
1838  (int)$id,
1839  SystemLogDatabaseAction::UPDATE,
1840  0,
1841  SystemLogErrorClassification::WARNING,
1842  $message . implode('. ', $passwordPolicyValidator->getValidationErrors()),
1843  -1,
1844  [
1845  'table' => $table,
1846  'uid' => (string)$id,
1847  ],
1848  $realPid
1849  );
1850 
1851  // Password not valid for existing user. Stopping here, password won't be changed
1852  if (!$isNewUser) {
1853  return [];
1854  }
1855  // Password not valid for new user. To prevent empty passwords in the database, we set a random password.
1856  $value = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(96);
1857  }
1858 
1859  // Get an instance of the current configured salted password strategy and hash the value
1860  $value = $newHashInstance->getHashedPassword($value);
1861  }
1862 
1863  return [
1864  'value' => $value,
1865  ];
1866  }
1867 
1881  protected function ‪checkValueForSlug(string $value, array $tcaFieldConf, string $table, $id, int $realPid, string $field, array $incomingFieldArray = []): array
1882  {
1883  $workspaceId = $this->BE_USER->workspace;
1884  $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $tcaFieldConf, $workspaceId);
1885  $fullRecord = array_replace_recursive($this->checkValue_currentRecord, $incomingFieldArray);
1886  // Generate a value if there is none, otherwise ensure that all characters are cleaned up
1887  if ($value === '') {
1888  $value = $helper->generate($fullRecord, $realPid);
1889  } else {
1890  $value = $helper->sanitize($value);
1891  }
1892 
1893  // Return directly in case no evaluations are defined
1894  if (empty($tcaFieldConf['eval'])) {
1895  return ['value' => $value];
1896  }
1897 
1898  $state = ‪RecordStateFactory::forName($table)
1899  ->fromArray($fullRecord, $realPid, $id);
1900  $evalCodesArray = ‪GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1901  if (in_array('unique', $evalCodesArray, true)) {
1902  $value = $helper->buildSlugForUniqueInTable($value, $state);
1903  }
1904  if (in_array('uniqueInSite', $evalCodesArray, true)) {
1905  $value = $helper->buildSlugForUniqueInSite($value, $state);
1906  }
1907  if (in_array('uniqueInPid', $evalCodesArray, true)) {
1908  $value = $helper->buildSlugForUniqueInPid($value, $state);
1909  }
1910 
1911  return ['value' => $value];
1912  }
1913 
1924  protected function ‪checkValueForLanguage(int $value, string $table, string $field): array
1925  {
1926  // If given table is localizable and the given field is the defined
1927  // languageField, check if the selected language is allowed for the user.
1928  // Note: Usually this method should never be reached, in case the language value is
1929  // not valid, since recordEditAccessInternals checks for proper permission beforehand.
1930  if (BackendUtility::isTableLocalizable($table)
1931  && (‪$GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '') === $field
1932  && !$this->BE_USER->checkLanguageAccess($value)
1933  ) {
1934  return [];
1935  }
1936  // @todo Should we also check if the language is allowed for the current site - if record has site context?
1937  return ['value' => $value];
1938  }
1939 
1950  protected function ‪checkValueForLink(string $value, array $tcaFieldConf, string $table, int|string $id, string $field): array
1951  {
1952  // Always trim the value
1953  $value = trim($value);
1954 
1955  // Early return if required validation fails
1956  // Note: The "required" check is evaluated but does not yet lead to an error, see
1957  // the comment in the DataHandler::validateValueForRequired() for more information.
1958  if (!$this->‪validateValueForRequired($tcaFieldConf, $value)) {
1959  return [];
1960  }
1961 
1962  // Early return if an empty allow list is defined for the link types
1963  if (is_array($tcaFieldConf['allowedTypes'] ?? false) && $tcaFieldConf['allowedTypes'] === []) {
1964  return [];
1965  }
1966 
1967  if ($value !== '') {
1968  // Extract the actual link from the link definition for further evaluation
1969  $linkParameter = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($value)['url'];
1970  if ($linkParameter === '') {
1971  $this->‪log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, '"{link}" is not a valid link definition for the field "{field}" of the table "{table}"', -1, ['link' => $value, 'field' => $field, 'table' => $table]);
1972  $value = '';
1973  } else {
1974  // Try to resolve the actual link type and compare with the allow list
1975  try {
1976  $linkData = GeneralUtility::makeInstance(LinkService::class)->resolve($linkParameter);
1977  $linkType = $linkData['type'] ?? '';
1978  $linkIdentifier = $linkData['identifier'] ?? '';
1979  if (is_array($tcaFieldConf['allowedTypes'] ?? false)
1980  && ($tcaFieldConf['allowedTypes'][0] ?? '') !== '*'
1981  && !in_array($linkType, $tcaFieldConf['allowedTypes'], true)
1982  && ($linkType !== 'record' || !in_array($linkIdentifier, $tcaFieldConf['allowedTypes'], true))
1983  ) {
1984  $message = $linkIdentifier !== ''
1985  ? 'Link type "record" with identifier "{type}" is not allowed for the field "{field}" of the table "{table}"'
1986  : 'Link type "{type}" is not allowed for the field "{field}" of the table "{table}"';
1987  $this->‪log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, $message, -1, ['type' => $linkIdentifier ?: $linkType, 'field' => $field, 'table' => $table]);
1988  $value = '';
1989  }
1990  } catch (UnknownLinkHandlerException $e) {
1991  $this->‪log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, '"{link}" is not a valid link for the field "{field}" of the table "{table}"', -1, ['link' => $value, 'field' => $field, 'table' => $table]);
1992  $value = '';
1993  }
1994  }
1995  }
1996 
1997  return ['value' => $value];
1998  }
1999 
2011  protected function ‪checkValueForCategory(
2012  array $result,
2013  string $value,
2014  array $tcaFieldConf,
2015  string $table,
2016  $id,
2017  string $status,
2018  string $field
2019  ): array {
2020  // Exploded comma-separated values and remove duplicates
2021  $valueArray = array_unique(‪GeneralUtility::trimExplode(',', $value, true));
2022  // If an exclusive key is found, discard all others:
2023  if ($tcaFieldConf['exclusiveKeys'] ?? false) {
2024  $exclusiveKeys = ‪GeneralUtility::trimExplode(',', $tcaFieldConf['exclusiveKeys']);
2025  foreach ($valueArray as $index => $key) {
2026  if (in_array($key, $exclusiveKeys, true)) {
2027  $valueArray = [$index => $key];
2028  break;
2029  }
2030  }
2031  }
2032  $unsetResult = false;
2033  if (str_contains($value, 'NEW')) {
2034  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2035  $this->remapStack[] = [
2036  'func' => 'checkValue_category_processDBdata',
2037  'args' => [$valueArray, $tcaFieldConf, $id, $status, $table, $field],
2038  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 4],
2039  'field' => $field,
2040  ];
2041  $unsetResult = true;
2042  } else {
2043  $valueArray = $this->‪checkValue_category_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field);
2044  }
2045  if ($unsetResult) {
2046  unset($result['value']);
2047  } else {
2048  $newVal = implode(',', $this->‪checkValue_checkMax($tcaFieldConf, $valueArray));
2049  $result['value'] = $newVal !== '' ? $newVal : 0;
2050  }
2051  return $result;
2052  }
2053 
2060  protected function ‪checkValueForDatetime(mixed $value, array $tcaFieldConf): array
2061  {
2062  $format = $tcaFieldConf['format'] ?? 'datetime';
2063  if (!in_array($format, ['datetime', 'date', 'time', 'timesec'], true)) {
2064  // Early return if format is not valid
2065  return [];
2066  }
2067 
2068  // Handle native date/time fields
2069  $isNativeDateTimeField = false;
2070  $nativeDateTimeFieldFormat = '';
2071  $nativeDateTimeFieldResetValue = '';
2072  $nativeDateTimeType = $tcaFieldConf['dbType'] ?? '';
2073  if (in_array($nativeDateTimeType, ‪QueryHelper::getDateTimeTypes(), true)) {
2074  $isNativeDateTimeField = true;
2075  $dateTimeFormats = ‪QueryHelper::getDateTimeFormats();
2076  $nativeDateTimeFieldFormat = $dateTimeFormats[$nativeDateTimeType]['format'];
2077  $nativeDateTimeFieldEmptyValue = $dateTimeFormats[$nativeDateTimeType]['empty'];
2078  $nativeDateTimeFieldResetValue = $dateTimeFormats[$nativeDateTimeType]['reset'];
2079  if (empty($value)) {
2080  $value = null;
2081  } else {
2082  // Convert the date/time into a timestamp for the sake of the checks
2083  // We expect the ISO 8601 $value to contain a UTC timezone specifier.
2084  // We explicitly fallback to UTC if no timezone specifier is given (e.g. for copy operations).
2085  $dateTime = new \DateTime((string)$value, new \DateTimeZone('UTC'));
2086  // The timestamp (UTC) returned by getTimestamp() will be converted to
2087  // a local time string by gmdate() later.
2088  $value = $value === $nativeDateTimeFieldEmptyValue ? null : $dateTime->getTimestamp();
2089  }
2090  }
2091 
2092  if (!$this->‪validateValueForRequired($tcaFieldConf, (string)$value)) {
2093  return [];
2094  }
2095 
2096  if ((string)$value !== '' && !‪MathUtility::canBeInterpretedAsInteger((string)$value)) {
2097  if (($format === 'time' || $format === 'timesec')) {
2098  $value = (new \DateTime((string)$value))->getTimestamp();
2099  } else {
2100  // The value we receive from JS is an ISO 8601 date, which is always in UTC. (the JS code works like that, on purpose!)
2101  // For instance "1999-11-11T11:11:11Z"
2102  // Since the user actually specifies the time in the server's local time, we need to mangle this
2103  // to reflect the server TZ. So we make this 1999-11-11T11:11:11+0200 (assuming Europe/Vienna here)
2104  // In the database we store the date in UTC (1999-11-11T09:11:11Z), hence we take the timestamp of this converted value.
2105  // For achieving this we work with timestamps only (which are UTC) and simply adjust it for the
2106  // TZ difference.
2107  try {
2108  // Make the date from JS a timestamp
2109  $value = (new \DateTime((string)$value))->getTimestamp();
2110  } catch (\Exception) {
2111  // set the default timezone value to achieve the value of 0 as a result
2112  $value = (int)date('Z', 0);
2113  }
2114 
2115  // @todo this hacky part is problematic when it comes to times around DST switch! Add test to prove that this is broken.
2116  $value -= (int)date('Z', $value);
2117  }
2118  }
2119 
2120  // Skip range validation, if the default value equals 0 and the input value is 0, "0" or an empty string.
2121  // This is needed for timestamp date fields with ['range']['lower'] set.
2122  $skipRangeValidation =
2123  isset($tcaFieldConf['default'], $value)
2124  && (int)$tcaFieldConf['default'] === 0
2125  && ($value === '' || $value === '0' || $value === 0);
2126 
2127  // Checking range of value:
2128  if (!$skipRangeValidation && is_array($tcaFieldConf['range'] ?? null)) {
2129  if (isset($tcaFieldConf['range']['upper']) && ceil($value) > (int)$tcaFieldConf['range']['upper']) {
2130  $value = (int)$tcaFieldConf['range']['upper'];
2131  }
2132  if (isset($tcaFieldConf['range']['lower']) && floor($value) < (int)$tcaFieldConf['range']['lower']) {
2133  $value = (int)$tcaFieldConf['range']['lower'];
2134  }
2135  }
2136 
2137  // Handle native date/time fields
2138  if ($isNativeDateTimeField) {
2139  if ($tcaFieldConf['nullable'] ?? false) {
2140  // Convert the timestamp back to a date/time if not null
2141  $value = $value !== null ? gmdate($nativeDateTimeFieldFormat, $value) : null;
2142  } else {
2143  // Convert the timestamp back to a date/time
2144  $value = $value !== null ? gmdate($nativeDateTimeFieldFormat, $value) : $nativeDateTimeFieldResetValue;
2145  }
2146  } else {
2147  // Ensure value is always an int if no native field is used
2148  $value = (int)$value;
2149  }
2150 
2151  $res['value'] = $value;
2152  return $res;
2153  }
2154 
2167  protected function ‪checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field)
2168  {
2169  $items = $tcaFieldConf['items'] ?? null;
2170  if (!empty($tcaFieldConf['itemsProcFunc'])) {
2171  $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
2172  $items = $processingService->getProcessingItems(
2173  $table,
2174  $realPid,
2175  $field,
2176  $this->checkValue_currentRecord,
2177  $tcaFieldConf,
2178  $tcaFieldConf['items']
2179  );
2180  }
2181 
2182  $itemC = 0;
2183  if ($items !== null) {
2184  $itemC = count($items);
2185  }
2186  if (!$itemC) {
2187  $itemC = 1;
2188  }
2189  $maxV = (2 ** $itemC) - 1;
2190  if ($value < 0) {
2191  // @todo: throw LogicException here? Negative values for checkbox items do not make sense and indicate a coding error.
2192  $value = 0;
2193  }
2194  if ($value > $maxV) {
2195  // @todo: This case is pretty ugly: If there is an itemsProcFunc registered, and if it returns a dynamic,
2196  // changing list of items, then it may happen that a value is transformed and vanished checkboxes
2197  // are permanently removed from the value.
2198  // Suggestion: Throw an exception instead? Maybe a specific, catchable exception that generates a
2199  // error message to the user - dynamic item sets via itemProcFunc on check would be a bad idea anyway.
2200  $value = (int)$value & $maxV;
2201  }
2202  if ($field && $value > 0 && !empty($tcaFieldConf['eval'])) {
2203  $evalCodesArray = ‪GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
2204  $otherRecordsWithSameValue = [];
2205  $maxCheckedRecords = 0;
2206  // @todo These checks do not consider the language of the current record (if available).
2207  if (in_array('maximumRecordsCheckedInPid', $evalCodesArray, true)) {
2208  $otherRecordsWithSameValue = $this->‪getRecordsWithSameValue($table, $id, $field, $value, $realPid);
2209  $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsCheckedInPid'];
2210  }
2211  if (in_array('maximumRecordsChecked', $evalCodesArray, true)) {
2212  $otherRecordsWithSameValue = $this->‪getRecordsWithSameValue($table, $id, $field, $value);
2213  $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsChecked'];
2214  }
2215 
2216  // there are more than enough records with value "1" in the DB
2217  // if so, set this value to "0" again
2218  if ($maxCheckedRecords && count($otherRecordsWithSameValue) >= $maxCheckedRecords) {
2219  $value = 0;
2220  $this->‪log(
2221  $table,
2222  $id,
2223  SystemLogDatabaseAction::CHECK,
2224  0,
2225  SystemLogErrorClassification::USER_ERROR,
2226  'Could not activate checkbox for field "{field}". A total of {max} record(s) can have this checkbox activated. Uncheck other records first in order to activate the checkbox of this record',
2227  -1,
2228  ['field' => $field, 'max' => $maxCheckedRecords]
2229  );
2230  }
2231  }
2232  $res['value'] = $value;
2233  return $res;
2234  }
2235 
2248  protected function ‪checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $pid, $field)
2249  {
2250  if (is_array($tcaFieldConf['items'])) {
2251  foreach ($tcaFieldConf['items'] as $set) {
2252  if ((string)$set['value'] === (string)$value) {
2253  $res['value'] = $value;
2254  break;
2255  }
2256  }
2257  }
2258 
2259  // if no value was found and an itemsProcFunc is defined, check that for the value
2260  if (!empty($tcaFieldConf['itemsProcFunc']) && empty($res['value'])) {
2261  $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
2262  $processedItems = $processingService->getProcessingItems(
2263  $table,
2264  $pid,
2265  $field,
2266  $this->checkValue_currentRecord,
2267  $tcaFieldConf,
2268  $tcaFieldConf['items']
2269  );
2270 
2271  foreach ($processedItems as $set) {
2272  if ((string)$set['value'] === (string)$value) {
2273  $res['value'] = $value;
2274  break;
2275  }
2276  }
2277  }
2278 
2279  return $res;
2280  }
2281 
2289  protected function ‪checkValueForJson(array|string $value, array $tcaFieldConf): array
2290  {
2291  if (is_string($value)) {
2292  if ($value === '') {
2293  $value = [];
2294  } else {
2295  try {
2296  $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
2297  if ($value === null) {
2298  // Unset value as it could not be decoded
2299  return [];
2300  }
2301  } catch (\JsonException) {
2302  // Unset value as it is invalid
2303  return [];
2304  }
2305  }
2306  }
2307 
2308  if (!$this->‪validateValueForRequired($tcaFieldConf, $value)) {
2309  // Unset value as it is required
2310  return [];
2311  }
2312 
2313  return [
2314  'value' => $value,
2315  ];
2316  }
2317 
2330  protected function ‪checkValueForGroupFolderSelect($res, $value, $tcaFieldConf, $table, $id, $status, $field)
2331  {
2332  // Detecting if value sent is an array and if so, implode it around a comma:
2333  if (is_array($value)) {
2334  $value = implode(',', $value);
2335  } else {
2336  $value = (string)$value;
2337  }
2338 
2339  // When values are sent as group or select they come as comma-separated values which are exploded by this function:
2340  $valueArray = $this->‪checkValue_group_select_explodeSelectGroupValue($value);
2341  // If multiple is not set, remove duplicates:
2342  if (!($tcaFieldConf['multiple'] ?? false)) {
2343  $valueArray = array_unique($valueArray);
2344  }
2345  // If an exclusive key is found, discard all others:
2346  if ($tcaFieldConf['type'] === 'select' && ($tcaFieldConf['exclusiveKeys'] ?? false)) {
2347  $exclusiveKeys = ‪GeneralUtility::trimExplode(',', $tcaFieldConf['exclusiveKeys']);
2348  foreach ($valueArray as $index => $key) {
2349  if (in_array($key, $exclusiveKeys, true)) {
2350  $valueArray = [$index => $key];
2351  break;
2352  }
2353  }
2354  }
2355  // This could be a good spot for parsing the array through a validation-function which checks if the values are correct (except that database references are not in their final form - but that is the point, isn't it?)
2356  // NOTE!!! Must check max-items of files before the later check because that check would just leave out file names if there are too many!!
2357  $valueArray = $this->‪applyFiltersToValues($tcaFieldConf, $valueArray);
2358  // Checking for select / authMode, removing elements from $valueArray if any of them is not allowed!
2359  if ($tcaFieldConf['type'] === 'select' && ($tcaFieldConf['authMode'] ?? false)) {
2360  $preCount = count($valueArray);
2361  foreach ($valueArray as $index => $key) {
2362  if (!$this->BE_USER->checkAuthMode($table, $field, $key)) {
2363  unset($valueArray[$index]);
2364  }
2365  }
2366  // During the check it turns out that the value / all values were removed - we respond by simply returning an empty array so nothing is written to DB for this field.
2367  if ($preCount && empty($valueArray)) {
2368  return [];
2369  }
2370  }
2371  // For select types which has a foreign table attached:
2372  $unsetResult = false;
2373  if ($tcaFieldConf['type'] === 'group' || ($tcaFieldConf['type'] === 'select' && ($tcaFieldConf['foreign_table'] ?? false))) {
2374  // check, if there is a NEW... id in the value, that should be substituted later
2375  if (str_contains($value, 'NEW')) {
2376  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2377  $this->remapStack[] = [
2378  'func' => 'checkValue_group_select_processDBdata',
2379  'args' => [$valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field],
2380  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 5],
2381  'field' => $field,
2382  ];
2383  $unsetResult = true;
2384  } else {
2385  $valueArray = $this->‪checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field);
2386  }
2387  }
2388  if (!$unsetResult) {
2389  $newVal = $this->‪checkValue_checkMax($tcaFieldConf, $valueArray);
2390  $res['value'] = $this->‪castReferenceValue(implode(',', $newVal), $tcaFieldConf, str_contains($value, 'NEW'));
2391  } else {
2392  unset($res['value']);
2393  }
2394  return $res;
2395  }
2396 
2406  protected function ‪checkValueForUuid(string $value, array $tcaFieldConf): array
2407  {
2408  if (Uuid::isValid($value)) {
2409  return ['value' => $value];
2410  }
2411 
2412  if ($tcaFieldConf['required'] ?? true) {
2413  return ['value' => (string)match ((int)($tcaFieldConf['version'] ?? 0)) {
2414  6 => Uuid::v6(),
2415  7 => Uuid::v7(),
2416  default => Uuid::v4()
2417  }];
2418  }
2419  // Unset invalid uuid - in case a field value is not required
2420  return [];
2421  }
2422 
2429  protected function ‪applyFiltersToValues(array $tcaFieldConfiguration, array $values)
2430  {
2431  if (!is_array($tcaFieldConfiguration['filter'] ?? null)) {
2432  return $values;
2433  }
2434  foreach ($tcaFieldConfiguration['filter'] as $filter) {
2435  if (empty($filter['userFunc'])) {
2436  continue;
2437  }
2438  $parameters = $filter['parameters'] ?? [];
2439  if (!is_array($parameters)) {
2440  $parameters = [];
2441  }
2442  $parameters['values'] = $values;
2443  $parameters['tcaFieldConfig'] = $tcaFieldConfiguration;
2444  $values = GeneralUtility::callUserFunction($filter['userFunc'], $parameters, $this);
2445  if (!is_array($values)) {
2446  throw new \RuntimeException('Expected userFunc filter "' . $filter['userFunc'] . '" to return an array. Got ' . gettype($values) . '.', 1336051942);
2447  }
2448  }
2449  return $values;
2450  }
2451 
2468  protected function ‪checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $field)
2469  {
2470  if (!is_array($value)) {
2471  $res['value'] = $value;
2472  return $res;
2473  }
2474 
2475  // This value is necessary for flex form processing to happen on flexform fields in page records when they are copied.
2476  // Problem: when copying a page, flexform XML comes along in the array for the new record - but since $this->checkValue_currentRecord
2477  // does not have a uid or pid for that sake, the FlexFormTools->getDataStructureIdentifier() function returns no good DS. For new
2478  // records we do know the expected PID, so we send that with this special parameter. Only active when larger than zero.
2480  if ($status === 'new') {
2481  $row['pid'] = $realPid;
2482  }
2483 
2484  // Get data structure. The methods may throw various exceptions, with some of them being
2485  // ok in certain scenarios, for instance on new record rows. Those are ok to "eat" here
2486  // and substitute with a dummy DS.
2487  try {
2488  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
2489  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
2490  ['config' => $tcaFieldConf],
2491  $table,
2492  $field,
2493  $row
2494  );
2495  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
2496  } catch (InvalidIdentifierException) {
2497  $dataStructureArray = ['sheets' => ['sDEF' => []]];
2498  }
2499 
2500  // Get current value array:
2501  $currentValueArray = (string)$curValue !== '' ? ‪GeneralUtility::xml2array($curValue) : [];
2502  if (!is_array($currentValueArray)) {
2503  $currentValueArray = [];
2504  }
2505  // Remove all old meta for languages...
2506  // Evaluation of input values:
2507  $value['data'] = $this->‪checkValue_flex_procInData($value['data'] ?? [], $currentValueArray['data'] ?? [], $dataStructureArray, [$table, $id, $curValue, $status, $realPid, $recFID, $tscPID]);
2508  // Create XML from input value:
2509  $xmlValue = $this->‪checkValue_flexArray2Xml($value);
2510 
2511  // Here we convert the currently submitted values BACK to an array, then merge the two and then BACK to XML again. This is needed to ensure the charsets are the same
2512  // (provided that the current value was already stored IN the charset that the new value is converted to).
2513  $xmlAsArray = ‪GeneralUtility::xml2array($xmlValue);
2514 
2515  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'] ?? [] as $className) {
2516  $hookObject = GeneralUtility::makeInstance($className);
2517  if (method_exists($hookObject, 'checkFlexFormValue_beforeMerge')) {
2518  $hookObject->checkFlexFormValue_beforeMerge($this, $currentValueArray, $xmlAsArray);
2519  }
2520  }
2521 
2522  ArrayUtility::mergeRecursiveWithOverrule($currentValueArray, $xmlAsArray);
2523  $xmlValue = $this->‪checkValue_flexArray2Xml($currentValueArray);
2524 
2525  $xmlAsArray = ‪GeneralUtility::xml2array($xmlValue);
2526  $xmlAsArray = $this->‪sortAndDeleteFlexSectionContainerElements($xmlAsArray, $dataStructureArray);
2527  $xmlValue = $this->‪checkValue_flexArray2Xml($xmlAsArray);
2528 
2529  $res['value'] = $xmlValue;
2530  return $res;
2531  }
2532 
2540  public function ‪checkValue_flexArray2Xml($array): string
2541  {
2542  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
2543  return $flexObj->flexArray2Xml($array);
2544  }
2545 
2551  private function ‪sortAndDeleteFlexSectionContainerElements(array $valueArray, array $dataStructure): array
2552  {
2553  foreach (($dataStructure['sheets'] ?? []) as $dataStructureSheetName => $dataStructureSheetDefinition) {
2554  if (!isset($dataStructureSheetDefinition['ROOT']['el']) || !is_array($dataStructureSheetDefinition['ROOT']['el'])) {
2555  continue;
2556  }
2557  $dataStructureFields = $dataStructureSheetDefinition['ROOT']['el'];
2558  foreach ($dataStructureFields as $dataStructureFieldName => $dataStructureFieldDefinition) {
2559  if (isset($dataStructureFieldDefinition['type']) && $dataStructureFieldDefinition['type'] === 'array'
2560  && isset($dataStructureFieldDefinition['section']) && (string)$dataStructureFieldDefinition['section'] === '1'
2561  ) {
2562  // Found a possible section within flex form data structure definition
2563  if (!is_array($valueArray['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'] ?? false)) {
2564  // No containers in data
2565  continue;
2566  }
2567  $newElements = [];
2568  $containerCounter = 0;
2569  foreach ($valueArray['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'] as $sectionKey => $sectionValues) {
2570  // Remove to-delete containers
2571  $action = $sectionValues['_ACTION'] ?? '';
2572  if ($action === 'DELETE') {
2573  continue;
2574  }
2575  if (($sectionValues['_ACTION'] ?? '') === '') {
2576  $sectionValues['_ACTION'] = $containerCounter;
2577  }
2578  $newElements[$sectionKey] = $sectionValues;
2579  $containerCounter++;
2580  }
2581  // Resort by action key
2582  uasort($newElements, function ($a, $b) {
2583  return (int)$a['_ACTION'] - (int)$b['_ACTION'];
2584  });
2585  foreach ($newElements as &$element) {
2586  // Do not store action key
2587  unset($element['_ACTION']);
2588  }
2589  $valueArray['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'] = $newElements;
2590  }
2591  }
2592  }
2593  return $valueArray;
2594  }
2595 
2608  public function ‪checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData = null)
2609  {
2610  [$table, $id, , $status] = $PP;
2611  $this->‪checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
2612  }
2613 
2629  public function ‪checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData = null)
2630  {
2631  if (!$tcaFieldConf['foreign_table']) {
2632  // Fatal error, inline fields should always have a foreign_table defined
2633  return false;
2634  }
2635  // When values are sent they come as comma-separated values which are exploded by this function:
2636  $valueArray = ‪GeneralUtility::trimExplode(',', $value);
2637  // Remove duplicates: (should not be needed)
2638  $valueArray = array_unique($valueArray);
2639  // Example for received data:
2640  // $value = 45,NEW4555fdf59d154,12,123
2641  // We need to decide whether we use the stack or can save the relation directly.
2642  if (!empty($value) && (str_contains($value, 'NEW') || !‪MathUtility::canBeInterpretedAsInteger($id))) {
2643  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2644  $this->remapStack[] = [
2645  'func' => 'checkValue_inline_processDBdata',
2646  'args' => [$valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData],
2647  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 4],
2648  'additionalData' => $additionalData,
2649  'field' => $field,
2650  ];
2651  unset($res['value']);
2652  } elseif ($value || ‪MathUtility::canBeInterpretedAsInteger($id)) {
2653  $res['value'] = $this->‪checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field);
2654  }
2655  return $res;
2656  }
2657 
2661  public function ‪checkValueForFile(
2662  array $res,
2663  string $value,
2664  array $tcaFieldConf,
2665  string $table,
2666  int|string $id,
2667  string $field,
2668  ?array $additionalData = null
2669  ): array {
2670  $valueArray = array_unique(‪GeneralUtility::trimExplode(',', $value));
2671  if ($value !== '' && (str_contains($value, 'NEW') || !‪MathUtility::canBeInterpretedAsInteger($id))) {
2672  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2673  $this->remapStack[] = [
2674  'func' => 'checkValue_file_processDBdata',
2675  'args' => [$valueArray, $tcaFieldConf, $id, $table],
2676  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 3],
2677  'additionalData' => $additionalData,
2678  'field' => $field,
2679  ];
2680  unset($res['value']);
2681  } elseif ($value !== '' || ‪MathUtility::canBeInterpretedAsInteger($id)) {
2682  $res['value'] = $this->‪checkValue_file_processDBdata($valueArray, $tcaFieldConf, $id, $table);
2683  }
2684  return $res;
2685  }
2686 
2696  public function ‪checkValue_checkMax($tcaFieldConf, $valueArray): array
2697  {
2698  // BTW, checking for min and max items here does NOT make any sense when MM is used because the above function
2699  // calls will just return an array with a single item (the count) if MM is used... Why didn't I perform the check
2700  // before? Probably because we could not evaluate the validity of record uids etc... Hmm...
2701  // NOTE to the comment: It's not really possible to check for too few items, because you must then determine first,
2702  // if the field is actually used regarding the CType.
2703  $maxitems = isset($tcaFieldConf['maxitems']) ? (int)$tcaFieldConf['maxitems'] : 99999;
2704  return array_slice($valueArray, 0, $maxitems);
2705  }
2706 
2707  /*********************************************
2708  *
2709  * Helper functions for evaluation functions.
2710  *
2711  ********************************************/
2724  public function ‪getUnique($table, $field, $value, $id, $newPid = 0)
2725  {
2726  if (!is_array(‪$GLOBALS['TCA'][$table]) || !is_array(‪$GLOBALS['TCA'][$table]['columns'][$field])) {
2727  // Field is not configured in TCA
2728  return $value;
2729  }
2730 
2731  if ((‪$GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode'] ?? '') === 'exclude') {
2732  $transOrigPointerField = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
2733  $l10nParent = (int)$this->checkValue_currentRecord[$transOrigPointerField];
2734  if ($l10nParent > 0) {
2735  // Current record is a translation and l10n_mode "exclude" just copies the value from source language
2736  return $value;
2737  }
2738  }
2739 
2740  $newValue = $originalValue = $value;
2741  $queryBuilder = $this->‪getUniqueCountStatement($newValue, $table, $field, (int)$id, (int)$newPid);
2742  // For as long as records with the test-value existing, try again (with incremented numbers appended)
2743  $statement = $queryBuilder->prepare();
2744  $result = $statement->executeQuery();
2745  if ($result->fetchOne()) {
2746  for ($counter = 0; $counter <= 100; $counter++) {
2747  $result->free();
2748  $newValue = $value . $counter;
2749  $statement->bindValue(1, $newValue, ‪Connection::PARAM_STR);
2750  $result = $statement->executeQuery();
2751  if (!$result->fetchOne()) {
2752  break;
2753  }
2754  }
2755  $result->free();
2756  }
2757 
2758  if ($originalValue !== $newValue) {
2759  $this->‪log($table, $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::WARNING, 'The value of the field "{field}" has been changed from "{originalValue}" to "{newValue}" as it is required to be unique', 1, ['field' => $field, 'originalValue' => $originalValue, 'newValue' => $newValue], $newPid);
2760  }
2761 
2762  return $newValue;
2763  }
2764 
2775  protected function ‪getUniqueCountStatement(
2776  string $value,
2777  string $table,
2778  string $field,
2779  int ‪$uid,
2780  int $pid
2781  ): QueryBuilder {
2782  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
2783  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
2784  $queryBuilder
2785  ->count('uid')
2786  ->from($table)
2787  ->where(
2788  $queryBuilder->expr()->eq($field, $queryBuilder->createPositionalParameter($value)),
2789  $queryBuilder->expr()->neq('uid', $queryBuilder->createPositionalParameter(‪$uid, ‪Connection::PARAM_INT))
2790  );
2791  // ignore translations of current record if field is configured with l10n_mode = "exclude"
2792  if ((‪$GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode'] ?? '') === 'exclude'
2793  && (‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '') !== ''
2794  && (‪$GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '') !== '') {
2795  $queryBuilder
2796  ->andWhere(
2797  $queryBuilder->expr()->or(
2798  // records without l10n_parent must be taken into account (in any language)
2799  $queryBuilder->expr()->eq(
2800  ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2801  $queryBuilder->createPositionalParameter(0, ‪Connection::PARAM_INT)
2802  ),
2803  // translations of other records must be taken into account
2804  $queryBuilder->expr()->neq(
2805  ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2806  $queryBuilder->createPositionalParameter(‪$uid, ‪Connection::PARAM_INT)
2807  )
2808  )
2809  );
2810  }
2811  if ($pid !== 0) {
2812  $queryBuilder->andWhere(
2813  $queryBuilder->expr()->eq('pid', $queryBuilder->createPositionalParameter($pid, ‪Connection::PARAM_INT))
2814  );
2815  } else {
2816  // pid>=0 for versioning
2817  $queryBuilder->andWhere(
2818  $queryBuilder->expr()->gte('pid', $queryBuilder->createPositionalParameter(0, ‪Connection::PARAM_INT))
2819  );
2820  }
2821  return $queryBuilder;
2822  }
2823 
2835  public function ‪getRecordsWithSameValue($tableName, ‪$uid, $fieldName, $value, $pageId = 0): array
2836  {
2837  $result = [];
2838  if (empty(‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
2839  return $result;
2840  }
2841 
2842  ‪$uid = (int)‪$uid;
2843  $pageId = (int)$pageId;
2844 
2845  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
2846  $queryBuilder->getRestrictions()->removeAll()
2847  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2848  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
2849 
2850  $queryBuilder->select('*')
2851  ->from($tableName)
2852  ->where(
2853  $queryBuilder->expr()->eq(
2854  $fieldName,
2855  $queryBuilder->createNamedParameter($value)
2856  ),
2857  $queryBuilder->expr()->neq(
2858  'uid',
2859  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
2860  )
2861  );
2862 
2863  if ($pageId) {
2864  $queryBuilder->andWhere(
2865  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT))
2866  );
2867  }
2868 
2869  return $queryBuilder->executeQuery()->fetchAllAssociative();
2870  }
2871 
2879  public function ‪checkValue_text_Eval($value, $evalArray, $is_in)
2880  {
2881  $res = [];
2883  $set = true;
2884  foreach ($evalArray as $func) {
2885  switch ($func) {
2886  case 'trim':
2887  $value = trim((string)$value);
2888  break;
2889  default:
2890  if (isset(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2891  if (class_exists($func)) {
2892  $evalObj = GeneralUtility::makeInstance($func);
2893  if (method_exists($evalObj, 'evaluateFieldValue')) {
2894  $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2895  }
2896  }
2897  }
2898  }
2899  }
2900  if ($set) {
2901  $res['value'] = $value;
2902  }
2903  return $res;
2904  }
2905 
2917  public function ‪checkValue_input_Eval($value, $evalArray, $is_in, string $table = '', $id = ''): array
2918  {
2919  $res = [];
2920  $set = true;
2921  foreach ($evalArray as $func) {
2922  switch ($func) {
2923  case 'year':
2924  $value = (int)$value;
2925  break;
2926  case 'md5':
2927  if (strlen($value) !== 32) {
2928  $set = false;
2929  }
2930  break;
2931  case 'trim':
2932  $value = trim($value);
2933  break;
2934  case 'upper':
2935  $value = mb_strtoupper($value, 'utf-8');
2936  break;
2937  case 'lower':
2938  $value = mb_strtolower($value, 'utf-8');
2939  break;
2940  case 'is_in':
2941  $c = mb_strlen($value);
2942  if ($c) {
2943  $newVal = '';
2944  for ($a = 0; $a < $c; $a++) {
2945  $char = mb_substr($value, $a, 1);
2946  if (str_contains($is_in, $char)) {
2947  $newVal .= $char;
2948  }
2949  }
2950  $value = $newVal;
2951  }
2952  break;
2953  case 'nospace':
2954  $value = str_replace(' ', '', $value);
2955  break;
2956  case 'alpha':
2957  $value = preg_replace('/[^a-zA-Z]/', '', $value);
2958  break;
2959  case 'num':
2960  $value = preg_replace('/[^0-9]/', '', $value);
2961  break;
2962  case 'alphanum':
2963  $value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
2964  break;
2965  case 'alphanum_x':
2966  $value = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
2967  break;
2968  case 'domainname':
2969  if (!preg_match('/^[a-z0-9.\\-]*$/i', $value)) {
2970  $value = (string)idn_to_ascii($value);
2971  }
2972  break;
2973  default:
2974  if (isset(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2975  if (class_exists($func)) {
2976  $evalObj = GeneralUtility::makeInstance($func);
2977  if (method_exists($evalObj, 'evaluateFieldValue')) {
2978  $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2979  }
2980  }
2981  }
2982  }
2983  }
2984  if ($set) {
2985  $res['value'] = $value;
2986  }
2987  return $res;
2988  }
2989 
3000  protected function ‪validateValueForRequired(array $tcaFieldConfig, mixed $value): bool
3001  {
3002  if (!isset($tcaFieldConfig['required']) || !$tcaFieldConfig['required']) {
3003  return true;
3004  }
3005  return !empty($value) || $value === '0';
3006  }
3007 
3020  public function ‪checkValue_category_processDBdata(
3021  array $valueArray,
3022  array $tcaFieldConf,
3023  $id,
3024  string $status,
3025  string $table,
3026  string $field
3027  ): array {
3028  $newRelations = implode(',', $valueArray);
3029  $relationHandler = $this->‪createRelationHandlerInstance();
3030  $relationHandler->start($newRelations, $tcaFieldConf['foreign_table'], '', 0, $table, $tcaFieldConf);
3031  if ($tcaFieldConf['MM'] ?? false) {
3032  $relationHandler->convertItemArray();
3033  if ($status === 'update') {
3034  $relationHandleForOldRelations = $this->‪createRelationHandlerInstance();
3035  $relationHandleForOldRelations->start('', $tcaFieldConf['foreign_table'], $tcaFieldConf['MM'], $id, $table, $tcaFieldConf);
3036  $oldRelations = implode(',', $relationHandleForOldRelations->getValueArray());
3037  $relationHandler->writeMM($tcaFieldConf['MM'], $id);
3038  if ($oldRelations !== $newRelations) {
3039  $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$field] = $oldRelations;
3040  $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$field] = $newRelations;
3041  } else {
3042  $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$field] = '';
3043  $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$field] = '';
3044  }
3045  } else {
3046  $this->dbAnalysisStore[] = [$relationHandler, $tcaFieldConf['MM'], $id, '', $table];
3047  }
3048  $valueArray = $relationHandler->countItems();
3049  } else {
3050  $valueArray = $relationHandler->getValueArray();
3051  }
3052  return $valueArray;
3053  }
3054 
3068  public function ‪checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
3069  {
3070  $tables = $type === 'group' ? $tcaFieldConf['allowed'] : $tcaFieldConf['foreign_table'];
3071  $prep = $type === 'group' ? ($tcaFieldConf['prepend_tname'] ?? '') : '';
3072  $newRelations = implode(',', $valueArray);
3073  $dbAnalysis = $this->‪createRelationHandlerInstance();
3074  $dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
3075  $dbAnalysis->start($newRelations, $tables, '', 0, $currentTable, $tcaFieldConf);
3076  if ($tcaFieldConf['MM'] ?? false) {
3077  // convert submitted items to use version ids instead of live ids
3078  // (only required for MM relations in a workspace context)
3079  $dbAnalysis->convertItemArray();
3080  if ($status === 'update') {
3081  $oldRelations_dbAnalysis = $this->‪createRelationHandlerInstance();
3082  $oldRelations_dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
3083  // Db analysis with $id will initialize with the existing relations
3084  $oldRelations_dbAnalysis->start('', $tables, $tcaFieldConf['MM'], $id, $currentTable, $tcaFieldConf);
3085  $oldRelations = implode(',', $oldRelations_dbAnalysis->getValueArray());
3086  $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, $prep);
3087  if ($oldRelations != $newRelations) {
3088  $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
3089  $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
3090  } else {
3091  $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
3092  $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
3093  }
3094  } else {
3095  $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
3096  }
3097  $valueArray = $dbAnalysis->countItems();
3098  } else {
3099  $valueArray = $dbAnalysis->getValueArray($prep);
3100  }
3101  // Here we should see if 1) the records exist anymore, 2) which are new and check if the BE_USER has read-access to the new ones.
3102  return $valueArray;
3103  }
3104 
3112  public function ‪checkValue_group_select_explodeSelectGroupValue($value): array
3113  {
3114  $valueArray = ‪GeneralUtility::trimExplode(',', $value, true);
3115  foreach ($valueArray as &$newVal) {
3116  $temp = explode('|', $newVal, 2);
3117  $newVal = str_replace(['|', ','], '', rawurldecode($temp[0]));
3118  }
3119  unset($newVal);
3120  return $valueArray;
3121  }
3122 
3137  public function ‪checkValue_flex_procInData($dataPart, $dataPart_current, $dataStructure, $pParams, $callBackFunc = '', array $workspaceOptions = [])
3138  {
3139  if (is_array($dataPart)) {
3140  foreach ($dataPart as $sKey => $sheetDef) {
3141  if (isset($dataStructure['sheets'][$sKey]) && is_array($dataStructure['sheets'][$sKey]) && is_array($sheetDef)) {
3142  foreach ($sheetDef as $lKey => $lData) {
3144  $dataPart[$sKey][$lKey],
3145  $dataPart_current[$sKey][$lKey] ?? null,
3146  $dataStructure['sheets'][$sKey]['ROOT']['el'] ?? null,
3147  $pParams,
3148  $callBackFunc,
3149  $sKey . '/' . $lKey . '/',
3150  $workspaceOptions
3151  );
3152  }
3153  }
3154  }
3155  }
3156  return $dataPart;
3157  }
3158 
3172  public function ‪checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions = []): void
3173  {
3174  if (!is_array($DSelements)) {
3175  return;
3176  }
3177 
3178  // For each DS element:
3179  foreach ($DSelements as $key => $dsConf) {
3180  // Array/Section:
3181  if (isset($DSelements[$key]['type']) && $DSelements[$key]['type'] === 'array') {
3182  if (!is_array($dataValues[$key]['el'] ?? null)) {
3183  continue;
3184  }
3185 
3186  if ($DSelements[$key]['section']) {
3187  foreach ($dataValues[$key]['el'] as $ik => $el) {
3188  if (!is_array($el)) {
3189  continue;
3190  }
3191 
3192  if (!is_array($dataValues_current[$key]['el'] ?? false)) {
3193  $dataValues_current[$key]['el'] = [];
3194  }
3195  $theKey = key($el);
3196  if (!is_array($dataValues[$key]['el'][$ik][$theKey]['el'] ?? false)) {
3197  continue;
3198  }
3199 
3201  $dataValues[$key]['el'][$ik][$theKey]['el'],
3202  $dataValues_current[$key]['el'][$ik][$theKey]['el'] ?? [],
3203  $DSelements[$key]['el'][$theKey]['el'] ?? [],
3204  $pParams,
3205  $callBackFunc,
3206  $structurePath . $key . '/el/' . $ik . '/' . $theKey . '/el/',
3207  $workspaceOptions
3208  );
3209  }
3210  } else {
3211  if (!isset($dataValues[$key]['el'])) {
3212  $dataValues[$key]['el'] = [];
3213  }
3214  $this->‪checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/', $workspaceOptions);
3215  }
3216  } else {
3217  $fieldConfiguration = $dsConf['config'] ?? null;
3218  // init with value from config for passthrough fields
3219  if (!empty($fieldConfiguration['type']) && $fieldConfiguration['type'] === 'passthrough') {
3220  if (!empty($dataValues_current[$key]['vDEF'])) {
3221  // If there is existing value, keep it
3222  $dataValues[$key]['vDEF'] = $dataValues_current[$key]['vDEF'];
3223  } elseif (
3224  !empty($fieldConfiguration['default'])
3225  && isset($pParams[1])
3227  ) {
3228  // If is new record and a default is specified for field, use it.
3229  $dataValues[$key]['vDEF'] = $fieldConfiguration['default'];
3230  }
3231  }
3232  if (!is_array($fieldConfiguration) || !isset($dataValues[$key]) || !is_array($dataValues[$key])) {
3233  continue;
3234  }
3235 
3236  foreach ($dataValues[$key] as $vKey => $data) {
3237  if ($callBackFunc) {
3238  if (is_object($this->callBackObj)) {
3239  $res = $this->callBackObj->{$callBackFunc}(
3240  $pParams,
3241  $fieldConfiguration,
3242  $dataValues[$key][$vKey] ?? null,
3243  $dataValues_current[$key][$vKey] ?? null,
3244  $structurePath . $key . '/' . $vKey . '/',
3245  $workspaceOptions
3246  );
3247  } else {
3248  $res = $this->{$callBackFunc}(
3249  $pParams,
3250  $fieldConfiguration,
3251  $dataValues[$key][$vKey] ?? null,
3252  $dataValues_current[$key][$vKey] ?? null,
3253  $structurePath . $key . '/' . $vKey . '/',
3254  $workspaceOptions
3255  );
3256  }
3257  } else {
3258  // Default
3259  [$CVtable, $CVid, $CVcurValue, $CVstatus, $CVrealPid, $CVrecFID, $CVtscPID] = $pParams;
3260 
3261  $additionalData = [
3262  'flexFormId' => $CVrecFID,
3263  'flexFormPath' => trim(rtrim($structurePath, '/') . '/' . $key . '/' . $vKey, '/'),
3264  ];
3265 
3266  $res = $this->‪checkValue_SW(
3267  [],
3268  $dataValues[$key][$vKey] ?? null,
3269  $fieldConfiguration,
3270  $CVtable,
3271  $CVid,
3272  $dataValues_current[$key][$vKey] ?? null,
3273  $CVstatus,
3274  $CVrealPid,
3275  $CVrecFID,
3276  '',
3277  $CVtscPID,
3278  $additionalData
3279  );
3280  }
3281  // Adding the value:
3282  if (isset($res['value'])) {
3283  $dataValues[$key][$vKey] = $res['value'];
3284  }
3285  }
3286  }
3287  }
3288  }
3289 
3301  protected function ‪checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field)
3302  {
3303  $foreignTable = $tcaFieldConf['foreign_table'];
3304  $valueArray = $this->‪applyFiltersToValues($tcaFieldConf, $valueArray);
3305  // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3306  $dbAnalysis = $this->‪createRelationHandlerInstance();
3307  $dbAnalysis->start(implode(',', $valueArray), $foreignTable, '', 0, $table, $tcaFieldConf);
3308  // IRRE with a pointer field (database normalization):
3309  if ($tcaFieldConf['foreign_field'] ?? false) {
3310  // update record in intermediate table (sorting & pointer uid to parent record)
3311  $dbAnalysis->writeForeignField($tcaFieldConf, $id, 0);
3312  $newValue = $dbAnalysis->countItems(false);
3313  } elseif ($this->‪getRelationFieldType($tcaFieldConf) === 'mm') {
3314  // In order to fully support all the MM stuff, directly call checkValue_group_select_processDBdata instead of repeating the needed code here
3315  $valueArray = $this->‪checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
3316  $newValue = $valueArray[0];
3317  } else {
3318  $valueArray = $dbAnalysis->getValueArray();
3319  // Checking that the number of items is correct:
3320  $valueArray = $this->‪checkValue_checkMax($tcaFieldConf, $valueArray);
3321  $newValue = $this->‪castReferenceValue(implode(',', $valueArray), $tcaFieldConf, ($status === 'new'));
3322  }
3323  return $newValue;
3324  }
3325 
3329  protected function ‪checkValue_file_processDBdata($valueArray, $tcaFieldConf, $id, $table): mixed
3330  {
3331  $valueArray = GeneralUtility::makeInstance(FileExtensionFilter::class)->filter(
3332  $valueArray,
3333  (string)($tcaFieldConf['allowed'] ?? ''),
3334  (string)($tcaFieldConf['disallowed'] ?? ''),
3335  $this
3336  );
3337 
3338  $dbAnalysis = $this->‪createRelationHandlerInstance();
3339  $dbAnalysis->start(implode(',', $valueArray), $tcaFieldConf['foreign_table'], '', 0, $table, $tcaFieldConf);
3340  $dbAnalysis->writeForeignField($tcaFieldConf, $id);
3341  return $dbAnalysis->countItems(false);
3342  }
3343 
3344  /*********************************************
3345  *
3346  * PROCESSING COMMANDS
3347  *
3348  ********************************************/
3355  public function ‪process_cmdmap()
3356  {
3357  // Editing frozen:
3358  if ($this->BE_USER->workspace !== 0 && ($this->BE_USER->workspaceRec['freeze'] ?? false)) {
3359  $this->‪log('sys_workspace', $this->BE_USER->workspace, SystemLogDatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'All editing in this workspace has been frozen');
3360  return false;
3361  }
3362  // Hook initialization:
3363  $hookObjectsArr = [];
3364  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
3365  $hookObj = GeneralUtility::makeInstance($className);
3366  if (method_exists($hookObj, 'processCmdmap_beforeStart')) {
3367  $hookObj->processCmdmap_beforeStart($this);
3368  }
3369  $hookObjectsArr[] = $hookObj;
3370  }
3371  $pasteDatamap = [];
3372  // Traverse command map:
3373  foreach ($this->cmdmap as $table => $idCommandArray) {
3374  // Check if the table may be modified!
3375  $modifyAccessList = $this->‪checkModifyAccessList($table);
3376  if (!$modifyAccessList) {
3377  $this->‪log($table, 0, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify table "{table}" without permission', 1, ['table' => $table]);
3378  }
3379  // Check basic permissions and circumstances:
3380  if (!isset(‪$GLOBALS['TCA'][$table]) || $this->‪tableReadOnly($table) || !$modifyAccessList) {
3381  continue;
3382  }
3383 
3384  // Traverse the command map:
3385  foreach ($idCommandArray as $id => $incomingCmdArray) {
3386  if (!is_array($incomingCmdArray)) {
3387  continue;
3388  }
3389 
3390  if ($table === 'pages') {
3391  // for commands on pages do a pagetree-refresh
3392  $this->pagetreeNeedsRefresh = true;
3393  }
3394 
3395  foreach ($incomingCmdArray as $command => $value) {
3396  $pasteUpdate = false;
3397  if (is_array($value) && isset($value['action']) && $value['action'] === 'paste') {
3398  // Extended paste command: $command is set to "move" or "copy"
3399  // $value['update'] holds field/value pairs which should be updated after copy/move operation
3400  // $value['target'] holds original $value (target of move/copy)
3401  $pasteUpdate = $value['update'];
3402  $value = $value['target'];
3403  }
3404  foreach ($hookObjectsArr as $hookObj) {
3405  if (method_exists($hookObj, 'processCmdmap_preProcess')) {
3406  $hookObj->processCmdmap_preProcess($command, $table, $id, $value, $this, $pasteUpdate);
3407  }
3408  }
3409  // Init copyMapping array:
3410  // Must clear this array before call from here to those functions:
3411  // Contains mapping information between new and old id numbers.
3412  $this->copyMappingArray = [];
3413  // process the command
3414  $commandIsProcessed = false;
3415  foreach ($hookObjectsArr as $hookObj) {
3416  if (method_exists($hookObj, 'processCmdmap')) {
3418  $hookObj->processCmdmap($command, $table, $id, $value, $commandIsProcessed, $this, $pasteUpdate);
3419  }
3420  }
3421  // Only execute default commands if a hook hasn't been processed the command already
3422  if (!$commandIsProcessed) {
3423  $procId = $id;
3424  $backupUseTransOrigPointerField = ‪$this->useTransOrigPointerField;
3425  // Branch, based on command
3426  switch ($command) {
3427  case 'move':
3428  $this->‪moveRecord($table, (int)$id, $value);
3429  break;
3430  case 'copy':
3431  $target = $value['target'] ?? $value;
3432  $ignoreLocalization = (bool)($value['ignoreLocalization'] ?? false);
3433  if ($table === 'pages') {
3434  $this->‪copyPages((int)$id, $target);
3435  } else {
3436  $this->‪copyRecord($table, (int)$id, $target, true, [], '', 0, $ignoreLocalization);
3437  }
3438  $procId = $this->copyMappingArray[$table][$id] ?? null;
3439  break;
3440  case 'localize':
3441  $this->useTransOrigPointerField = true;
3442  $this->‪localize($table, (int)$id, $value);
3443  break;
3444  case 'copyToLanguage':
3445  $this->useTransOrigPointerField = false;
3446  $this->‪localize($table, (int)$id, $value);
3447  break;
3448  case 'inlineLocalizeSynchronize':
3449  $this->‪inlineLocalizeSynchronize($table, (int)$id, $value);
3450  break;
3451  case 'delete':
3452  $this->‪deleteAction($table, (int)$id);
3453  break;
3454  case 'undelete':
3455  $this->‪undeleteRecord((string)$table, (int)$id);
3456  break;
3457  }
3458  $this->useTransOrigPointerField = $backupUseTransOrigPointerField;
3459  if (is_array($pasteUpdate) && $procId > 0) {
3460  $pasteDatamap[$table][$procId] = $pasteUpdate;
3461  }
3462  }
3463  foreach ($hookObjectsArr as $hookObj) {
3464  if (method_exists($hookObj, 'processCmdmap_postProcess')) {
3465  $hookObj->processCmdmap_postProcess($command, $table, $id, $value, $this, $pasteUpdate, $pasteDatamap);
3466  }
3467  }
3468  // Merging the copy-array info together for remapping purposes.
3469  ArrayUtility::mergeRecursiveWithOverrule($this->copyMappingArray_merged, $this->copyMappingArray);
3470  }
3471  }
3472  }
3473  $copyTCE = $this->‪getLocalTCE();
3474  $copyTCE->start($pasteDatamap, [], $this->BE_USER);
3475  $copyTCE->process_datamap();
3476  $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3477  unset($copyTCE);
3478 
3479  // Finally, before exit, check if there are ID references to remap.
3480  // This might be the case if versioning or copying has taken place!
3481  $this->‪remapListedDBRecords();
3482  $this->‪processRemapStack();
3483  foreach ($hookObjectsArr as $hookObj) {
3484  if (method_exists($hookObj, 'processCmdmap_afterFinish')) {
3485  $hookObj->processCmdmap_afterFinish($this);
3486  }
3487  }
3488  if ($this->‪isOuterMostInstance()) {
3489  $this->referenceIndexUpdater->update();
3490  $this->‪processClearCacheQueue();
3491  $this->‪resetNestedElementCalls();
3492  }
3493  }
3494 
3495  /*********************************************
3496  *
3497  * Cmd: Copying
3498  *
3499  ********************************************/
3514  public function ‪copyRecord($table, ‪$uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '', $language = 0, $ignoreLocalization = false)
3515  {
3516  ‪$uid = ($origUid = (int)‪$uid);
3517  // Only copy if the table is defined in $GLOBALS['TCA'], a uid is given and the record wasn't copied before:
3518  if (empty(‪$GLOBALS['TCA'][$table]) || ‪$uid === 0) {
3519  return null;
3520  }
3521  if ($this->‪isRecordCopied($table, ‪$uid)) {
3522  return null;
3523  }
3524 
3525  // Fetch record with permission check
3527 
3528  // This checks if the record can be selected which is all that a copy action requires.
3529  if ($row === false) {
3530  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy record "{table}:{uid}" which does not exist or you do not have permission to read', -1, ['table' => $table, 'uid' => ‪$uid]);
3531  return null;
3532  }
3533 
3534  // NOT using \TYPO3\CMS\Backend\Utility\BackendUtility::getTSCpid() because we need the real pid - not the ID of a page, if the input is a page...
3535  $tscPID = (int)BackendUtility::getTSconfig_pidValue($table, ‪$uid, $destPid);
3536 
3537  // Check if table is allowed on destination page
3538  if (!$this->‪isTableAllowedForThisPage($tscPID, $table)) {
3539  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to insert record "{table}:{uid}" on a page ({pid}) that can\'t store record type', -1, ['table' => $table, 'uid' => ‪$uid, 'pid' => $tscPID]);
3540  return null;
3541  }
3542 
3543  $fullLanguageCheckNeeded = $table !== 'pages';
3544  // Used to check language and general editing rights
3545  if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, ‪$uid, false, false, $fullLanguageCheckNeeded)) {
3546  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy record "{table}:{uid}" without having permissions to do so [{reason}]', -1, ['table' => $table, 'uid' => ‪$uid, 'reason' => $this->BE_USER->errorMsg]);
3547  return null;
3548  }
3549 
3550  $data = [];
3551  $nonFields = array_unique(‪GeneralUtility::trimExplode(',', 'uid,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,t3ver_oid,t3ver_wsid,t3ver_state,t3ver_stage,' . $excludeFields, true));
3552  BackendUtility::workspaceOL($table, $row, $this->BE_USER->workspace);
3553  $row = BackendUtility::purgeComputedPropertiesFromRecord($row);
3554 
3555  // Initializing:
3556  $theNewID = ‪StringUtility::getUniqueId('NEW');
3557  $enableField = ‪$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] ?? '';
3558  $headerField = ‪$GLOBALS['TCA'][$table]['ctrl']['label'];
3559  // Getting "copy-after" fields if applicable:
3560  $copyAfterFields = $destPid < 0 ? $this->‪fixCopyAfterDuplFields((string)$table, (int)abs($destPid)) : [];
3561  // Page TSconfig related:
3562  $TSConfig = BackendUtility::getPagesTSconfig($tscPID)['TCEMAIN.'] ?? [];
3563  $tE = $this->‪getTableEntries($table, $TSConfig);
3564  // Traverse ALL fields of the selected record:
3565  foreach ($row as $field => $value) {
3566  if (!in_array($field, $nonFields, true)) {
3567  // Get TCA configuration for the field:
3568  $conf = ‪$GLOBALS['TCA'][$table]['columns'][$field]['config'] ?? [];
3569  // Preparation/Processing of the value:
3570  // "pid" is hardcoded of course:
3571  // isset() won't work here, since values can be NULL in each of the arrays
3572  // except setDefaultOnCopyArray, since we exploded that from a string
3573  if ($field === 'pid') {
3574  $value = $destPid;
3575  } elseif (array_key_exists($field, $overrideValues)) {
3576  // Override value...
3577  $value = $overrideValues[$field];
3578  } elseif (array_key_exists($field, $copyAfterFields)) {
3579  // Copy-after value if available:
3580  $value = $copyAfterFields[$field];
3581  } else {
3582  // Hide at copy may override:
3583  if ($first && $field == $enableField
3584  && (‪$GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] ?? false)
3585  && !$this->neverHideAtCopy
3586  && !($tE['disableHideAtCopy'] ?? false)
3587  ) {
3588  $value = 1;
3589  }
3590  // Prepend label on copy:
3591  if ($first && $field == $headerField
3592  && (‪$GLOBALS['TCA'][$table]['ctrl']['prependAtCopy'] ?? false)
3593  && !($tE['disablePrependAtCopy'] ?? false)
3594  ) {
3595  $value = $this->‪getCopyHeader($table, $this->‪resolvePid($table, $destPid), $field, $this->‪clearPrefixFromValue($table, $value), 0);
3596  }
3597  // Processing based on the TCA config field type (files, references, flexforms...)
3598  $value = $this->‪copyRecord_procBasedOnFieldType($table, ‪$uid, $field, $value, $row, $conf, $tscPID, $language);
3599  }
3600  // Add value to array.
3601  $data[$table][$theNewID][$field] = $value;
3602  }
3603  }
3604  // Overriding values:
3605  if (‪$GLOBALS['TCA'][$table]['ctrl']['editlock'] ?? false) {
3606  $data[$table][$theNewID][‪$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3607  }
3608  // Setting original UID:
3609  if (‪$GLOBALS['TCA'][$table]['ctrl']['origUid'] ?? false) {
3610  $data[$table][$theNewID][‪$GLOBALS['TCA'][$table]['ctrl']['origUid']] = ‪$uid;
3611  }
3612  // Do the copy by simply submitting the array through DataHandler:
3613  $copyTCE = $this->‪getLocalTCE();
3614  $copyTCE->start($data, [], $this->BE_USER);
3615  $copyTCE->process_datamap();
3616  // Getting the new UID:
3617  $theNewSQLID = $copyTCE->substNEWwithIDs[$theNewID] ?? null;
3618  if ($theNewSQLID) {
3619  $this->copyMappingArray[$table][$origUid] = $theNewSQLID;
3620  // Keep automatically versionized record information:
3621  if (isset($copyTCE->autoVersionIdMap[$table][$theNewSQLID])) {
3622  $this->autoVersionIdMap[$table][$theNewSQLID] = $copyTCE->autoVersionIdMap[$table][$theNewSQLID];
3623  }
3624  }
3625  $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3626  unset($copyTCE);
3627  if (!$ignoreLocalization && $language == 0) {
3628  //repointing the new translation records to the parent record we just created
3629  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
3630  $overrideValues[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
3631  }
3632  // This value is evaluated in DataMapItem->getType() so it is very important
3633  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3634  $overrideValues[‪$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = 0;
3635  }
3636  $this->‪copyL10nOverlayRecords($table, ‪$uid, $destPid, $first, $overrideValues, $excludeFields);
3637  }
3638 
3639  return $theNewSQLID;
3640  }
3641 
3650  public function ‪copyPages(‪$uid, $destPid): void
3651  {
3652  // Initialize:
3653  ‪$uid = (int)‪$uid;
3654  $destPid = (int)$destPid;
3655 
3656  $copyTablesAlongWithPage = $this->‪getAllowedTablesToCopyWhenCopyingAPage();
3657  // Begin to copy pages if we're allowed to:
3658  if ($this->admin || in_array('pages', $copyTablesAlongWithPage, true)) {
3659  // Copy this page we're on. And set first-flag (this will trigger that the record is hidden if that is configured)
3660  // This method also copies the localizations of a page
3661  $theNewRootID = $this->‪copySpecificPage($uid, $destPid, $copyTablesAlongWithPage, true);
3662  // If we're going to copy recursively
3663  if ($theNewRootID && $this->copyTree) {
3664  // Get ALL subpages to copy (read-permissions are respected!):
3665  $CPtable = $this->‪int_pageTreeInfo([], $uid, (int)$this->copyTree, $theNewRootID);
3666  // Now copying the subpages:
3667  foreach ($CPtable as $thePageUid => $thePagePid) {
3668  $newPid = $this->copyMappingArray['pages'][$thePagePid] ?? null;
3669  if (isset($newPid)) {
3670  $this->‪copySpecificPage($thePageUid, $newPid, $copyTablesAlongWithPage);
3671  } else {
3672  $this->‪log('pages', $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Something went wrong during copying branch');
3673  break;
3674  }
3675  }
3676  }
3677  } else {
3678  $this->‪log('pages', $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy page {uid} without permission to this table', -1, ['uid' => ‪$uid]);
3679  }
3680  }
3681 
3689  protected function ‪getAllowedTablesToCopyWhenCopyingAPage(): array
3690  {
3691  // Finding list of tables to copy.
3692  // These are the tables, the user may modify
3693  $copyTablesArray = $this->admin ? $this->‪compileAdminTables() : explode(',', $this->BE_USER->groupData['tables_modify']);
3694  // If not all tables are allowed then make a list of allowed tables.
3695  // That is the tables that figure in both allowed tables AND the copyTable-list
3696  if (!str_contains($this->copyWhichTables, '*')) {
3697  $definedTablesToCopy = ‪GeneralUtility::trimExplode(',', $this->copyWhichTables, true);
3698  // Pages are always allowed
3699  $definedTablesToCopy[] = 'pages';
3700  $definedTablesToCopy = array_flip($definedTablesToCopy);
3701  foreach ($copyTablesArray as $k => $table) {
3702  if (!$table || !isset($definedTablesToCopy[$table])) {
3703  unset($copyTablesArray[$k]);
3704  }
3705  }
3706  }
3707  $copyTablesArray = array_unique($copyTablesArray);
3708  return $copyTablesArray;
3709  }
3720  public function ‪copySpecificPage(‪$uid, $destPid, $copyTablesArray, $first = false)
3721  {
3722  // Copy the page itself:
3723  $theNewRootID = $this->‪copyRecord('pages', $uid, $destPid, $first);
3724  $currentWorkspaceId = (int)$this->BE_USER->workspace;
3725  // If a new page was created upon the copy operation we will proceed with all the tables ON that page:
3726  ‪if ($theNewRootID) {
3727  foreach ($copyTablesArray as $table) {
3728  // All records under the page is copied.
3729  if ($table && is_array(‪$GLOBALS['TCA'][$table] ?? false) && $table !== 'pages') {
3730  ‪$fields = ['uid'];
3731  $languageField = null;
3732  $transOrigPointerField = null;
3733  $translationSourceField = null;
3734  if (BackendUtility::isTableLocalizable($table)) {
3735  $languageField = ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'];
3736  $transOrigPointerField = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3737  ‪$fields[] = $languageField;
3738  ‪$fields[] = $transOrigPointerField;
3739  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3740  $translationSourceField = ‪$GLOBALS['TCA'][$table]['ctrl']['translationSource'];
3741  ‪$fields[] = $translationSourceField;
3742  }
3743  }
3744  $isTableWorkspaceEnabled = BackendUtility::isTableWorkspaceEnabled($table);
3745  if ($isTableWorkspaceEnabled) {
3746  ‪$fields[] = 't3ver_oid';
3747  ‪$fields[] = 't3ver_state';
3748  ‪$fields[] = 't3ver_wsid';
3749  }
3750  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
3751  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
3752  $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $currentWorkspaceId));
3753  $queryBuilder
3754  ->select(...‪$fields)
3755  ->from($table)
3756  ->where(
3757  $queryBuilder->expr()->eq(
3758  'pid',
3759  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
3760  )
3761  );
3762  if (!empty(‪$GLOBALS['TCA'][$table]['ctrl']['sortby'])) {
3763  $queryBuilder->orderBy(‪$GLOBALS['TCA'][$table]['ctrl']['sortby'], 'DESC');
3764  }
3765  $queryBuilder->addOrderBy('uid');
3766  try {
3767  $result = $queryBuilder->executeQuery();
3768  $rows = [];
3769  $movedLiveIds = [];
3770  $movedLiveRecords = [];
3771  while ($row = $result->fetchAssociative()) {
3772  if ($isTableWorkspaceEnabled && VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER) {
3773  $movedLiveIds[(int)$row['t3ver_oid']] = (int)$row['uid'];
3774  }
3775  $rows[(int)$row['uid']] = $row;
3776  }
3777  // Resolve placeholders of workspace versions
3778  if (!empty($rows) && $currentWorkspaceId > 0 && $isTableWorkspaceEnabled) {
3779  // If a record was moved within the page, the PlainDataResolver needs the moved record
3780  // but not the original live version, otherwise the moved record is not considered at all.
3781  // For this reason, we find the live ids, where there was also a moved record in the SQL
3782  // query above in $movedLiveIds and now we removed them before handing them over to PlainDataResolver.
3783  // see changeContentSortingAndCopyDraftPage test
3784  foreach ($movedLiveIds as $liveId => $movePlaceHolderId) {
3785  if (isset($rows[$liveId])) {
3786  $movedLiveRecords[$movePlaceHolderId] = $rows[$liveId];
3787  unset($rows[$liveId]);
3788  }
3789  }
3790  $rows = array_reverse(
3792  $table,
3793  implode(',', ‪$fields),
3794  ‪$GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '',
3795  array_keys($rows)
3796  ),
3797  true
3798  );
3799  foreach ($movedLiveRecords as $movePlaceHolderId => $liveRecord) {
3800  $rows[$movePlaceHolderId] = $liveRecord;
3801  }
3802  }
3803  if (is_array($rows)) {
3804  $languageSourceMap = [];
3805  $overrideValues = $translationSourceField ? [$translationSourceField => 0] : [];
3806  $doRemap = false;
3807  foreach ($rows as $row) {
3808  // Skip localized records that will be processed in
3809  // copyL10nOverlayRecords() on copying the default language record
3810  $transOrigPointer = $row[$transOrigPointerField] ?? 0;
3811  if (!empty($languageField)
3812  && $row[$languageField] > 0
3813  && $transOrigPointer > 0
3814  && (isset($rows[$transOrigPointer]) || isset($movedLiveIds[$transOrigPointer]))
3815  ) {
3816  continue;
3817  }
3818  // Copying each of the underlying records...
3819  $newUid = $this->‪copyRecord($table, $row['uid'], $theNewRootID, false, $overrideValues);
3820  if ($translationSourceField) {
3821  $languageSourceMap[$row['uid']] = $newUid;
3822  if ($row[$languageField] > 0) {
3823  $doRemap = true;
3824  }
3825  }
3826  }
3827  if ($doRemap) {
3828  //remap is needed for records in non-default language records in the "free mode"
3829  $this->‪copy_remapTranslationSourceField($table, $rows, $languageSourceMap);
3830  }
3831  }
3832  } catch (DBALException $e) {
3833  $databaseErrorMessage = $e->getPrevious()->getMessage();
3834  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'An SQL error occurred: {reason}', -1, ['reason' => $databaseErrorMessage]);
3835  }
3836  }
3837  }
3838  $this->‪processRemapStack();
3839  return $theNewRootID;
3840  }
3841  return null;
3842  }
3843 
3860  public function ‪copyRecord_raw($table, ‪$uid, $pid, $overrideArray = [], array $workspaceOptions = [])
3861  {
3862  ‪$uid = (int)‪$uid;
3863  // Stop any actions if the record is marked to be deleted:
3864  // (this can occur if IRRE elements are versionized and child elements are removed)
3865  if ($this->‪isElementToBeDeleted($table, ‪$uid)) {
3866  return null;
3867  }
3868  // Only copy if the table is defined in TCA, a uid is given and the record wasn't copied before:
3869  if (!‪$GLOBALS['TCA'][$table] || !‪$uid || $this->‪isRecordCopied($table, ‪$uid)) {
3870  return null;
3871  }
3872 
3873  // Fetch record with permission check
3875 
3876  // This checks if the record can be selected which is all that a copy action requires.
3877  if ($row === false) {
3878  $this->‪log(
3879  $table,
3880  ‪$uid,
3881  SystemLogDatabaseAction::INSERT,
3882  0,
3883  SystemLogErrorClassification::USER_ERROR,
3884  'Attempt to rawcopy/versionize record which either does not exist or you don\'t have permission to read'
3885  );
3886  return null;
3887  }
3888 
3889  // Set up fields which should not be processed. They are still written - just passed through no-questions-asked!
3890  $nonFields = ['uid', 'pid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody'];
3891 
3892  // Merge in override array.
3893  $row = array_merge($row, $overrideArray);
3894  // Traverse ALL fields of the selected record:
3895  foreach ($row as $field => $value) {
3897  if (!in_array($field, $nonFields, true)) {
3898  // Get TCA configuration for the field:
3899  $conf = ‪$GLOBALS['TCA'][$table]['columns'][$field]['config'] ?? false;
3900  if (is_array($conf)) {
3901  // Processing based on the TCA config field type (files, references, flexforms...)
3902  $value = $this->‪copyRecord_procBasedOnFieldType($table, ‪$uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3903  }
3904  // Add value to array.
3905  $row[$field] = $value;
3906  }
3907  }
3908  $row['pid'] = $pid;
3909  // Setting original UID:
3910  if (‪$GLOBALS['TCA'][$table]['ctrl']['origUid'] ?? '') {
3911  $row[‪$GLOBALS['TCA'][$table]['ctrl']['origUid']] = ‪$uid;
3912  }
3913  // Do the copy by internal function
3914  $theNewSQLID = $this->‪insertNewCopyVersion($table, $row, $pid);
3915 
3916  // When a record is copied in workspace (eg. to create a delete placeholder record for a live record), records
3917  // pointing to that record need a reference index update. This is for instance the case in FAL, if a sys_file_reference
3918  // that refers e.g. to a tt_content record is marked as deleted. The tt_content record then needs a reference index update.
3919  // This scenario seems to currently only show up if in workspaces, so the refindex update is restricted to this for now.
3920  if (!empty($workspaceOptions)) {
3921  $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, (int)$row['uid'], (int)$this->BE_USER->workspace);
3922  }
3923 
3924  if ($theNewSQLID) {
3925  $this->‪dbAnalysisStoreExec();
3926  $this->dbAnalysisStore = [];
3927  return $this->copyMappingArray[$table][‪$uid] = $theNewSQLID;
3928  }
3929  return null;
3930  }
3931 
3942  public function ‪insertNewCopyVersion($table, $fieldArray, $realPid)
3943  {
3944  $id = ‪StringUtility::getUniqueId('NEW');
3945  // $fieldArray is set as current record.
3946  // The point is that when new records are created as copies with flex type fields there might be a field containing information about which DataStructure to use and without that information the flexforms cannot be correctly processed.... This should be OK since the $checkValueRecord is used by the flexform evaluation only anyways...
3947  $this->checkValue_currentRecord = $fieldArray;
3948  // Makes sure that transformations aren't processed on the copy.
3949  $backupDontProcessTransformations = ‪$this->dontProcessTransformations;
3950  $this->dontProcessTransformations = true;
3951  // Traverse record and input-process each value:
3952  foreach ($fieldArray as $field => $fieldValue) {
3953  if (isset(‪$GLOBALS['TCA'][$table]['columns'][$field])) {
3954  // Evaluating the value.
3955  $res = $this->‪checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0, $fieldArray);
3956  if (isset($res['value'])) {
3957  $fieldArray[$field] = $res['value'];
3958  }
3959  }
3960  }
3961  // System fields being set:
3962  if (‪$GLOBALS['TCA'][$table]['ctrl']['crdate'] ?? false) {
3963  $fieldArray[‪$GLOBALS['TCA'][$table]['ctrl']['crdate']] = ‪$GLOBALS['EXEC_TIME'];
3964  }
3965  if (‪$GLOBALS['TCA'][$table]['ctrl']['tstamp'] ?? false) {
3966  $fieldArray[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = ‪$GLOBALS['EXEC_TIME'];
3967  }
3968  // Finally, insert record:
3969  $this->‪insertDB($table, $id, $fieldArray, BackendUtility::isTableWorkspaceEnabled($table));
3970  // Resets dontProcessTransformations to the previous state.
3971  $this->dontProcessTransformations = $backupDontProcessTransformations;
3972  // Return new id:
3973  return $this->substNEWwithIDs[$id] ?? null;
3974  }
3975 
3992  public function ‪copyRecord_procBasedOnFieldType($table, ‪$uid, $field, $value, $row, $conf, $realDestPid, $language = 0, array $workspaceOptions = [])
3993  {
3994  $relationFieldType = $this->‪getRelationFieldType($conf);
3995  // Get the localization mode for the current (parent) record (keep|select):
3996  // Register if there are references to take care of or MM is used on an inline field (no change to value):
3997  if ($this->‪isReferenceField($conf) || $relationFieldType === 'mm') {
3998  $value = $this->‪copyRecord_processManyToMany($table, ‪$uid, $field, $value, $conf, $language);
3999  } elseif ($relationFieldType !== false) {
4000  $value = $this->‪copyRecord_processRelation($table, ‪$uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions);
4001  }
4002  // For "flex" fieldtypes we need to traverse the structure for two reasons: If there are file references they have to be prepended with absolute paths and if there are database reference they MIGHT need to be remapped (still done in remapListedDBRecords())
4003  if (isset($conf['type']) && $conf['type'] === 'flex') {
4004  // Get current value array:
4005  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
4006  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
4007  ['config' => $conf],
4008  $table,
4009  $field,
4010  $row
4011  );
4012  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
4013  $currentValue = is_string($value) ? ‪GeneralUtility::xml2array($value) : null;
4014  // Traversing the XML structure, processing files:
4015  if (is_array($currentValue)) {
4016  $currentValue['data'] = $this->‪checkValue_flex_procInData($currentValue['data'] ?? [], [], $dataStructureArray, [$table, ‪$uid, $field, $realDestPid], 'copyRecord_flexFormCallBack', $workspaceOptions);
4017  // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
4018  $value = $currentValue;
4019  }
4020  }
4021  return $value;
4022  }
4023 
4035  protected function ‪copyRecord_processManyToMany($table, ‪$uid, $field, $value, $conf, $language)
4036  {
4037  $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
4038  $allowedTablesArray = ‪GeneralUtility::trimExplode(',', $allowedTables, true);
4039  $prependName = $conf['type'] === 'group' ? ($conf['prepend_tname'] ?? '') : '';
4040  $mmTable = !empty($conf['MM']) ? $conf['MM'] : '';
4041 
4042  $dbAnalysis = $this->‪createRelationHandlerInstance();
4043  $dbAnalysis->start($value, $allowedTables, $mmTable, ‪$uid, $table, $conf);
4044  $purgeItems = false;
4045 
4046  // Check if referenced records of select or group fields should also be localized in general.
4047  // A further check is done in the loop below for each table name.
4048  if ($language > 0 && $mmTable === '' && !empty($conf['localizeReferencesAtParentLocalization'])) {
4049  // Check whether allowed tables can be localized.
4050  $localizeTables = [];
4051  foreach ($allowedTablesArray as $allowedTable) {
4052  $localizeTables[$allowedTable] = BackendUtility::isTableLocalizable($allowedTable);
4053  }
4054 
4055  foreach ($dbAnalysis->itemArray as $index => $item) {
4056  // No action required, if referenced tables cannot be localized (current value will be used).
4057  if (empty($localizeTables[$item['table']])) {
4058  continue;
4059  }
4060 
4061  // Since select or group fields can reference many records, check whether there's already a localization.
4062  $recordLocalization = BackendUtility::getRecordLocalization($item['table'], $item['id'], $language);
4063  if ($recordLocalization) {
4064  $dbAnalysis->itemArray[$index]['id'] = $recordLocalization[0]['uid'];
4065  } elseif ($this->‪isNestedElementCallRegistered($item['table'], $item['id'], 'localize-' . $language) === false) {
4066  $dbAnalysis->itemArray[$index]['id'] = $this->‪localize($item['table'], $item['id'], $language);
4067  }
4068  }
4069  $purgeItems = true;
4070  }
4071 
4072  if ($purgeItems || $mmTable !== '') {
4073  $dbAnalysis->purgeItemArray();
4074  $value = implode(',', $dbAnalysis->getValueArray($prependName));
4075  }
4076  // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected.
4077  if ($value) {
4078  $this->registerDBList[$table][‪$uid][$field] = $value;
4079  }
4080 
4081  return $value;
4082  }
4083 
4097  protected function ‪copyRecord_processRelation(
4098  $table,
4099  ‪$uid,
4100  $field,
4101  $value,
4102  $row,
4103  $conf,
4104  $realDestPid,
4105  $language,
4106  array $workspaceOptions
4107  ) {
4108  // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
4109  $dbAnalysis = $this->‪createRelationHandlerInstance();
4110  $dbAnalysis->start($value, $conf['foreign_table'], '', ‪$uid, $table, $conf);
4111  // Walk through the items, copy them and remember the new id:
4112  foreach ($dbAnalysis->itemArray as $k => $v) {
4113  $newId = null;
4114  // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
4115  if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[‪$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
4116  // Children should be localized when the parent gets localized the first time, just do it:
4117  $newId = $this->‪localize($v['table'], $v['id'], $language);
4118  } else {
4119  if (!‪MathUtility::canBeInterpretedAsInteger($realDestPid)) {
4120  $newId = $this->‪copyRecord($v['table'], $v['id'], -(int)($v['id']));
4121  // If the destination page id is a NEW string, keep it on the same page
4122  } elseif ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($v['table'])) {
4123  // A filled $workspaceOptions indicated that this call
4124  // has it's origin in previous versionizeRecord() processing
4125  if (!empty($workspaceOptions)) {
4126  // Versions use live default id, thus the "new"
4127  // id is the original live default child record
4128  $newId = $v['id'];
4129  $this->‪versionizeRecord(
4130  $v['table'],
4131  $v['id'],
4132  $workspaceOptions['label'] ?? 'Auto-created for WS #' . $this->BE_USER->workspace,
4133  $workspaceOptions['delete'] ?? false
4134  );
4135  // Otherwise just use plain copyRecord() to create placeholders etc.
4136  } else {
4137  // If a record has been copied already during this request,
4138  // prevent superfluous duplication and use the existing copy
4139  if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
4140  $newId = $this->copyMappingArray[$v['table']][$v['id']];
4141  } else {
4142  $newId = $this->‪copyRecord($v['table'], $v['id'], $realDestPid);
4143  }
4144  }
4145  } elseif ($this->BE_USER->workspace > 0 && !BackendUtility::isTableWorkspaceEnabled($v['table'])) {
4146  // We are in workspace context creating a new parent version and have a child table
4147  // that is not workspace aware. We don't do anything with this child.
4148  continue;
4149  } else {
4150  // If a record has been copied already during this request,
4151  // prevent superfluous duplication and use the existing copy
4152  if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
4153  $newId = $this->copyMappingArray[$v['table']][$v['id']];
4154  } else {
4155  $newId = $this->‪copyRecord_raw($v['table'], $v['id'], $realDestPid, [], $workspaceOptions);
4156  }
4157  }
4158  }
4159  // If the current field is set on a page record, update the pid of related child records:
4160  if ($table === 'pages') {
4161  $this->registerDBPids[$v['table']][$v['id']] = ‪$uid;
4162  } elseif (isset($this->registerDBPids[$table][‪$uid])) {
4163  $this->registerDBPids[$v['table']][$v['id']] = $this->registerDBPids[$table][‪$uid];
4164  }
4165  $dbAnalysis->itemArray[$k]['id'] = $newId;
4166  }
4167  // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4168  $value = implode(',', $dbAnalysis->getValueArray());
4169  $this->registerDBList[$table][‪$uid][$field] = $value;
4170 
4171  return $value;
4172  }
4173 
4188  public function ‪copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $workspaceOptions): array
4189  {
4190  // Extract parameters:
4191  [$table, ‪$uid, $field, $realDestPid] = $pParams;
4192  // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
4193  if (($this->‪isReferenceField($dsConf) || $this->‪getRelationFieldType($dsConf) !== false) && (string)$dataValue !== '') {
4194  $dataValue = $this->‪copyRecord_procBasedOnFieldType($table, ‪$uid, $field, $dataValue, [], $dsConf, $realDestPid, 0, $workspaceOptions);
4195  $this->registerDBList[$table][‪$uid][$field] = 'FlexForm_reference';
4196  }
4197  // Return
4198  return ['value' => $dataValue];
4199  }
4200 
4212  public function ‪copyL10nOverlayRecords($table, ‪$uid, $destPid, $first = false, $overrideValues = [], $excludeFields = ''): void
4213  {
4214  // There's no need to perform this for tables that are not localizable
4215  if (!BackendUtility::isTableLocalizable($table)) {
4216  return;
4217  }
4218 
4219  $languageField = ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? null;
4220  $transOrigPointerField = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null;
4221 
4222  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
4223  $queryBuilder->getRestrictions()->removeAll()
4224  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4225  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
4226 
4227  $queryBuilder->select('*')
4228  ->from($table)
4229  ->where(
4230  $queryBuilder->expr()->eq(
4231  $transOrigPointerField,
4232  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT, ':pointer')
4233  )
4234  );
4235 
4236  // Never copy the actual placeholders around, as the newly copied records are
4237  // always created as new record / new placeholder pairs
4238  if (BackendUtility::isTableWorkspaceEnabled($table)) {
4239  $queryBuilder->andWhere(
4240  $queryBuilder->expr()->neq(
4241  't3ver_state',
4242  VersionState::DELETE_PLACEHOLDER->value
4243  )
4244  );
4245  }
4246 
4247  // If $destPid is < 0, get the pid of the record with uid equal to abs($destPid)
4248  $tscPID = BackendUtility::getTSconfig_pidValue($table, ‪$uid, $destPid) ?? 0;
4249  // Get the localized records to be copied
4250  $l10nRecords = $queryBuilder->executeQuery()->fetchAllAssociative();
4251  if (is_array($l10nRecords)) {
4252  $localizedDestPids = [];
4253  // If $destPid < 0, then it is the uid of the original language record we are inserting after
4254  if ($destPid < 0) {
4255  // Get the localized records of the record we are inserting after
4256  $queryBuilder->setParameter('pointer', abs($destPid), ‪Connection::PARAM_INT);
4257  $destL10nRecords = $queryBuilder->executeQuery()->fetchAllAssociative();
4258  // Index the localized record uids by language
4259  if (is_array($destL10nRecords)) {
4260  foreach ($destL10nRecords as ‪$record) {
4261  $localizedDestPids[‪$record[$languageField]] = -‪$record['uid'];
4262  }
4263  }
4264  }
4265  $languageSourceMap = [
4266  ‪$uid => $overrideValues[$transOrigPointerField],
4267  ];
4268  // Copy the localized records after the corresponding localizations of the destination record
4269  foreach ($l10nRecords as ‪$record) {
4270  $localizedDestPid = (int)($localizedDestPids[‪$record[$languageField]] ?? 0);
4271  if ($localizedDestPid < 0) {
4272  $newUid = $this->‪copyRecord($table, ‪$record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, ‪$record[‪$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4273  } else {
4274  $newUid = $this->‪copyRecord($table, ‪$record['uid'], $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, ‪$record[‪$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4275  }
4276  $languageSourceMap[‪$record['uid']] = $newUid;
4277  }
4278  $this->‪copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap);
4279  }
4280  }
4281 
4289  protected function ‪copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap): void
4290  {
4291  if (empty(‪$GLOBALS['TCA'][$table]['ctrl']['translationSource']) || empty(‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
4292  return;
4293  }
4294  $translationSourceFieldName = ‪$GLOBALS['TCA'][$table]['ctrl']['translationSource'];
4295  $translationParentFieldName = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
4296 
4297  //We can avoid running these update queries by sorting the $l10nRecords by languageSource dependency (in copyL10nOverlayRecords)
4298  //and first copy records depending on default record (and map the field).
4299  foreach ($l10nRecords as ‪$record) {
4300  $oldSourceUid = ‪$record[$translationSourceFieldName];
4301  if ($oldSourceUid <= 0 && ‪$record[$translationParentFieldName] > 0) {
4302  //BC fix - in connected mode 'translationSource' field should not be 0
4303  $oldSourceUid = ‪$record[$translationParentFieldName];
4304  }
4305  if ($oldSourceUid > 0) {
4306  if (empty($languageSourceMap[$oldSourceUid])) {
4307  // we don't have mapping information available e.g when copyRecord returned null
4308  continue;
4309  }
4310  $newFieldValue = $languageSourceMap[$oldSourceUid];
4311  $updateFields = [
4312  $translationSourceFieldName => $newFieldValue,
4313  ];
4314  if (isset($languageSourceMap[‪$record['uid']])) {
4315  $this->connectionPool->getConnectionForTable($table)
4316  ->update($table, $updateFields, ['uid' => (int)$languageSourceMap[‪$record['uid']]]);
4317  if ($this->BE_USER->workspace > 0) {
4318  $this->connectionPool->getConnectionForTable($table)
4319  ->update($table, $updateFields, ['t3ver_oid' => (int)$languageSourceMap[‪$record['uid']], 't3ver_wsid' => $this->BE_USER->workspace]);
4320  }
4321  }
4322  }
4323  }
4324  }
4325 
4326  /*********************************************
4327  *
4328  * Cmd: Moving, Localizing
4329  *
4330  ********************************************/
4339  public function ‪moveRecord($table, ‪$uid, $destPid): void
4340  {
4341  if (!‪$GLOBALS['TCA'][$table]) {
4342  return;
4343  }
4344 
4345  // In case the record to be moved turns out to be an offline version,
4346  // we have to find the live version and work on that one.
4347  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, ‪$uid, 'uid')) {
4348  ‪$uid = $lookForLiveVersion['uid'];
4349  }
4350  // Initialize:
4351  $destPid = (int)$destPid;
4352  // Get this before we change the pid (for logging)
4353  $propArr = $this->‪getRecordProperties($table, ‪$uid);
4354  $moveRec = $this->‪getRecordProperties($table, ‪$uid, true);
4355  // This is the actual pid of the moving to destination
4356  $resolvedPid = $this->‪resolvePid($table, $destPid);
4357  // Finding out, if the record may be moved from where it is. If the record is a non-page, then it depends on edit-permissions.
4358  // If the record is a page, then there are two options: If the page is moved within itself,
4359  // (same pid) it's edit-perms of the pid. If moved to another place then its both delete-perms of the pid and new-page perms on the destination.
4360  if ($table !== 'pages' || $resolvedPid == $moveRec['pid']) {
4361  // Edit rights for the record...
4362  $mayMoveAccess = $this->‪checkRecordUpdateAccess($table, ‪$uid);
4363  } else {
4364  $mayMoveAccess = $this->‪doesRecordExist($table, ‪$uid, ‪Permission::PAGE_DELETE);
4365  }
4366  // Finding out, if the record may be moved TO another place. Here we check insert-rights (non-pages = edit, pages = new),
4367  // unless the pages are moved on the same pid, then edit-rights are checked
4368  if ($table !== 'pages' || $resolvedPid != $moveRec['pid']) {
4369  // Insert rights for the record...
4370  $mayInsertAccess = $this->‪checkRecordInsertAccess($table, $resolvedPid, SystemLogDatabaseAction::MOVE);
4371  } else {
4372  $mayInsertAccess = $this->‪checkRecordUpdateAccess($table, ‪$uid);
4373  }
4374  // Checking if there is anything else disallowing moving the record by checking if editing is allowed
4375  $fullLanguageCheckNeeded = $table !== 'pages';
4376  $mayEditAccess = $this->BE_USER->recordEditAccessInternals($table, ‪$uid, false, false, $fullLanguageCheckNeeded);
4377  // If moving is allowed, begin the processing:
4378  if (!$mayEditAccess) {
4379  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record "{title}" ({table}:{uid}) without having permissions to do so [{reason}]', 14, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid, 'reason' => $this->BE_USER->errorMsg], $propArr['event_pid']);
4380  return;
4381  }
4382 
4383  if (!$mayMoveAccess) {
4384  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record "{title}" ({table}:{uid}) without having permissions to do so', 14, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid], $propArr['event_pid']);
4385  return;
4386  }
4387 
4388  if (!$mayInsertAccess) {
4389  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record "{title}" ({table}:{uid}) without having permissions to insert', 14, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid], $propArr['event_pid']);
4390  return;
4391  }
4392 
4393  $recordWasMoved = false;
4394  // Move the record via a hook, used e.g. for versioning
4395  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] ?? [] as $className) {
4396  $hookObj = GeneralUtility::makeInstance($className);
4397  if (method_exists($hookObj, 'moveRecord')) {
4399  $hookObj->moveRecord($table, ‪$uid, $destPid, $propArr, $moveRec, $resolvedPid, $recordWasMoved, $this);
4400  }
4401  }
4402  // Move the record if a hook hasn't moved it yet
4403  if (!$recordWasMoved) {
4404  $this->‪moveRecord_raw($table, ‪$uid, $destPid);
4405  }
4406  }
4407 
4418  public function ‪moveRecord_raw($table, ‪$uid, $destPid): void
4419  {
4420  $sortColumn = ‪$GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
4421  $origDestPid = $destPid;
4422  // This is the actual pid of the moving to destination
4423  $resolvedPid = $this->‪resolvePid($table, $destPid);
4424  // Checking if the pid is negative, but no sorting row is defined. In that case, find the correct pid.
4425  // Basically this check make the error message 4-13 meaning less... But you can always remove this check if you
4426  // prefer the error instead of a no-good action (which is to move the record to its own page...)
4427  if (($destPid < 0 && !$sortColumn) || $destPid >= 0) {
4428  $destPid = $resolvedPid;
4429  }
4430  // Get this before we change the pid (for logging)
4431  $propArr = $this->‪getRecordProperties($table, ‪$uid);
4432  $moveRec = $this->‪getRecordProperties($table, ‪$uid, true);
4433  // Prepare user defined objects (if any) for hooks which extend this function:
4434  $hookObjectsArr = [];
4435  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] ?? [] as $className) {
4436  $hookObjectsArr[] = GeneralUtility::makeInstance($className);
4437  }
4438  // Timestamp field:
4439  $updateFields = [];
4440  if (‪$GLOBALS['TCA'][$table]['ctrl']['tstamp'] ?? false) {
4441  $updateFields[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = ‪$GLOBALS['EXEC_TIME'];
4442  }
4443 
4444  // Check if this is a translation of a page, if so then it just needs to be kept "sorting" in sync
4445  // Usually called from moveL10nOverlayRecords()
4446  if ($table === 'pages') {
4447  $defaultLanguagePageUid = $this->‪getDefaultLanguagePageId((int)$uid);
4448  // In workspaces, the default language page may have been moved to a different pid than the
4449  // default language page record of live workspace. In this case, localized pages need to be
4450  // moved to the pid of the workspace move record.
4451  $defaultLanguagePageWorkspaceOverlay = BackendUtility::getWorkspaceVersionOfRecord((int)$this->BE_USER->workspace, 'pages', $defaultLanguagePageUid, 'uid');
4452  if (is_array($defaultLanguagePageWorkspaceOverlay)) {
4453  $defaultLanguagePageUid = (int)$defaultLanguagePageWorkspaceOverlay['uid'];
4454  }
4455  if ($defaultLanguagePageUid !== (int)‪$uid) {
4456  // If the default language page has been moved, localized pages need to be moved to
4457  // that pid and sorting, too.
4458  $originalTranslationRecord = $this->‪recordInfo($table, $defaultLanguagePageUid);
4459  $updateFields[$sortColumn] = $originalTranslationRecord[$sortColumn];
4460  $destPid = $originalTranslationRecord['pid'];
4461  }
4462  }
4463 
4464  // Insert as first element on page (where uid = $destPid)
4465  if ($destPid >= 0) {
4466  if ($table !== 'pages' || $this->‪destNotInsideSelf($destPid, ‪$uid)) {
4467  // Clear cache before moving
4468  [$parentUid] = BackendUtility::getTSCpid($table, ‪$uid, '');
4469  $this->‪registerRecordIdForPageCacheClearing($table, ‪$uid, $parentUid);
4470  // Setting PID
4471  $updateFields['pid'] = $destPid;
4472  // Table is sorted by 'sortby'
4473  if ($sortColumn && !isset($updateFields[$sortColumn])) {
4474  $sortNumber = $this->‪getSortNumber($table, ‪$uid, $destPid);
4475  $updateFields[$sortColumn] = $sortNumber;
4476  }
4477  // Check for child records that have also to be moved
4478  $this->‪moveRecord_procFields($table, ‪$uid, $destPid);
4479  // Create query for update:
4480  $this->connectionPool->getConnectionForTable($table)
4481  ->update($table, $updateFields, ['uid' => (int)‪$uid]);
4482  // Check for the localizations of that element
4483  $this->‪moveL10nOverlayRecords($table, ‪$uid, $destPid, $destPid);
4484  // Call post-processing hooks:
4485  foreach ($hookObjectsArr as $hookObj) {
4486  if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4487  $hookObj->moveRecord_firstElementPostProcess($table, ‪$uid, $destPid, $moveRec, $updateFields, $this);
4488  }
4489  }
4490 
4491  $this->‪getRecordHistoryStore()->moveRecord($table, ‪$uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4492  if ($this->enableLogging) {
4493  // Logging...
4494  $oldpagePropArr = $this->‪getRecordProperties('pages', $propArr['pid']);
4495  if ($destPid != $propArr['pid']) {
4496  // Logged to old page
4497  $newPropArr = $this->‪getRecordProperties($table, ‪$uid);
4498  $newpagePropArr = $this->‪getRecordProperties('pages', $destPid);
4499  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, $destPid, SystemLogErrorClassification::MESSAGE, 'Moved record "{title}" ({table}:{uid}) to page "{pageTitle}" ({pid})', 2, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid, 'pageTitle' => $newpagePropArr['header'], 'pid' => $newPropArr['pid']], $propArr['pid']);
4500  // Logged to new page
4501  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, $destPid, SystemLogErrorClassification::MESSAGE, 'Moved record "{title}" ({table}:{uid}) from page "{pageTitle}" ({pid}))', 3, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid, 'pageTitle' => $oldpagePropArr['header'], 'pid' => $propArr['pid']], $destPid);
4502  } else {
4503  // Logged to new page
4504  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, $destPid, SystemLogErrorClassification::MESSAGE, 'Moved record "{title}" ({table}:{uid}) on page "{pageTitle}" ({pid})', 4, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid, 'pageTitle' => $oldpagePropArr['header'], 'pid' => $propArr['pid']], $destPid);
4505  }
4506  }
4507  // Clear cache after moving
4509  $this->‪fixUniqueInPid($table, ‪$uid);
4510  $this->‪fixUniqueInSite($table, (int)‪$uid);
4511  if ($table === 'pages') {
4512  $this->‪fixUniqueInSiteForSubpages((int)$uid);
4513  }
4514  } elseif ($this->enableLogging) {
4515  $destPropArr = $this->‪getRecordProperties('pages', $destPid);
4516  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move page "{title}" ({uid}) to inside of its own rootline (at page "{pageTitle}" ({pid}))', 10, ['title' => $propArr['header'], 'uid' => ‪$uid, 'pageTitle' => $destPropArr['header'], 'pid' => $destPid], $propArr['pid']);
4517  }
4518  } elseif ($sortColumn) {
4519  // Put after another record
4520  // Table is being sorted
4521  // Save the position to which the original record is requested to be moved
4522  $originalRecordDestinationPid = $destPid;
4523  $sortInfo = $this->‪getSortNumber($table, ‪$uid, $destPid);
4524  // If not an array, there was an error (which is already logged)
4525  if (is_array($sortInfo)) {
4526  // Setting the destPid to the new pid of the record.
4527  $destPid = $sortInfo['pid'];
4528  if ($table !== 'pages' || $this->‪destNotInsideSelf($destPid, ‪$uid)) {
4529  // clear cache before moving
4531  // We now update the pid and sortnumber (if not set for page translations)
4532  $updateFields['pid'] = $destPid;
4533  if (!isset($updateFields[$sortColumn])) {
4534  $updateFields[$sortColumn] = $sortInfo['sortNumber'];
4535  }
4536  // Check for child records that have also to be moved
4537  $this->‪moveRecord_procFields($table, ‪$uid, $destPid);
4538  // Create query for update:
4539  $this->connectionPool->getConnectionForTable($table)
4540  ->update($table, $updateFields, ['uid' => (int)‪$uid]);
4541  // Check for the localizations of that element
4542  $this->‪moveL10nOverlayRecords($table, ‪$uid, $destPid, $originalRecordDestinationPid);
4543  // Call post-processing hooks:
4544  foreach ($hookObjectsArr as $hookObj) {
4545  if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4546  $hookObj->moveRecord_afterAnotherElementPostProcess($table, ‪$uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4547  }
4548  }
4549  $this->‪getRecordHistoryStore()->moveRecord($table, ‪$uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4550  if ($this->enableLogging) {
4551  // Logging...
4552  $oldpagePropArr = $this->‪getRecordProperties('pages', $propArr['pid']);
4553  if ($destPid != $propArr['pid']) {
4554  // Logged to old page
4555  $newPropArr = $this->‪getRecordProperties($table, ‪$uid);
4556  $newpagePropArr = $this->‪getRecordProperties('pages', $destPid);
4557  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::MESSAGE, 'Moved record "{title}" ({table}:{uid}) to page "{pageTitle}" ({pid})', 2, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid, 'pageTitle' => $newpagePropArr['header'], 'pid' => $newPropArr['pid']], $propArr['pid']);
4558  // Logged to old page
4559  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::MESSAGE, 'Moved record "{title}" ({table}:{uid}) from page "{pageTitle}" ({pid})', 3, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid, 'pageTitle' => $oldpagePropArr['header'], 'pid' => $propArr['pid']], $destPid);
4560  } else {
4561  // Logged to old page
4562  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::MESSAGE, 'Moved record "{title}" ({table}:{uid}) on page "{pageTitle}" ({pid})', 4, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid, 'pageTitle' => $oldpagePropArr['header'], 'pid' => $propArr['pid']], $destPid);
4563  }
4564  }
4565  // Clear cache after moving
4567  $this->‪fixUniqueInPid($table, ‪$uid);
4568  $this->‪fixUniqueInSite($table, (int)‪$uid);
4569  if ($table === 'pages') {
4570  $this->‪fixUniqueInSiteForSubpages((int)$uid);
4571  }
4572  } elseif ($this->enableLogging) {
4573  $destPropArr = $this->‪getRecordProperties('pages', $destPid);
4574  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move page "{title}" ({uid}) to inside of its own rootline (at page "{pageTitle}" [{pid}])', 10, ['title' => $propArr['header'], 'uid' => ‪$uid, 'pageTitle' => $destPropArr['header'], 'pid' => $destPid], $propArr['pid']);
4575  }
4576  } else {
4577  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record "{title}" ({table}:{uid}) to after another record, although the table has no sorting row', 13, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid], $propArr['event_pid']);
4578  }
4579  }
4580  }
4581 
4591  public function ‪moveRecord_procFields($table, ‪$uid, $destPid): void
4592  {
4593  $row = BackendUtility::getRecordWSOL($table, ‪$uid);
4594  if (is_array($row) && (int)$destPid !== (int)$row['pid']) {
4595  $conf = ‪$GLOBALS['TCA'][$table]['columns'];
4596  foreach ($row as $field => $value) {
4597  $this->‪moveRecord_procBasedOnFieldType($table, ‪$uid, $destPid, $value, $conf[$field]['config'] ?? []);
4598  }
4599  }
4600  }
4601 
4612  public function ‪moveRecord_procBasedOnFieldType($table, ‪$uid, $destPid, $value, $conf): void
4613  {
4614  if (($conf['behaviour']['disableMovingChildrenWithParent'] ?? false)
4615  || !in_array($this->‪getRelationFieldType($conf), ['list', 'field'], true)
4616  ) {
4617  return;
4618  }
4619 
4620  if ($table === 'pages') {
4621  // If the relations are related to a page record, make sure they reside at that page and not at its parent
4622  $destPid = ‪$uid;
4623  }
4624 
4625  $dbAnalysis = $this->‪createRelationHandlerInstance();
4626  $dbAnalysis->start($value, $conf['foreign_table'], '', ‪$uid, $table, $conf);
4627 
4628  // Moving records to a positive destination will insert each
4629  // record at the beginning, thus the order is reversed here:
4630  foreach (array_reverse($dbAnalysis->itemArray) as $item) {
4631  $this->‪moveRecord($item['table'], $item['id'], $destPid);
4632  }
4633  }
4634 
4644  public function ‪moveL10nOverlayRecords($table, ‪$uid, $destPid, $originalRecordDestinationPid): void
4645  {
4646  // There's no need to perform this for non-localizable tables
4647  if (!BackendUtility::isTableLocalizable($table)) {
4648  return;
4649  }
4650 
4651  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
4652  $queryBuilder->getRestrictions()->removeAll()
4653  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4654  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->BE_USER->workspace));
4655 
4656  $languageField = ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'];
4657  $transOrigPointerField = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null;
4658  $l10nRecords = $queryBuilder->select('*')
4659  ->from($table)
4660  ->where(
4661  $queryBuilder->expr()->eq(
4662  $transOrigPointerField,
4663  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT, ':pointer')
4664  )
4665  )
4666  ->executeQuery()
4667  ->fetchAllAssociative();
4668 
4669  if (is_array($l10nRecords)) {
4670  $localizedDestPids = [];
4671  // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4672  if ($originalRecordDestinationPid < 0) {
4673  // Get the localized records of the record we are inserting after
4674  $queryBuilder->setParameter('pointer', abs($originalRecordDestinationPid), ‪Connection::PARAM_INT);
4675  $destL10nRecords = $queryBuilder->executeQuery()->fetchAllAssociative();
4676  // Index the localized record uids by language
4677  if (is_array($destL10nRecords)) {
4678  foreach ($destL10nRecords as ‪$record) {
4679  $localizedDestPids[‪$record[$languageField]] = -‪$record['uid'];
4680  }
4681  }
4682  }
4683  // Move the localized records after the corresponding localizations of the destination record
4684  foreach ($l10nRecords as ‪$record) {
4685  $localizedDestPid = (int)($localizedDestPids[‪$record[$languageField]] ?? 0);
4686  if ($localizedDestPid < 0) {
4687  $this->‪moveRecord($table, ‪$record['uid'], $localizedDestPid);
4688  } else {
4689  $this->‪moveRecord($table, ‪$record['uid'], $destPid);
4690  }
4691  }
4692  }
4693  }
4694 
4704  public function ‪localize($table, ‪$uid, $language)
4705  {
4706  $newId = false;
4707  ‪$uid = (int)‪$uid;
4708  if (!‪$GLOBALS['TCA'][$table] || !‪$uid || $this->‪isNestedElementCallRegistered($table, ‪$uid, 'localize-' . (string)$language) !== false) {
4709  return false;
4710  }
4711 
4712  $this->‪registerNestedElementCall($table, ‪$uid, 'localize-' . (string)$language);
4713  if (empty(‪$GLOBALS['TCA'][$table]['ctrl']['languageField']) || empty(‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
4714  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::LOCALIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Localization failed; "languageField" and "transOrigPointerField" must be defined for the table {table}', -1, ['table' => $table]);
4715  return false;
4716  }
4717 
4718  if (!$this->‪doesRecordExist($table, ‪$uid, ‪Permission::PAGE_SHOW)) {
4719  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::LOCALIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to localize record {table}:{uid} without permission', -1, ['table' => $table, 'uid' => (int)‪$uid]);
4720  return false;
4721  }
4722 
4723  // Getting workspace overlay if possible - this will localize versions in workspace if any
4724  $row = BackendUtility::getRecordWSOL($table, ‪$uid);
4725  if (!is_array($row)) {
4726  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::LOCALIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to localize record {table}:{uid} that did not exist', -1, ['table' => $table, 'uid' => (int)‪$uid]);
4727  return false;
4728  }
4729 
4730  [$pageId] = BackendUtility::getTSCpid($table, ‪$uid, '');
4731  // Try to fetch the site language from the pages' associated site
4732  $siteLanguage = $this->‪getSiteLanguageForPage((int)$pageId, (int)$language);
4733  if ($siteLanguage === null) {
4734  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::LOCALIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Language ID "{languageId}" not found for page {pageId}', -1, ['languageId' => (int)$language, 'pageId' => (int)$pageId]);
4735  return false;
4736  }
4737 
4738  // Make sure that records which are translated from another language than the default language have a correct
4739  // localization source set themselves, before translating them to another language.
4740  if ((int)$row[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4741  && $row[‪$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0) {
4742  $localizationParentRecord = BackendUtility::getRecord(
4743  $table,
4744  $row[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
4745  );
4746  if ((int)$localizationParentRecord[‪$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== 0) {
4747  $this->‪log($table, $localizationParentRecord['uid'], SystemLogDatabaseAction::LOCALIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Localization failed: Source record {table}:{originalRecordId} contained a reference to an original record that is not a default record (which is strange)', -1, ['table' => $table, 'originalRecordId' => $localizationParentRecord['uid']]);
4748  return false;
4749  }
4750  }
4751 
4752  // Default language records must never have a localization parent as they are the origin of any translation.
4753  if ((int)$row[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4754  && (int)$row[‪$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4755  $this->‪log($table, $row['uid'], SystemLogDatabaseAction::LOCALIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Localization failed: Source record {table}:{uid} contained a reference to an original default record but is a default record itself (which is strange)', -1, ['table' => $table, 'uid' => (int)$row['uid']]);
4756  return false;
4757  }
4758 
4759  $recordLocalizations = BackendUtility::getRecordLocalization($table, ‪$uid, $language, 'AND pid=' . (int)$row['pid']);
4760 
4761  if (!empty($recordLocalizations)) {
4762  $this->‪log(
4763  $table,
4764  ‪$uid,
4765  SystemLogDatabaseAction::LOCALIZE,
4766  0,
4767  SystemLogErrorClassification::USER_ERROR,
4768  'Localization failed: There already are localizations ({localizations}) for language {language} of the "{table}" record {uid}',
4769  -1,
4770  [
4771  'localizations' => implode(', ', array_column($recordLocalizations, 'uid')),
4772  'language' => $language,
4773  'table' => $table,
4774  'uid' => ‪$uid,
4775  ]
4776  );
4777  return false;
4778  }
4779 
4780  // Initialize:
4781  $overrideValues = [];
4782  // Set override values:
4783  $overrideValues[‪$GLOBALS['TCA'][$table]['ctrl']['languageField']] = (int)$language;
4784  // If the translated record is a default language record, set it's uid as localization parent of the new record.
4785  // If translating from any other language, no override is needed; we just can copy the localization parent of
4786  // the original record (which is pointing to the correspondent default language record) to the new record.
4787  // In copy / free mode the TransOrigPointer field is always set to 0, as no connection to the localization parent is wanted in that case.
4788  // For pages, there is no "copy/free mode".
4789  if (($this->useTransOrigPointerField || $table === 'pages') && (int)$row[‪$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4790  $overrideValues[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = ‪$uid;
4791  } elseif (!$this->useTransOrigPointerField) {
4792  $overrideValues[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = 0;
4793  }
4794  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
4795  $overrideValues[‪$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = ‪$uid;
4796  }
4797  // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4798  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['type'])) {
4799  // @todo: Possible bug here? type can be something like 'table:field', which is then null in $row, writing null to $overrideValues
4800  $overrideValues[‪$GLOBALS['TCA'][$table]['ctrl']['type']] = $row[‪$GLOBALS['TCA'][$table]['ctrl']['type']] ?? null;
4801  }
4802  // Set exclude Fields:
4803  foreach (‪$GLOBALS['TCA'][$table]['columns'] as $fN => $fCfg) {
4804  $translateToMsg = '';
4805  // Check if we are just prefixing:
4806  if (isset($fCfg['l10n_mode'], $fCfg['config']['type'])
4807  && $fCfg['l10n_mode'] === 'prefixLangTitle'
4808  && (
4809  $fCfg['config']['type'] === 'text'
4810  || $fCfg['config']['type'] === 'input'
4811  || $fCfg['config']['type'] === 'email'
4812  || $fCfg['config']['type'] === 'link'
4813  )
4814  && (string)$row[$fN] !== ''
4815  ) {
4816  $TSConfig = BackendUtility::getPagesTSconfig($pageId)['TCEMAIN.'] ?? [];
4817  $tableEntries = $this->‪getTableEntries($table, $TSConfig);
4818  if (!empty($TSConfig['translateToMessage']) && !($tableEntries['disablePrependAtCopy'] ?? false)) {
4819  $translateToMsg = $this->‪getLanguageService()->sL($TSConfig['translateToMessage']);
4820  $translateToMsg = @sprintf($translateToMsg, $siteLanguage->getTitle());
4821  }
4822 
4823  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processTranslateToClass'] ?? [] as $className) {
4824  $hookObj = GeneralUtility::makeInstance($className);
4825  if (method_exists($hookObj, 'processTranslateTo_copyAction')) {
4826  // @todo Deprecate passing an array and pass the full SiteLanguage object instead
4827  $hookObj->processTranslateTo_copyAction(
4828  $row[$fN],
4829  ['uid' => $siteLanguage->getLanguageId(), 'title' => $siteLanguage->getTitle()],
4830  $this,
4831  $fN
4832  );
4833  }
4834  }
4835  if (!empty($translateToMsg)) {
4836  $overrideValues[$fN] = '[' . $translateToMsg . '] ' . $row[$fN];
4837  } else {
4838  $overrideValues[$fN] = $row[$fN];
4839  }
4840  }
4841  if (($fCfg['config']['MM'] ?? false) && !empty($fCfg['config']['MM_oppositeUsage'])) {
4842  // We are localizing the 'local' side of an MM relation. (eg. localizing a category).
4843  // In this case, MM relations connected to the default lang record should not be copied,
4844  // so we set an override here to not trigger mm handling of 'items' field for this.
4845  $overrideValues[$fN] = 0;
4846  }
4847  }
4848 
4849  if ($table !== 'pages') {
4850  // Get the uid of record after which this localized record should be inserted
4851  $previousUid = $this->‪getPreviousLocalizedRecordUid($table, ‪$uid, $row['pid'], $language);
4852  // Execute the copy:
4853  $newId = $this->‪copyRecord($table, ‪$uid, -$previousUid, true, $overrideValues, '', $language);
4854  } else {
4855  // Create new page which needs to contain the same pid as the original page
4856  $overrideValues['pid'] = $row['pid'];
4857  // Take over the hidden state of the original language state, this is done due to legacy reasons where-as
4858  // pages_language_overlay was set to "hidden -> default=0" but pages hidden -> default 1"
4859  if (!empty(‪$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
4860  $hiddenFieldName = ‪$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
4861  $overrideValues[$hiddenFieldName] = $row[$hiddenFieldName] ?? ‪$GLOBALS['TCA'][$table]['columns'][$hiddenFieldName]['config']['default'];
4862  // Override by TCA "hideAtCopy" or pageTS "disableHideAtCopy"
4863  // Only for visible pages to get the same behaviour as for copy
4864  if (!$overrideValues[$hiddenFieldName]) {
4865  $TSConfig = BackendUtility::getPagesTSconfig(‪$uid)['TCEMAIN.'] ?? [];
4866  $tableEntries = $this->‪getTableEntries($table, $TSConfig);
4867  if (
4868  (‪$GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] ?? false)
4869  && !$this->neverHideAtCopy
4870  && !($tableEntries['disableHideAtCopy'] ?? false)
4871  ) {
4872  $overrideValues[$hiddenFieldName] = 1;
4873  }
4874  }
4875  }
4876  $temporaryId = ‪StringUtility::getUniqueId('NEW');
4877  $copyTCE = $this->‪getLocalTCE();
4878  $copyTCE->start([$table => [$temporaryId => $overrideValues]], [], $this->BE_USER);
4879  $copyTCE->process_datamap();
4880  // Getting the new UID as if it had been copied:
4881  $theNewSQLID = $copyTCE->substNEWwithIDs[$temporaryId];
4882  if ($theNewSQLID) {
4883  $this->copyMappingArray[$table][‪$uid] = $theNewSQLID;
4884  $newId = $theNewSQLID;
4885  }
4886  }
4887 
4888  return $newId;
4889  }
4890 
4907  protected function ‪inlineLocalizeSynchronize($table, $id, array $command): void
4908  {
4909  $parentRecord = BackendUtility::getRecordWSOL($table, $id);
4910 
4911  // In case the parent record is the default language record, fetch the localization
4912  if (empty($parentRecord[‪$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
4913  // Fetch the live record
4914  // @todo: this needs to be revisited, as getRecordLocalization() does a WorkspaceRestriction
4915  // based on $GLOBALS[BE_USER], which could differ from the $this->BE_USER->workspace value
4916  $parentRecordLocalization = BackendUtility::getRecordLocalization($table, $id, $command['language'], 'AND t3ver_oid=0');
4917  if (empty($parentRecordLocalization)) {
4918  $this->‪log($table, $id, SystemLogDatabaseAction::LOCALIZE, 0, SystemLogErrorClassification::MESSAGE, 'Localization for parent record {table}:{uid} cannot be fetched', -1, ['table' => $table, 'uid' => (int)$id], $this->‪eventPid($table, $id, $parentRecord['pid']));
4919  return;
4920  }
4921  $parentRecord = $parentRecordLocalization[0];
4922  $id = $parentRecord['uid'];
4923  // Process overlay for current selected workspace
4924  BackendUtility::workspaceOL($table, $parentRecord);
4925  }
4926 
4927  $field = $command['field'] ?? '';
4928  $language = $command['language'] ?? 0;
4929  $action = $command['action'] ?? '';
4930  $ids = $command['ids'] ?? [];
4931 
4932  if (!$field || !($action === 'localize' || $action === 'synchronize') && empty($ids) || !isset(‪$GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4933  return;
4934  }
4935 
4936  $config = ‪$GLOBALS['TCA'][$table]['columns'][$field]['config'];
4937  $foreignTable = $config['foreign_table'];
4938 
4939  $transOrigPointer = (int)$parentRecord[‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
4940  $childTransOrigPointerField = ‪$GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
4941 
4942  if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) {
4943  return;
4944  }
4945 
4946  $relationFieldType = $this->‪getRelationFieldType($config);
4947  if ($relationFieldType === false) {
4948  return;
4949  }
4950 
4951  $transOrigRecord = BackendUtility::getRecordWSOL($table, $transOrigPointer);
4952 
4953  $removeArray = [];
4954  $mmTable = $relationFieldType === 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : '';
4955  // Fetch children from original language parent:
4956  $dbAnalysisOriginal = $this->‪createRelationHandlerInstance();
4957  $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $table, $config);
4958  $elementsOriginal = [];
4959  foreach ($dbAnalysisOriginal->itemArray as $item) {
4960  $elementsOriginal[$item['id']] = $item;
4961  }
4962  unset($dbAnalysisOriginal);
4963  // Fetch children from current localized parent:
4964  $dbAnalysisCurrent = $this->‪createRelationHandlerInstance();
4965  $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config);
4966  // Perform synchronization: Possibly removal of already localized records:
4967  if ($action === 'synchronize') {
4968  foreach ($dbAnalysisCurrent->itemArray as $index => $item) {
4969  $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']);
4970  if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) {
4971  $childTransOrigPointer = $childRecord[$childTransOrigPointerField];
4972  // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it:
4973  if (!isset($elementsOriginal[$childTransOrigPointer])) {
4974  unset($dbAnalysisCurrent->itemArray[$index]);
4975  $removeArray[$item['table']][$item['id']]['delete'] = 1;
4976  }
4977  }
4978  }
4979  }
4980  // Perform synchronization/localization: Possibly add unlocalized records for original language:
4981  if ($action === 'localize' || $action === 'synchronize') {
4982  foreach ($elementsOriginal as $originalId => $item) {
4983  if ($this->‪isRecordLocalized((string)$item['table'], (int)$item['id'], (int)$language)) {
4984  continue;
4985  }
4986  $item['id'] = $this->‪localize($item['table'], $item['id'], $language);
4987 
4988  if (is_int($item['id'])) {
4989  $item['id'] = $this->‪overlayAutoVersionId($item['table'], $item['id']);
4990  }
4991  $dbAnalysisCurrent->itemArray[] = $item;
4992  }
4993  } elseif (!empty($ids)) {
4994  foreach ($ids as $childId) {
4995  if (!‪MathUtility::canBeInterpretedAsInteger($childId) || !isset($elementsOriginal[$childId])) {
4996  continue;
4997  }
4998  $item = $elementsOriginal[$childId];
4999  if ($this->‪isRecordLocalized((string)$item['table'], (int)$item['id'], (int)$language)) {
5000  continue;
5001  }
5002  $item['id'] = $this->‪localize($item['table'], $item['id'], $language);
5003  if (is_int($item['id'])) {
5004  $item['id'] = $this->‪overlayAutoVersionId($item['table'], $item['id']);
5005  }
5006  $dbAnalysisCurrent->itemArray[] = $item;
5007  }
5008  }
5009  // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
5010  $value = implode(',', $dbAnalysisCurrent->getValueArray());
5011  $this->registerDBList[$table][$id][$field] = $value;
5012  // Remove child records (if synchronization requested it):
5013  if (is_array($removeArray) && !empty($removeArray)) {
5014  $tce = GeneralUtility::makeInstance(self::class, $this->referenceIndexUpdater);
5015  $tce->enableLogging = ‪$this->enableLogging;
5016  $tce->start([], $removeArray, $this->BE_USER);
5017  $tce->process_cmdmap();
5018  unset($tce);
5019  }
5020  $updateFields = [];
5021  // Handle, reorder and store relations:
5022  if ($relationFieldType === 'list') {
5023  $updateFields = [$field => $value];
5024  } elseif ($relationFieldType === 'field') {
5025  $dbAnalysisCurrent->writeForeignField($config, $id);
5026  $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
5027  } elseif ($relationFieldType === 'mm') {
5028  $dbAnalysisCurrent->writeMM($config['MM'], $id);
5029  $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
5030  }
5031  // Update field referencing to child records of localized parent record:
5032  if (!empty($updateFields)) {
5033  $this->‪updateDB($table, $id, $updateFields);
5034  }
5035  if (isset($parentRecord['_ORIG_uid']) && (int)$parentRecord['_ORIG_uid'] !== (int)$id) {
5036  // If there is a ws overlay of the record, then the relation has been attached to *this*
5037  // record, even though the uids point to live. We still need to update refindex of the overlay
5038  // to reflect this relation.
5039  $this->‪updateRefIndex($table, (int)$parentRecord['_ORIG_uid']);
5040  }
5041  }
5042 
5046  protected function ‪isRecordLocalized(string $table, int ‪$uid, int $language): bool
5047  {
5048  $row = BackendUtility::getRecordWSOL($table, ‪$uid);
5049  $localizations = BackendUtility::getRecordLocalization($table, ‪$uid, $language, 'pid=' . (int)$row['pid']);
5050  return !empty($localizations);
5051  }
5052 
5053  /*********************************************
5054  *
5055  * Cmd: delete
5056  *
5057  ********************************************/
5065  public function ‪deleteAction($table, $id): void
5066  {
5067  $recordToDelete = BackendUtility::getRecord($table, $id);
5068 
5069  if (is_array($recordToDelete) && isset($recordToDelete['t3ver_wsid']) && (int)$recordToDelete['t3ver_wsid'] !== 0) {
5070  // When dealing with a workspace record, use discard.
5071  $this->‪discard($table, null, $recordToDelete);
5072  return;
5073  }
5074 
5075  // Record asked to be deleted was found:
5076  if (is_array($recordToDelete)) {
5077  $recordWasDeleted = false;
5078  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
5079  $hookObj = GeneralUtility::makeInstance($className);
5080  if (method_exists($hookObj, 'processCmdmap_deleteAction')) {
5082  $hookObj->processCmdmap_deleteAction($table, $id, $recordToDelete, $recordWasDeleted, $this);
5083  }
5084  }
5085  // Delete the record if a hook hasn't deleted it yet
5086  if (!$recordWasDeleted) {
5087  $this->‪deleteEl($table, $id);
5088  }
5089  }
5090  }
5091 
5102  public function ‪deleteEl(string $table, int ‪$uid, bool $noRecordCheck = false, bool $forceHardDelete = false, bool $deleteRecordsOnPage = true): void
5103  {
5104  if ($table === 'pages') {
5105  $this->‪deletePages($uid, $noRecordCheck, $forceHardDelete, $deleteRecordsOnPage);
5106  } else {
5109  $this->‪deleteRecord($table, ‪$uid, $noRecordCheck, $forceHardDelete);
5110  }
5111  }
5112 
5119  protected function ‪discardLocalizedWorkspaceVersionsOfRecord(string $table, int ‪$uid): void
5120  {
5121  if (!BackendUtility::isTableLocalizable($table)
5122  || !BackendUtility::isTableWorkspaceEnabled($table)
5123  || !$this->BE_USER->recordEditAccessInternals($table, ‪$uid)
5124  ) {
5125  return;
5126  }
5127  $languageField = ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'];
5128  $localizationParentFieldName = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
5129  $liveRecord = BackendUtility::getRecord($table, ‪$uid);
5130  if ((int)($liveRecord[$languageField] ?? 0) !== 0 || (int)($liveRecord['t3ver_wsid'] ?? 0) !== 0) {
5131  // Don't do anything if we're not deleting a live record in default language
5132  return;
5133  }
5134  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
5135  $queryBuilder->getRestrictions()->removeAll();
5136  $queryBuilder = $queryBuilder->select('*')->from($table)
5137  ->where(
5138  // workspace elements
5139  $queryBuilder->expr()->gt('t3ver_wsid', $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)),
5140  // with sys_language_uid > 0
5141  $queryBuilder->expr()->gt($languageField, $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)),
5142  // in state 'new'
5143  $queryBuilder->expr()->eq('t3ver_state', $queryBuilder->createNamedParameter(VersionState::NEW_PLACEHOLDER->value, ‪Connection::PARAM_INT)),
5144  // with "l10n_parent" set to uid of live record
5145  $queryBuilder->expr()->eq($localizationParentFieldName, $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT))
5146  );
5147  $result = $queryBuilder->executeQuery();
5148  while ($row = $result->fetchAssociative()) {
5149  // BE user must be put into this workspace temporarily so stuff like refindex updating
5150  // is properly registered for this workspace when discarding records in there.
5151  $currentUserWorkspace = $this->BE_USER->workspace;
5152  $this->BE_USER->workspace = (int)$row['t3ver_wsid'];
5153  $this->‪discard($table, null, $row);
5154  // Switch user back to original workspace
5155  $this->BE_USER->workspace = $currentUserWorkspace;
5156  }
5157  }
5158 
5167  protected function ‪discardWorkspaceVersionsOfRecord($table, ‪$uid): void
5168  {
5169  $versions = BackendUtility::selectVersionsOfRecord($table, ‪$uid, '*', null);
5170  if ($versions === null) {
5171  // Null is returned by selectVersionsOfRecord() when table is not workspace aware.
5172  return;
5173  }
5174  foreach ($versions as ‪$record) {
5175  if (‪$record['_CURRENT_VERSION'] ?? false) {
5176  // The live record is included in the result from selectVersionsOfRecord()
5177  // and marked as '_CURRENT_VERSION'. Skip this one.
5178  continue;
5179  }
5180  // BE user must be put into this workspace temporarily so stuff like refindex updating
5181  // is properly registered for this workspace when discarding records in there.
5182  $currentUserWorkspace = $this->BE_USER->workspace;
5183  $this->BE_USER->workspace = (int)‪$record['t3ver_wsid'];
5184  $this->‪discard($table, null, ‪$record);
5185  // Switch user back to original workspace
5186  $this->BE_USER->workspace = $currentUserWorkspace;
5187  }
5188  }
5189 
5202  public function ‪deleteRecord(string $table, int ‪$uid, bool $noRecordCheck = false, bool $forceHardDelete = false): void
5203  {
5204  $currentUserWorkspace = $this->BE_USER->workspace;
5205  if (!‪$GLOBALS['TCA'][$table] || !‪$uid) {
5206  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::DELETE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to delete record without delete-permissions [{reason}]', -1, ['reason' => $this->BE_USER->errorMsg]);
5207  return;
5208  }
5209  // Skip processing already deleted records
5210  if (!$forceHardDelete && $this->‪hasDeletedRecord($table, ‪$uid)) {
5211  return;
5212  }
5213 
5214  // Checking if there is anything else disallowing deleting the record by checking if editing is allowed
5215  $fullLanguageAccessCheck = true;
5216  if ($table === 'pages') {
5217  // If this is a page translation, the full language access check should not be done
5218  $defaultLanguagePageId = $this->‪getDefaultLanguagePageId($uid);
5219  if ($defaultLanguagePageId !== ‪$uid) {
5220  $fullLanguageAccessCheck = false;
5221  }
5222  }
5223  $hasEditAccess = $this->BE_USER->recordEditAccessInternals($table, ‪$uid, false, $forceHardDelete, $fullLanguageAccessCheck);
5224  if (!$hasEditAccess) {
5225  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::DELETE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to delete record without delete-permissions');
5226  return;
5227  }
5228  if ($table === 'pages') {
5229  $perms = ‪Permission::PAGE_DELETE;
5230  } elseif ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
5231  // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
5232  $perms = ‪Permission::PAGE_EDIT;
5233  } else {
5234  $perms = ‪Permission::CONTENT_EDIT;
5235  }
5236  if (!$noRecordCheck && !$this->‪doesRecordExist($table, ‪$uid, $perms)) {
5237  return;
5238  }
5239 
5240  $recordToDelete = [];
5241  $recordWorkspaceId = 0;
5242  if (BackendUtility::isTableWorkspaceEnabled($table)) {
5243  $recordToDelete = BackendUtility::getRecord($table, ‪$uid);
5244  $recordWorkspaceId = (int)($recordToDelete['t3ver_wsid'] ?? 0);
5245  }
5246 
5247  // Clear cache before deleting the record, else the correct page cannot be identified by clear_cache
5248  [$parentUid] = BackendUtility::getTSCpid($table, ‪$uid, '');
5249  $this->‪registerRecordIdForPageCacheClearing($table, ‪$uid, $parentUid);
5250  $deleteField = ‪$GLOBALS['TCA'][$table]['ctrl']['delete'] ?? false;
5251  $databaseErrorMessage = '';
5252  if ($recordWorkspaceId > 0) {
5253  // If this is a workspace record, use discard
5254  $this->BE_USER->workspace = $recordWorkspaceId;
5255  $this->‪discard($table, null, $recordToDelete);
5256  // Switch user back to original workspace
5257  $this->BE_USER->workspace = $currentUserWorkspace;
5258  } elseif ($deleteField && !$forceHardDelete) {
5259  $updateFields = [
5260  $deleteField => 1,
5261  ];
5262  if (‪$GLOBALS['TCA'][$table]['ctrl']['tstamp'] ?? false) {
5263  $updateFields[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = ‪$GLOBALS['EXEC_TIME'];
5264  }
5265  // before deleting this record, check for child records or references
5266  $this->‪deleteRecord_procFields($table, ‪$uid);
5267  try {
5268  // Delete all l10n records as well
5269  $this->deletedRecords[$table][] = ‪$uid;
5270  $this->‪deleteL10nOverlayRecords($table, ‪$uid);
5271  $this->connectionPool->getConnectionForTable($table)
5272  ->update($table, $updateFields, ['uid' => ‪$uid]);
5273  } catch (DBALException $e) {
5274  $databaseErrorMessage = $e->getPrevious()->getMessage();
5275  }
5276  } else {
5277  // Delete the hard way...:
5278  try {
5279  $this->‪hardDeleteSingleRecord($table, ‪$uid);
5280  $this->deletedRecords[$table][] = ‪$uid;
5281  $this->‪deleteL10nOverlayRecords($table, ‪$uid);
5282  } catch (DBALException $e) {
5283  $databaseErrorMessage = $e->getPrevious()->getMessage();
5284  }
5285  }
5286  if ($this->enableLogging) {
5287  $state = SystemLogDatabaseAction::DELETE;
5288  if ($databaseErrorMessage === '') {
5289  if ($forceHardDelete) {
5290  $message = 'Record "{title}" ({table}:{uid}) was deleted unrecoverable from page "{pageTitle}" ({pid})';
5291  } else {
5292  $message = 'Record "{title}" ({table}:{uid}) was deleted from page "{pageTitle}" ({pid})';
5293  }
5294  $propArr = $this->‪getRecordProperties($table, ‪$uid);
5295  $pagePropArr = $this->‪getRecordProperties('pages', $propArr['pid']);
5296 
5297  $this->‪log($table, ‪$uid, $state, 0, SystemLogErrorClassification::MESSAGE, $message, 0, [
5298  'title' => $propArr['header'],
5299  'table' => $table,
5300  'uid' => ‪$uid,
5301  'pageTitle' => $pagePropArr['header'],
5302  'pid' => $propArr['pid'],
5303  ], $propArr['event_pid']);
5304  } else {
5305  $this->‪log($table, ‪$uid, $state, 0, SystemLogErrorClassification::SYSTEM_ERROR, $databaseErrorMessage);
5306  }
5307  }
5308 
5309  // Add history entry
5310  $this->‪getRecordHistoryStore()->deleteRecord($table, ‪$uid, $this->correlationId);
5311 
5312  // Update reference index with table/uid on left side (recuid)
5313  $this->‪updateRefIndex($table, ‪$uid);
5314  // Update reference index with table/uid on right side (ref_uid). Important if children of a relation are deleted.
5315  $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, ‪$uid, $currentUserWorkspace);
5316  }
5317 
5327  public function ‪deletePages(int ‪$uid, bool $force = false, bool $forceHardDelete = false, bool $deleteRecordsOnPage = true): void
5328  {
5329  if (‪$uid === 0) {
5330  $this->‪log('pages', $uid, SystemLogDatabaseAction::DELETE, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'Deleting all pages starting from the root-page is disabled', -1, [], 0);
5331  return;
5332  }
5333  // Getting list of pages to delete:
5334  if ($force) {
5335  // Returns the branch WITHOUT permission checks, so it cannot return null
5336  $res = $this->‪doesBranchExist($uid, ‪Permission::NOTHING);
5337  if (is_array($res)) {
5338  $res[] = ‪$uid;
5339  }
5340  } else {
5341  $res = $this->‪canDeletePage($uid);
5342  }
5343  // Perform deletion if no error occurred
5344  if (is_array($res)) {
5345  foreach ($res as $deleteId) {
5346  $this->‪deleteSpecificPage($deleteId, $forceHardDelete, $deleteRecordsOnPage);
5347  }
5348  } else {
5349  $this->‪log(
5350  'pages',
5351  $uid,
5352  SystemLogDatabaseAction::DELETE,
5353  0,
5354  SystemLogErrorClassification::SYSTEM_ERROR,
5355  $res,
5356  );
5357  }
5358  }
5359 
5369  protected function ‪deleteSpecificPage(int ‪$uid, bool $forceHardDelete, bool $deleteRecordsOnPage): void
5370  {
5371  if (!‪$uid) {
5372  // Early void return on invalid uid
5373  return;
5374  }
5375 
5376  // Delete either a default language page or a translated page
5377  $pageIdInDefaultLanguage = $this->‪getDefaultLanguagePageId($uid);
5378  $isPageTranslation = false;
5379  $pageLanguageId = 0;
5380  if ($pageIdInDefaultLanguage !== ‪$uid) {
5381  // For translated pages, translated records in other tables (eg. tt_content) for the
5382  // to-delete translated page have their pid field set to the uid of the default language record,
5383  // NOT the uid of the translated page record.
5384  // If a translated page is deleted, only translations of records in other tables of this language
5385  // should be deleted. The code checks if the to-delete page is a translated page and
5386  // adapts the query for other tables to use the uid of the default language page as pid together
5387  // with the language id of the translated page.
5388  $isPageTranslation = true;
5389  $pageLanguageId = $this->‪pageInfo($uid, ‪$GLOBALS['TCA']['pages']['ctrl']['languageField']);
5390  }
5391 
5392  if ($deleteRecordsOnPage) {
5393  $tableNames = $this->‪compileAdminTables();
5394  foreach ($tableNames as $table) {
5395  if ($table === 'pages' || ($isPageTranslation && !BackendUtility::isTableLocalizable($table))) {
5396  // Skip pages table. And skip table if not translatable, but a translated page is deleted
5397  continue;
5398  }
5399 
5400  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
5401  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5402  $queryBuilder
5403  ->select('uid')
5404  ->from($table)
5405  // order by uid is needed here to process possible live records first - overlays always
5406  // have a higher uid. Otherwise dbms like postgres may return rows in arbitrary order,
5407  // leading to hard to debug issues. This is especially relevant for the
5408  // discardWorkspaceVersionsOfRecord() call below.
5409  ->addOrderBy('uid');
5410 
5411  if ($isPageTranslation) {
5412  // Only delete records in the specified language
5413  $queryBuilder->where(
5414  $queryBuilder->expr()->eq(
5415  'pid',
5416  $queryBuilder->createNamedParameter($pageIdInDefaultLanguage, ‪Connection::PARAM_INT)
5417  ),
5418  $queryBuilder->expr()->eq(
5419  ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'],
5420  $queryBuilder->createNamedParameter($pageLanguageId, ‪Connection::PARAM_INT)
5421  )
5422  );
5423  } else {
5424  // Delete all records on this page
5425  $queryBuilder->where(
5426  $queryBuilder->expr()->eq(
5427  'pid',
5428  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
5429  )
5430  );
5431  }
5432 
5433  $currentUserWorkspace = $this->BE_USER->workspace;
5434  if ($currentUserWorkspace !== 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5435  // If we are in a workspace, make sure only records of this workspace are deleted.
5436  $queryBuilder->andWhere(
5437  $queryBuilder->expr()->eq(
5438  't3ver_wsid',
5439  $queryBuilder->createNamedParameter($currentUserWorkspace, ‪Connection::PARAM_INT)
5440  )
5441  );
5442  }
5443 
5444  $statement = $queryBuilder->executeQuery();
5445 
5446  while ($row = $statement->fetchAssociative()) {
5447  // Delete any further workspace overlays of the record in question, then delete the record.
5448  $this->‪discardWorkspaceVersionsOfRecord($table, $row['uid']);
5449  $this->‪deleteRecord($table, (int)$row['uid'], true, $forceHardDelete);
5450  }
5451  }
5452  }
5453 
5454  // Delete any further workspace overlays of the record in question, then delete the record.
5455  $this->‪discardWorkspaceVersionsOfRecord('pages', $uid);
5456  $this->‪deleteRecord('pages', $uid, true, $forceHardDelete);
5457  }
5458 
5466  public function ‪canDeletePage(‪$uid)
5467  {
5468  ‪$uid = (int)‪$uid;
5469  $isTranslatedPage = null;
5470 
5471  // If we may at all delete this page
5472  // If this is a page translation, do the check against the perms_* of the default page
5473  // Because it is currently only deleting the translation
5474  $defaultLanguagePageId = $this->‪getDefaultLanguagePageId($uid);
5475  if ($defaultLanguagePageId !== ‪$uid) {
5476  if ($this->‪doesRecordExist('pages', (int)$defaultLanguagePageId, ‪Permission::PAGE_DELETE)) {
5477  $isTranslatedPage = true;
5478  } else {
5479  return 'Attempt to delete page without permissions';
5480  }
5481  } elseif (!$this->‪doesRecordExist('pages', $uid, ‪Permission::PAGE_DELETE)) {
5482  return 'Attempt to delete page without permissions';
5483  }
5484 
5485  $pagesInBranch = $this->‪doesBranchExist($uid, ‪Permission::PAGE_DELETE);
5486  if ($pagesInBranch === null) {
5487  return 'Attempt to delete pages in branch without permissions';
5488  }
5489 
5490  $pagesInBranch[] = ‪$uid;
5491 
5492  if ($disallowedTables = $this->‪checkForRecordsFromDisallowedTables($pagesInBranch)) {
5493  return 'Attempt to delete records from disallowed tables (' . implode(', ', $disallowedTables) . ')';
5494  }
5495 
5496  foreach ($pagesInBranch as $pageInBranch) {
5497  if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, !$isTranslatedPage)) {
5498  return 'Attempt to delete page which has prohibited localizations';
5499  }
5500  }
5501  return $pagesInBranch;
5502  }
5503 
5512  public function ‪cannotDeleteRecord($table, $id)
5513  {
5514  if ($table === 'pages') {
5515  $res = $this->‪canDeletePage($id);
5516  return is_array($res) ? false : $res;
5517  }
5518  if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
5519  // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
5520  $perms = ‪Permission::PAGE_EDIT;
5521  } else {
5522  $perms = ‪Permission::CONTENT_EDIT;
5523  }
5524  return $this->‪doesRecordExist($table, $id, $perms) ? false : 'No permission to delete record';
5525  }
5526 
5536  public function ‪deleteRecord_procFields($table, ‪$uid): void
5537  {
5538  $conf = ‪$GLOBALS['TCA'][$table]['columns'];
5539  $row = BackendUtility::getRecord($table, ‪$uid, '*', '', false);
5540  if (empty($row)) {
5541  return;
5542  }
5543  foreach ($row as $field => $value) {
5544  $this->‪deleteRecord_procBasedOnFieldType($table, ‪$uid, $value, $conf[$field]['config'] ?? []);
5545  }
5546  }
5547 
5559  public function ‪deleteRecord_procBasedOnFieldType($table, ‪$uid, $value, $conf): void
5560  {
5561  if (!isset($conf['type'])) {
5562  return;
5563  }
5564 
5565  if ($conf['type'] === 'inline' || $conf['type'] === 'file') {
5566  if (in_array($this->‪getRelationFieldType($conf), ['list', 'field'], true)) {
5567  $dbAnalysis = $this->‪createRelationHandlerInstance();
5568  $dbAnalysis->start($value, $conf['foreign_table'], '', ‪$uid, $table, $conf);
5569  $dbAnalysis->undeleteRecord = true;
5570 
5571  // non type save comparison is intended!
5572  if (!isset($conf['behaviour']['enableCascadingDelete'])
5573  || $conf['behaviour']['enableCascadingDelete'] != false
5574  ) {
5575  // Walk through the items and remove them
5576  foreach ($dbAnalysis->itemArray as $v) {
5577  $this->‪deleteAction($v['table'], $v['id']);
5578  }
5579  }
5580  }
5581  } elseif ($this->‪isReferenceField($conf)) {
5582  $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5583  $dbAnalysis = $this->‪createRelationHandlerInstance();
5584  $dbAnalysis->start($value, $allowedTables, $conf['MM'] ?? '', ‪$uid, $table, $conf);
5585  foreach ($dbAnalysis->itemArray as $v) {
5586  $this->‪updateRefIndex($v['table'], $v['id']);
5587  }
5588  }
5589  }
5590 
5598  public function ‪deleteL10nOverlayRecords($table, ‪$uid): void
5599  {
5600  // Check whether table can be localized
5601  if (!BackendUtility::isTableLocalizable($table)) {
5602  return;
5603  }
5604 
5605  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
5606  $queryBuilder->getRestrictions()->removeAll()
5607  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
5608  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
5609 
5610  $queryBuilder->select('*')
5611  ->from($table)
5612  ->where(
5613  $queryBuilder->expr()->eq(
5614  ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5615  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
5616  )
5617  );
5618 
5619  $result = $queryBuilder->executeQuery();
5620  while (‪$record = $result->fetchAssociative()) {
5621  // Ignore workspace delete placeholders. Those records have been marked for
5622  // deletion before - deleting them again in a workspace would revert that state.
5623  if ((int)$this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5624  BackendUtility::workspaceOL($table, ‪$record, $this->BE_USER->workspace);
5625  if (VersionState::tryFrom(‪$record['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) {
5626  continue;
5627  }
5628  }
5629  $this->‪deleteAction($table, (int)(‪$record['t3ver_oid'] ?? 0) > 0 ? (int)‪$record['t3ver_oid'] : (int)‪$record['uid']);
5630  }
5631  }
5632 
5633  /*********************************************
5634  *
5635  * Cmd: undelete / restore
5636  *
5637  ********************************************/
5638 
5649  protected function ‪undeleteRecord(string $table, int ‪$uid): void
5650  {
5651  ‪$record = BackendUtility::getRecord($table, ‪$uid, '*', '', false);
5652  $deleteField = (string)(‪$GLOBALS['TCA'][$table]['ctrl']['delete'] ?? '');
5653  $timestampField = (string)(‪$GLOBALS['TCA'][$table]['ctrl']['tstamp'] ?? '');
5654 
5655  if (‪$record === null
5656  || $deleteField === ''
5657  || !isset(‪$record[$deleteField])
5658  || (bool)‪$record[$deleteField] === false
5659  || ($timestampField !== '' && !isset(‪$record[$timestampField]))
5660  || (int)$this->BE_USER->workspace > 0
5661  || (BackendUtility::isTableWorkspaceEnabled($table) && (int)(‪$record['t3ver_wsid'] ?? 0) > 0)
5662  ) {
5663  // Return early and silently, if:
5664  // * Record not found
5665  // * Table is not soft-delete aware
5666  // * Record does not have deleted field - db analyzer not up-to-date?
5667  // * Record is not deleted - may eventually happen via recursion with self referencing records?
5668  // * Table is tstamp aware, but field does not exist - db analyzer not up-to-date?
5669  // * User is in a workspace - does not make sense
5670  // * Record is in a workspace - workspace records are not soft-delete aware
5671  return;
5672  }
5673 
5674  $recordPid = (int)(‪$record['pid'] ?? 0);
5675  if ($recordPid > 0) {
5676  // Record is not on root level. Parent page record must exist and must not be deleted itself.
5677  $page = BackendUtility::getRecord('pages', $recordPid, 'deleted', '', false);
5678  if ($page === null || !isset($page['deleted']) || (bool)$page['deleted'] === true) {
5679  $this->‪log(
5680  $table,
5681  ‪$uid,
5682  SystemLogDatabaseAction::DELETE,
5683  0,
5684  SystemLogErrorClassification::USER_ERROR,
5685  'Record "{table}:{uid}" can\'t be restored: The page "{pid}" containing it does not exist or is soft-deleted',
5686  0,
5687  [
5688  'table' => $table,
5689  'uid' => ‪$uid,
5690  'pid' => $recordPid,
5691  ],
5692  $recordPid
5693  );
5694  return;
5695  }
5696  }
5697 
5698  // @todo: When restoring a not-default language record, it should be verified the default language
5699  // @todo: record is *not* set to deleted. Maybe even verify a possible l10n_source chain is not deleted?
5700 
5701  if (!$this->BE_USER->recordEditAccessInternals($table, ‪$record, false, true)) {
5702  // User misses access permissions to record
5703  $this->‪log(
5704  $table,
5705  ‪$uid,
5706  SystemLogDatabaseAction::DELETE,
5707  0,
5708  SystemLogErrorClassification::USER_ERROR,
5709  'Record "{table}:{uid}" can\'t be restored: Insufficient user permissions',
5710  0,
5711  [
5712  'table' => $table,
5713  'uid' => ‪$uid,
5714  ],
5715  $recordPid
5716  );
5717  return;
5718  }
5719 
5720  // Restore referenced child records
5722 
5723  // Restore record
5724  $updateFields[$deleteField] = 0;
5725  if ($timestampField !== '') {
5726  $updateFields[$timestampField] = ‪$GLOBALS['EXEC_TIME'];
5727  }
5728  $this->connectionPool->getConnectionForTable($table)
5729  ->update(
5730  $table,
5731  $updateFields,
5732  ['uid' => ‪$uid]
5733  );
5734 
5735  if ($this->enableLogging) {
5736  $this->‪log(
5737  $table,
5738  ‪$uid,
5739  SystemLogDatabaseAction::INSERT,
5740  0,
5741  SystemLogErrorClassification::MESSAGE,
5742  'Record "{table}:{uid}" was restored on page {pid}',
5743  0,
5744  [
5745  'table' => $table,
5746  'uid' => ‪$uid,
5747  'pid' => $recordPid,
5748  ],
5749  $recordPid
5750  );
5751  }
5752 
5753  // Register cache clearing of page, or parent page if a page is restored.
5754  $this->‪registerRecordIdForPageCacheClearing($table, ‪$uid, $recordPid);
5755  // Add history entry
5756  $this->‪getRecordHistoryStore()->undeleteRecord($table, ‪$uid, $this->correlationId);
5757  // Update reference index with table/uid on left side (recuid)
5758  $this->‪updateRefIndex($table, ‪$uid);
5759  // Update reference index with table/uid on right side (ref_uid). Important if children of a relation were restored.
5760  $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, ‪$uid, 0);
5761  }
5762 
5771  protected function ‪undeleteRecordRelations(string $table, int ‪$uid, array ‪$record): void
5772  {
5773  foreach (‪$record as $fieldName => $value) {
5774  $fieldConfig = ‪$GLOBALS['TCA'][$table]['columns'][$fieldName]['config'] ?? [];
5775  $fieldType = (string)($fieldConfig['type'] ?? '');
5776  if (empty($fieldConfig) || !is_array($fieldConfig) || $fieldType === '') {
5777  continue;
5778  }
5779  $foreignTable = (string)($fieldConfig['foreign_table'] ?? '');
5780  if ($fieldType === 'inline' || $fieldType === 'file') {
5781  // @todo: Inline MM not handled here, and what about group / select?
5782  if (!in_array($this->‪getRelationFieldType($fieldConfig), ['list', 'field'], true)) {
5783  continue;
5784  }
5785  $relationHandler = $this->‪createRelationHandlerInstance();
5786  $relationHandler->start($value, $foreignTable, '', ‪$uid, $table, $fieldConfig);
5787  $relationHandler->undeleteRecord = true;
5788  foreach ($relationHandler->itemArray as $reference) {
5789  $this->‪undeleteRecord($reference['table'], (int)$reference['id']);
5790  }
5791  } elseif ($this->‪isReferenceField($fieldConfig)) {
5792  $allowedTables = $fieldType === 'group' ? ($fieldConfig['allowed'] ?? '') : $foreignTable;
5793  $relationHandler = $this->‪createRelationHandlerInstance();
5794  $relationHandler->start($value, $allowedTables, $fieldConfig['MM'] ?? '', ‪$uid, $table, $fieldConfig);
5795  foreach ($relationHandler->itemArray as $reference) {
5796  // @todo: Unsure if this is ok / enough. Needs coverage.
5797  $this->‪updateRefIndex($reference['table'], $reference['id']);
5798  }
5799  }
5800  }
5801  }
5802 
5803  /*********************************************
5804  *
5805  * Cmd: Workspace discard & flush
5806  *
5807  ********************************************/
5808 
5822  public function ‪discard(string $table, ?int ‪$uid, array ‪$record = null): void
5823  {
5824  if (‪$uid === null && ‪$record === null) {
5825  throw new \RuntimeException('Either record $uid or $record row must be given', 1600373491);
5826  }
5827 
5828  // Fetch record we are dealing with if not given
5829  if (‪$record === null) {
5830  ‪$record = BackendUtility::getRecord($table, (int)‪$uid);
5831  }
5832  if (!is_array(‪$record)) {
5833  return;
5834  }
5835  ‪$uid = (int)‪$record['uid'];
5836 
5837  // Call hook and return if hook took care of the element
5838  $recordWasDiscarded = false;
5839  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
5840  $hookObj = GeneralUtility::makeInstance($className);
5841  if (method_exists($hookObj, 'processCmdmap_discardAction')) {
5843  $hookObj->processCmdmap_discardAction($table, ‪$uid, ‪$record, $recordWasDiscarded);
5844  }
5845  }
5846 
5847  $userWorkspace = (int)$this->BE_USER->workspace;
5848  ‪if ($recordWasDiscarded
5849  || $userWorkspace === 0
5850  || !BackendUtility::isTableWorkspaceEnabled($table)
5851  || $this->‪hasDeletedRecord($table, ‪$uid)
5852  ) {
5853  return;
5854  }
5855 
5856  // Gather versioned record
5857  if ((int)‪$record['t3ver_wsid'] === 0) {
5858  ‪$record = BackendUtility::getWorkspaceVersionOfRecord($userWorkspace, $table, ‪$uid);
5859  }
5860  if (!is_array(‪$record)) {
5861  return;
5862  }
5863  $versionRecord = ‪$record;
5864 
5865  // User access checks
5866  if ($userWorkspace !== (int)$versionRecord['t3ver_wsid']) {
5867  $this->‪log($table, $versionRecord['uid'], SystemLogDatabaseAction::DISCARD, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to discard workspace record {table}:{uid} failed: Different workspace', -1, ['table' => $table, 'uid' => (int)$versionRecord['uid']]);
5868  return;
5869  }
5870  if ($errorCode = $this->‪workspaceCannotEditOfflineVersion($table, $versionRecord)) {
5871  $this->‪log($table, $versionRecord['uid'], SystemLogDatabaseAction::DISCARD, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to discard workspace record {table}:{uid} failed: {reason}', -1, ['table' => $table, 'uid' => (int)$versionRecord['uid'], 'reason' => $errorCode]);
5872  return;
5873  }
5874  if (!$this->‪checkRecordUpdateAccess($table, $versionRecord['uid'])) {
5875  $this->‪log($table, $versionRecord['uid'], SystemLogDatabaseAction::DISCARD, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to discard workspace record {table}:{uid} failed: User has no edit access', -1, ['table' => $table, 'uid' => (int)$versionRecord['uid']]);
5876  return;
5877  }
5878  $fullLanguageAccessCheck = !($table === 'pages' && (int)$versionRecord[‪$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] !== 0);
5879  if (!$this->BE_USER->recordEditAccessInternals($table, $versionRecord, false, true, $fullLanguageAccessCheck)) {
5880  $this->‪log($table, $versionRecord['uid'], SystemLogDatabaseAction::DISCARD, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to discard workspace record {table}:{uid} failed: User has no delete access', -1, ['table' => $table, 'uid' => (int)$versionRecord['uid']]);
5881  return;
5882  }
5883 
5884  // Perform discard operations
5885  $versionState = VersionState::tryFrom($versionRecord['t3ver_state'] ?? 0);
5886  if ($table === 'pages' && $versionState === VersionState::NEW_PLACEHOLDER) {
5887  // When discarding a new page, there can be new sub pages and new records.
5888  // Those need to be discarded, otherwise they'd end up as records without parent page.
5889  $this->‪discardSubPagesAndRecordsOnPage($versionRecord);
5890  }
5891 
5892  $this->‪discardLocalizationOverlayRecords($table, $versionRecord);
5893  $this->‪discardRecordRelations($table, $versionRecord);
5894  $this->‪discardCsvReferencesToRecord($table, $versionRecord);
5895  $this->‪hardDeleteSingleRecord($table, (int)$versionRecord['uid']);
5896  $this->deletedRecords[$table][] = (int)$versionRecord['uid'];
5897  $this->‪registerReferenceIndexRowsForDrop($table, (int)$versionRecord['uid'], $userWorkspace);
5898  $this->‪getRecordHistoryStore()->deleteRecord($table, (int)$versionRecord['uid'], $this->correlationId);
5899  $this->‪log(
5900  $table,
5901  (int)$versionRecord['uid'],
5902  SystemLogDatabaseAction::DELETE,
5903  0,
5904  SystemLogErrorClassification::MESSAGE,
5905  'Record {table}:{uid} was deleted unrecoverable from page {pid}',
5906  0,
5907  ['table' => $table, 'uid' => $versionRecord['uid'], 'pid' => $versionRecord['pid']],
5908  (int)$versionRecord['pid']
5909  );
5910  }
5911 
5918  protected function ‪discardSubPagesAndRecordsOnPage(array $page): void
5919  {
5920  $isLocalizedPage = false;
5921  $sysLanguageId = (int)$page[‪$GLOBALS['TCA']['pages']['ctrl']['languageField']];
5922  $versionState = VersionState::tryFrom($page['t3ver_state'] ?? 0);
5923  if ($sysLanguageId > 0) {
5924  // New or moved localized page.
5925  // Discard records on this page localization, but no sub pages.
5926  // Records of a translated page have the pid set to the default language page uid. Found in l10n_parent.
5927  // @todo: Discard other page translations that inherit from this?! (l10n_source field)
5928  $isLocalizedPage = true;
5929  $pid = (int)$page[‪$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']];
5930  } elseif ($versionState === VersionState::NEW_PLACEHOLDER) {
5931  // New default language page.
5932  // Discard any sub pages and all other records of this page, including any page localizations.
5933  // The t3ver_state=1 record is incoming here. Records on this page have their pid field set to the uid
5934  // of this record. So, since t3ver_state=1 does not have an online counter-part, the actual UID is used here.
5935  $pid = (int)$page['uid'];
5936  } else {
5937  // Moved default language page.
5938  // Discard any sub pages and all other records of this page, including any page localizations.
5939  $pid = (int)$page['t3ver_oid'];
5940  }
5941  $tables = $this->‪compileAdminTables();
5942  foreach ($tables as $table) {
5943  if (($isLocalizedPage && $table === 'pages')
5944  || ($isLocalizedPage && !BackendUtility::isTableLocalizable($table))
5945  || !BackendUtility::isTableWorkspaceEnabled($table)
5946  ) {
5947  continue;
5948  }
5949  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
5950  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5951  $queryBuilder->select('*')
5952  ->from($table)
5953  ->where(
5954  $queryBuilder->expr()->eq(
5955  'pid',
5956  $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)
5957  ),
5958  $queryBuilder->expr()->eq(
5959  't3ver_wsid',
5960  $queryBuilder->createNamedParameter((int)$this->BE_USER->workspace, ‪Connection::PARAM_INT)
5961  )
5962  );
5963  if ($isLocalizedPage) {
5964  // Add sys_language_uid = x restriction if discarding a localized page
5965  $queryBuilder->andWhere(
5966  $queryBuilder->expr()->eq(
5967  ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'],
5968  $queryBuilder->createNamedParameter($sysLanguageId, ‪Connection::PARAM_INT)
5969  )
5970  );
5971  }
5972  $statement = $queryBuilder->executeQuery();
5973  while ($row = $statement->fetchAssociative()) {
5974  $this->‪discard($table, null, $row);
5975  }
5976  }
5977  }
5978 
5985  protected function ‪discardRecordRelations(string $table, array ‪$record): void
5986  {
5987  foreach (‪$record as $field => $value) {
5988  $fieldConfig = ‪$GLOBALS['TCA'][$table]['columns'][$field]['config'] ?? null;
5989  if (!isset($fieldConfig['type'])) {
5990  continue;
5991  }
5992  if ($fieldConfig['type'] === 'inline' || $fieldConfig['type'] === 'file') {
5993  $foreignTable = (string)($fieldConfig['foreign_table'] ?? '');
5994  if ($foreignTable === ''
5995  || (isset($fieldConfig['behaviour']['enableCascadingDelete'])
5996  && (bool)$fieldConfig['behaviour']['enableCascadingDelete'] === false)
5997  ) {
5998  continue;
5999  }
6000  if (in_array($this->‪getRelationFieldType($fieldConfig), ['list', 'field'], true)) {
6001  $dbAnalysis = $this->‪createRelationHandlerInstance();
6002  $dbAnalysis->start($value, $fieldConfig['foreign_table'], '', (int)‪$record['uid'], $table, $fieldConfig);
6003  $dbAnalysis->undeleteRecord = true;
6004  foreach ($dbAnalysis->itemArray as $relationRecord) {
6005  $this->‪discard($relationRecord['table'], (int)$relationRecord['id']);
6006  }
6007  }
6008  } elseif ($this->‪isReferenceField($fieldConfig) && !empty($fieldConfig['MM'])) {
6009  $this->‪discardMmRelations($table, $fieldConfig, ‪$record);
6010  }
6011  // @todo not inline and not mm - probably not handled correctly and has no proper test coverage yet
6012  }
6013  }
6014 
6034  protected function ‪discardCsvReferencesToRecord(string $table, array ‪$record): void
6035  {
6036  // @see test workspaces Group Discard createContentAndCreateElementRelationAndDiscardElement
6037  // Records referencing the to-discard record.
6038  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
6039  $statement = $queryBuilder->select('tablename', 'recuid', 'field')
6040  ->from('sys_refindex')
6041  ->where(
6042  $queryBuilder->expr()->eq('workspace', $queryBuilder->createNamedParameter(‪$record['t3ver_wsid'], ‪Connection::PARAM_INT)),
6043  $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter($table)),
6044  $queryBuilder->expr()->eq('ref_uid', $queryBuilder->createNamedParameter(‪$record['uid'], ‪Connection::PARAM_INT))
6045  )
6046  ->executeQuery();
6047  while ($row = $statement->fetchAssociative()) {
6048  // For each record referencing the to-discard record, see if it is a CSV group field definition.
6049  // If so, update that record to drop both the possible "uid" and "table_name_uid" variants from the list.
6050  $fieldTca = ‪$GLOBALS['TCA'][$row['tablename']]['columns'][$row['field']]['config'] ?? [];
6051  $groupAllowed = ‪GeneralUtility::trimExplode(',', $fieldTca['allowed'] ?? '', true);
6052  // @todo: "select" may be affected too, but it has no coverage to show this, yet?
6053  if (($fieldTca['type'] ?? '') === 'group'
6054  && empty($fieldTca['MM'])
6055  && (in_array('*', $groupAllowed, true) || in_array($table, $groupAllowed, true))
6056  ) {
6057  // Note it would be possible to a) update multiple records with only one DB call, and b) combine the
6058  // select and update to a single update query by doing the CSV manipulation as string function in sql.
6059  // That's harder to get right though and probably not *that* beneficial performance-wise since we're
6060  // most likely dealing with a very small number of records here anyways. Still, an optimization should
6061  // be considered after we drop TCA 'prepend_tname' handling and always rely only on "table_name_uid"
6062  // variant for CSV storage.
6063 
6064  // Get that record
6065  $recordReferencingDiscardedRecord = BackendUtility::getRecord($row['tablename'], $row['recuid'], $row['field']);
6066  if (!$recordReferencingDiscardedRecord) {
6067  continue;
6068  }
6069  // Drop "uid" and "table_name_uid" from list
6070  $listOfRelatedRecords = ‪GeneralUtility::trimExplode(',', $recordReferencingDiscardedRecord[$row['field']], true);
6071  $listOfRelatedRecordsWithoutDiscardedRecord = array_diff($listOfRelatedRecords, [‪$record['uid'], $table . '_' . ‪$record['uid']]);
6072  if ($listOfRelatedRecords !== $listOfRelatedRecordsWithoutDiscardedRecord) {
6073  // Update record if list changed
6074  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($row['tablename']);
6075  $queryBuilder->update($row['tablename'])
6076  ->set($row['field'], implode(',', $listOfRelatedRecordsWithoutDiscardedRecord))
6077  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($row['recuid'], ‪Connection::PARAM_INT)))
6078  ->executeStatement();
6079  }
6080  }
6081  }
6082  }
6083 
6092  protected function ‪discardMmRelations(string $table, array $fieldConfig, array ‪$record): void
6093  {
6094  $recordUid = (int)‪$record['uid'];
6095  $mmTableName = $fieldConfig['MM'];
6096  // left - non foreign - uid_local vs. right - foreign - uid_foreign decision
6097  $relationUidFieldName = isset($fieldConfig['MM_opposite_field']) ? 'uid_foreign' : 'uid_local';
6098  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($mmTableName);
6099  $queryBuilder->delete($mmTableName)->where(
6100  // uid_local = given uid OR uid_foreign = given uid
6101  $queryBuilder->expr()->eq($relationUidFieldName, $queryBuilder->createNamedParameter($recordUid, ‪Connection::PARAM_INT))
6102  );
6103  if (!empty($fieldConfig['MM_table_where']) && is_string($fieldConfig['MM_table_where'])) {
6104  $queryBuilder->andWhere(
6105  ‪QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$recordUid, ‪QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), $fieldConfig['MM_table_where'])))
6106  );
6107  }
6108  $mmMatchFields = $fieldConfig['MM_match_fields'] ?? [];
6109  foreach ($mmMatchFields as $fieldName => $fieldValue) {
6110  $queryBuilder->andWhere(
6111  $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($fieldValue))
6112  );
6113  }
6114  $queryBuilder->executeStatement();
6115 
6116  // refindex treatment for mm relation handling: If the to discard record is foreign side of an mm relation,
6117  // there may be other refindex rows that become obsolete when that record is discarded. See Modify
6118  // addCategoryRelation sys_category-29->tt_content-298. We thus register an update for references
6119  // to this item (right side - ref_table, ref_uid) in reference index updater to catch these.
6120  if ($relationUidFieldName === 'uid_foreign') {
6121  $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, $recordUid, (int)‪$record['t3ver_wsid']);
6122  }
6123  }
6124 
6131  protected function ‪discardLocalizationOverlayRecords(string $table, array ‪$record): void
6132  {
6133  if (!BackendUtility::isTableLocalizable($table)) {
6134  return;
6135  }
6136  ‪$uid = (int)‪$record['uid'];
6137  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
6138  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6139  $statement = $queryBuilder->select('*')
6140  ->from($table)
6141  ->where(
6142  $queryBuilder->expr()->eq(
6143  ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
6144  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
6145  ),
6146  $queryBuilder->expr()->eq(
6147  't3ver_wsid',
6148  $queryBuilder->createNamedParameter((int)$this->BE_USER->workspace, ‪Connection::PARAM_INT)
6149  )
6150  )
6151  ->executeQuery();
6152  while (‪$record = $statement->fetchAssociative()) {
6153  $this->‪discard($table, null, ‪$record);
6154  }
6155  }
6156 
6157  /*********************************************
6158  *
6159  * Cmd: Versioning
6160  *
6161  ********************************************/
6174  public function ‪versionizeRecord($table, $id, $label, $delete = false)
6175  {
6176  $id = (int)$id;
6177  // Stop any actions if the record is marked to be deleted:
6178  // (this can occur if IRRE elements are versionized and child elements are removed)
6179  if ($this->‪isElementToBeDeleted($table, $id)) {
6180  return null;
6181  }
6182  if (!BackendUtility::isTableWorkspaceEnabled($table) || $id <= 0) {
6183  $this->‪log($table, $id, SystemLogDatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Versioning is not supported for this table {table}:{uid}', -1, ['table' => $table, 'uid' => (int)$id]);
6184  return null;
6185  }
6186 
6187  // Fetch record with permission check
6188  $row = $this->‪recordInfoWithPermissionCheck($table, $id, ‪Permission::PAGE_SHOW);
6189 
6190  // This checks if the record can be selected which is all that a copy action requires.
6191  if ($row === false) {
6192  $this->‪log($table, $id, SystemLogDatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'The record does not exist or you don\'t have correct permissions to make a new version (copy) of this record "{table}:{uid}"', -1, ['table' => $table, 'uid' => (int)$id]);
6193  return null;
6194  }
6195 
6196  // Record must be online record, otherwise we would create a version of a version
6197  if (($row['t3ver_oid'] ?? 0) > 0) {
6198  $this->‪log($table, $id, SystemLogDatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Record "{table}:{uid}" you wanted to versionize was already a version in archive (record has an online ID)', -1, ['table' => $table, 'uid' => (int)$id]);
6199  return null;
6200  }
6201 
6202  if ($delete && $errorCode = $this->‪cannotDeleteRecord($table, $id)) {
6203  $this->‪log($table, $id, SystemLogDatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Record {table}:{uid} cannot be deleted: {reason}', -1, ['table' => $table, 'uid' => (int)$id, 'reason' => $errorCode]);
6204  return null;
6205  }
6206 
6207  // Set up the values to override when making a raw-copy:
6208  $overrideArray = [
6209  't3ver_oid' => $id,
6210  't3ver_wsid' => $this->BE_USER->workspace,
6211  't3ver_state' => $delete ? VersionState::DELETE_PLACEHOLDER->value : VersionState::DEFAULT_STATE->value,
6212  't3ver_stage' => 0,
6213  ];
6214  if (‪$GLOBALS['TCA'][$table]['ctrl']['editlock'] ?? false) {
6215  $overrideArray[‪$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
6216  }
6217  // Checking if the record already has a version in the current workspace of the backend user
6218  $versionRecord = ['uid' => null];
6219  if ($this->BE_USER->workspace !== 0) {
6220  // Look for version already in workspace:
6221  $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
6222  }
6223  // Create new version of the record and return the new uid
6224  if (empty($versionRecord['uid'])) {
6225  // Create raw-copy and return result:
6226  // The information of the label to be used for the workspace record
6227  // as well as the information whether the record shall be removed
6228  // must be forwarded (creating delete placeholders on a workspace are
6229  // done by copying the record and override several fields).
6230  $workspaceOptions = [
6231  'delete' => $delete,
6232  'label' => $label,
6233  ];
6234  return $this->‪copyRecord_raw($table, $id, (int)$row['pid'], $overrideArray, $workspaceOptions);
6235  }
6236  // Reuse the existing record and return its uid
6237  // (prior to TYPO3 CMS 6.2, an error was thrown here, which
6238  // did not make much sense since the information is available)
6239  return $versionRecord['uid'];
6240  }
6241 
6255  public function ‪versionPublishManyToManyRelations(string $table, array $liveRecord, array $workspaceRecord, int $fromWorkspace): void
6256  {
6257  if (!is_array(‪$GLOBALS['TCA'][$table]['columns'])) {
6258  return;
6259  }
6260  $toDeleteRegistry = [];
6261  $toUpdateRegistry = [];
6262  foreach (‪$GLOBALS['TCA'][$table]['columns'] as $dbFieldName => $dbFieldConfig) {
6263  if (empty($dbFieldConfig['config']['type'])) {
6264  continue;
6265  }
6266  if (!empty($dbFieldConfig['config']['MM']) && $this->‪isReferenceField($dbFieldConfig['config'])) {
6267  $toDeleteRegistry[] = $dbFieldConfig['config'];
6268  $toUpdateRegistry[] = $dbFieldConfig['config'];
6269  }
6270  if ($dbFieldConfig['config']['type'] === 'flex') {
6271  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
6272  // Find possible mm tables attached to live record flex from data structures, mark as to delete
6273  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier($dbFieldConfig, $table, $dbFieldName, $liveRecord);
6274  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
6275  foreach (($dataStructureArray['sheets'] ?? []) as $flexSheetDefinition) {
6276  foreach (($flexSheetDefinition['ROOT']['el'] ?? []) as $flexFieldDefinition) {
6277  if (is_array($flexFieldDefinition) && $this->‪flexFieldDefinitionIsMmRelation($flexFieldDefinition)) {
6278  $toDeleteRegistry[] = $flexFieldDefinition['config'];
6279  }
6280  }
6281  }
6282  // Find possible mm tables attached to workspace record flex from data structures, mark as to update uid
6283  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier($dbFieldConfig, $table, $dbFieldName, $workspaceRecord);
6284  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
6285  foreach (($dataStructureArray['sheets'] ?? []) as $flexSheetDefinition) {
6286  foreach (($flexSheetDefinition['ROOT']['el'] ?? []) as $flexFieldDefinition) {
6287  if (is_array($flexFieldDefinition) && $this->‪flexFieldDefinitionIsMmRelation($flexFieldDefinition)) {
6288  $toUpdateRegistry[] = $flexFieldDefinition['config'];
6289  }
6290  }
6291  }
6292  }
6293  }
6294 
6295  // Delete mm table relations of live record
6296  foreach ($toDeleteRegistry as $config) {
6297  $uidFieldName = $this->‪mmRelationIsLocalSide($config) ? 'uid_local' : 'uid_foreign';
6298  $mmTableName = $config['MM'];
6299  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($mmTableName);
6300  $queryBuilder->delete($mmTableName);
6301  $queryBuilder->where($queryBuilder->expr()->eq(
6302  $uidFieldName,
6303  $queryBuilder->createNamedParameter((int)$liveRecord['uid'], ‪Connection::PARAM_INT)
6304  ));
6305  if ($this->‪mmQueryShouldUseTablenamesColumn($config)) {
6306  $queryBuilder->andWhere($queryBuilder->expr()->eq(
6307  'tablenames',
6308  $queryBuilder->createNamedParameter($table)
6309  ));
6310  }
6311  $queryBuilder->executeStatement();
6312  }
6313 
6314  // Update mm table relations of workspace record to uid of live record
6315  foreach ($toUpdateRegistry as $config) {
6316  $mmRelationIsLocalSide = $this->‪mmRelationIsLocalSide($config);
6317  $uidFieldName = $mmRelationIsLocalSide ? 'uid_local' : 'uid_foreign';
6318  $mmTableName = $config['MM'];
6319  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($mmTableName);
6320  $queryBuilder->update($mmTableName);
6321  $queryBuilder->set($uidFieldName, (int)$liveRecord['uid'], true, ‪Connection::PARAM_INT);
6322  $queryBuilder->where($queryBuilder->expr()->eq(
6323  $uidFieldName,
6324  $queryBuilder->createNamedParameter((int)$workspaceRecord['uid'], ‪Connection::PARAM_INT)
6325  ));
6326  if ($this->‪mmQueryShouldUseTablenamesColumn($config)) {
6327  $queryBuilder->andWhere($queryBuilder->expr()->eq(
6328  'tablenames',
6329  $queryBuilder->createNamedParameter($table)
6330  ));
6331  }
6332  $queryBuilder->executeStatement();
6333 
6334  if (!$mmRelationIsLocalSide) {
6335  // refindex treatment for mm relation handling: If the to publish record is foreign side of an mm relation, we need
6336  // to instruct refindex updater to update all local side references for the live record the current workspace record
6337  // has on foreign side. See ManyToMany Publish addCategoryRelation, this will create the sys_category-31->tt_content-297 entry.
6338  $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, (int)$workspaceRecord['uid'], $fromWorkspace, 0);
6339  // Similar, when in mm foreign side and relations are deleted in live during publish, other relations pointing to the
6340  // same local side record may need updates due to different sorting, and the former refindex entry of the live record
6341  // needs updates. See ManyToMany Publish deleteCategoryRelation scenario.
6342  $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, (int)$liveRecord['uid'], 0);
6343  }
6344  }
6345  }
6346 
6351  private function ‪flexFieldDefinitionIsMmRelation(array $flexFieldDefinition): bool
6352  {
6353  return ($flexFieldDefinition['type'] ?? '') !== 'array' // is a field, not a section
6354  && is_array($flexFieldDefinition['config'] ?? false) // config array exists
6355  && $this->‪isReferenceField($flexFieldDefinition['config']) // select, group, category
6356  && !empty($flexFieldDefinition['config']['MM']); // MM exists
6357  }
6358 
6365  private function ‪mmQueryShouldUseTablenamesColumn(array $config): bool
6366  {
6367  if ($this->‪mmRelationIsLocalSide($config)) {
6368  return false;
6369  }
6370  if ($config['type'] === 'group' && !empty($config['prepend_tname'])) {
6371  // prepend_tname in MM on foreign side forces 'tablenames' column
6372  // @todo: See if we can get rid of prepend_tname in MM altogether?
6373  return true;
6374  }
6375  if ($config['type'] === 'group' && is_string($config['allowed'] ?? false)
6376  && (str_contains($config['allowed'], ',') || $config['allowed'] === '*')
6377  ) {
6378  // 'allowed' with *, or more than one table
6379  // @todo: Neither '*' nor 'multiple tables' make sense for MM on foreign side.
6380  // There is a hint in the docs about this, too. Sanitize in TCA bootstrap?!
6381  return true;
6382  }
6383  $localSideTableName = $config['type'] === 'group' ? $config['allowed'] ?? '' : $config['foreign_table'] ?? '';
6384  $localSideFieldName = $config['MM_opposite_field'] ?? '';
6385  $localSideAllowed = ‪$GLOBALS['TCA'][$localSideTableName]['columns'][$localSideFieldName]['config']['allowed'] ?? '';
6386  // Local side with 'allowed' = '*' or multiple tables forces 'tablenames' column
6387  return $localSideAllowed === '*' || str_contains($localSideAllowed, ',');
6388  }
6389 
6394  private function ‪mmRelationIsLocalSide(array $config): bool
6395  {
6396  return empty($config['MM_opposite_field']);
6397  }
6398 
6399  /*********************************************
6400  *
6401  * Cmd: Helper functions
6402  *
6403  ********************************************/
6404 
6408  protected function ‪getLocalTCE(): DataHandler
6409  {
6410  $copyTCE = GeneralUtility::makeInstance(DataHandler::class, $this->referenceIndexUpdater);
6411  $copyTCE->copyTree = ‪$this->copyTree;
6412  $copyTCE->enableLogging = ‪$this->enableLogging;
6413  // Transformations should NOT be carried out during copy
6414  $copyTCE->dontProcessTransformations = true;
6415  // make sure the isImporting flag is transferred, so all hooks know if
6416  // the current process is an import process
6417  $copyTCE->isImporting = ‪$this->isImporting;
6418  $copyTCE->bypassAccessCheckForRecords = ‪$this->bypassAccessCheckForRecords;
6419  $copyTCE->bypassWorkspaceRestrictions = ‪$this->bypassWorkspaceRestrictions;
6420  return $copyTCE;
6421  }
6422 
6427  public function ‪remapListedDBRecords(): void
6428  {
6429  if (!empty($this->registerDBList)) {
6430  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
6431  foreach ($this->registerDBList as $table => $records) {
6432  foreach ($records as ‪$uid => ‪$fields) {
6433  $newData = [];
6434  $theUidToUpdate = $this->copyMappingArray_merged[$table][‪$uid] ?? null;
6435  $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
6436  foreach (‪$fields as $fieldName => $value) {
6437  $conf = ‪$GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
6438  switch ($conf['type']) {
6439  case 'group':
6440  case 'select':
6441  case 'category':
6442  $vArray = $this->‪remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
6443  if (is_array($vArray)) {
6444  $newData[$fieldName] = implode(',', $vArray);
6445  }
6446  break;
6447  case 'flex':
6448  if ($value === 'FlexForm_reference') {
6449  // This will fetch the new row for the element
6450  $origRecordRow = $this->‪recordInfo($table, $theUidToUpdate);
6451  if (is_array($origRecordRow)) {
6452  BackendUtility::workspaceOL($table, $origRecordRow);
6453  // Get current data structure and value array:
6454  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
6455  ['config' => $conf],
6456  $table,
6457  $fieldName,
6458  $origRecordRow
6459  );
6460  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
6461  $currentValueArray = ‪GeneralUtility::xml2array($origRecordRow[$fieldName]);
6462  // Do recursive processing of the XML data:
6463  $currentValueArray['data'] = $this->‪checkValue_flex_procInData($currentValueArray['data'], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
6464  // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
6465  if (is_array($currentValueArray['data'])) {
6466  $newData[$fieldName] = $this->‪checkValue_flexArray2Xml($currentValueArray);
6467  }
6468  }
6469  }
6470  break;
6471  case 'inline':
6472  $this->‪remapListedDBRecords_procInline($conf, $value, ‪$uid, $table);
6473  break;
6474  case 'file':
6475  $this->‪remapListedDBRecords_procFile($conf, $value, ‪$uid, $table);
6476  break;
6477  default:
6478  $this->logger->debug('Field type should not appear here: {type}', ['type' => $conf['type']]);
6479  }
6480  }
6481  // If any fields were changed, those fields are updated!
6482  if (!empty($newData)) {
6483  $this->‪updateDB($table, $theUidToUpdate_saveTo, $newData);
6484  }
6485  }
6486  }
6487  }
6488  }
6489 
6501  public function ‪remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue): array
6502  {
6503  // Extract parameters:
6504  [$table, ‪$uid, $field] = $pParams;
6505  // If references are set for this field, set flag so they can be corrected later:
6506  if ($this->‪isReferenceField($dsConf) && (string)$dataValue !== '') {
6507  $vArray = $this->‪remapListedDBRecords_procDBRefs($dsConf, $dataValue, ‪$uid, $table);
6508  if (is_array($vArray)) {
6509  $dataValue = implode(',', $vArray);
6510  }
6511  }
6512  // Return
6513  return ['value' => $dataValue];
6514  }
6515 
6527  public function ‪remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
6528  {
6529  // Initialize variables
6530  // Will be set TRUE if an upgrade should be done...
6531  $set = false;
6532  // Allowed tables for references.
6533  $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
6534  // Table name to prepend the UID
6535  $prependName = $conf['type'] === 'group' ? ($conf['prepend_tname'] ?? '') : '';
6536  // Which tables that should possibly not be remapped
6537  $dontRemapTables = ‪GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'] ?? '', true);
6538  // Convert value to list of references:
6539  $dbAnalysis = $this->‪createRelationHandlerInstance();
6540  $dbAnalysis->registerNonTableValues = $conf['type'] === 'select' && ($conf['allowNonIdValues'] ?? false);
6541  $dbAnalysis->start($value, $allowedTables, $conf['MM'] ?? '', $MM_localUid, $table, $conf);
6542  // Traverse those references and map IDs:
6543  foreach ($dbAnalysis->itemArray as $k => $v) {
6544  $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']] ?? 0;
6545  if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
6546  $dbAnalysis->itemArray[$k]['id'] = $mapID;
6547  $set = true;
6548  }
6549  }
6550  if (!empty($conf['MM'])) {
6551  // Purge invalid items (live/version)
6552  $dbAnalysis->purgeItemArray();
6553  if ($dbAnalysis->isPurged()) {
6554  $set = true;
6555  }
6556 
6557  // If record has been versioned/copied in this process, handle invalid relations of the live record
6558  $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
6559  $originalId = 0;
6560  if (!empty($this->copyMappingArray_merged[$table])) {
6561  $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
6562  }
6563  if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
6564  $liveRelations = $this->‪createRelationHandlerInstance();
6565  $liveRelations->setWorkspaceId(0);
6566  $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
6567  // Purge invalid relations in the live workspace ("0")
6568  $liveRelations->purgeItemArray(0);
6569  if ($liveRelations->isPurged()) {
6570  $liveRelations->writeMM($conf['MM'], $liveId, $prependName);
6571  }
6572  }
6573  }
6574  // If a change has been done, set the new value(s)
6575  if ($set) {
6576  if ($conf['MM'] ?? false) {
6577  $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
6578  } else {
6579  return $dbAnalysis->getValueArray($prependName);
6580  }
6581  }
6582  return null;
6583  }
6584 
6594  public function ‪remapListedDBRecords_procInline($conf, $value, ‪$uid, $table): void
6595  {
6596  $theUidToUpdate = $this->copyMappingArray_merged[$table][‪$uid] ?? null;
6597  if ($conf['foreign_table']) {
6598  $relationFieldType = $this->‪getRelationFieldType($conf);
6599  if ($relationFieldType === 'mm') {
6600  $this->‪remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
6601  } elseif ($relationFieldType !== false) {
6602  $dbAnalysis = $this->‪createRelationHandlerInstance();
6603  $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
6604 
6605  $updatePidForRecords = [];
6606  // Update values for specific versioned records
6607  foreach ($dbAnalysis->itemArray as &$item) {
6608  $updatePidForRecords[$item['table']][] = $item['id'];
6609  $versionedId = $this->‪getAutoVersionId($item['table'], $item['id']);
6610  if ($versionedId !== null) {
6611  $updatePidForRecords[$item['table']][] = $versionedId;
6612  $item['id'] = $versionedId;
6613  }
6614  }
6615 
6616  // Update child records if using pointer fields ('foreign_field'):
6617  if ($relationFieldType === 'field') {
6618  $dbAnalysis->writeForeignField($conf, ‪$uid, $theUidToUpdate);
6619  }
6620  $thePidToUpdate = null;
6621  // If the current field is set on a page record, update the pid of related child records:
6622  if ($table === 'pages') {
6623  $thePidToUpdate = $theUidToUpdate;
6624  } elseif (isset($this->registerDBPids[$table][‪$uid])) {
6625  $thePidToUpdate = $this->registerDBPids[$table][‪$uid];
6626  $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate] ?? null;
6627  }
6628 
6629  // Update child records if change to pid is required
6630  if ($thePidToUpdate && !empty($updatePidForRecords)) {
6631  // Ensure that only the default language page is used as PID
6632  $thePidToUpdate = $this->‪getDefaultLanguagePageId($thePidToUpdate);
6633  // @todo: this can probably go away
6634  // ensure, only live page ids are used as 'pid' values
6635  $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
6636  if ($liveId !== null) {
6637  $thePidToUpdate = $liveId;
6638  }
6639  $updateValues = ['pid' => $thePidToUpdate];
6640  foreach ($updatePidForRecords as $tableName => $uids) {
6641  if (empty($tableName)) {
6642  continue;
6643  }
6644  $conn = $this->connectionPool->getConnectionForTable($tableName);
6645  foreach ($uids as $updateUid) {
6646  $conn->update($tableName, $updateValues, ['uid' => $updateUid]);
6647  }
6648  }
6649  }
6650  }
6651  }
6652  }
6653 
6659  public function ‪remapListedDBRecords_procFile($conf, $value, ‪$uid, $table): void
6660  {
6661  $thePidToUpdate = null;
6662  $updatePidForRecords = [];
6663  $theUidToUpdate = $this->copyMappingArray_merged[$table][‪$uid] ?? null;
6664 
6665  $dbAnalysis = $this->‪createRelationHandlerInstance();
6666  $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
6667 
6668  foreach ($dbAnalysis->itemArray as &$item) {
6669  $updatePidForRecords[$item['table']][] = $item['id'];
6670  $versionedId = $this->‪getAutoVersionId($item['table'], $item['id']);
6671  if ($versionedId !== null) {
6672  $updatePidForRecords[$item['table']][] = $versionedId;
6673  $item['id'] = $versionedId;
6674  }
6675  }
6676  unset($item);
6677 
6678  $dbAnalysis->writeForeignField($conf, ‪$uid, $theUidToUpdate);
6679 
6680  if ($table === 'pages') {
6681  $thePidToUpdate = $theUidToUpdate;
6682  } elseif (isset($this->registerDBPids[$table][‪$uid])) {
6683  $thePidToUpdate = $this->registerDBPids[$table][‪$uid];
6684  $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate] ?? null;
6685  }
6686 
6687  if ($thePidToUpdate && $updatePidForRecords !== []) {
6688  $thePidToUpdate = $this->‪getDefaultLanguagePageId($thePidToUpdate);
6689  $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
6690  if ($liveId !== null) {
6691  $thePidToUpdate = $liveId;
6692  }
6693  $updateValues = ['pid' => $thePidToUpdate];
6694  foreach ($updatePidForRecords as $tableName => $uids) {
6695  if (empty($tableName)) {
6696  continue;
6697  }
6698  $conn = $this->connectionPool->getConnectionForTable($tableName);
6699  foreach ($uids as $updateUid) {
6700  $conn->update($tableName, $updateValues, ['uid' => $updateUid]);
6701  }
6702  }
6703  }
6704  }
6705 
6711  public function ‪processRemapStack(): void
6712  {
6713  // Processes the remap stack:
6714  $remapFlexForms = [];
6715  $hookPayload = [];
6716 
6717  $newValue = null;
6718  foreach ($this->remapStack as $remapAction) {
6719  // If no position index for the arguments was set, skip this remap action:
6720  if (!is_array($remapAction['pos'])) {
6721  continue;
6722  }
6723  // Load values from the argument array in remapAction:
6724  $isNew = false;
6725  $field = $remapAction['field'];
6726  $id = $remapAction['args'][$remapAction['pos']['id']];
6727  $rawId = $id;
6728  $table = $remapAction['args'][$remapAction['pos']['table']];
6729  $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
6730  $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
6731  $additionalData = $remapAction['additionalData'] ?? [];
6732  // The record is new and has one or more new ids (in case of versioning/workspaces):
6733  if (str_contains($id, 'NEW')) {
6734  $isNew = true;
6735  // Replace NEW...-ID with real uid:
6736  $id = $this->substNEWwithIDs[$id] ?? '';
6737  // If the new parent record is on a non-live workspace or versionized, it has another new id:
6738  if (isset($this->autoVersionIdMap[$table][$id])) {
6739  $id = $this->autoVersionIdMap[$table][$id];
6740  }
6741  $remapAction['args'][$remapAction['pos']['id']] = $id;
6742  }
6743  // Replace relations to NEW...-IDs in field value (uids of child records):
6744  if (is_array($valueArray)) {
6745  foreach ($valueArray as $key => $value) {
6746  if (str_contains($value, 'NEW')) {
6747  if (!str_contains($value, '_')) {
6748  $affectedTable = $tcaFieldConf['foreign_table'] ?? '';
6749  $prependTable = false;
6750  } else {
6751  $parts = explode('_', $value);
6752  $value = array_pop($parts);
6753  $affectedTable = implode('_', $parts);
6754  $prependTable = true;
6755  }
6756  $value = $this->substNEWwithIDs[$value] ?? '';
6757  // The record is new, but was also auto-versionized and has another new id:
6758  if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
6759  $value = $this->autoVersionIdMap[$affectedTable][$value];
6760  }
6761  if ($prependTable) {
6762  $value = $affectedTable . '_' . $value;
6763  }
6764  // Set a hint that this was a new child record:
6765  $this->newRelatedIDs[$affectedTable][] = $value;
6766  $valueArray[$key] = $value;
6767  }
6768  }
6769  $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
6770  }
6771  // Process the arguments with the defined function:
6772  if (!empty($remapAction['func'])) {
6773  $callable = [$this, $remapAction['func']];
6774  if (is_callable($callable)) {
6775  $newValue = $callable(...$remapAction['args']);
6776  }
6777  }
6778  // If array is returned, check for maxitems condition, if string is returned this was already done:
6779  if (is_array($newValue)) {
6780  $newValue = implode(',', $this->‪checkValue_checkMax($tcaFieldConf, $newValue));
6781  // The reference casting is only required if
6782  // checkValue_group_select_processDBdata() returns an array
6783  $newValue = $this->‪castReferenceValue($newValue, $tcaFieldConf, $isNew);
6784  }
6785  // Update in database (list of children (csv) or number of relations (foreign_field)):
6786  if (!empty($field)) {
6787  $fieldArray = [$field => $newValue];
6788  if (‪$GLOBALS['TCA'][$table]['ctrl']['tstamp'] ?? false) {
6789  $fieldArray[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = ‪$GLOBALS['EXEC_TIME'];
6790  }
6791  $this->‪updateDB($table, $id, $fieldArray);
6792  } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
6793  // Collect data to update FlexForms
6794  $flexFormId = $additionalData['flexFormId'];
6795  $flexFormPath = $additionalData['flexFormPath'];
6796 
6797  if (!isset($remapFlexForms[$flexFormId])) {
6798  $remapFlexForms[$flexFormId] = [];
6799  }
6800 
6801  $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
6802  }
6803 
6804  // Collect elements that shall trigger processDatamap_afterDatabaseOperations
6805  if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
6806  $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
6807  if (!isset($hookPayload[$table][$rawId])) {
6808  $hookPayload[$table][$rawId] = [
6809  'status' => $hookArgs['status'],
6810  'fieldArray' => $hookArgs['fieldArray'],
6811  'hookObjects' => $hookArgs['hookObjectsArr'],
6812  ];
6813  }
6814  $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
6815  }
6816  }
6817 
6818  if ($remapFlexForms) {
6819  foreach ($remapFlexForms as $flexFormId => $modifications) {
6820  $this->‪updateFlexFormData((string)$flexFormId, $modifications);
6821  }
6822  }
6823 
6824  foreach ($hookPayload as $tableName => $rawIdPayload) {
6825  foreach ($rawIdPayload as $rawId => $payload) {
6826  foreach ($payload['hookObjects'] as $hookObject) {
6827  if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
6828  continue;
6829  }
6830  $hookObject->processDatamap_afterDatabaseOperations(
6831  $payload['status'],
6832  $tableName,
6833  $rawId,
6834  $payload['fieldArray'],
6835  $this
6836  );
6837  }
6838  }
6839  }
6840  // Processes the remap stack actions:
6841  foreach ($this->remapStackActions as $action) {
6842  if (isset($action['callback'], $action['arguments'])) {
6843  $action['callback'](...$action['arguments']);
6844  }
6845  }
6846  // Reset:
6847  $this->remapStack = [];
6848  $this->remapStackRecords = [];
6849  $this->remapStackActions = [];
6850  }
6851 
6858  protected function ‪updateFlexFormData($flexFormId, array $modifications): void
6859  {
6860  [$table, ‪$uid, $field] = explode(':', $flexFormId, 3);
6861  if (!‪MathUtility::canBeInterpretedAsInteger(‪$uid) && !empty($this->substNEWwithIDs[‪$uid])) {
6862  ‪$uid = $this->substNEWwithIDs[‪$uid];
6863  }
6864  ‪$record = $this->‪recordInfo($table, ‪$uid);
6865  if (!$table || !‪$uid || !$field || !is_array(‪$record)) {
6866  return;
6867  }
6868  BackendUtility::workspaceOL($table, ‪$record);
6869  // Get current data structure and value array:
6870  $valueStructure = ‪GeneralUtility::xml2array(‪$record[$field]);
6871  // Do recursive processing of the XML data:
6872  foreach ($modifications as $path => $value) {
6873  $valueStructure['data'] = ‪ArrayUtility::setValueByPath(
6874  $valueStructure['data'],
6875  $path,
6876  $value
6877  );
6878  }
6879  if (is_array($valueStructure['data'])) {
6880  // The return value should be compiled back into XML
6881  $values = [
6882  $field => $this->‪checkValue_flexArray2Xml($valueStructure),
6883  ];
6884  $this->‪updateDB($table, ‪$uid, $values);
6885  }
6886  }
6887 
6897  public function ‪addRemapAction($table, $id, callable $callback, array $arguments): void
6898  {
6899  $this->remapStackActions[] = [
6900  'affects' => [
6901  'table' => $table,
6902  'id' => $id,
6903  ],
6904  'callback' => $callback,
6905  'arguments' => $arguments,
6906  ];
6907  }
6908 
6921  public function ‪getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &‪$registerDBList): void
6922  {
6923  if (!isset(‪$registerDBList[$table][$id]) || !is_array(‪$registerDBList[$table][$id])) {
6924  return;
6925  }
6926  foreach ($incomingFieldArray as $field => $value) {
6927  $foreignTable = ‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['foreign_table'] ?? '';
6928  if ((‪$registerDBList[$table][$id][$field] ?? false)
6929  && !empty($foreignTable)
6930  ) {
6931  $newValueArray = [];
6932  $origValueArray = is_array($value) ? $value : explode(',', $value);
6933  // Update the uids of the copied records, but also take care about new records:
6934  foreach ($origValueArray as $childId) {
6935  $newValueArray[] = $this->autoVersionIdMap[$foreignTable][$childId] ?? $childId;
6936  }
6937  // Set the changed value to the $incomingFieldArray
6938  $incomingFieldArray[$field] = implode(',', $newValueArray);
6939  }
6940  }
6941  // Clean up the $registerDBList array:
6942  unset(‪$registerDBList[$table][$id]);
6943  if (empty(‪$registerDBList[$table])) {
6944  unset(‪$registerDBList[$table]);
6945  }
6946  }
6947 
6954  protected function ‪hardDeleteSingleRecord(string $table, int ‪$uid): void
6955  {
6956  $this->connectionPool->getConnectionForTable($table)
6957  ->delete($table, ['uid' => ‪$uid], [‪Connection::PARAM_INT]);
6958  }
6959 
6960  /*****************************
6961  *
6962  * Access control / Checking functions
6963  *
6964  *****************************/
6972  public function ‪checkModifyAccessList($table)
6973  {
6974  $res = $this->admin || (!$this->‪tableAdminOnly($table) && isset($this->BE_USER->groupData['tables_modify']) && ‪GeneralUtility::inList($this->BE_USER->groupData['tables_modify'], $table));
6975  // Hook 'checkModifyAccessList': Post-processing of the state of access
6976  foreach ($this->‪getCheckModifyAccessListHookObjects() as $hookObject) {
6978  $hookObject->checkModifyAccessList($res, $table, $this);
6979  }
6980  return $res;
6981  }
6982 
6991  public function ‪isRecordInWebMount($table, $id)
6992  {
6993  if (!isset($this->isRecordInWebMount_Cache[$table . ':' . $id])) {
6994  $recP = $this->‪getRecordProperties($table, $id);
6995  $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->‪isInWebMount($recP['event_pid']);
6996  }
6997  return $this->isRecordInWebMount_Cache[$table . ':' . $id];
6998  }
6999 
7007  public function ‪isInWebMount($pid)
7008  {
7009  if (!isset($this->isInWebMount_Cache[$pid])) {
7010  $this->isInWebMount_Cache[$pid] = $this->BE_USER->isInWebMount($pid);
7011  }
7012  return $this->isInWebMount_Cache[$pid];
7013  }
7014 
7023  public function ‪checkRecordUpdateAccess($table, $id)
7024  {
7025  $res = false;
7026  if (‪$GLOBALS['TCA'][$table] && (int)$id > 0) {
7027  $cacheId = 'checkRecordUpdateAccess_' . $table . '_' . $id;
7028  // If information is cached, return it
7029  $cachedValue = $this->runtimeCache->get($cacheId);
7030  if (!empty($cachedValue)) {
7031  // @todo: This cache is at least broken with false results.
7032  // Caching 'false' as result below makes !empty() here never kick in, so
7033  // caching negative result does not work and always triggers code execution.
7034  // Also, CF tends to mix up false as cache-value with 'there is no cache entry',
7035  // depending on used cache backend, which also may be the reason int 1 is used
7036  // instead of bool true, so '@return bool' annotation is clearly invalid.
7037  // Note there is another cache in doesRecordExist_pageLookUp() code path, too.
7038  return $cachedValue;
7039  }
7040  if ($table === 'pages' || ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap))) {
7041  // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
7042  $perms = ‪Permission::PAGE_EDIT;
7043  } else {
7044  $perms = ‪Permission::CONTENT_EDIT;
7045  }
7046  if ($this->‪doesRecordExist($table, $id, $perms)) {
7047  $res = 1;
7048  }
7049  // Cache the result
7050  $this->runtimeCache->set($cacheId, $res);
7051  }
7052  return $res;
7053  }
7054 
7064  public function ‪checkRecordInsertAccess($insertTable, $pid, $action = SystemLogDatabaseAction::INSERT)
7065  {
7066  $pid = (int)$pid;
7067  if ($pid < 0) {
7068  return false;
7069  }
7070  // If information is cached, return it
7071  if (isset($this->recInsertAccessCache[$insertTable][$pid])) {
7072  return $this->recInsertAccessCache[$insertTable][$pid];
7073  }
7074 
7075  $res = false;
7076  if ($insertTable === 'pages') {
7077  $perms = ‪Permission::PAGE_NEW;
7078  } elseif (($insertTable === 'sys_file_reference') && array_key_exists('pages', $this->datamap)) {
7079  // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
7080  $perms = ‪Permission::PAGE_EDIT;
7081  } else {
7082  $perms = ‪Permission::CONTENT_EDIT;
7083  }
7084  $pageExists = (bool)$this->‪doesRecordExist('pages', $pid, $perms);
7085  // If either admin and root-level or if page record exists and 1) if 'pages' you may create new ones 2) if page-content, new content items may be inserted on the $pid page
7086  if ($pageExists || $pid === 0 && ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($insertTable))) {
7087  // Check permissions
7088  if ($this->‪isTableAllowedForThisPage($pid, $insertTable)) {
7089  $res = true;
7090  // Cache the result
7091  $this->recInsertAccessCache[$insertTable][$pid] = $res;
7092  } elseif ($this->enableLogging) {
7093  $propArr = $this->‪getRecordProperties('pages', $pid);
7094  $this->‪log($insertTable, $pid, $action, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to insert record on page "{pageTitle}" ({pid}) where table "{table}" is not allowed', 11, ['pageTitle' => $propArr['header'], 'pid' => $pid, 'table' => $insertTable], $propArr['event_pid']);
7095  }
7096  } elseif ($this->enableLogging) {
7097  $propArr = $this->‪getRecordProperties('pages', $pid);
7098  $this->‪log($insertTable, $pid, $action, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to insert a record on page "{pageTitle}" ({pid}) from table "{table}" without permissions or non-existing page', 12, ['pageTitle' => $propArr['header'], 'pid' => $pid, 'table' => $insertTable], $propArr['event_pid']);
7099  }
7100  return $res;
7101  }
7102 
7111  protected function ‪isTableAllowedForThisPage(int $pageUid, $checkTable): bool
7112  {
7113  $rootLevelSetting = (int)(‪$GLOBALS['TCA'][$checkTable]['ctrl']['rootLevel'] ?? 0);
7114  // Check if rootLevel flag is set and we're trying to insert on rootLevel - and reversed - and that the table is not "pages" which are allowed anywhere.
7115  if ($checkTable !== 'pages' && $rootLevelSetting !== -1 && ($rootLevelSetting xor !$pageUid)) {
7116  return false;
7117  }
7118  $allowed = false;
7119  // Check root-level
7120  if (!$pageUid) {
7121  if ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($checkTable)) {
7122  $allowed = true;
7123  }
7124  return $allowed;
7125  }
7126  // Check non-root-level
7127  $doktype = $this->‪pageInfo($pageUid, 'doktype');
7128  return GeneralUtility::makeInstance(PageDoktypeRegistry::class)->isRecordTypeAllowedForDoktype($checkTable, (int)$doktype);
7129  }
7130 
7142  public function ‪doesRecordExist($table, $id, int $perms): bool
7143  {
7144  return $this->‪recordInfoWithPermissionCheck($table, $id, $perms, 'uid, pid') !== false;
7145  }
7146 
7156  protected function ‪doesRecordExist_pageLookUp($id, $perms, $columns = ['uid']): array|false
7157  {
7158  $permission = new Permission($perms);
7159  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
7160  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7161  $queryBuilder
7162  ->select(...$columns)
7163  ->from('pages')
7164  ->where($queryBuilder->expr()->eq(
7165  'uid',
7166  $queryBuilder->createNamedParameter($id, ‪Connection::PARAM_INT)
7167  ));
7168  if (!$permission->nothingIsGranted() && !$this->admin) {
7169  $queryBuilder->andWhere($this->BE_USER->getPagePermsClause($perms));
7170  }
7171  if (!$this->admin && ‪$GLOBALS['TCA']['pages']['ctrl']['editlock'] &&
7172  ($permission->editPagePermissionIsGranted() || $permission->deletePagePermissionIsGranted() || $permission->editContentPermissionIsGranted())
7173  ) {
7174  $queryBuilder->andWhere($queryBuilder->expr()->eq(
7175  ‪$GLOBALS['TCA']['pages']['ctrl']['editlock'],
7176  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
7177  ));
7178  }
7179  return $queryBuilder->executeQuery()->fetchAssociative();
7180  }
7181 
7193  protected function ‪doesBranchExist(int $pid, int $permissions, array $pageIdsInBranch = []): ?array
7194  {
7195  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
7196  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7197  $result = $queryBuilder
7198  ->select('uid', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody')
7199  ->from('pages')
7200  ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)))
7201  ->orderBy('sorting')
7202  ->executeQuery();
7203  while ($row = $result->fetchAssociative()) {
7204  // IF admin, then it's OK
7205  if ($this->admin || $this->BE_USER->doesUserHaveAccess($row, $permissions)) {
7206  $pageIdsInBranch[] = (int)$row['uid'];
7207  // Follow the subpages recursively
7208  $pageIdsInBranch = $this->‪doesBranchExist((int)$row['uid'], $permissions, $pageIdsInBranch);
7209  if ($pageIdsInBranch === null) {
7210  return null;
7211  }
7212  } else {
7213  // No permissions
7214  return null;
7215  }
7216  }
7217  return $pageIdsInBranch;
7218  }
7219 
7227  public function ‪tableReadOnly($table): bool
7228  {
7229  // Returns TRUE if table is readonly
7230  return (bool)(‪$GLOBALS['TCA'][$table]['ctrl']['readOnly'] ?? false);
7231  }
7232 
7240  public function ‪tableAdminOnly($table): bool
7241  {
7242  // Returns TRUE if table is admin-only
7243  return !empty(‪$GLOBALS['TCA'][$table]['ctrl']['adminOnly']);
7244  }
7245 
7255  public function ‪destNotInsideSelf($destinationId, $id): bool
7256  {
7257  $loopCheck = 100;
7258  $destinationId = (int)$destinationId;
7259  $id = (int)$id;
7260  if ($destinationId === $id) {
7261  return false;
7262  }
7263  while ($destinationId !== 0 && $loopCheck > 0) {
7264  $loopCheck--;
7265  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
7266  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7267  $result = $queryBuilder
7268  ->select('pid', 'uid', 't3ver_oid', 't3ver_wsid')
7269  ->from('pages')
7270  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($destinationId, ‪Connection::PARAM_INT)))
7271  ->executeQuery();
7272  if ($row = $result->fetchAssociative()) {
7273  // Ensure that the moved location is used as the PID value
7274  BackendUtility::workspaceOL('pages', $row, $this->BE_USER->workspace);
7275  if ($row['pid'] == $id) {
7276  return false;
7277  }
7278  $destinationId = (int)$row['pid'];
7279  } else {
7280  return false;
7281  }
7282  }
7283  return true;
7284  }
7285 
7293  public function ‪getExcludeListArray(): array
7294  {
7295  $list = [];
7296  if (isset($this->BE_USER->groupData['non_exclude_fields'])) {
7297  $nonExcludeFieldsArray = array_flip(‪GeneralUtility::trimExplode(',', $this->BE_USER->groupData['non_exclude_fields']));
7298  foreach (‪$GLOBALS['TCA'] as $table => $tableConfiguration) {
7299  if (isset($tableConfiguration['columns'])) {
7300  foreach ($tableConfiguration['columns'] as $field => $config) {
7301  $isExcludeField = ($config['exclude'] ?? false);
7302  $isOnlyVisibleForAdmins = (‪$GLOBALS['TCA'][$table]['columns'][$field]['displayCond'] ?? '') === 'HIDE_FOR_NON_ADMINS';
7303  $editorHasPermissionForThisField = isset($nonExcludeFieldsArray[$table . ':' . $field]);
7304  if ($isOnlyVisibleForAdmins || ($isExcludeField && !$editorHasPermissionForThisField)) {
7305  $list[] = $table . '-' . $field;
7306  }
7307  }
7308  }
7309  }
7310  }
7311  return $list;
7312  }
7313 
7322  public function ‪doesPageHaveUnallowedTables($page_uid, int $doktype): array
7323  {
7324  $page_uid = (int)$page_uid;
7325  if (!$page_uid) {
7326  // Not a number. Probably a new page
7327  return [];
7328  }
7329  $allowedTables = GeneralUtility::makeInstance(PageDoktypeRegistry::class)->getAllowedTypesForDoktype($doktype);
7330  // If all tables are allowed, return early
7331  if (in_array('*', $allowedTables, true)) {
7332  return [];
7333  }
7334  $tableList = [];
7335  $allTableNames = $this->‪compileAdminTables();
7336  foreach ($allTableNames as $table) {
7337  // If the table is not in the allowed list, check if there are records...
7338  if (in_array($table, $allowedTables, true)) {
7339  continue;
7340  }
7341  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
7342  $queryBuilder->getRestrictions()->removeAll();
7343  $count = $queryBuilder
7344  ->count('uid')
7345  ->from($table)
7346  ->where($queryBuilder->expr()->eq(
7347  'pid',
7348  $queryBuilder->createNamedParameter($page_uid, ‪Connection::PARAM_INT)
7349  ))
7350  ->executeQuery()
7351  ->fetchOne();
7352  if ($count) {
7353  $tableList[] = $table;
7354  }
7355  }
7356  return $tableList;
7357  }
7358 
7359  /*****************************
7360  *
7361  * Information lookup
7362  *
7363  *****************************/
7373  protected function ‪pageInfo(int $id, string $field): int|string|null
7374  {
7375  if (!isset($this->pageCache[$id])) {
7376  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
7377  $queryBuilder->getRestrictions()->removeAll();
7378  $row = $queryBuilder
7379  ->select('*')
7380  ->from('pages')
7381  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, ‪Connection::PARAM_INT)))
7382  ->executeQuery()
7383  ->fetchAssociative();
7384  if ($row) {
7385  $this->pageCache[$id] = $row;
7386  }
7387  }
7388  return $this->pageCache[$id][$field];
7389  }
7390 
7400  public function ‪recordInfo($table, $id)
7401  {
7402  // Skip, if searching for NEW records or there's no TCA table definition
7403  if ((int)$id === 0 || !isset(‪$GLOBALS['TCA'][$table])) {
7404  return null;
7405  }
7406  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
7407  $queryBuilder->getRestrictions()->removeAll();
7408  $result = $queryBuilder
7409  ->select('*')
7410  ->from($table)
7411  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, ‪Connection::PARAM_INT)))
7412  ->executeQuery()
7413  ->fetchAssociative();
7414  return $result ?: null;
7415  }
7416 
7427  protected function ‪recordInfoWithPermissionCheck(string $table, int $id, int $perms, string $fieldList = '*')
7428  {
7429  if ($this->bypassAccessCheckForRecords) {
7430  $columns = ‪GeneralUtility::trimExplode(',', $fieldList, true);
7431  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
7432  $queryBuilder->getRestrictions()->removeAll();
7433  ‪$record = $queryBuilder->select(...$columns)
7434  ->from($table)
7435  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, ‪Connection::PARAM_INT)))
7436  ->executeQuery()
7437  ->fetchAssociative();
7438  return ‪$record ?: false;
7439  }
7440  if (!$perms) {
7441  throw new \RuntimeException('Internal ERROR: no permissions to check for non-admin user', 1270853920);
7442  }
7443  // For all tables: Check if record exists:
7444  $isWebMountRestrictionIgnored = BackendUtility::isWebMountRestrictionIgnored($table);
7445  if (is_array(‪$GLOBALS['TCA'][$table]) && $id > 0 && ($this->admin || $isWebMountRestrictionIgnored || $this->‪isRecordInWebMount($table, $id))) {
7446  $columns = ‪GeneralUtility::trimExplode(',', $fieldList, true);
7447  if ($table !== 'pages') {
7448  // Find record without checking page
7449  // @todo: This should probably check for editlock
7450  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
7451  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7452  ‪$output = $queryBuilder
7453  ->select(...$columns)
7454  ->from($table)
7455  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, ‪Connection::PARAM_INT)))
7456  ->executeQuery()
7457  ->fetchAssociative();
7458  // If record found, check page as well:
7459  if (is_array(‪$output)) {
7460  // Looking up the page for record:
7461  $pageRec = $this->‪doesRecordExist_pageLookUp(‪$output['pid'], $perms);
7462  // Return TRUE if either a page was found OR if the PID is zero AND the user is ADMIN (in which case the record is at root-level):
7463  $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
7464  if (is_array($pageRec) || !‪$output['pid'] && ($this->admin || $isRootLevelRestrictionIgnored)) {
7465  return ‪$output;
7466  }
7467  }
7468  return false;
7469  }
7470  return $this->‪doesRecordExist_pageLookUp($id, $perms, $columns);
7471  }
7472  return false;
7473  }
7474 
7487  public function ‪getRecordProperties($table, $id, $noWSOL = false)
7488  {
7489  $row = $table === 'pages' && !$id ? ['title' => '[root-level]', 'uid' => 0, 'pid' => 0] : $this->‪recordInfo($table, $id);
7490  if (!$noWSOL) {
7491  BackendUtility::workspaceOL($table, $row);
7492  }
7493  return $this->‪getRecordPropertiesFromRow($table, $row);
7494  }
7495 
7504  public function ‪getRecordPropertiesFromRow($table, $row)
7505  {
7506  if (‪$GLOBALS['TCA'][$table]) {
7507  $liveUid = ($row['t3ver_oid'] ?? null) ?: ($row['uid'] ?? null);
7508  return [
7509  'header' => BackendUtility::getRecordTitle($table, $row),
7510  'pid' => $row['pid'] ?? null,
7511  'event_pid' => $this->‪eventPid($table, (int)$liveUid, $row['pid'] ?? null),
7512  't3ver_state' => BackendUtility::isTableWorkspaceEnabled($table) ? ($row['t3ver_state'] ?? '') : '',
7513  ];
7514  }
7515  return null;
7516  }
7517 
7525  public function ‪eventPid($table, ‪$uid, $pid)
7526  {
7527  return $table === 'pages' ? ‪$uid : $pid;
7528  }
7529 
7530  /*********************************************
7531  *
7532  * Storing data to Database Layer
7533  *
7534  ********************************************/
7544  public function ‪updateDB($table, $id, $fieldArray): void
7545  {
7546  if (is_array($fieldArray) && is_array(‪$GLOBALS['TCA'][$table]) && (int)$id) {
7547  // Do NOT update the UID field, ever!
7548  unset($fieldArray['uid']);
7549  if (!empty($fieldArray)) {
7550  $fieldArray = $this->‪insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
7551  $connection = $this->connectionPool->getConnectionForTable($table);
7552  $updateErrorMessage = '';
7553  try {
7554  // Execute the UPDATE query:
7555  $connection->update($table, $fieldArray, ['uid' => (int)$id]);
7556  } catch (DBALException $e) {
7557  $updateErrorMessage = $e->getPrevious()->getMessage();
7558  }
7559  // If succeeds, do...:
7560  if ($updateErrorMessage === '') {
7561  // Update reference index:
7562  $this->‪updateRefIndex($table, $id);
7563  // Set History data
7564  $historyEntryId = 0;
7565  if (isset($this->historyRecords[$table . ':' . $id])) {
7566  $historyEntryId = $this->‪getRecordHistoryStore()->modifyRecord($table, $id, $this->historyRecords[$table . ':' . $id], $this->correlationId);
7567  }
7568  if ($this->enableLogging) {
7569  $newRow = $fieldArray;
7570  $newRow['uid'] = $id;
7571  // Set log entry:
7572  $propArr = $this->‪getRecordPropertiesFromRow($table, $newRow);
7573  $isOfflineVersion = (bool)($newRow['t3ver_oid'] ?? 0);
7574  if ($isOfflineVersion) {
7575  $this->‪log($table, $id, SystemLogDatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, 'Record "{title}" ({table}:{uid}) was updated (Offline version)', 10, ['title' => $propArr['header'], 'table' => $table, 'uid' => $id, 'history' => $historyEntryId], $propArr['event_pid']);
7576  } else {
7577  $this->‪log($table, $id, SystemLogDatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, 'Record "{title}" ({table}:{uid}) was updated', 10, ['title' => $propArr['header'], 'table' => $table, 'uid' => $id, 'history' => $historyEntryId], $propArr['event_pid']);
7578  }
7579  }
7580  // Clear cache for relevant pages:
7581  $this->‪registerRecordIdForPageCacheClearing($table, $id);
7582  // Unset the pageCache for the id if table was page.
7583  if ($table === 'pages') {
7584  unset($this->pageCache[$id]);
7585  }
7586  } else {
7587  $this->‪log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: "{reason}" ({table}:{uid})', 12, ['reason' => $updateErrorMessage, 'table' => $table, 'uid' => $id]);
7588  }
7589  }
7590  }
7591  }
7592 
7606  public function ‪insertDB($table, $id, $fieldArray, $newVersion = false, $suggestedUid = 0, $dontSetNewIdIndex = false)
7607  {
7608  if (is_array($fieldArray) && is_array(‪$GLOBALS['TCA'][$table]) && isset($fieldArray['pid'])) {
7609  // Do NOT insert the UID field, ever!
7610  unset($fieldArray['uid']);
7611  // Check for "suggestedUid".
7612  // This feature is used by the import functionality to force a new record to have a certain UID value.
7613  // This is only recommended for use when the destination server is a passive mirror of another server.
7614  // As a security measure this feature is available only for Admin Users (for now)
7615  // The value of $this->suggestedInsertUids["table":"uid"] is either string 'DELETE' (ext:impexp) to trigger
7616  // a blind delete of any possibly existing row before insert with forced uid, or boolean true (testing-framework)
7617  // to only force the uid insert and skipping deletion of an existing row.
7618  $suggestedUid = (int)$suggestedUid;
7619  if ($this->BE_USER->isAdmin() && $suggestedUid && ($this->suggestedInsertUids[$table . ':' . $suggestedUid] ?? false)) {
7620  // When the value of ->suggestedInsertUids[...] is "DELETE" it will try to remove the previous record
7621  if ($this->suggestedInsertUids[$table . ':' . $suggestedUid] === 'DELETE') {
7622  $this->‪hardDeleteSingleRecord($table, (int)$suggestedUid);
7623  }
7624  $fieldArray['uid'] = $suggestedUid;
7625  }
7626  $fieldArray = $this->‪insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
7627  $connection = $this->connectionPool->getConnectionForTable($table);
7628  $insertErrorMessage = '';
7629  try {
7630  // Execute the INSERT query:
7631  $connection->insert($table, $fieldArray);
7632  } catch (DBALException $e) {
7633  $insertErrorMessage = $e->getPrevious()->getMessage();
7634  }
7635  // If succees, do...:
7636  if ($insertErrorMessage === '') {
7637  // Set mapping for NEW... -> real uid:
7638  // the NEW_id now holds the 'NEW....' -id
7639  $NEW_id = $id;
7640  $id = $this->‪postProcessDatabaseInsert($connection, $table, $suggestedUid);
7641 
7642  if (!$dontSetNewIdIndex) {
7643  $this->substNEWwithIDs[$NEW_id] = $id;
7644  $this->substNEWwithIDs_table[$NEW_id] = $table;
7645  }
7646  $newRow = [];
7647  if ($this->enableLogging) {
7648  $newRow = $fieldArray;
7649  $newRow['uid'] = $id;
7650  }
7651  // Update reference index:
7652  $this->‪updateRefIndex($table, $id);
7653 
7654  // Store in history
7655  $this->‪getRecordHistoryStore()->addRecord($table, $id, $newRow, $this->correlationId);
7656 
7657  if ($newVersion) {
7658  if ($this->enableLogging) {
7659  $propArr = $this->‪getRecordPropertiesFromRow($table, $newRow);
7660  $this->‪log($table, $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::MESSAGE, 'New version created "{table}:{uid}". UID of new version is "{offlineUid}"', 10, ['table' => $table, 'uid' => $fieldArray['t3ver_oid'], 'offlineUid' => $id], $propArr['event_pid'], $NEW_id);
7661  }
7662  } else {
7663  if ($this->enableLogging) {
7664  $propArr = $this->‪getRecordPropertiesFromRow($table, $newRow);
7665  $page_propArr = $this->‪getRecordProperties('pages', $propArr['pid']);
7666  $this->‪log($table, $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::MESSAGE, 'Record "{title}" ({table}:{uid}) was inserted on page "{pageTitle}" ({pid})', 10, ['title' => $propArr['header'], 'table' => $table, 'uid' => $id, 'pageTitle' => $page_propArr['header'], 'pid' => $newRow['pid']], $newRow['pid'], $NEW_id);
7667  }
7668  // Clear cache for relevant pages:
7669  $this->‪registerRecordIdForPageCacheClearing($table, $id);
7670  }
7671  return $id;
7672  }
7673  $this->‪log($table, 0, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: "{reason}" ({table}:{uid})', 12, ['reason' => $insertErrorMessage, 'table' => $table, 'uid' => $id]);
7674  }
7675  return null;
7676  }
7677 
7687  public function ‪setHistory($table, $id): void
7688  {
7689  if (isset($this->historyRecords[$table . ':' . $id])) {
7690  $this->‪getRecordHistoryStore()->modifyRecord(
7691  $table,
7692  $id,
7693  $this->historyRecords[$table . ':' . $id],
7694  $this->correlationId
7695  );
7696  }
7697  }
7698 
7699  protected function ‪getRecordHistoryStore(): ‪RecordHistoryStore
7700  {
7701  return GeneralUtility::makeInstance(
7702  RecordHistoryStore::class,
7704  (int)$this->BE_USER->user['uid'],
7705  (int)$this->BE_USER->getOriginalUserIdWhenInSwitchUserMode(),
7706  ‪$GLOBALS['EXEC_TIME'],
7707  $this->BE_USER->workspace
7708  );
7709  }
7710 
7720  public function ‪updateRefIndex($table, ‪$uid, int $workspace = null): void
7721  {
7722  if ($workspace === null) {
7723  $workspace = (int)$this->BE_USER->workspace;
7724  }
7725  $this->referenceIndexUpdater->registerForUpdate((string)$table, (int)‪$uid, $workspace);
7726  }
7727 
7739  public function ‪registerReferenceIndexRowsForDrop(string $table, int ‪$uid, int $workspace): void
7740  {
7741  $this->referenceIndexUpdater->registerForDrop($table, ‪$uid, $workspace);
7742  }
7743 
7750  public function ‪registerReferenceIndexUpdateForReferencesToItem(string $table, int ‪$uid, int $workspace, int $targetWorkspace = null): void
7751  {
7752  $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, ‪$uid, $workspace, $targetWorkspace);
7753  }
7754 
7755  /*********************************************
7756  *
7757  * Misc functions
7758  *
7759  ********************************************/
7791  public function ‪getSortNumber($table, ‪$uid, $pid)
7792  {
7793  $sortColumn = ‪$GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7794  if (!$sortColumn) {
7795  return null;
7796  }
7797 
7798  $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($table);
7799  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
7800  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7801 
7802  $queryBuilder
7803  ->select($sortColumn, 'pid', 'uid')
7804  ->from($table);
7805  if ($considerWorkspaces) {
7806  $queryBuilder->addSelect('t3ver_state');
7807  }
7808 
7809  // find and return the sorting value for the first record on that pid
7810  if ($pid >= 0) {
7811  // Fetches the first record (lowest sorting) under this pid
7812  $queryBuilder
7813  ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)));
7814 
7815  if ($considerWorkspaces) {
7816  $queryBuilder->andWhere(
7817  $queryBuilder->expr()->or(
7818  $queryBuilder->expr()->eq('t3ver_oid', 0),
7819  $queryBuilder->expr()->eq('t3ver_state', VersionState::MOVE_POINTER->value)
7820  )
7821  );
7822  }
7823  $row = $queryBuilder
7824  ->orderBy($sortColumn, 'ASC')
7825  ->addOrderBy('uid', 'ASC')
7826  ->setMaxResults(1)
7827  ->executeQuery()
7828  ->fetchAssociative();
7829 
7830  if (!empty($row)) {
7831  // The top record was the record itself, so we return its current sorting value
7832  if ($row['uid'] == ‪$uid) {
7833  return $row[$sortColumn];
7834  }
7835  // If the record sorting value < 1 we must resort all the records under this pid
7836  if ($row[$sortColumn] < 1) {
7837  $this->‪increaseSortingOfFollowingRecords($table, (int)$pid);
7838  // Lowest sorting value after full resorting is $sortIntervals
7839  return ‪$this->sortIntervals;
7840  }
7841  // Sorting number between current top element and zero
7842  return (int)floor($row[$sortColumn] / 2);
7843  }
7844  // No records, so we choose the default value as sorting-number
7845  return ‪$this->sortIntervals;
7846  }
7847 
7848  // Find and return first possible sorting value AFTER record with given uid ($pid)
7849  // Fetches the record which is supposed to be the prev record
7850  $row = $queryBuilder
7851  ->where($queryBuilder->expr()->eq(
7852  'uid',
7853  $queryBuilder->createNamedParameter(abs($pid), ‪Connection::PARAM_INT)
7854  ))
7855  ->executeQuery()
7856  ->fetchAssociative();
7857 
7858  // There is a previous record
7859  if (!empty($row)) {
7860  $row += [
7861  't3ver_state' => 0,
7862  'uid' => 0,
7863  ];
7864  // Look if the record UID happens to be a versioned record. If so, find its live version.
7865  // If this is already a moved record in workspace, this is not needed
7866  if (VersionState::tryFrom($row['t3ver_state'] ?? 0) !== VersionState::MOVE_POINTER && $lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $row['uid'], $sortColumn . ',pid,uid')) {
7867  $row = $lookForLiveVersion;
7868  } elseif ($considerWorkspaces && $this->BE_USER->workspace > 0) {
7869  // In case the previous record is moved in the workspace, we need to fetch the information from this specific record
7870  $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $row['uid'], $sortColumn . ',pid,uid,t3ver_state');
7871  if (is_array($versionedRecord) && VersionState::tryFrom($versionedRecord['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER) {
7872  $row = $versionedRecord;
7873  }
7874  }
7875  // If the record should be inserted after itself, keep the current sorting information:
7876  if ((int)$row['uid'] === (int)‪$uid) {
7877  $sortNumber = $row[$sortColumn];
7878  } else {
7879  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
7880  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7881 
7882  $queryBuilder
7883  ->select($sortColumn, 'pid', 'uid')
7884  ->from($table)
7885  ->where(
7886  $queryBuilder->expr()->eq(
7887  'pid',
7888  $queryBuilder->createNamedParameter($row['pid'], ‪Connection::PARAM_INT)
7889  ),
7890  $queryBuilder->expr()->gte(
7891  $sortColumn,
7892  $queryBuilder->createNamedParameter($row[$sortColumn], ‪Connection::PARAM_INT)
7893  )
7894  )
7895  ->orderBy($sortColumn, 'ASC')
7896  ->addOrderBy('uid', 'DESC')
7897  ->setMaxResults(2);
7898 
7899  if ($considerWorkspaces) {
7900  $queryBuilder->andWhere(
7901  $queryBuilder->expr()->or(
7902  $queryBuilder->expr()->eq('t3ver_oid', 0),
7903  $queryBuilder->expr()->eq('t3ver_state', VersionState::MOVE_POINTER->value)
7904  )
7905  );
7906  }
7907 
7908  $subResults = $queryBuilder->executeQuery()->fetchAllAssociative();
7909  // Fetches the next record in order to calculate the in-between sortNumber
7910  if (count($subResults) === 2) {
7911  // There was a record afterward, fetch that
7912  $subrow = array_pop($subResults);
7913  // The sortNumber is found in between these values
7914  $sortNumber = $row[$sortColumn] + floor(($subrow[$sortColumn] - $row[$sortColumn]) / 2);
7915  // The sortNumber happened NOT to be between the two surrounding numbers, so we'll have to resort the list
7916  if ($sortNumber <= $row[$sortColumn] || $sortNumber >= $subrow[$sortColumn]) {
7917  $this->‪increaseSortingOfFollowingRecords($table, (int)$row['pid'], (int)$row[$sortColumn]);
7918  $sortNumber = $row[$sortColumn] + ‪$this->sortIntervals;
7919  }
7920  } else {
7921  // If after the last record in the list, we just add the sortInterval to the last sortvalue
7922  $sortNumber = $row[$sortColumn] + ‪$this->sortIntervals;
7923  }
7924  }
7925  return ['pid' => $row['pid'], 'sortNumber' => $sortNumber];
7926  }
7927  if ($this->enableLogging) {
7928  $propArr = $this->‪getRecordProperties($table, ‪$uid);
7929  // OK, don't insert $propArr['event_pid'] here...
7930  $this->‪log($table, ‪$uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record "{title}" ({table}:{uid}) to after a non-existing record ({target})', 1, ['title' => $propArr['header'], 'table' => $table, 'uid' => ‪$uid, 'target' => abs($pid)], $propArr['pid']);
7931  }
7932  // There MUST be a previous record or else this cannot work
7933  return false;
7934  }
7935 
7946  protected function ‪increaseSortingOfFollowingRecords(string $table, int $pid, int $sortingValue = null): void
7947  {
7948  $sortBy = ‪$GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7949  if ($sortBy) {
7950  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
7951  $queryBuilder
7952  ->update($table)
7953  ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)))
7954  ->set($sortBy, $queryBuilder->quoteIdentifier($sortBy) . ' + ' . $this->sortIntervals . ' + ' . $this->sortIntervals, false);
7955  if ($sortingValue !== null) {
7956  $queryBuilder->andWhere($queryBuilder->expr()->gt($sortBy, $sortingValue));
7957  }
7958  if (BackendUtility::isTableWorkspaceEnabled($table)) {
7959  $queryBuilder
7960  ->andWhere(
7961  $queryBuilder->expr()->eq('t3ver_oid', 0)
7962  );
7963  }
7964 
7965  $deleteColumn = ‪$GLOBALS['TCA'][$table]['ctrl']['delete'] ?? '';
7966  if ($deleteColumn) {
7967  $queryBuilder->andWhere($queryBuilder->expr()->eq($deleteColumn, 0));
7968  }
7969 
7970  $queryBuilder->executeStatement();
7971  }
7972  }
7973 
8006  protected function ‪getPreviousLocalizedRecordUid($table, ‪$uid, $pid, $targetLanguage)
8007  {
8008  $previousLocalizedRecordUid = ‪$uid;
8009  $sortColumn = ‪$GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
8010  if (!$sortColumn) {
8011  return $previousLocalizedRecordUid;
8012  }
8013 
8014  // Typically l10n_parent
8015  $transOrigPointerField = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
8016  // Typically sys_language_uid
8017  $languageField = ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'];
8018 
8019  $select = [$sortColumn, $languageField, $transOrigPointerField, 'pid', 'uid'];
8020  // For content elements, we also need the colPos
8021  if ($table === 'tt_content') {
8022  $select[] = 'colPos';
8023  }
8024 
8025  // Get the sort value and some other details of the source language record
8026  $row = BackendUtility::getRecord($table, ‪$uid, implode(',', $select));
8027  if (!is_array($row)) {
8028  // This if may be obsolete ... didn't the callee already check if the source record exists?
8029  return $previousLocalizedRecordUid;
8030  }
8031 
8032  // Try to find a "before" record in source language
8033  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
8034  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
8035  $queryBuilder
8036  ->select(...$select)
8037  ->from($table)
8038  ->where(
8039  $queryBuilder->expr()->eq(
8040  'pid',
8041  $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)
8042  ),
8043  $queryBuilder->expr()->eq(
8044  $languageField,
8045  $queryBuilder->createNamedParameter($row[$languageField], ‪Connection::PARAM_INT)
8046  ),
8047  $queryBuilder->expr()->lt(
8048  $sortColumn,
8049  $queryBuilder->createNamedParameter($row[$sortColumn], ‪Connection::PARAM_INT)
8050  )
8051  )
8052  ->orderBy($sortColumn, 'DESC')
8053  ->addOrderBy('uid', 'DESC')
8054  ->setMaxResults(1);
8055  if ($table === 'tt_content') {
8056  $queryBuilder->andWhere(
8057  $queryBuilder->expr()->eq(
8058  'colPos',
8059  $queryBuilder->createNamedParameter($row['colPos'], ‪Connection::PARAM_INT)
8060  )
8061  );
8062  }
8063  // If there is a "before" record in source language, see if it is localized to target language.
8064  // If so, return uid of target language record.
8065  if ($previousRow = $queryBuilder->executeQuery()->fetchAssociative()) {
8066  $previousLocalizedRecord = BackendUtility::getRecordLocalization($table, $previousRow['uid'], $targetLanguage, 'pid=' . (int)$pid);
8067  if (isset($previousLocalizedRecord[0]) && is_array($previousLocalizedRecord[0])) {
8068  $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
8069  }
8070  }
8071 
8072  return $previousLocalizedRecordUid;
8073  }
8074 
8083  public function ‪newFieldArray($table): array
8084  {
8085  $fieldArray = [];
8086  if (is_array(‪$GLOBALS['TCA'][$table]['columns'])) {
8087  foreach (‪$GLOBALS['TCA'][$table]['columns'] as $field => $content) {
8088  if (isset($this->defaultValues[$table][$field])) {
8089  $fieldArray[$field] = $this->defaultValues[$table][$field];
8090  } elseif (isset($content['config']['default'])) {
8091  $fieldArray[$field] = $content['config']['default'];
8092  }
8093  }
8094  }
8095  return $fieldArray;
8096  }
8097 
8104  protected function ‪addDefaultPermittedLanguageIfNotSet(string $table, array $incomingFieldArray, int $pageId): array
8105  {
8106  $languageFieldName = ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '';
8107  if (empty($languageFieldName) || isset($incomingFieldArray[$languageFieldName])) {
8108  return $incomingFieldArray;
8109  }
8110  try {
8111  $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageId);
8112  foreach ($site->getAvailableLanguages($this->BE_USER, false, $pageId) as $languageId => $language) {
8113  $incomingFieldArray[$languageFieldName] = $languageId;
8114  break;
8115  }
8116  } catch (SiteNotFoundException) {
8117  // No site found, do not set a default language if nothing was set explicitly
8118  }
8119  return $incomingFieldArray;
8120  }
8121 
8132  protected function ‪getSiteLanguageForPage(int $pageId, int $languageId): ?SiteLanguage
8133  {
8134  try {
8135  // Try to fetch the site language from the pages' associated site
8136  $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageId);
8137  return $site->getLanguageById($languageId);
8138  } catch (SiteNotFoundException | \InvalidArgumentException $e) {
8139  // In case no site language could be found, we might deal with the root node,
8140  // we therefore try to fetch the site language from all available sites.
8141  // NOTE: This has side effects, in case the SAME ID is used for different languages in different sites!
8142  $sites = GeneralUtility::makeInstance(SiteFinder::class)->getAllSites();
8143  foreach ($sites as $site) {
8144  try {
8145  return $site->getLanguageById($languageId);
8146  } catch (\InvalidArgumentException $e) {
8147  // language not found in site, continue
8148  continue;
8149  }
8150  }
8151  }
8152 
8153  return null;
8154  }
8155 
8166  public function ‪compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
8167  {
8168  $connection = $this->connectionPool->getConnectionForTable($table);
8169  $queryBuilder = $connection->createQueryBuilder();
8170  $queryBuilder->getRestrictions()->removeAll();
8171  $currentRecord = $queryBuilder->select('*')
8172  ->from($table)
8173  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, ‪Connection::PARAM_INT)))
8174  ->executeQuery()
8175  ->fetchAssociative();
8176  // If the current record exists (which it should...), begin comparison:
8177  if (is_array($currentRecord)) {
8178  $currentRecord = BackendUtility::convertDatabaseRowValuesToPhp($table, $currentRecord);
8179  $tableDetails = $connection->getSchemaInformation()->introspectTable($table);
8180  $columnRecordTypes = [];
8181  foreach ($currentRecord as $columnName => $_) {
8182  $columnRecordTypes[$columnName] = '';
8183  $type = $tableDetails->getColumn($columnName)->getType();
8184  if ($type instanceof IntegerType) {
8185  $columnRecordTypes[$columnName] = 'int';
8186  } elseif ($type instanceof JsonType) {
8187  $columnRecordTypes[$columnName] = 'json';
8188  }
8189  }
8190  // Unset the fields which are similar:
8191  foreach ($fieldArray as $col => $val) {
8192  $fieldConfiguration = ‪$GLOBALS['TCA'][$table]['columns'][$col]['config'] ?? [];
8193  $isNullField = $fieldConfiguration['nullable'] ?? false;
8194 
8195  // Unset fields if stored and submitted values are equal - except the current field holds MM relations.
8196  // In general this avoids to store superfluous data which also will be visualized in the editing history.
8197  if (empty($fieldConfiguration['MM']) && $this->‪isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $columnRecordTypes[$col], $isNullField)) {
8198  unset($fieldArray[$col]);
8199  } else {
8200  if (!isset($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col])) {
8201  $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
8202  } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
8203  $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
8204  }
8205  if (!isset($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col])) {
8206  $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
8207  } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
8208  $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
8209  }
8210  }
8211  }
8212  } else {
8213  // If the current record does not exist this is an error anyways and we just return an empty array here.
8214  $fieldArray = [];
8215  }
8216  return $fieldArray;
8217  }
8218 
8231  protected function ‪isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull = false)
8232  {
8233  // No NULL values are allowed, this is the regular behaviour.
8234  // Thus, check whether strings are the same or whether integer values are empty ("0" or "").
8235  if (!$allowNull) {
8236  switch ($storedType) {
8237  case 'json':
8238  $result = $submittedValue === $storedValue;
8239  break;
8240  case 'int':
8241  $result = (int)$storedValue === (int)$submittedValue;
8242  break;
8243  default:
8244  $result = (string)$submittedValue === (string)$storedValue;
8245  }
8246  // Null values are allowed, but currently there's a real (not NULL) value.
8247  // Thus, ensure no NULL value was submitted and fallback to the regular behaviour.
8248  } elseif ($storedValue !== null) {
8249  $result = (
8250  $submittedValue !== null
8251  && $this->‪isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, false)
8252  );
8253  // Null values are allowed, and currently there's a NULL value.
8254  // Thus, check whether a NULL value was submitted.
8255  } else {
8256  $result = ($submittedValue === null);
8257  }
8258 
8259  return $result;
8260  }
8261 
8267  public function ‪disableDeleteClause(): void
8268  {
8269  $this->‪disableDeleteClause = true;
8270  }
8271 
8279  public function ‪deleteClause($table): string
8280  {
8281  // Returns the proper delete-clause if any for a table from TCA
8282  if (!$this->‪disableDeleteClause && $GLOBALS['TCA'][$table]['ctrl']['delete']) {
8283  return ' AND ' . $table . '.' . ‪$GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0';
8284  }
8285  return '';
8286  }
8287 
8291  protected function ‪addDeleteRestriction(QueryRestrictionContainerInterface $restrictions): void
8292  {
8293  if (!$this->‪disableDeleteClause) {
8294  $restrictions->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8295  }
8296  }
8297 
8306  protected function ‪getOriginalParentOfRecord($table, ‪$uid)
8307  {
8308  if (isset(self::$recordPidsForDeletedRecords[$table][‪$uid])) {
8309  return self::$recordPidsForDeletedRecords[$table][‪$uid];
8310  }
8311  [$parentUid] = BackendUtility::getTSCpid($table, ‪$uid, '');
8312  return [$parentUid];
8313  }
8314 
8323  public function ‪getTableEntries($table, $TSconfig): array
8324  {
8325  $tA = is_array($TSconfig['table.'][$table . '.'] ?? false) ? $TSconfig['table.'][$table . '.'] : [];
8326  $dA = is_array($TSconfig['default.'] ?? false) ? $TSconfig['default.'] : [];
8327  ArrayUtility::mergeRecursiveWithOverrule($dA, $tA);
8328  return $dA;
8329  }
8330 
8339  public function ‪getPID($table, ‪$uid)
8340  {
8341  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
8342  $queryBuilder->getRestrictions()->removeAll();
8343  $queryBuilder->select('pid')
8344  ->from($table)
8345  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)));
8346  if ($row = $queryBuilder->executeQuery()->fetchAssociative()) {
8347  return $row['pid'];
8348  }
8349  return false;
8350  }
8351 
8357  public function ‪dbAnalysisStoreExec(): void
8358  {
8359  foreach ($this->dbAnalysisStore as $action) {
8360  $idIsInteger = ‪MathUtility::canBeInterpretedAsInteger($action[2]);
8361  // If NEW id is not found in substitution array (due to errors), continue.
8362  if (!$idIsInteger && !isset($this->substNEWwithIDs[$action[2]])) {
8363  continue;
8364  }
8365  $id = BackendUtility::wsMapId($action[4], $idIsInteger ? $action[2] : $this->substNEWwithIDs[$action[2]]);
8366  if ($id) {
8367  $action[0]->writeMM($action[1], $id, $action[3]);
8368  }
8369  }
8370  }
8371 
8383  public function ‪int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
8384  {
8385  if ($counter) {
8386  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
8387  $restrictions = $queryBuilder->getRestrictions()->removeAll();
8388  $this->‪addDeleteRestriction($restrictions);
8389  $queryBuilder
8390  ->select('uid')
8391  ->from('pages')
8392  ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)))
8393  ->orderBy('sorting', 'DESC');
8394  if (!$this->admin) {
8395  $queryBuilder->andWhere($this->BE_USER->getPagePermsClause(‪Permission::PAGE_SHOW));
8396  }
8397  if ((int)$this->BE_USER->workspace === 0) {
8398  $queryBuilder->andWhere(
8399  $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT))
8400  );
8401  } else {
8402  $queryBuilder->andWhere($queryBuilder->expr()->in(
8403  't3ver_wsid',
8404  $queryBuilder->createNamedParameter([0, $this->BE_USER->workspace], ‪Connection::PARAM_INT_ARRAY)
8405  ));
8406  }
8407  $result = $queryBuilder->executeQuery();
8408 
8409  $pages = [];
8410  while ($row = $result->fetchAssociative()) {
8411  $pages[$row['uid']] = $row;
8412  }
8413 
8414  // Resolve placeholders of workspace versions
8415  if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
8416  $pages = array_reverse(
8418  'pages',
8419  'uid',
8420  'sorting',
8421  array_keys($pages)
8422  ),
8423  true
8424  );
8425  }
8426 
8427  foreach ($pages as $page) {
8428  if ($page['uid'] != $rootID) {
8429  $CPtable[$page['uid']] = $pid;
8430  // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
8431  if ($counter - 1) {
8432  $CPtable = $this->‪int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
8433  }
8434  }
8435  }
8436  }
8437  return $CPtable;
8438  }
8439 
8443  protected function ‪compileAdminTables(): array
8444  {
8445  return array_keys(‪$GLOBALS['TCA']);
8446  }
8447 
8455  public function ‪fixUniqueInPid($table, ‪$uid): void
8456  {
8457  if (empty(‪$GLOBALS['TCA'][$table])) {
8458  return;
8459  }
8460  $curData = $this->‪recordInfo($table, ‪$uid);
8461  $newData = [];
8462  foreach (‪$GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
8463  if (($conf['config']['type'] === 'input' || $conf['config']['type'] === 'email') && (string)$curData[$field] !== '') {
8464  $evalCodesArray = ‪GeneralUtility::trimExplode(',', $conf['config']['eval'] ?? '', true);
8465  if (in_array('uniqueInPid', $evalCodesArray, true)) {
8466  $newV = $this->‪getUnique($table, $field, $curData[$field], ‪$uid, $curData['pid']);
8467  if ((string)$newV !== (string)$curData[$field]) {
8468  $newData[$field] = $newV;
8469  }
8470  }
8471  }
8472  }
8473  // IF there are changed fields, then update the database
8474  if (!empty($newData)) {
8475  $this->‪updateDB($table, ‪$uid, $newData);
8476  }
8477  }
8478 
8486  protected function ‪fixUniqueInSite(string $table, int ‪$uid): bool
8487  {
8488  $curData = $this->‪recordInfo($table, ‪$uid);
8489  $workspaceId = $this->BE_USER->workspace;
8490  $newData = [];
8491  foreach (‪$GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
8492  if ($conf['config']['type'] === 'slug' && (string)$curData[$field] !== '') {
8493  $evalCodesArray = ‪GeneralUtility::trimExplode(',', $conf['config']['eval'] ?? '', true);
8494  if (in_array('uniqueInSite', $evalCodesArray, true)) {
8495  $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $conf['config'], $workspaceId);
8496  $state = ‪RecordStateFactory::forName($table)->fromArray($curData);
8497  $newValue = $helper->buildSlugForUniqueInSite($curData[$field], $state);
8498  if ((string)$newValue !== (string)$curData[$field]) {
8499  $newData[$field] = $newValue;
8500  }
8501  }
8502  }
8503  }
8504  // IF there are changed fields, then update the database
8505  if (!empty($newData)) {
8506  $this->‪updateDB($table, ‪$uid, $newData);
8507  return true;
8508  }
8509  return false;
8510  }
8511 
8515  protected function ‪fixUniqueInSiteForSubpages(int $pageId): void
8516  {
8517  // Get ALL subpages to update - read-permissions are respected
8518  $subPages = $this->‪int_pageTreeInfo([], $pageId, 99, $pageId);
8519  // Now fix uniqueInSite for subpages
8520  foreach ($subPages as $thePageUid => $thePagePid) {
8521  $recordWasModified = $this->‪fixUniqueInSite('pages', $thePageUid);
8522  if ($recordWasModified) {
8523  // @todo: Add logging and history - but how? we don't know the data that was in the system before
8524  }
8525  }
8526  }
8527 
8537  protected function ‪fixCopyAfterDuplFields(string $table, int $prevUid): array
8538  {
8539  if (!(‪$GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'] ?? false)
8540  || ($prevData = $this->‪recordInfo($table, $prevUid)) === null
8541  ) {
8542  return [];
8543  }
8544 
8545  $newData = [];
8546  foreach (‪GeneralUtility::trimExplode(',', ‪$GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true) as $field) {
8547  if ((‪$GLOBALS['TCA'][$table]['columns'][$field] ?? false) && !isset($newData[$field])) {
8548  $newData[$field] = $prevData[$field];
8549  }
8550  }
8551  return $newData;
8552  }
8553 
8567  protected function ‪castReferenceValue($value, array $configuration, bool $isNew)
8568  {
8569  if ((string)$value !== '') {
8570  return $value;
8571  }
8572 
8573  if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) {
8574  return 0;
8575  }
8576 
8577  if (!$isNew && isset($configuration['renderType']) && $configuration['renderType'] === 'selectCheckBox') {
8578  return '';
8579  }
8580 
8581  if (array_key_exists('default', $configuration)) {
8582  return $configuration['default'];
8583  }
8584 
8585  return $value;
8586  }
8587 
8595  public function ‪isReferenceField($conf): bool
8596  {
8597  if (!isset($conf['type'])) {
8598  return false;
8599  }
8600  return ($conf['type'] === 'group') || (($conf['type'] === 'select' || $conf['type'] === 'category') && !empty($conf['foreign_table']));
8601  }
8602 
8611  public function ‪getRelationFieldType($conf): bool|string
8612  {
8613  if (
8614  empty($conf['foreign_table'])
8615  || !in_array($conf['type'] ?? '', ['inline', 'file'], true)
8616  || ($conf['type'] === 'file' && !($conf['foreign_field'] ?? false))
8617  ) {
8618  return false;
8619  }
8620  if ($conf['foreign_field'] ?? false) {
8621  // The reference to the parent is stored in a pointer field in the child record
8622  return 'field';
8623  }
8624  if ($conf['MM'] ?? false) {
8625  // Regular MM intermediate table is used to store data
8626  return 'mm';
8627  }
8628  // An item list (separated by comma) is stored (like select type is doing)
8629  return 'list';
8630  }
8631 
8644  public function ‪getCopyHeader($table, $pid, $field, $value, $count, $prevTitle = '')
8645  {
8646  // Set title value to check for:
8647  $checkTitle = $value;
8648  if ($count > 0) {
8649  $checkTitle = $value . rtrim(' ' . sprintf($this->‪prependLabel($table), $count));
8650  }
8651  // Do check:
8652  if ($prevTitle != $checkTitle || $count < 100) {
8653  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
8654  $this->‪addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
8655  $rowCount = $queryBuilder
8656  ->count('uid')
8657  ->from($table)
8658  ->where(
8659  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)),
8660  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($checkTitle))
8661  )
8662  ->executeQuery()
8663  ->fetchOne();
8664  if ($rowCount) {
8665  return $this->‪getCopyHeader($table, $pid, $field, $value, $count + 1, $checkTitle);
8666  }
8667  }
8668  // Default is to just return the current input title if no other was returned before:
8669  return $checkTitle;
8670  }
8671 
8679  protected function ‪prependLabel($table): string
8680  {
8681  return $this->‪getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
8682  }
8683 
8692  public function ‪resolvePid($table, $pid): int
8693  {
8694  $pid = (int)$pid;
8695  if ($pid < 0) {
8696  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
8697  $queryBuilder->getRestrictions()->removeAll();
8698  $row = $queryBuilder
8699  ->select('pid')
8700  ->from($table)
8701  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(abs($pid), ‪Connection::PARAM_INT)))
8702  ->executeQuery()
8703  ->fetchAssociative();
8704  $pid = (int)$row['pid'];
8705  }
8706  return $pid;
8707  }
8708 
8717  public function ‪clearPrefixFromValue($table, $value)
8718  {
8719  $regex = '/\s' . sprintf(preg_quote($this->‪prependLabel($table)), '[0-9]*') . '$/';
8720  return @preg_replace($regex, '', $value);
8721  }
8722 
8730  protected function ‪checkForRecordsFromDisallowedTables(array $pageIds): ?array
8731  {
8732  if ($this->admin) {
8733  return null;
8734  }
8735  $disallowedTables = [];
8736  if (!empty($pageIds)) {
8737  $tableNames = $this->‪compileAdminTables();
8738  foreach ($tableNames as $table) {
8739  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
8740  $queryBuilder->getRestrictions()->removeAll()
8741  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8742  $count = $queryBuilder->count('uid')
8743  ->from($table)
8744  ->where($queryBuilder->expr()->in(
8745  'pid',
8746  $queryBuilder->createNamedParameter($pageIds, ‪Connection::PARAM_INT_ARRAY)
8747  ))
8748  ->executeQuery()
8749  ->fetchOne();
8750  if ($count && ($this->‪tableReadOnly($table) || !$this->‪checkModifyAccessList($table))) {
8751  $disallowedTables[] = $table;
8752  }
8753  }
8754  }
8755  return !empty($disallowedTables) ? $disallowedTables : null;
8756  }
8757 
8766  public function ‪isRecordCopied($table, ‪$uid): bool
8767  {
8768  // If the record was copied:
8769  if (isset($this->copyMappingArray[$table][‪$uid])) {
8770  return true;
8771  }
8772  if (isset($this->copyMappingArray[$table]) && in_array(‪$uid, array_values($this->copyMappingArray[$table]))) {
8773  return true;
8774  }
8775  return false;
8776  }
8777 
8778  /******************************
8779  *
8780  * Clearing cache
8781  *
8782  ******************************/
8783 
8794  public function ‪registerRecordIdForPageCacheClearing($table, ‪$uid, $pid = null): void
8795  {
8796  if (!is_array(static::$recordsToClearCacheFor[$table] ?? false)) {
8797  static::$recordsToClearCacheFor[$table] = [];
8798  }
8799  static::$recordsToClearCacheFor[$table][] = (int)‪$uid;
8800  if ($pid !== null) {
8801  if (!isset(static::$recordPidsForDeletedRecords[$table]) || !is_array(static::$recordPidsForDeletedRecords[$table])) {
8802  static::$recordPidsForDeletedRecords[$table] = [];
8803  }
8804  static::$recordPidsForDeletedRecords[$table][‪$uid][] = (int)$pid;
8805  }
8806  }
8807 
8811  protected function ‪processClearCacheQueue(): void
8812  {
8813  $tagsToClear = [];
8814  $clearCacheCommands = [];
8815 
8816  foreach (static::$recordsToClearCacheFor as $table => $uids) {
8817  foreach (array_unique($uids) as ‪$uid) {
8818  if (!isset(‪$GLOBALS['TCA'][$table]) || ‪$uid <= 0) {
8819  return;
8820  }
8821  // For move commands we may get more then 1 parent.
8822  $pageUids = $this->‪getOriginalParentOfRecord($table, ‪$uid);
8823  foreach ($pageUids as $originalParent) {
8824  [$tagsToClearFromPrepare, $clearCacheCommandsFromPrepare]
8825  = $this->‪prepareCacheFlush($table, ‪$uid, $originalParent);
8826  $tagsToClear = array_merge($tagsToClear, $tagsToClearFromPrepare);
8827  $clearCacheCommands = array_merge($clearCacheCommands, $clearCacheCommandsFromPrepare);
8828  }
8829  }
8830  }
8831 
8832  $this->cacheManager->flushCachesInGroupByTags('pages', array_keys($tagsToClear));
8833 
8834  // Filter duplicate cache commands from cacheQueue
8835  $clearCacheCommands = array_unique($clearCacheCommands);
8836  // Execute collected clear cache commands from page TSconfig
8837  foreach ($clearCacheCommands as $command) {
8838  $this->‪clear_cacheCmd($command);
8839  }
8840 
8841  // Reset the cache clearing array
8842  static::$recordsToClearCacheFor = [];
8843 
8844  // Reset the original pid array
8845  static::$recordPidsForDeletedRecords = [];
8846  }
8847 
8857  protected function ‪prepareCacheFlush($table, ‪$uid, $pid): array
8858  {
8859  $tagsToClear = [];
8860  $clearCacheCommands = [];
8861  $pageUid = 0;
8862  $clearCacheEnabled = true;
8863  // Get Page TSconfig relevant:
8864  $TSConfig = BackendUtility::getPagesTSconfig($pid)['TCEMAIN.'] ?? [];
8865 
8866  if (!empty($TSConfig['clearCache_disable'])) {
8867  $clearCacheEnabled = false;
8868  }
8869 
8870  if ($clearCacheEnabled && $this->BE_USER->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
8871  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
8872  $queryBuilder->getRestrictions()
8873  ->removeAll()
8874  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8875  $count = $queryBuilder
8876  ->count('uid')
8877  ->from($table)
8878  ->where(
8879  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)),
8880  $queryBuilder->expr()->eq('t3ver_oid', 0)
8881  )
8882  ->executeQuery()
8883  ->fetchOne();
8884  if ($count === 0) {
8885  $clearCacheEnabled = false;
8886  }
8887  }
8888 
8889  if ($clearCacheEnabled) {
8890  $pageIdsThatNeedCacheFlush = [];
8891  if ($table === 'pages') {
8892  // If table is "pages", Find out if the record is a localized one and get the default page
8893  $pageUid = $this->‪getDefaultLanguagePageId($uid);
8894 
8895  // Builds list of pages on the SAME level as this page (siblings)
8896  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
8897  $queryBuilder->getRestrictions()
8898  ->removeAll()
8899  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8900  $siblings = $queryBuilder
8901  ->select('A.pid AS pid', 'B.uid AS uid')
8902  ->from('pages', 'A')
8903  ->from('pages', 'B')
8904  ->where(
8905  $queryBuilder->expr()->eq('A.uid', $queryBuilder->createNamedParameter($pageUid, ‪Connection::PARAM_INT)),
8906  $queryBuilder->expr()->eq('B.pid', $queryBuilder->quoteIdentifier('A.pid')),
8907  $queryBuilder->expr()->gte('A.pid', $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT))
8908  )
8909  ->executeQuery();
8910 
8911  $parentPageId = 0;
8912  while ($row_tmp = $siblings->fetchAssociative()) {
8913  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
8914  $parentPageId = (int)$row_tmp['pid'];
8915  // Add children as well:
8916  if ($TSConfig['clearCache_pageSiblingChildren'] ?? false) {
8917  $siblingChildrenQuery = $this->connectionPool->getQueryBuilderForTable('pages');
8918  $siblingChildrenQuery->getRestrictions()
8919  ->removeAll()
8920  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8921  $siblingChildren = $siblingChildrenQuery
8922  ->select('uid')
8923  ->from('pages')
8924  ->where($siblingChildrenQuery->expr()->eq(
8925  'pid',
8926  $siblingChildrenQuery->createNamedParameter($row_tmp['uid'], ‪Connection::PARAM_INT)
8927  ))
8928  ->executeQuery();
8929  while ($row_tmp2 = $siblingChildren->fetchAssociative()) {
8930  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
8931  }
8932  }
8933  }
8934  // Finally, add the parent page as well when clearing a specific page
8935  if ($parentPageId > 0) {
8936  $pageIdsThatNeedCacheFlush[] = $parentPageId;
8937  }
8938  // Add grandparent as well if configured
8939  if ($TSConfig['clearCache_pageGrandParent'] ?? false) {
8940  $parentQuery = $this->connectionPool->getQueryBuilderForTable('pages');
8941  $parentQuery->getRestrictions()
8942  ->removeAll()
8943  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8944  $row_tmp = $parentQuery
8945  ->select('pid')
8946  ->from('pages')
8947  ->where($parentQuery->expr()->eq(
8948  'uid',
8949  $parentQuery->createNamedParameter($parentPageId, ‪Connection::PARAM_INT)
8950  ))
8951  ->executeQuery()
8952  ->fetchAssociative();
8953  if (!empty($row_tmp)) {
8954  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
8955  }
8956  }
8957  } else {
8958  // For other tables than "pages", delete cache for the records "parent page".
8959  $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->‪getPID($table, ‪$uid);
8960  // Add the parent page as well
8961  if ($TSConfig['clearCache_pageGrandParent'] ?? false) {
8962  $parentQuery = $this->connectionPool->getQueryBuilderForTable('pages');
8963  $parentQuery->getRestrictions()
8964  ->removeAll()
8965  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8966  $parentPageRecord = $parentQuery
8967  ->select('pid')
8968  ->from('pages')
8969  ->where($parentQuery->expr()->eq(
8970  'uid',
8971  $parentQuery->createNamedParameter($pageUid, ‪Connection::PARAM_INT)
8972  ))
8973  ->executeQuery()
8974  ->fetchAssociative();
8975  if (!empty($parentPageRecord)) {
8976  $pageIdsThatNeedCacheFlush[] = (int)$parentPageRecord['pid'];
8977  }
8978  }
8979  }
8980  // Call pre-processing function for clearing of cache for page ids:
8981  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8982  $_params = ['pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => ‪$uid, 'functionID' => 'clear_cache()'];
8983  // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8984  GeneralUtility::callUserFunction($funcName, $_params, $this);
8985  }
8986  // Delete cache for selected pages:
8987  foreach ($pageIdsThatNeedCacheFlush as $pageId) {
8988  $tagsToClear['pageId_' . $pageId] = true;
8989  }
8990  // Queue delete cache for current table and record
8991  $tagsToClear[$table] = true;
8992  $tagsToClear[$table . '_' . ‪$uid] = true;
8993  }
8994  // Clear cache for pages entered in TSconfig:
8995  if (!empty($TSConfig['clearCacheCmd'])) {
8996  $commands = ‪GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
8997  $clearCacheCommands = array_unique($commands);
8998  }
8999  // Call post-processing function for clear-cache:
9000  $_params = ['table' => $table, 'uid' => ‪$uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig, 'tags' => $tagsToClear, 'clearCacheEnabled' => $clearCacheEnabled];
9001  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
9002  GeneralUtility::callUserFunction($_funcRef, $_params, $this);
9003  }
9004  return [
9005  $tagsToClear,
9006  $clearCacheCommands,
9007  ];
9008  }
9009 
9045  public function ‪clear_cacheCmd($cacheCmd): void
9046  {
9047  if (is_object($this->BE_USER)) {
9048  $this->BE_USER->writeLog(SystemLogType::CACHE, SystemLogCacheAction::CLEAR, SystemLogErrorClassification::MESSAGE, 0, 'User {username} has cleared the cache (cacheCmd={command})', ['username' => $this->BE_USER->user['username'], 'command' => $cacheCmd]);
9049  }
9050  $userTsConfig = $this->BE_USER->getTSConfig();
9051  switch (strtolower($cacheCmd)) {
9052  case 'pages':
9053  if ($this->admin || ($userTsConfig['options.']['clearCache.']['pages'] ?? false)) {
9054  $this->cacheManager->flushCachesInGroup('pages');
9055  }
9056  break;
9057  case 'all':
9058  // allow to clear all caches if the TS config option is enabled or the option is not explicitly
9059  // disabled for admins (which could clear all caches by default). The latter option is useful
9060  // for big production sites where it should be possible to restrict the cache clearing for some admins.
9061  if (($userTsConfig['options.']['clearCache.']['all'] ?? false)
9062  || ($this->admin && (bool)($userTsConfig['options.']['clearCache.']['all'] ?? true))
9063  ) {
9064  $this->cacheManager->flushCaches();
9065 
9066  // Delete Opcode Cache
9067  GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
9068 
9069  // Delete DI Cache only on development context
9070  if (‪Environment::getContext()->isDevelopment()) {
9071  $container = GeneralUtility::makeInstance(ContainerInterface::class);
9072  $container->get('cache.di')->getBackend()->forceFlush();
9073  }
9074  }
9075  break;
9076  }
9077 
9078  $tagsToFlush = [];
9079  // Clear cache for a page ID!
9081  $list_cache = [$cacheCmd];
9082  // Call pre-processing function for clearing of cache for page ids:
9083  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
9084  $_params = ['pageIdArray' => &$list_cache, 'cacheCmd' => $cacheCmd, 'functionID' => 'clear_cacheCmd()'];
9085  // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
9086  GeneralUtility::callUserFunction($funcName, $_params, $this);
9087  }
9088  // Delete cache for selected pages:
9089  if (is_array($list_cache)) {
9090  foreach ($list_cache as $pageId) {
9091  $tagsToFlush[] = 'pageId_' . (int)$pageId;
9092  }
9093  }
9094  }
9095  // flush cache by tag
9096  if (str_starts_with(strtolower($cacheCmd), 'cachetag:')) {
9097  $cacheTag = substr($cacheCmd, 9);
9098  $tagsToFlush[] = $cacheTag;
9099  }
9100  // process caching framework operations
9101  if (!empty($tagsToFlush)) {
9102  $this->cacheManager->flushCachesInGroupByTags('pages', $tagsToFlush);
9103  }
9104 
9105  // Call post-processing function for clear-cache:
9106  $_params = ['cacheCmd' => strtolower($cacheCmd), 'tags' => $tagsToFlush];
9107  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
9108  GeneralUtility::callUserFunction($_funcRef, $_params, $this);
9109  }
9110  }
9111 
9112  /*****************************
9113  *
9114  * Logging
9115  *
9116  *****************************/
9135  public function ‪log($table, $recuid, $action, $recpid, $error, ‪$details, $details_nr = -1, $data = [], $event_pid = -1, $NEWid = '')
9136  {
9137  if (!$this->enableLogging) {
9138  return 0;
9139  }
9140  // Type value for DataHandler
9141  if (!$this->storeLogMessages) {
9142  ‪$details = '';
9143  }
9144  if ($error > 0) {
9145  $detailMessage = ‪$details;
9146  if (is_array($data)) {
9147  $detailMessage = $this->formatLogDetails($detailMessage, $data);
9148  }
9149  $this->errorLog[] = '[' . SystemLogType::DB . '.' . $action . '.' . $details_nr . ']: ' . $detailMessage;
9150  }
9151  return $this->BE_USER->writelog(SystemLogType::DB, $action, $error, $details_nr, ‪$details, $data, $table, abs((int)$recuid), $recpid, $event_pid, $NEWid);
9152  }
9153 
9161  public function ‪printLogErrorMessages(): array
9162  {
9163  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_log');
9164  $queryBuilder->getRestrictions()->removeAll();
9165  $result = $queryBuilder
9166  ->select('*')
9167  ->from('sys_log')
9168  ->where(
9169  $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(SystemLogType::DB, ‪Connection::PARAM_INT)),
9170  $queryBuilder->expr()->eq(
9171  'userid',
9172  $queryBuilder->createNamedParameter($this->BE_USER->user['uid'], ‪Connection::PARAM_INT)
9173  ),
9174  $queryBuilder->expr()->eq(
9175  'tstamp',
9176  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], ‪Connection::PARAM_INT)
9177  ),
9178  $queryBuilder->expr()->neq('error', $queryBuilder->createNamedParameter(SystemLogErrorClassification::MESSAGE, ‪Connection::PARAM_INT))
9179  )
9180  ->executeQuery();
9181 
9182  $affectedRecords = [];
9183  while ($row = $result->fetchAssociative()) {
9184  $affectedRecords[] = $row['tablename'] . '.' . $row['recuid'];
9185 
9186  $msg = $this->formatLogDetails($row['details'], $row['log_data'] ?? '');
9187  $msg = $row['error'] . ': ' . $msg;
9188  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', $row['error'] === SystemLogErrorClassification::WARNING ? ContextualFeedbackSeverity::WARNING : ContextualFeedbackSeverity::ERROR, true);
9189  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
9190  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
9191  $defaultFlashMessageQueue->enqueue($flashMessage);
9192  }
9193 
9194  return $affectedRecords;
9195  }
9196 
9197  /*****************************
9198  *
9199  * Internal (do not use outside Core!)
9200  *
9201  *****************************/
9202 
9210  protected function ‪getDefaultLanguagePageId(int $pageId): int
9211  {
9212  $localizationParentFieldName = ‪$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
9213  $row = $this->‪recordInfo('pages', $pageId);
9214  $localizationParent = (int)($row[$localizationParentFieldName] ?? 0);
9215  if ($localizationParent > 0) {
9216  return $localizationParent;
9217  }
9218  return $pageId;
9219  }
9220 
9231  public function ‪insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
9232  {
9233  $result = $fieldArray;
9234  foreach ($fieldArray as $field => $value) {
9236  && isset(‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['type'])
9237  && in_array(‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['type'], ['inline', 'file'], true)
9238  && (‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['foreign_field'] ?? false)
9239  ) {
9240  $result[$field] = count(‪GeneralUtility::trimExplode(',', $value, true));
9241  }
9242  }
9243  return $result;
9244  }
9245 
9255  public function ‪hasDeletedRecord($tableName, ‪$uid)
9256  {
9257  return
9258  !empty($this->deletedRecords[$tableName])
9259  && in_array(‪$uid, $this->deletedRecords[$tableName])
9260  ;
9261  }
9262 
9270  public function ‪getAutoVersionId($table, $id): ?int
9271  {
9272  $result = null;
9273  if (isset($this->autoVersionIdMap[$table][$id])) {
9274  $result = (int)trim($this->autoVersionIdMap[$table][$id]);
9275  }
9276  return $result;
9277  }
9278 
9286  protected function ‪overlayAutoVersionId($table, $id)
9287  {
9288  $autoVersionId = $this->‪getAutoVersionId($table, $id);
9289  if ($autoVersionId !== null) {
9290  $id = $autoVersionId;
9291  }
9292  return $id;
9293  }
9294 
9305  protected function ‪resolveVersionedRecords($tableName, $fieldNames, $sortingField, array $liveIds)
9306  {
9307  $connection = $this->connectionPool->getConnectionForTable($tableName);
9308  $sortingStatement = !empty($sortingField)
9309  ? [$connection->quoteIdentifier($sortingField)]
9310  : null;
9311  $resolver = GeneralUtility::makeInstance(
9312  PlainDataResolver::class,
9313  $tableName,
9314  $liveIds,
9315  $sortingStatement
9316  );
9317 
9318  $resolver->setWorkspaceId($this->BE_USER->workspace);
9319  $resolver->setKeepDeletePlaceholder(false);
9320  $resolver->setKeepMovePlaceholder(false);
9321  $resolver->setKeepLiveIds(true);
9322  $recordIds = $resolver->get();
9323 
9324  $records = [];
9325  foreach ($recordIds as $recordId) {
9326  $records[$recordId] = BackendUtility::getRecord($tableName, $recordId, $fieldNames);
9327  }
9328 
9329  return $records;
9330  }
9331 
9344  protected function ‪workspaceAllowAutoCreation(string $table, $id, $recpid): bool
9345  {
9346  // No version can be created in live workspace
9347  if ($this->BE_USER->workspace === 0) {
9348  return false;
9349  }
9350  // No versioning support for this table, so no version can be created
9351  if (!BackendUtility::isTableWorkspaceEnabled($table)) {
9352  return false;
9353  }
9354  if ($recpid < 0) {
9355  return false;
9356  }
9357  // There must be no existing version of this record in workspace
9358  if (BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid')) {
9359  return false;
9360  }
9361  return true;
9362  }
9363 
9373  public function ‪workspaceCannotEditOfflineVersion(string $table, array ‪$record)
9374  {
9375  $versionState = VersionState::tryFrom(‪$record['t3ver_state'] ?? 0);
9376  if ($versionState === VersionState::NEW_PLACEHOLDER || (int)‪$record['t3ver_oid'] > 0) {
9377  return $this->‪workspaceCannotEditRecord($table, ‪$record);
9378  }
9379  return 'Not an offline version';
9380  }
9381 
9394  public function ‪workspaceCannotEditRecord($table, $recData): string|false
9395  {
9396  // Only test if the user is in a workspace
9397  if ($this->BE_USER->workspace === 0) {
9398  return false;
9399  }
9400  $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
9401  if (!is_array($recData)) {
9402  $recData = BackendUtility::getRecord(
9403  $table,
9404  $recData,
9405  'pid' . ($tableSupportsVersioning ? ',t3ver_oid,t3ver_wsid,t3ver_state,t3ver_stage' : '')
9406  );
9407  }
9408  if (is_array($recData)) {
9409  // We are testing a "version" (identified by having a t3ver_oid): it can be edited provided
9410  // that workspace matches and versioning is enabled for the table.
9411  $versionState = VersionState::tryFrom($recData['t3ver_state'] ?? 0);
9412  if ($tableSupportsVersioning
9413  && (
9414  $versionState === VersionState::NEW_PLACEHOLDER || (int)(($recData['t3ver_oid'] ?? 0) > 0)
9415  )
9416  ) {
9417  if ((int)$recData['t3ver_wsid'] !== $this->BE_USER->workspace) {
9418  // So does workspace match?
9419  return 'Workspace ID of record didn\'t match current workspace';
9420  }
9421  // So is the user allowed to "use" the edit stage within the workspace?
9422  return $this->BE_USER->workspaceCheckStageForCurrent(0)
9423  ? false
9424  : 'User\'s access level did not allow for editing';
9425  }
9426  // Check if we are testing a "live" record
9427  if ($this->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
9428  // Live records are OK in the current workspace
9429  return false;
9430  }
9431  // If not offline, output error
9432  return 'Online record was not in a workspace';
9433  }
9434  return 'No record';
9435  }
9436 
9444  protected function ‪getOuterMostInstance()
9445  {
9446  if (!isset($this->outerMostInstance)) {
9447  $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS));
9448  foreach ($stack as $stackItem) {
9449  if (isset($stackItem['object']) && $stackItem['object'] instanceof self) {
9450  $this->outerMostInstance = $stackItem['object'];
9451  break;
9452  }
9453  }
9454  }
9456  }
9457 
9463  public function ‪isOuterMostInstance(): bool
9464  {
9465  return $this->‪getOuterMostInstance() === $this;
9466  }
9467 
9476  protected function ‪isNestedElementCallRegistered($table, $id, ‪$identifier): bool
9477  {
9478  // @todo: Stop abusing runtime cache as singleton DTO, needs explicit modeling.
9479  $nestedElementCalls = (array)$this->runtimeCache->get(self::CACHE_IDENTIFIER_NESTED_ELEMENT_CALLS_PREFIX);
9480  return isset($nestedElementCalls[‪$identifier][$table][$id]);
9481  }
9482 
9491  protected function ‪registerNestedElementCall($table, $id, ‪$identifier): void
9492  {
9493  $nestedElementCalls = (array)$this->runtimeCache->get(self::CACHE_IDENTIFIER_NESTED_ELEMENT_CALLS_PREFIX);
9494  $nestedElementCalls[‪$identifier][$table][$id] = true;
9495  $this->runtimeCache->set(self::CACHE_IDENTIFIER_NESTED_ELEMENT_CALLS_PREFIX, $nestedElementCalls);
9496  }
9497 
9501  protected function ‪resetNestedElementCalls(): void
9502  {
9503  $this->runtimeCache->remove(self::CACHE_IDENTIFIER_NESTED_ELEMENT_CALLS_PREFIX);
9504  }
9505 
9517  protected function ‪isElementToBeDeleted($table, $id)
9518  {
9519  // @todo: Stop abusing runtime cache as singleton DTO, needs explicit modeling.
9520  $elementsToBeDeleted = (array)$this->runtimeCache->get(self::CACHE_IDENTIFIER_ELEMENTS_TO_BE_DELETED);
9521  return isset($elementsToBeDeleted[$table][$id]);
9522  }
9523 
9529  protected function ‪registerElementsToBeDeleted(): void
9530  {
9531  $elementsToBeDeleted = (array)$this->runtimeCache->get(self::CACHE_IDENTIFIER_ELEMENTS_TO_BE_DELETED);
9532  $this->runtimeCache->set(self::CACHE_IDENTIFIER_ELEMENTS_TO_BE_DELETED, array_merge($elementsToBeDeleted, $this->‪getCommandMapElements('delete')));
9533  }
9534 
9540  protected function ‪resetElementsToBeDeleted(): void
9541  {
9542  $this->runtimeCache->remove(self::CACHE_IDENTIFIER_ELEMENTS_TO_BE_DELETED);
9543  }
9544 
9551  protected function ‪unsetElementsToBeDeleted(array $elements): array
9552  {
9553  $elements = ArrayUtility::arrayDiffKeyRecursive($elements, $this->‪getCommandMapElements('delete'));
9554  foreach ($elements as $key => $value) {
9555  if (empty($value)) {
9556  unset($elements[$key]);
9557  }
9558  }
9559  return $elements;
9560  }
9561 
9567  protected function ‪getCommandMapElements(string $needle): array
9568  {
9569  $elements = [];
9570  foreach ($this->cmdmap as $tableName => $idArray) {
9571  foreach ($idArray as $id => $commandArray) {
9572  foreach ($commandArray as $command => $value) {
9573  if ($value && $command == $needle) {
9574  $elements[$tableName][$id] = true;
9575  }
9576  }
9577  }
9578  }
9579  return $elements;
9580  }
9581 
9586  protected function ‪controlActiveElements(): void
9587  {
9588  if (!empty($this->control['active'])) {
9589  $this->‪setNullValues(
9590  $this->control['active'],
9591  $this->datamap
9592  );
9593  }
9594  }
9595 
9604  protected function ‪setNullValues(array $active, array &$haystack): void
9605  {
9606  foreach ($active as $key => $value) {
9607  // Nested data is processes recursively
9608  if (is_array($value)) {
9609  $this->‪setNullValues(
9610  $value,
9611  $haystack[$key]
9612  );
9613  } elseif ($value == 0) {
9614  // Field has not been activated in the user interface,
9615  // thus a NULL value shall be stored in the database
9616  $haystack[$key] = null;
9617  }
9618  }
9619  }
9620 
9622  {
9623  $this->correlationId = ‪$correlationId;
9624  }
9625 
9626  public function ‪getCorrelationId(): ?‪CorrelationId
9627  {
9628  return ‪$this->correlationId;
9629  }
9630 
9635  protected function ‪postProcessDatabaseInsert(‪Connection $connection, string $tableName, int $suggestedUid): int
9636  {
9637  if ($suggestedUid !== 0 && $connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
9638  $this->‪postProcessPostgresqlInsert($connection, $tableName);
9639  // The last inserted id on postgresql is actually the last value generated by the sequence.
9640  // On a forced UID insert this might not be the actual value or the sequence might not even
9641  // have generated a value yet.
9642  // Return the actual ID we forced on insert as a surrogate.
9643  return $suggestedUid;
9644  }
9645  $id = $connection->‪lastInsertId();
9646  return (int)$id;
9647  }
9648 
9654  protected function ‪postProcessPostgresqlInsert(Connection $connection, string $tableName): void
9655  {
9656  $queryBuilder = $connection->createQueryBuilder();
9657  $queryBuilder->getRestrictions()->removeAll();
9658  $row = $queryBuilder->select('PGT.schemaname', 'S.relname', 'C.attname', 'T.relname AS tablename')
9659  ->from('pg_class', 'S')
9660  ->from('pg_depend', 'D')
9661  ->from('pg_class', 'T')
9662  ->from('pg_attribute', 'C')
9663  ->from('pg_tables', 'PGT')
9664  ->where(
9665  $queryBuilder->expr()->eq('S.relkind', $queryBuilder->quote('S')),
9666  $queryBuilder->expr()->eq('S.oid', $queryBuilder->quoteIdentifier('D.objid')),
9667  $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('T.oid')),
9668  $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('C.attrelid')),
9669  $queryBuilder->expr()->eq('D.refobjsubid', $queryBuilder->quoteIdentifier('C.attnum')),
9670  $queryBuilder->expr()->eq('T.relname', $queryBuilder->quoteIdentifier('PGT.tablename')),
9671  $queryBuilder->expr()->eq('PGT.tablename', $queryBuilder->quote($tableName))
9672  )
9673  ->setMaxResults(1)
9674  ->executeQuery()
9675  ->fetchAssociative();
9676  if ($row !== false) {
9677  $connection->executeStatement(
9678  sprintf(
9679  'SELECT SETVAL(%s, COALESCE(MAX(%s), 0)+1, FALSE) FROM %s',
9680  $connection->quote($row['schemaname'] . '.' . $row['relname']),
9681  $connection->quoteIdentifier($row['attname']),
9682  $connection->quoteIdentifier($row['schemaname'] . '.' . $row['tablename'])
9683  )
9684  );
9685  }
9686  }
9687 
9689  {
9690  $isWorkspacesLoaded = ‪ExtensionManagementUtility::isLoaded('workspaces');
9691  $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
9692  $relationHandler->setWorkspaceId($this->BE_USER->workspace);
9693  $relationHandler->setUseLiveReferenceIds($isWorkspacesLoaded);
9694  $relationHandler->setUseLiveParentIds($isWorkspacesLoaded);
9695  $relationHandler->setReferenceIndexUpdater($this->referenceIndexUpdater);
9696  return $relationHandler;
9697  }
9698 
9699  protected function ‪getLanguageService(): ‪LanguageService
9700  {
9701  return ‪$GLOBALS['LANG'];
9702  }
9703 
9707  public function ‪getHistoryRecords(): array
9708  {
9709  return ‪$this->historyRecords;
9710  }
9711 }
‪TYPO3\CMS\Core\SysLog\Action\Cache
Definition: Cache.php:24
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForNumber
‪checkValueForNumber(mixed $value, array $tcaFieldConf)
Definition: DataHandler.php:1631
‪TYPO3\CMS\Core\DataHandling\DataHandler\updateRefIndex
‪updateRefIndex($table, $uid, int $workspace=null)
Definition: DataHandler.php:7718
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:94
‪TYPO3\CMS\Core\DataHandling\DataHandler\tableAdminOnly
‪bool tableAdminOnly($table)
Definition: DataHandler.php:7238
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_group_select_explodeSelectGroupValue
‪array checkValue_group_select_explodeSelectGroupValue($value)
Definition: DataHandler.php:3110
‪TYPO3\CMS\Core\DataHandling\DataHandler\fixUniqueInSiteForSubpages
‪fixUniqueInSiteForSubpages(int $pageId)
Definition: DataHandler.php:8513
‪TYPO3\CMS\Core\DataHandling\DataHandler\copy_remapTranslationSourceField
‪copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap)
Definition: DataHandler.php:4287
‪TYPO3\CMS\Core\DataHandling\DataHandler\$recordsToClearCacheFor
‪static array $recordsToClearCacheFor
Definition: DataHandler.php:463
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkRecordInsertAccess
‪bool checkRecordInsertAccess($insertTable, $pid, $action=SystemLogDatabaseAction::INSERT)
Definition: DataHandler.php:7062
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForPassword
‪array checkValueForPassword(string $value, array $tcaFieldConf, string $table, int|string $id, int $realPid, array $incomingFieldArray=[])
Definition: DataHandler.php:1768
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkRecordUpdateAccess
‪bool checkRecordUpdateAccess($table, $id)
Definition: DataHandler.php:7021
‪TYPO3\CMS\Core\DataHandling\DataHandler\getDefaultLanguagePageId
‪int getDefaultLanguagePageId(int $pageId)
Definition: DataHandler.php:9208
‪TYPO3\CMS\Core\Database\Query\QueryHelper\getDateTimeFormats
‪static array getDateTimeFormats()
Definition: QueryHelper.php:183
‪TYPO3\CMS\Core\DataHandling\DataHandler\moveRecord
‪moveRecord($table, $uid, $destPid)
Definition: DataHandler.php:4337
‪TYPO3\CMS\Core\DataHandling\DataHandler\recordInfo
‪array null recordInfo($table, $id)
Definition: DataHandler.php:7398
‪TYPO3\CMS\Core\DataHandling\DataHandler\$connectionPool
‪readonly ConnectionPool $connectionPool
Definition: DataHandler.php:473
‪TYPO3\CMS\Core\DataHandling\DataHandler\copyRecord
‪int null copyRecord($table, $uid, $destPid, $first=false, $overrideValues=[], $excludeFields='', $language=0, $ignoreLocalization=false)
Definition: DataHandler.php:3512
‪TYPO3\CMS\Core\DataHandling\DataHandler\$reverseOrder
‪bool $reverseOrder
Definition: DataHandler.php:116
‪TYPO3\CMS\Core\DataHandling\DataHandler\$substNEWwithIDs
‪array $substNEWwithIDs
Definition: DataHandler.php:224
‪TYPO3\CMS\Core\DataHandling\DataHandler\$outerMostInstance
‪self $outerMostInstance
Definition: DataHandler.php:458
‪TYPO3\CMS\Core\DataHandling\DataHandler\$isInWebMount_Cache
‪array $isInWebMount_Cache
Definition: DataHandler.php:364
‪TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory
Definition: PasswordHashFactory.php:27
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\USER_BACKEND
‪const USER_BACKEND
Definition: RecordHistoryStore.php:39
‪TYPO3\CMS\Core\DataHandling\DataHandler\registerNestedElementCall
‪registerNestedElementCall($table, $id, $identifier)
Definition: DataHandler.php:9489
‪TYPO3\CMS\Core\DataHandling\DataHandler\initializeSlugFieldsToEmptyString
‪initializeSlugFieldsToEmptyString(string $tableName, array $fieldValues)
Definition: DataHandler.php:1099
‪TYPO3\CMS\Core\DataHandling\DataHandler\copyL10nOverlayRecords
‪copyL10nOverlayRecords($table, $uid, $destPid, $first=false, $overrideValues=[], $excludeFields='')
Definition: DataHandler.php:4210
‪TYPO3\CMS\Core\DataHandling\DataHandler\clearPrefixFromValue
‪string clearPrefixFromValue($table, $value)
Definition: DataHandler.php:8715
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_text_Eval
‪array checkValue_text_Eval($value, $evalArray, $is_in)
Definition: DataHandler.php:2877
‪TYPO3\CMS\Core\DataHandling\DataHandler\deleteEl
‪deleteEl(string $table, int $uid, bool $noRecordCheck=false, bool $forceHardDelete=false, bool $deleteRecordsOnPage=true)
Definition: DataHandler.php:5100
‪TYPO3\CMS\Core\DataHandling\DataHandler\$isRecordInWebMount_Cache
‪array $isRecordInWebMount_Cache
Definition: DataHandler.php:359
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Core\DataHandling\DataHandler\validateValueForRequired
‪bool validateValueForRequired(array $tcaFieldConfig, mixed $value)
Definition: DataHandler.php:2998
‪TYPO3\CMS\Core\DataHandling\DataHandler\increaseSortingOfFollowingRecords
‪increaseSortingOfFollowingRecords(string $table, int $pid, int $sortingValue=null)
Definition: DataHandler.php:7944
‪TYPO3\CMS\Core\DataHandling\DataHandler\getLocalTCE
‪getLocalTCE()
Definition: DataHandler.php:6406
‪TYPO3\CMS\Core\DataHandling\DataHandler\fixUniqueInPid
‪fixUniqueInPid($table, $uid)
Definition: DataHandler.php:8453
‪TYPO3\CMS\Core\DataHandling\DataHandler\discardLocalizationOverlayRecords
‪discardLocalizationOverlayRecords(string $table, array $record)
Definition: DataHandler.php:6129
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForCheck
‪array checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field)
Definition: DataHandler.php:2165
‪TYPO3\CMS\Core\DataHandling\DataHandler\$remapStack
‪array $remapStack
Definition: DataHandler.php:414
‪TYPO3\CMS\Core\DataHandling\DataHandler\deleteRecord
‪deleteRecord(string $table, int $uid, bool $noRecordCheck=false, bool $forceHardDelete=false)
Definition: DataHandler.php:5200
‪TYPO3\CMS\Core\DataHandling\DataHandler\$pageCache
‪array $pageCache
Definition: DataHandler.php:371
‪TYPO3\CMS\Core\DataHandling\DataHandler\$storeLogMessages
‪bool $storeLogMessages
Definition: DataHandler.php:105
‪TYPO3\CMS\Core\DataHandling\DataHandler\getUnique
‪string getUnique($table, $field, $value, $id, $newPid=0)
Definition: DataHandler.php:2722
‪TYPO3\CMS\Core\DataHandling\DataHandler\canDeletePage
‪int[] string canDeletePage($uid)
Definition: DataHandler.php:5464
‪TYPO3\CMS\Core\DataHandling\DataHandler\registerRecordIdForPageCacheClearing
‪registerRecordIdForPageCacheClearing($table, $uid, $pid=null)
Definition: DataHandler.php:8792
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForColor
‪array checkValueForColor(string $value, array $tcaFieldConf)
Definition: DataHandler.php:1683
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_file_processDBdata
‪checkValue_file_processDBdata($valueArray, $tcaFieldConf, $id, $table)
Definition: DataHandler.php:3327
‪TYPO3\CMS\Core\DataHandling\DataHandler\$cmdmap
‪array $cmdmap
Definition: DataHandler.php:327
‪TYPO3\CMS\Core\DataHandling\DataHandler\getOriginalParentOfRecord
‪int[] getOriginalParentOfRecord($table, $uid)
Definition: DataHandler.php:8304
‪TYPO3\CMS\Core\DataHandling\DataHandler\$checkStoredRecords
‪$checkStoredRecords
Definition: DataHandler.php:119
‪TYPO3\CMS\Core\DataHandling\DataHandler\isElementToBeDeleted
‪bool isElementToBeDeleted($table, $id)
Definition: DataHandler.php:9515
‪TYPO3\CMS\Core\Database\Query\QueryHelper\quoteDatabaseIdentifiers
‪static quoteDatabaseIdentifiers(Connection $connection, string $sql)
Definition: QueryHelper.php:224
‪TYPO3\CMS\Core\DataHandling\DataHandler\isInWebMount
‪bool isInWebMount($pid)
Definition: DataHandler.php:7005
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_NEW
‪const PAGE_NEW
Definition: Permission.php:50
‪TYPO3\CMS\Core\DataHandling\DataHandler\unsetElementsToBeDeleted
‪unsetElementsToBeDeleted(array $elements)
Definition: DataHandler.php:9549
‪TYPO3\CMS\Core\DataHandling\DataHandler\disableDeleteClause
‪disableDeleteClause()
Definition: DataHandler.php:8265
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkForRecordsFromDisallowedTables
‪string[] null checkForRecordsFromDisallowedTables(array $pageIds)
Definition: DataHandler.php:8728
‪TYPO3\CMS\Webhooks\Message\$details
‪identifier readonly UriInterface readonly array $details
Definition: MfaVerificationErrorOccurredMessage.php:37
‪TYPO3\CMS\Core\Exception
Definition: Exception.php:21
‪TYPO3\CMS\Core\DataHandling\DataHandler\hook_processDatamap_afterDatabaseOperations
‪hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray)
Definition: DataHandler.php:657
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_SW
‪array checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $tscPID, array $additionalData=null)
Definition: DataHandler.php:1442
‪TYPO3\CMS\Core\DataHandling\DataHandler\isSubmittedValueEqualToStoredValue
‪bool isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull=false)
Definition: DataHandler.php:8229
‪TYPO3\CMS\Core\DataHandling\DataHandler\createRelationHandlerInstance
‪createRelationHandlerInstance()
Definition: DataHandler.php:9686
‪TYPO3\CMS\Core\DataHandling\DataHandler\compareFieldArrayWithCurrentAndUnset
‪array compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
Definition: DataHandler.php:8164
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForText
‪array checkValueForText($value, $tcaFieldConf, $table, $realPid, $field)
Definition: DataHandler.php:1540
‪TYPO3\CMS\Core\DataHandling\DataHandler\doesBranchExist
‪array< int > null doesBranchExist(int $pid, int $permissions, array $pageIdsInBranch=[])
Definition: DataHandler.php:7191
‪TYPO3\CMS\Core\DataHandling\DataHandler\copyPages
‪copyPages($uid, $destPid)
Definition: DataHandler.php:3648
‪TYPO3\CMS\Core\DataHandling\DataHandler\getCopyHeader
‪string getCopyHeader($table, $pid, $field, $value, $count, $prevTitle='')
Definition: DataHandler.php:8642
‪TYPO3\CMS\Core\DataHandling\DataHandler\moveRecord_procBasedOnFieldType
‪moveRecord_procBasedOnFieldType($table, $uid, $destPid, $value, $conf)
Definition: DataHandler.php:4610
‪TYPO3\CMS\Core\DataHandling\DataHandler\deleteRecord_procFields
‪deleteRecord_procFields($table, $uid)
Definition: DataHandler.php:5534
‪TYPO3\CMS\Core\Database\RelationHandler
Definition: RelationHandler.php:36
‪TYPO3\CMS\Core\DataHandling\DataHandler\getTableEntries
‪array getTableEntries($table, $TSconfig)
Definition: DataHandler.php:8321
‪TYPO3\CMS\Core\Versioning\VersionState
‪VersionState
Definition: VersionState.php:22
‪TYPO3\CMS\Core\DataHandling\DataHandler\recordInfoWithPermissionCheck
‪array< string, mixed > false recordInfoWithPermissionCheck(string $table, int $id, int $perms, string $fieldList=' *')
Definition: DataHandler.php:7425
‪TYPO3\CMS\Core\DataHandling\DataHandler\$errorLog
‪array $errorLog
Definition: DataHandler.php:257
‪TYPO3\CMS\Core\DataHandling\DataHandler\remapListedDBRecords_procInline
‪remapListedDBRecords_procInline($conf, $value, $uid, $table)
Definition: DataHandler.php:6592
‪TYPO3\CMS\Core\DataHandling\DataHandler\destNotInsideSelf
‪bool destNotInsideSelf($destinationId, $id)
Definition: DataHandler.php:7253
‪TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException
Definition: InvalidPasswordHashException.php:25
‪TYPO3\CMS\Core\DataHandling\DataHandler\getOuterMostInstance
‪DataHandler getOuterMostInstance()
Definition: DataHandler.php:9442
‪TYPO3\CMS\Core\DataHandling\DataHandler\setHistory
‪setHistory($table, $id)
Definition: DataHandler.php:7685
‪TYPO3\CMS\Core\DataHandling\DataHandler\getExcludeListArray
‪array getExcludeListArray()
Definition: DataHandler.php:7291
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_flex_procInData_travDS
‪checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions=[])
Definition: DataHandler.php:3170
‪TYPO3\CMS\Core\Type\Bitmask\Permission\NOTHING
‪const NOTHING
Definition: Permission.php:30
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForGroupFolderSelect
‪array checkValueForGroupFolderSelect($res, $value, $tcaFieldConf, $table, $id, $status, $field)
Definition: DataHandler.php:2328
‪TYPO3\CMS\Core\DataHandling\DataHandler\mmRelationIsLocalSide
‪mmRelationIsLocalSide(array $config)
Definition: DataHandler.php:6392
‪TYPO3\CMS\Core\DataHandling\DataHandler\$callBackObj
‪object $callBackObj
Definition: DataHandler.php:200
‪TYPO3\CMS\Core\DataHandling\DataHandler\$BE_USER
‪BackendUserAuthentication $BE_USER
Definition: DataHandler.php:280
‪TYPO3\CMS\Core\DataHandling
Definition: DataHandler.php:16
‪TYPO3\CMS\Core\DataHandling\DataHandler\isTableAllowedForThisPage
‪bool isTableAllowedForThisPage(int $pageUid, $checkTable)
Definition: DataHandler.php:7109
‪TYPO3\CMS\Core\DataHandling\DataHandler\resolveFieldConfigurationAndRespectColumnsOverrides
‪resolveFieldConfigurationAndRespectColumnsOverrides(string $table, string $field)
Definition: DataHandler.php:1411
‪TYPO3\CMS\Core\DataHandling\DataHandler\discard
‪discard(string $table, ?int $uid, array $record=null)
Definition: DataHandler.php:5820
‪TYPO3\CMS\Core\DataHandling\DataHandler\resetNestedElementCalls
‪resetNestedElementCalls()
Definition: DataHandler.php:9499
‪TYPO3\CMS\Core\DataHandling\DataHandler\resolveSortingAndPidForNewRecord
‪array resolveSortingAndPidForNewRecord(string $table, int $pid, array $fieldArray)
Definition: DataHandler.php:1120
‪TYPO3\CMS\Core\DataHandling\Model\CorrelationId\forScope
‪static forScope(string $scope)
Definition: CorrelationId.php:42
‪TYPO3\CMS\Core\DataHandling\DataHandler\doesRecordExist
‪bool doesRecordExist($table, $id, int $perms)
Definition: DataHandler.php:7140
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_flex_procInData
‪array checkValue_flex_procInData($dataPart, $dataPart_current, $dataStructure, $pParams, $callBackFunc='', array $workspaceOptions=[])
Definition: DataHandler.php:3135
‪TYPO3\CMS\Core\DataHandling\DataHandler\workspaceCannotEditOfflineVersion
‪string workspaceCannotEditOfflineVersion(string $table, array $record)
Definition: DataHandler.php:9371
‪TYPO3\CMS\Core\DataHandling\DataHandler\isOuterMostInstance
‪isOuterMostInstance()
Definition: DataHandler.php:9461
‪TYPO3\CMS\Core\DataHandling\DataHandler\isRecordCopied
‪bool isRecordCopied($table, $uid)
Definition: DataHandler.php:8764
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪TYPO3\CMS\Core\DataHandling\DataHandler\dbAnalysisStoreExec
‪dbAnalysisStoreExec()
Definition: DataHandler.php:8355
‪TYPO3\CMS\Core\DataHandling\DataHandler\registerReferenceIndexUpdateForReferencesToItem
‪registerReferenceIndexUpdateForReferencesToItem(string $table, int $uid, int $workspace, int $targetWorkspace=null)
Definition: DataHandler.php:7748
‪TYPO3\CMS\Core\DataHandling\DataHandler\CACHE_IDENTIFIER_ELEMENTS_TO_BE_DELETED
‪const CACHE_IDENTIFIER_ELEMENTS_TO_BE_DELETED
Definition: DataHandler.php:479
‪TYPO3\CMS\Core\DataHandling\DataHandler\$excludedTablesAndFields
‪array $excludedTablesAndFields
Definition: DataHandler.php:301
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Core\DataHandling\DataHandler\$enableLogging
‪bool $enableLogging
Definition: DataHandler.php:110
‪TYPO3\CMS\Core\DataHandling\DataHandler\castReferenceValue
‪int string castReferenceValue($value, array $configuration, bool $isNew)
Definition: DataHandler.php:8565
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForRadio
‪array checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $pid, $field)
Definition: DataHandler.php:2246
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_checkMax
‪array checkValue_checkMax($tcaFieldConf, $valueArray)
Definition: DataHandler.php:2694
‪TYPO3\CMS\Core\SysLog\Action\Database
Definition: Database.php:24
‪TYPO3\CMS\Core\DataHandling\DataHandler\getSortNumber
‪int array bool null getSortNumber($table, $uid, $pid)
Definition: DataHandler.php:7789
‪TYPO3\CMS\Core\DataHandling\DataHandler\$substNEWwithIDs_table
‪array $substNEWwithIDs_table
Definition: DataHandler.php:231
‪TYPO3\CMS\Core\DataHandling\DataHandler\tableReadOnly
‪bool tableReadOnly($table)
Definition: DataHandler.php:7225
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForEmail
‪array checkValueForEmail(string $value, array $tcaFieldConf, string $table, int|string $id, int $realPid, string $field)
Definition: DataHandler.php:1709
‪TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface
Definition: QueryRestrictionContainerInterface.php:25
‪TYPO3\CMS\Core\DataHandling\DataHandler\copyRecord_processRelation
‪string copyRecord_processRelation( $table, $uid, $field, $value, $row, $conf, $realDestPid, $language, array $workspaceOptions)
Definition: DataHandler.php:4095
‪TYPO3\CMS\Core\DataHandling\DataHandler\getRecordsWithSameValue
‪getRecordsWithSameValue($tableName, $uid, $fieldName, $value, $pageId=0)
Definition: DataHandler.php:2833
‪TYPO3\CMS\Core\DataHandling\DataHandler\postProcessPostgresqlInsert
‪postProcessPostgresqlInsert(Connection $connection, string $tableName)
Definition: DataHandler.php:9652
‪TYPO3\CMS\Core\DataHandling\DataHandler\getAutoVersionId
‪getAutoVersionId($table, $id)
Definition: DataHandler.php:9268
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForFlex
‪array checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $field)
Definition: DataHandler.php:2466
‪TYPO3\CMS\Core\DataHandling\DataHandler\$remapStackActions
‪array $remapStackActions
Definition: DataHandler.php:427
‪TYPO3\CMS\Core\DataHandling\DataHandler\$pagePermissionAssembler
‪PagePermissionAssembler $pagePermissionAssembler
Definition: DataHandler.php:296
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:57
‪TYPO3\CMS\Core\DataHandling\DataHandler\deleteAction
‪deleteAction($table, $id)
Definition: DataHandler.php:5063
‪TYPO3\CMS\Core\DataHandling\DataHandler\getCorrelationId
‪getCorrelationId()
Definition: DataHandler.php:9624
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkModifyAccessList
‪bool checkModifyAccessList($table)
Definition: DataHandler.php:6970
‪TYPO3\CMS\Core\DataHandling\DataHandler\resolvePid
‪int resolvePid($table, $pid)
Definition: DataHandler.php:8690
‪TYPO3\CMS\Core\DataHandling\DataHandler\inlineLocalizeSynchronize
‪inlineLocalizeSynchronize($table, $id, array $command)
Definition: DataHandler.php:4905
‪TYPO3\CMS\Core\DataHandling\DataHandler\getAllowedTablesToCopyWhenCopyingAPage
‪getAllowedTablesToCopyWhenCopyingAPage()
Definition: DataHandler.php:3687
‪TYPO3\CMS\Core\DataHandling\DataHandler\$recInsertAccessCache
‪array $recInsertAccessCache
Definition: DataHandler.php:354
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\isLoaded
‪static isLoaded(string $key)
Definition: ExtensionManagementUtility.php:55
‪TYPO3\CMS\Core\DataHandling\DataHandler\getCheckModifyAccessListHookObjects
‪array getCheckModifyAccessListHookObjects()
Definition: DataHandler.php:682
‪TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory
Definition: RecordStateFactory.php:26
‪TYPO3\CMS\Core\DataHandling\DataHandler\fixCopyAfterDuplFields
‪array fixCopyAfterDuplFields(string $table, int $prevUid)
Definition: DataHandler.php:8535
‪TYPO3\CMS\Core\DataHandling\DataHandler\overlayAutoVersionId
‪int overlayAutoVersionId($table, $id)
Definition: DataHandler.php:9284
‪TYPO3\CMS\Core\DataHandling\DataHandler\$cacheManager
‪readonly CacheManager $cacheManager
Definition: DataHandler.php:471
‪TYPO3\CMS\Core\DataHandling\DataHandler\$bypassWorkspaceRestrictions
‪bool $bypassWorkspaceRestrictions
Definition: DataHandler.php:151
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility
Definition: ExtensionManagementUtility.php:32
‪TYPO3\CMS\Core\DataHandling\DataHandler\setControl
‪setControl(array $control)
Definition: DataHandler.php:503
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore
Definition: RecordHistoryStore.php:31
‪TYPO3\CMS\Core\Type\ContextualFeedbackSeverity
‪ContextualFeedbackSeverity
Definition: ContextualFeedbackSeverity.php:25
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:27
‪TYPO3\CMS\Core\DataHandling\DataHandler\discardSubPagesAndRecordsOnPage
‪discardSubPagesAndRecordsOnPage(array $page)
Definition: DataHandler.php:5916
‪TYPO3\CMS\Core\DataHandling\DataHandler\hasDeletedRecord
‪bool hasDeletedRecord($tableName, $uid)
Definition: DataHandler.php:9253
‪TYPO3\CMS\Core\DataHandling\DataHandler\discardWorkspaceVersionsOfRecord
‪discardWorkspaceVersionsOfRecord($table, $uid)
Definition: DataHandler.php:5165
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor
Definition: DataMapProcessor.php:59
‪TYPO3\CMS\Core\DataHandling\DataHandler\remapListedDBRecords_procFile
‪remapListedDBRecords_procFile($conf, $value, $uid, $table)
Definition: DataHandler.php:6657
‪TYPO3\CMS\Core\DataHandling\DataHandler\updateDB
‪updateDB($table, $id, $fieldArray)
Definition: DataHandler.php:7542
‪TYPO3\CMS\Core\DataHandling\DataHandler\prepareCacheFlush
‪array prepareCacheFlush($table, $uid, $pid)
Definition: DataHandler.php:8855
‪TYPO3\CMS\Core\DataHandling\DataHandler\moveL10nOverlayRecords
‪moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
Definition: DataHandler.php:4642
‪TYPO3\CMS\Core\DataHandling\DataHandler\setDefaultsFromUserTS
‪setDefaultsFromUserTS($userTS)
Definition: DataHandler.php:596
‪TYPO3\CMS\Core\DataHandling\DataHandler\remapListedDBRecords
‪remapListedDBRecords()
Definition: DataHandler.php:6425
‪TYPO3\CMS\Core\DataHandling\DataHandler\getRelationFieldType
‪string bool getRelationFieldType($conf)
Definition: DataHandler.php:8609
‪TYPO3\CMS\Core\DataHandling\DataHandler\discardRecordRelations
‪discardRecordRelations(string $table, array $record)
Definition: DataHandler.php:5983
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue
‪array checkValue($table, $field, $value, $id, $status, $realPid, $tscPID, $incomingFieldArray=[])
Definition: DataHandler.php:1316
‪TYPO3\CMS\Core\DataHandling\DataHandler\getVersionizedIncomingFieldArray
‪getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
Definition: DataHandler.php:6919
‪TYPO3\CMS\Core\DataHandling\DataHandler\undeleteRecord
‪undeleteRecord(string $table, int $uid)
Definition: DataHandler.php:5647
‪TYPO3\CMS\Core\DataHandling\DataHandler\localize
‪int bool localize($table, $uid, $language)
Definition: DataHandler.php:4702
‪TYPO3\CMS\Core\DataHandling\DataHandler\moveRecord_procFields
‪moveRecord_procFields($table, $uid, $destPid)
Definition: DataHandler.php:4589
‪TYPO3\CMS\Core\DataHandling\DataHandler\$datamap
‪array $datamap
Definition: DataHandler.php:317
‪TYPO3\CMS\Core\DataHandling\DataHandler\$registerDBPids
‪array $registerDBPids
Definition: DataHandler.php:394
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException
Definition: InvalidIdentifierException.php:21
‪TYPO3\CMS\Core\DataHandling\DataHandler\isReferenceField
‪bool isReferenceField($conf)
Definition: DataHandler.php:8593
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Core\DataHandling\DataHandler\$userid
‪int $userid
Definition: DataHandler.php:287
‪TYPO3\CMS\Core\DataHandling\DataHandler\undeleteRecordRelations
‪undeleteRecordRelations(string $table, int $uid, array $record)
Definition: DataHandler.php:5769
‪TYPO3\CMS\Core\DataHandling\DataHandler\$remapStackRecords
‪array $remapStackRecords
Definition: DataHandler.php:422
‪TYPO3\CMS\Core\DataHandling\DataHandler\$copyMappingArray
‪array $copyMappingArray
Definition: DataHandler.php:407
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForDatetime
‪checkValueForDatetime(mixed $value, array $tcaFieldConf)
Definition: DataHandler.php:2058
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForUuid
‪array checkValueForUuid(string $value, array $tcaFieldConf)
Definition: DataHandler.php:2404
‪TYPO3\CMS\Core\DataHandling\DataHandler\$newRelatedIDs
‪array $newRelatedIDs
Definition: DataHandler.php:238
‪TYPO3\CMS\Core\DataHandling\DataHandler\$suggestedInsertUids
‪array $suggestedInsertUids
Definition: DataHandler.php:192
‪TYPO3\CMS\Core\DataHandling\DataHandler\$checkValue_currentRecord
‪array $checkValue_currentRecord
Definition: DataHandler.php:445
‪TYPO3\CMS\Core\DataHandling\DataHandler\deletePages
‪deletePages(int $uid, bool $force=false, bool $forceHardDelete=false, bool $deleteRecordsOnPage=true)
Definition: DataHandler.php:5325
‪TYPO3\CMS\Core\DataHandling\Model\CorrelationId
Definition: CorrelationId.php:29
‪TYPO3\CMS\Core\Database\Query\QueryHelper\getDateTimeTypes
‪static array getDateTimeTypes()
Definition: QueryHelper.php:211
‪TYPO3\CMS\Core\PasswordPolicy\NEW_USER_PASSWORD
‪@ NEW_USER_PASSWORD
Definition: PasswordPolicyAction.php:27
‪TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator
Definition: PasswordPolicyValidator.php:27
‪TYPO3\CMS\Core\DataHandling\DataHandler\$disableDeleteClause
‪bool $disableDeleteClause
Definition: DataHandler.php:450
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForInput
‪array checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field)
Definition: DataHandler.php:1589
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools
Definition: FlexFormTools.php:40
‪TYPO3\CMS\Core\DataHandling\DataHandler\$defaultValues
‪array $defaultValues
Definition: DataHandler.php:183
‪TYPO3\CMS\Core\Database\Connection\createQueryBuilder
‪createQueryBuilder()
Definition: Connection.php:114
‪TYPO3\CMS\Core\DataHandling\DataHandler\$pagetreeNeedsRefresh
‪bool $pagetreeNeedsRefresh
Definition: DataHandler.php:269
‪TYPO3\CMS\Core\DataHandling\DataHandler\deleteRecord_procBasedOnFieldType
‪deleteRecord_procBasedOnFieldType($table, $uid, $value, $conf)
Definition: DataHandler.php:5557
‪TYPO3\CMS\Core\DataHandling\DataHandler\clear_cacheCmd
‪clear_cacheCmd($cacheCmd)
Definition: DataHandler.php:9043
‪TYPO3\CMS\Core\DataHandling\DataHandler\$sortIntervals
‪int $sortIntervals
Definition: DataHandler.php:348
‪TYPO3\CMS\Webhooks\Message\$record
‪identifier readonly int readonly array $record
Definition: PageModificationMessage.php:36
‪TYPO3\CMS\Core\DataHandling\DataHandler\insertNewCopyVersion
‪int null insertNewCopyVersion($table, $fieldArray, $realPid)
Definition: DataHandler.php:3940
‪TYPO3\CMS\Core\DataHandling\DataHandler\versionizeRecord
‪int null versionizeRecord($table, $id, $label, $delete=false)
Definition: DataHandler.php:6172
‪TYPO3\CMS\Core\Html\RteHtmlParser
Definition: RteHtmlParser.php:40
‪TYPO3\CMS\Core\DataHandling\DataHandler\moveRecord_raw
‪moveRecord_raw($table, $uid, $destPid)
Definition: DataHandler.php:4416
‪TYPO3\CMS\Core\DataHandling\DataHandler\$dbAnalysisStore
‪array $dbAnalysisStore
Definition: DataHandler.php:379
‪TYPO3\CMS\Core\DataHandling\DataHandler\remapListedDBRecords_flexFormCallBack
‪array remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue)
Definition: DataHandler.php:6499
‪TYPO3\CMS\Core\DataHandling\DataHandler\eventPid
‪int eventPid($table, $uid, $pid)
Definition: DataHandler.php:7523
‪TYPO3\CMS\Core\DataHandling\DataHandler\insertUpdateDB_preprocessBasedOnFieldType
‪array insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
Definition: DataHandler.php:9229
‪TYPO3\CMS\Core\DataHandling\DataHandler\start
‪start(array $dataMap, array $commandMap, ?BackendUserAuthentication $backendUser=null)
Definition: DataHandler.php:517
‪TYPO3\CMS\Core\SysLog\Error
Definition: Error.php:24
‪TYPO3\CMS\Core\Database\Connection\quoteIdentifier
‪string quoteIdentifier(string $identifier)
Definition: Connection.php:129
‪TYPO3\CMS\Core\DataHandling\DataHandler\getCommandMapElements
‪getCommandMapElements(string $needle)
Definition: DataHandler.php:9565
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_input_Eval
‪array checkValue_input_Eval($value, $evalArray, $is_in, string $table='', $id='')
Definition: DataHandler.php:2915
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForCategory
‪checkValueForCategory(array $result, string $value, array $tcaFieldConf, string $table, $id, string $status, string $field)
Definition: DataHandler.php:2009
‪TYPO3\CMS\Core\DataHandling\DataHandler\getPID
‪int false getPID($table, $uid)
Definition: DataHandler.php:8337
‪TYPO3\CMS\Core\DataHandling\DataHandler\$correlationId
‪CorrelationId $correlationId
Definition: DataHandler.php:206
‪TYPO3\CMS\Core\DataHandling\DataHandler\addRemapAction
‪addRemapAction($table, $id, callable $callback, array $arguments)
Definition: DataHandler.php:6895
‪TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter
Definition: FileExtensionFilter.php:31
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\DataHandling\DataHandler\deleteL10nOverlayRecords
‪deleteL10nOverlayRecords($table, $uid)
Definition: DataHandler.php:5596
‪TYPO3\CMS\Core\DataHandling\DataHandler\int_pageTreeInfo
‪array int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
Definition: DataHandler.php:8381
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:35
‪TYPO3\CMS\Core\DataHandling\DataHandler\discardLocalizedWorkspaceVersionsOfRecord
‪discardLocalizedWorkspaceVersionsOfRecord(string $table, int $uid)
Definition: DataHandler.php:5117
‪TYPO3\CMS\Core\DataHandling\DataHandler\$isImporting
‪bool $isImporting
Definition: DataHandler.php:131
‪TYPO3\CMS\Core\DataHandling\DataHandler\addDefaultPermittedLanguageIfNotSet
‪addDefaultPermittedLanguageIfNotSet(string $table, array $incomingFieldArray, int $pageId)
Definition: DataHandler.php:8102
‪TYPO3\CMS\Core\Service\OpcodeCacheService
Definition: OpcodeCacheService.php:27
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForSlug
‪array checkValueForSlug(string $value, array $tcaFieldConf, string $table, $id, int $realPid, string $field, array $incomingFieldArray=[])
Definition: DataHandler.php:1879
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_group_select_processDBdata
‪array checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
Definition: DataHandler.php:3066
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_inline_processDBdata
‪string checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field)
Definition: DataHandler.php:3299
‪TYPO3\CMS\Core\DataHandling\DataHandler\$copyWhichTables
‪string $copyWhichTables
Definition: DataHandler.php:165
‪TYPO3\CMS\Core\DataHandling\DataHandler\getPreviousLocalizedRecordUid
‪int getPreviousLocalizedRecordUid($table, $uid, $pid, $targetLanguage)
Definition: DataHandler.php:8004
‪TYPO3\CMS\Core\DataHandling\DataHandler\copySpecificPage
‪int null copySpecificPage($uid, $destPid, $copyTablesArray, $first=false)
Definition: DataHandler.php:3718
‪TYPO3\CMS\Core\DataHandling\DataHandler\$historyRecords
‪array $historyRecords
Definition: DataHandler.php:337
‪TYPO3\CMS\Core\DataHandling\DataHandler\process_cmdmap
‪void bool process_cmdmap()
Definition: DataHandler.php:3353
‪TYPO3\CMS\Core\DataHandling\DataHandler\discardCsvReferencesToRecord
‪discardCsvReferencesToRecord(string $table, array $record)
Definition: DataHandler.php:6032
‪TYPO3\CMS\Core\DataHandling\DataHandler\getUniqueCountStatement
‪QueryBuilder getUniqueCountStatement(string $value, string $table, string $field, int $uid, int $pid)
Definition: DataHandler.php:2773
‪TYPO3\CMS\Core\DataHandling\DataHandler\getHistoryRecords
‪getHistoryRecords()
Definition: DataHandler.php:9705
‪TYPO3\CMS\Core\DataHandling\DataHandler\registerElementsToBeDeleted
‪registerElementsToBeDeleted()
Definition: DataHandler.php:9527
‪TYPO3\CMS\Core\DataHandling\DataHandler\processClearCacheQueue
‪processClearCacheQueue()
Definition: DataHandler.php:8809
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_category_processDBdata
‪array checkValue_category_processDBdata(array $valueArray, array $tcaFieldConf, $id, string $status, string $table, string $field)
Definition: DataHandler.php:3018
‪TYPO3\CMS\Core\DataHandling\DataHandler\workspaceAllowAutoCreation
‪bool workspaceAllowAutoCreation(string $table, $id, $recpid)
Definition: DataHandler.php:9342
‪TYPO3\CMS\Core\DataHandling\DataHandler\copyRecord_flexFormCallBack
‪array copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $workspaceOptions)
Definition: DataHandler.php:4186
‪TYPO3\CMS\Core\DataHandling\DataHandler\setCorrelationId
‪setCorrelationId(CorrelationId $correlationId)
Definition: DataHandler.php:9619
‪TYPO3\CMS\Core\DataHandling\DataHandler\pageInfo
‪string int null pageInfo(int $id, string $field)
Definition: DataHandler.php:7371
‪TYPO3\CMS\Core\DataHandling\DataHandler\copyRecord_procBasedOnFieldType
‪array string null copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language=0, array $workspaceOptions=[])
Definition: DataHandler.php:3990
‪$output
‪$output
Definition: annotationChecker.php:114
‪TYPO3\CMS\Core\DataHandling\DataHandler\setMirror
‪setMirror($mirror)
Definition: DataHandler.php:569
‪TYPO3\CMS\Core\DataHandling\DataHandler\$runtimeCache
‪readonly FrontendInterface $runtimeCache
Definition: DataHandler.php:472
‪TYPO3\CMS\Core\DataHandling\DataHandler\$bypassAccessCheckForRecords
‪bool $bypassAccessCheckForRecords
Definition: DataHandler.php:157
‪TYPO3\CMS\Core\DataHandling\DataHandler\controlActiveElements
‪controlActiveElements()
Definition: DataHandler.php:9584
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_flexArray2Xml
‪string checkValue_flexArray2Xml($array)
Definition: DataHandler.php:2538
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Core\DataHandling\DataHandler\processRemapStack
‪processRemapStack()
Definition: DataHandler.php:6709
‪TYPO3\CMS\Core\DataHandling\DataHandler\prependLabel
‪string prependLabel($table)
Definition: DataHandler.php:8677
‪TYPO3\CMS\Core\DataHandling\DataHandler\$copyTree
‪int $copyTree
Definition: DataHandler.php:173
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValue_inline
‪checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData=null)
Definition: DataHandler.php:2606
‪TYPO3\CMS\Core\DataHandling\DataHandler\$admin
‪bool $admin
Definition: DataHandler.php:294
‪TYPO3\CMS\Core\DataHandling\DataHandler\deleteSpecificPage
‪deleteSpecificPage(int $uid, bool $forceHardDelete, bool $deleteRecordsOnPage)
Definition: DataHandler.php:5367
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:27
‪TYPO3\CMS\Core\DataHandling\DataHandler\applyFiltersToValues
‪array mixed applyFiltersToValues(array $tcaFieldConfiguration, array $values)
Definition: DataHandler.php:2427
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForInline
‪array false checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData=null)
Definition: DataHandler.php:2627
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForFile
‪checkValueForFile(array $res, string $value, array $tcaFieldConf, string $table, int|string $id, string $field, ?array $additionalData=null)
Definition: DataHandler.php:2659
‪TYPO3\CMS\Core\Type\Bitmask\Permission\CONTENT_EDIT
‪const CONTENT_EDIT
Definition: Permission.php:55
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪TYPO3\CMS\Core\Database\Query\QueryHelper\stripLogicalOperatorPrefix
‪static string stripLogicalOperatorPrefix(string $constraint)
Definition: QueryHelper.php:171
‪TYPO3\CMS\Core\DataHandling\DataHandler\flexFieldDefinitionIsMmRelation
‪flexFieldDefinitionIsMmRelation(array $flexFieldDefinition)
Definition: DataHandler.php:6349
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:26
‪TYPO3\CMS\Core\DataHandling\PagePermissionAssembler
Definition: PagePermissionAssembler.php:34
‪TYPO3\CMS\Core\DataHandling\DataHandler\remapListedDBRecords_procDBRefs
‪array null remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
Definition: DataHandler.php:6525
‪TYPO3\CMS\Core\DataHandling\DataHandler\$autoVersionIdMap
‪array $autoVersionIdMap
Definition: DataHandler.php:217
‪TYPO3\CMS\Core\DataHandling\DataHandler\deleteClause
‪string deleteClause($table)
Definition: DataHandler.php:8277
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\PasswordPolicy\Event\EnrichPasswordValidationContextDataEvent
Definition: EnrichPasswordValidationContextDataEvent.php:30
‪TYPO3\CMS\Core\DataHandling\DataHandler\sortAndDeleteFlexSectionContainerElements
‪sortAndDeleteFlexSectionContainerElements(array $valueArray, array $dataStructure)
Definition: DataHandler.php:2549
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Core\DataHandling\DataHandler\getLanguageService
‪getLanguageService()
Definition: DataHandler.php:9697
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_EDIT
‪const PAGE_EDIT
Definition: Permission.php:40
‪if
‪if(PHP_SAPI !=='cli')
Definition: checkNamespaceIntegrity.php:27
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_DELETE
‪const PAGE_DELETE
Definition: Permission.php:45
‪TYPO3\CMS\Core\DataHandling\DataHandler\copyRecord_raw
‪int null copyRecord_raw($table, $uid, $pid, $overrideArray=[], array $workspaceOptions=[])
Definition: DataHandler.php:3858
‪TYPO3\CMS\Core\DataHandling\DataHandler\CACHE_IDENTIFIER_NESTED_ELEMENT_CALLS_PREFIX
‪const CACHE_IDENTIFIER_NESTED_ELEMENT_CALLS_PREFIX
Definition: DataHandler.php:478
‪TYPO3\CMS\Core\DataHandling\DataHandler\isRecordInWebMount
‪bool isRecordInWebMount($table, $id)
Definition: DataHandler.php:6989
‪TYPO3\CMS\Core\DataHandling\DataHandler\$checkModifyAccessListHookObjects
‪array $checkModifyAccessListHookObjects
Definition: DataHandler.php:452
‪TYPO3\CMS\Core\DataHandling\DataHandler\$useTransOrigPointerField
‪bool $useTransOrigPointerField
Definition: DataHandler.php:143
‪TYPO3\CMS\Core\Utility\ArrayUtility\setValueByPath
‪static array setValueByPath(array $array, string|array|\ArrayAccess $path, mixed $value, string $delimiter='/')
Definition: ArrayUtility.php:261
‪TYPO3\CMS\Core\DataHandling\DataHandler\doesPageHaveUnallowedTables
‪array doesPageHaveUnallowedTables($page_uid, int $doktype)
Definition: DataHandler.php:7320
‪TYPO3\CMS\Core\DataHandling\DataHandler\setNullValues
‪setNullValues(array $active, array &$haystack)
Definition: DataHandler.php:9602
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\DataHandling\DataHandler\$dontProcessTransformations
‪bool $dontProcessTransformations
Definition: DataHandler.php:136
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForInternalReferences
‪array checkValueForInternalReferences(array $res, $value, $tcaFieldConf, $table, $id, $field)
Definition: DataHandler.php:1498
‪TYPO3\CMS\Core\Utility\GeneralUtility\inList
‪static bool inList($list, $item)
Definition: GeneralUtility.php:422
‪TYPO3\CMS\Core\DataHandling\DataHandler\mmQueryShouldUseTablenamesColumn
‪mmQueryShouldUseTablenamesColumn(array $config)
Definition: DataHandler.php:6363
‪TYPO3\CMS\Core\DataHandling\DataHandler\postProcessDatabaseInsert
‪postProcessDatabaseInsert(Connection $connection, string $tableName, int $suggestedUid)
Definition: DataHandler.php:9633
‪TYPO3\CMS\Core\DataHandling\DataHandler\copyRecord_processManyToMany
‪string copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language)
Definition: DataHandler.php:4033
‪TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction
‪PasswordPolicyAction
Definition: PasswordPolicyAction.php:24
‪TYPO3\CMS\Core\DataHandling\DataHandler\applyDefaultsForFieldArray
‪applyDefaultsForFieldArray(string $table, int $pageId, array $prepopulatedFieldArray)
Definition: DataHandler.php:621
‪TYPO3\CMS\Core\DataHandling\DataHandler\doesRecordExist_pageLookUp
‪doesRecordExist_pageLookUp($id, $perms, $columns=['uid'])
Definition: DataHandler.php:7154
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Core\Crypto\Random
Definition: Random.php:27
‪TYPO3\CMS\Core\DataHandling\DataHandler\getRecordProperties
‪array getRecordProperties($table, $id, $noWSOL=false)
Definition: DataHandler.php:7485
‪TYPO3\CMS\Core\DataHandling\DataHandler\resolveVersionedRecords
‪array resolveVersionedRecords($tableName, $fieldNames, $sortingField, array $liveIds)
Definition: DataHandler.php:9303
‪TYPO3\CMS\Core\DataHandling\DataHandler\addDeleteRestriction
‪addDeleteRestriction(QueryRestrictionContainerInterface $restrictions)
Definition: DataHandler.php:8289
‪TYPO3\CMS\Core\Database\Connection\lastInsertId
‪numeric string lastInsertId()
Definition: Connection.php:402
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\DataHandling\DataHandlerCheckModifyAccessListHookInterface
Definition: DataHandlerCheckModifyAccessListHookInterface.php:22
‪TYPO3\CMS\Core\DataHandling\DataHandler\fillInFieldArray
‪array fillInFieldArray($table, $id, array $fieldArray, array $incomingFieldArray, $realPid, $status, $tscPID)
Definition: DataHandler.php:1163
‪TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface\add
‪QueryRestrictionContainerInterface add(QueryRestrictionInterface $restriction)
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForLanguage
‪array checkValueForLanguage(int $value, string $table, string $field)
Definition: DataHandler.php:1922
‪TYPO3\CMS\Core\DataHandling\DataHandler\printLogErrorMessages
‪non empty string[] printLogErrorMessages()
Definition: DataHandler.php:9159
‪TYPO3\CMS\Core\DataHandling\DataHandler\process_datamap
‪bool void process_datamap()
Definition: DataHandler.php:708
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\DataHandling\DataHandler\__construct
‪__construct(ReferenceIndexUpdater $referenceIndexUpdater=null)
Definition: DataHandler.php:486
‪TYPO3\CMS\Core\DataHandling\DataHandler\getRecordHistoryStore
‪getRecordHistoryStore()
Definition: DataHandler.php:7697
‪TYPO3\CMS\Core\DataHandling\DataHandler\discardMmRelations
‪discardMmRelations(string $table, array $fieldConfig, array $record)
Definition: DataHandler.php:6090
‪TYPO3\CMS\Core\Utility\StringUtility
Definition: StringUtility.php:24
‪TYPO3\CMS\Core\DataHandling\DataHandler\getSiteLanguageForPage
‪getSiteLanguageForPage(int $pageId, int $languageId)
Definition: DataHandler.php:8130
‪TYPO3\CMS\Core\DataHandling\DataHandler\registerReferenceIndexRowsForDrop
‪registerReferenceIndexRowsForDrop(string $table, int $uid, int $workspace)
Definition: DataHandler.php:7737
‪TYPO3\CMS\Core\Configuration\Richtext
Definition: Richtext.php:34
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForJson
‪array checkValueForJson(array|string $value, array $tcaFieldConf)
Definition: DataHandler.php:2287
‪TYPO3\CMS\Core\DataHandling\DataHandler\$neverHideAtCopy
‪bool $neverHideAtCopy
Definition: DataHandler.php:126
‪TYPO3\CMS\Core\DataHandling\ReferenceIndexUpdater
Definition: ReferenceIndexUpdater.php:35
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT_ARRAY
‪const PARAM_INT_ARRAY
Definition: Connection.php:72
‪TYPO3\CMS\Core\Core\Environment\getContext
‪static getContext()
Definition: Environment.php:128
‪TYPO3\CMS\Core\DataHandling\DataHandler\$checkStoredRecords_loose
‪$checkStoredRecords_loose
Definition: DataHandler.php:121
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkValueForLink
‪array checkValueForLink(string $value, array $tcaFieldConf, string $table, int|string $id, string $field)
Definition: DataHandler.php:1948
‪TYPO3\CMS\Core\DataHandling\DataHandler\log
‪int log($table, $recuid, $action, $recpid, $error, $details, $details_nr=-1, $data=[], $event_pid=-1, $NEWid='')
Definition: DataHandler.php:9133
‪TYPO3\CMS\Core\DataHandling\DataHandler\isNestedElementCallRegistered
‪bool isNestedElementCallRegistered($table, $id, $identifier)
Definition: DataHandler.php:9474
‪TYPO3\CMS\Core\DataHandling\DataHandler\hardDeleteSingleRecord
‪hardDeleteSingleRecord(string $table, int $uid)
Definition: DataHandler.php:6952
‪TYPO3\CMS\Core\DataHandling\DataHandler\$referenceIndexUpdater
‪ReferenceIndexUpdater $referenceIndexUpdater
Definition: DataHandler.php:436
‪TYPO3\CMS\Core\Utility\GeneralUtility\xml2array
‪static array string xml2array(string $string, string $NSprefix='', bool $reportDocTag=false)
Definition: GeneralUtility.php:1265
‪TYPO3\CMS\Core\DataHandling\DataHandler\updateFlexFormData
‪updateFlexFormData($flexFormId, array $modifications)
Definition: DataHandler.php:6856
‪TYPO3\CMS\Core\Messaging\FlashMessageService
Definition: FlashMessageService.php:27
‪TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory\forName
‪static forName(string $name)
Definition: RecordStateFactory.php:29
‪TYPO3\CMS\Core\DataHandling\DataHandler\$pagetreeRefreshFieldsFromPages
‪array $pagetreeRefreshFieldsFromPages
Definition: DataHandler.php:262
‪TYPO3\CMS\Core\DataHandling\DataHandler\fixUniqueInSite
‪bool fixUniqueInSite(string $table, int $uid)
Definition: DataHandler.php:8484
‪TYPO3\CMS\Core\DataHandling\DataHandler\resetElementsToBeDeleted
‪resetElementsToBeDeleted()
Definition: DataHandler.php:9538
‪TYPO3\CMS\Core\DataHandling\DataHandler\getRecordPropertiesFromRow
‪array null getRecordPropertiesFromRow($table, $row)
Definition: DataHandler.php:7502
‪TYPO3\CMS\Core\DataHandling\DataHandler\cannotDeleteRecord
‪string cannotDeleteRecord($table, $id)
Definition: DataHandler.php:5510
‪TYPO3\CMS\Core\DataHandling\DataHandler\insertDB
‪int null insertDB($table, $id, $fieldArray, $newVersion=false, $suggestedUid=0, $dontSetNewIdIndex=false)
Definition: DataHandler.php:7604
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Core\DataHandling\DataHandler\isRecordLocalized
‪isRecordLocalized(string $table, int $uid, int $language)
Definition: DataHandler.php:5044
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\DataHandling\DataHandler\$mmHistoryRecords
‪array $mmHistoryRecords
Definition: DataHandler.php:332
‪TYPO3\CMS\Core\Utility\StringUtility\getUniqueId
‪static getUniqueId(string $prefix='')
Definition: StringUtility.php:57
‪TYPO3\CMS\Core\SysLog\Type
Definition: Type.php:28
‪TYPO3\CMS\Core\DataHandling\DataHandler\$control
‪array $control
Definition: DataHandler.php:307
‪TYPO3\CMS\Core\DataHandling\DataHandler\versionPublishManyToManyRelations
‪versionPublishManyToManyRelations(string $table, array $liveRecord, array $workspaceRecord, int $fromWorkspace)
Definition: DataHandler.php:6253
‪TYPO3\CMS\Core\DataHandling\DataHandler\workspaceCannotEditRecord
‪string false workspaceCannotEditRecord($table, $recData)
Definition: DataHandler.php:9392
‪TYPO3\CMS\Core\DataHandling\DataHandler\$registerDBList
‪array $registerDBList
Definition: DataHandler.php:387
‪TYPO3\CMS\Core\DataHandling\DataHandler\$recordPidsForDeletedRecords
‪static array $recordPidsForDeletedRecords
Definition: DataHandler.php:469
‪TYPO3\CMS\Core\Log\LogDataTrait
Definition: LogDataTrait.php:25
‪TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData
Definition: ContextData.php:28
‪TYPO3\CMS\Core\DataHandling\DataHandler\$copyMappingArray_merged
‪array $copyMappingArray_merged
Definition: DataHandler.php:245
‪TYPO3\CMS\Core\DataHandling\DataHandler\newFieldArray
‪array newFieldArray($table)
Definition: DataHandler.php:8081
‪TYPO3\CMS\Core\DataHandling\DataHandler\$deletedRecords
‪array $deletedRecords
Definition: DataHandler.php:250
‪TYPO3\CMS\Core\DataHandling\DataHandler\compileAdminTables
‪compileAdminTables()
Definition: DataHandler.php:8441
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39