TYPO3 CMS  TYPO3_8-7
DataHandler.php
Go to the documentation of this file.
1 <?php
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
56 
71 {
72  // *********************
73  // Public variables you can configure before using the class:
74  // *********************
81  public $storeLogMessages = true;
82 
88  public $enableLogging = true;
89 
96  public $reverseOrder = false;
97 
104  public $checkSimilar = true;
105 
112  public $checkStoredRecords = true;
113 
120 
127  public $deleteTree = false;
128 
134  public $neverHideAtCopy = false;
135 
141  public $isImporting = false;
142 
149 
157  protected $useTransOrigPointerField = true;
158 
166  public $updateModeL10NdiffData = true;
167 
175 
183 
190  public $bypassFileHandling = false;
191 
199 
206  public $copyWhichTables = '*';
207 
215  public $copyTree = 0;
216 
225  public $defaultValues = [];
226 
234  public $overrideValues = [];
235 
243  public $alternativeFileName = [];
244 
250  public $alternativeFilePath = [];
251 
259  public $data_disableFields = [];
260 
270  public $suggestedInsertUids = [];
271 
278  public $callBackObj;
279 
280  // *********************
281  // Internal variables (mapping arrays) which can be used (read-only) from outside
282  // *********************
288  public $autoVersionIdMap = [];
289 
295  public $substNEWwithIDs = [];
296 
303 
309  public $newRelatedIDs = [];
310 
317 
323  protected $deletedRecords = [];
324 
330  public $copiedFileMap = [];
331 
337  public $RTEmagic_copyIndex = [];
338 
344  public $errorLog = [];
345 
351  public $pagetreeRefreshFieldsFromPages = ['pid', 'sorting', 'deleted', 'hidden', 'title', 'doktype', 'is_siteroot', 'fe_group', 'nav_hide', 'nav_title', 'module', 'starttime', 'endtime', 'content_from_pid', 'extendToSubpages'];
352 
358  public $pagetreeNeedsRefresh = false;
359 
360  // *********************
361  // Internal Variables, do not touch.
362  // *********************
363 
364  // Variables set in init() function:
365 
371  public $BE_USER;
372 
378  public $userid;
379 
385  public $username;
386 
392  public $admin;
393 
400  'user' => 'show,edit,delete,new,editcontent',
401  'group' => 'show,edit,new,editcontent',
402  'everybody' => ''
403  ];
404 
410  protected $excludedTablesAndFields = [];
411 
418  protected $control = [];
419 
425  public $datamap = [];
426 
432  public $cmdmap = [];
433 
439  protected $mmHistoryRecords = [];
440 
446  protected $historyRecords = [];
447 
448  // Internal static:
454  public $pMap = [
455  'show' => 1,
456  // 1st bit
457  'edit' => 2,
458  // 2nd bit
459  'delete' => 4,
460  // 3rd bit
461  'new' => 8,
462  // 4th bit
463  'editcontent' => 16
464  ];
465 
471  public $sortIntervals = 256;
472 
473  // Internal caching arrays
482 
491 
500 
508  public $isInWebMount_Cache = [];
509 
517  public $cachedTSconfig = [];
518 
526  public $pageCache = [];
527 
535  public $checkWorkspaceCache = [];
536 
537  // Other arrays:
543  public $dbAnalysisStore = [];
544 
550  public $removeFilesStore = [];
551 
557  public $uploadedFileArray = [];
558 
564  public $registerDBList = [];
565 
571  public $registerDBPids = [];
572 
583  public $copyMappingArray = [];
584 
590  public $remapStack = [];
591 
598  public $remapStackRecords = [];
599 
605  protected $remapStackChildIds = [];
606 
612  protected $remapStackActions = [];
613 
619  protected $remapStackRefIndex = [];
620 
626  public $updateRefIndexStack = [];
627 
634  public $callFromImpExp = false;
635 
636  // Various
643  public $fileFunc;
644 
651 
657  public $autoVersioningUpdate = false;
658 
664  protected $disableDeleteClause = false;
665 
670 
675 
682  protected $outerMostInstance = null;
683 
689  protected static $recordsToClearCacheFor = [];
690 
697  protected static $recordPidsForDeletedRecords = [];
698 
704  protected $runtimeCache = null;
705 
711  protected $cachePrefixNestedElementCalls = 'core-datahandler-nestedElementCalls-';
712 
716  public function __construct()
717  {
718  $this->checkStoredRecords = (bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecords'];
719  $this->checkStoredRecords_loose = (bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecordsLoose'];
720  $this->runtimeCache = $this->getRuntimeCache();
721  }
722 
726  public function setControl(array $control)
727  {
728  $this->control = $control;
729  }
730 
740  public function start($data, $cmd, $altUserObject = null)
741  {
742  // Initializing BE_USER
743  $this->BE_USER = is_object($altUserObject) ? $altUserObject : $GLOBALS['BE_USER'];
744  $this->userid = $this->BE_USER->user['uid'] ?? 0;
745  $this->username = $this->BE_USER->user['username'] ?? '';
746  $this->admin = $this->BE_USER->user['admin'] ?? false;
747  if ($this->BE_USER->uc['recursiveDelete'] ?? false) {
748  $this->deleteTree = 1;
749  }
750  if ($GLOBALS['TYPO3_CONF_VARS']['BE']['explicitConfirmationOfTranslation'] && $this->updateModeL10NdiffData === true) {
751  $this->updateModeL10NdiffData = false;
752  }
753  // Initializing default permissions for pages
754  $defaultPermissions = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions'];
755  if (isset($defaultPermissions['user'])) {
756  $this->defaultPermissions['user'] = $defaultPermissions['user'];
757  }
758  if (isset($defaultPermissions['group'])) {
759  $this->defaultPermissions['group'] = $defaultPermissions['group'];
760  }
761  if (isset($defaultPermissions['everybody'])) {
762  $this->defaultPermissions['everybody'] = $defaultPermissions['everybody'];
763  }
764  // generates the excludelist, based on TCA/exclude-flag and non_exclude_fields for the user:
765  if (!$this->admin) {
766  $this->excludedTablesAndFields = array_flip($this->getExcludeListArray());
767  }
768  // Setting the data and cmd arrays
769  if (is_array($data)) {
770  reset($data);
771  $this->datamap = $data;
772  }
773  if (is_array($cmd)) {
774  reset($cmd);
775  $this->cmdmap = $cmd;
776  }
777  }
778 
785  public function setMirror($mirror)
786  {
787  if (!is_array($mirror)) {
788  return;
789  }
790 
791  foreach ($mirror as $table => $uid_array) {
792  if (!isset($this->datamap[$table])) {
793  continue;
794  }
795 
796  foreach ($uid_array as $id => $uidList) {
797  if (!isset($this->datamap[$table][$id])) {
798  continue;
799  }
800 
801  $theIdsInArray = GeneralUtility::trimExplode(',', $uidList, true);
802  foreach ($theIdsInArray as $copyToUid) {
803  $this->datamap[$table][$copyToUid] = $this->datamap[$table][$id];
804  }
805  }
806  }
807  }
808 
814  public function setDefaultsFromUserTS($userTS)
815  {
816  if (!is_array($userTS)) {
817  return;
818  }
819 
820  foreach ($userTS as $k => $v) {
821  $k = mb_substr($k, 0, -1);
822  if (!$k || !is_array($v) || !isset($GLOBALS['TCA'][$k])) {
823  continue;
824  }
825 
826  if (is_array($this->defaultValues[$k])) {
827  $this->defaultValues[$k] = array_merge($this->defaultValues[$k], $v);
828  } else {
829  $this->defaultValues[$k] = $v;
830  }
831  }
832  }
833 
840  public function process_uploads($postFiles)
841  {
842  if (!is_array($postFiles)) {
843  return;
844  }
845 
846  // Editing frozen:
847  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
848  if ($this->enableLogging) {
849  $this->newlog('All editing in this workspace has been frozen!', 1);
850  }
851  return;
852  }
853  $subA = reset($postFiles);
854  if (is_array($subA)) {
855  if (is_array($subA['name']) && is_array($subA['type']) && is_array($subA['tmp_name']) && is_array($subA['size'])) {
856  // Initialize the uploadedFilesArray:
857  $this->uploadedFileArray = [];
858  // For each entry:
859  foreach ($subA as $key => $values) {
860  $this->process_uploads_traverseArray($this->uploadedFileArray, $values, $key);
861  }
862  } else {
863  $this->uploadedFileArray = $subA;
864  }
865  }
866  }
867 
877  public function process_uploads_traverseArray(&$outputArr, $inputArr, $keyToSet)
878  {
879  if (is_array($inputArr)) {
880  foreach ($inputArr as $key => $value) {
881  $this->process_uploads_traverseArray($outputArr[$key], $inputArr[$key], $keyToSet);
882  }
883  } else {
884  $outputArr[$keyToSet] = $inputArr;
885  }
886  }
887 
888  /*********************************************
889  *
890  * HOOKS
891  *
892  *********************************************/
906  public function hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray)
907  {
908  // Process hook directly:
909  if (!isset($this->remapStackRecords[$table][$id])) {
910  foreach ($hookObjectsArr as $hookObj) {
911  if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
912  $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);
913  }
914  }
915  } else {
916  $this->remapStackRecords[$table][$id]['processDatamap_afterDatabaseOperations'] = [
917  'status' => $status,
918  'fieldArray' => $fieldArray,
919  'hookObjectsArr' => $hookObjectsArr
920  ];
921  }
922  }
923 
932  {
933  if (!isset($this->checkModifyAccessListHookObjects)) {
934  $this->checkModifyAccessListHookObjects = [];
935  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'])) {
936  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] as $classData) {
937  $hookObject = GeneralUtility::getUserObj($classData);
938  if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
939  throw new \UnexpectedValueException($classData . ' must implement interface ' . DataHandlerCheckModifyAccessListHookInterface::class, 1251892472);
940  }
941  $this->checkModifyAccessListHookObjects[] = $hookObject;
942  }
943  }
944  }
946  }
947 
948  /*********************************************
949  *
950  * PROCESSING DATA
951  *
952  *********************************************/
959  public function process_datamap()
960  {
961  $this->controlActiveElements();
962 
963  // Keep versionized(!) relations here locally:
964  $registerDBList = [];
966  $this->datamap = $this->unsetElementsToBeDeleted($this->datamap);
967  // Editing frozen:
968  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
969  if ($this->enableLogging) {
970  $this->newlog('All editing in this workspace has been frozen!', 1);
971  }
972  return false;
973  }
974  // First prepare user defined objects (if any) for hooks which extend this function:
975  $hookObjectsArr = [];
976  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'])) {
977  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] as $classRef) {
978  $hookObject = GeneralUtility::getUserObj($classRef);
979  if (method_exists($hookObject, 'processDatamap_beforeStart')) {
980  $hookObject->processDatamap_beforeStart($this);
981  }
982  $hookObjectsArr[] = $hookObject;
983  }
984  }
985  // Pre-process data-map and synchronize localization states
986  $this->datamap = DataMapProcessor::instance($this->datamap, $this->BE_USER)->process();
987  // 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.
988  $orderOfTables = [];
989  // Set pages first.
990  if (isset($this->datamap['pages'])) {
991  $orderOfTables[] = 'pages';
992  }
993  $orderOfTables = array_unique(array_merge($orderOfTables, array_keys($this->datamap)));
994  // Process the tables...
995  foreach ($orderOfTables as $table) {
996  // Check if
997  // - table is set in $GLOBALS['TCA'],
998  // - table is NOT readOnly
999  // - the table is set with content in the data-array (if not, there's nothing to process...)
1000  // - permissions for tableaccess OK
1001  $modifyAccessList = $this->checkModifyAccessList($table);
1002  if ($this->enableLogging && !$modifyAccessList) {
1003  $this->log($table, 0, 2, 0, 1, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
1004  }
1005  if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->datamap[$table]) || !$modifyAccessList) {
1006  continue;
1007  }
1008 
1009  if ($this->reverseOrder) {
1010  $this->datamap[$table] = array_reverse($this->datamap[$table], 1);
1011  }
1012  // For each record from the table, do:
1013  // $id is the record uid, may be a string if new records...
1014  // $incomingFieldArray is the array of fields
1015  foreach ($this->datamap[$table] as $id => $incomingFieldArray) {
1016  if (!is_array($incomingFieldArray)) {
1017  continue;
1018  }
1019  $theRealPid = null;
1020 
1021  // Hook: processDatamap_preProcessFieldArray
1022  foreach ($hookObjectsArr as $hookObj) {
1023  if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
1024  $hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
1025  }
1026  }
1027  // ******************************
1028  // Checking access to the record
1029  // ******************************
1030  $createNewVersion = false;
1031  $recordAccess = false;
1032  $old_pid_value = '';
1033  $this->autoVersioningUpdate = false;
1034  // Is it a new record? (Then Id is a string)
1036  // Get a fieldArray with default values
1037  $fieldArray = $this->newFieldArray($table);
1038  // A pid must be set for new records.
1039  if (isset($incomingFieldArray['pid'])) {
1040  // $value = the pid
1041  $pid_value = $incomingFieldArray['pid'];
1042  // Checking and finding numerical pid, it may be a string-reference to another value
1043  $OK = 1;
1044  // If a NEW... id
1045  if (strstr($pid_value, 'NEW')) {
1046  if ($pid_value[0] === '-') {
1047  $negFlag = -1;
1048  $pid_value = substr($pid_value, 1);
1049  } else {
1050  $negFlag = 1;
1051  }
1052  // Trying to find the correct numerical value as it should be mapped by earlier processing of another new record.
1053  if (isset($this->substNEWwithIDs[$pid_value])) {
1054  if ($negFlag === 1) {
1055  $old_pid_value = $this->substNEWwithIDs[$pid_value];
1056  }
1057  $pid_value = (int)($negFlag * $this->substNEWwithIDs[$pid_value]);
1058  } else {
1059  $OK = 0;
1060  }
1061  }
1062  $pid_value = (int)$pid_value;
1063  // The $pid_value is now the numerical pid at this point
1064  if ($OK) {
1065  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
1066  // Points to a page on which to insert the element, possibly in the top of the page
1067  if ($pid_value >= 0) {
1068  // If this table is sorted we better find the top sorting number
1069  if ($sortRow) {
1070  $fieldArray[$sortRow] = $this->getSortNumber($table, 0, $pid_value);
1071  }
1072  // The numerical pid is inserted in the data array
1073  $fieldArray['pid'] = $pid_value;
1074  } else {
1075  // points to another record before ifself
1076  // If this table is sorted we better find the top sorting number
1077  if ($sortRow) {
1078  // Because $pid_value is < 0, getSortNumber returns an array
1079  $tempArray = $this->getSortNumber($table, 0, $pid_value);
1080  $fieldArray['pid'] = $tempArray['pid'];
1081  $fieldArray[$sortRow] = $tempArray['sortNumber'];
1082  } else {
1083  // Here we fetch the PID of the record that we point to...
1084  $tempdata = $this->recordInfo($table, abs($pid_value), 'pid');
1085  $fieldArray['pid'] = $tempdata['pid'];
1086  }
1087  }
1088  }
1089  }
1090  $theRealPid = $fieldArray['pid'];
1091  // Now, check if we may insert records on this pid.
1092  if ($theRealPid >= 0) {
1093  // Checks if records can be inserted on this $pid.
1094  $recordAccess = $this->checkRecordInsertAccess($table, $theRealPid);
1095  if ($recordAccess) {
1096  $this->addDefaultPermittedLanguageIfNotSet($table, $incomingFieldArray);
1097  $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $incomingFieldArray, true);
1098  if (!$recordAccess) {
1099  if ($this->enableLogging) {
1100  $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', 1);
1101  }
1102  } elseif (!$this->bypassWorkspaceRestrictions) {
1103  // Workspace related processing:
1104  // If LIVE records cannot be created in the current PID due to workspace restrictions, prepare creation of placeholder-record
1105  if ($res = $this->BE_USER->workspaceAllowLiveRecordsInPID($theRealPid, $table)) {
1106  if ($res < 0) {
1107  $recordAccess = false;
1108  if ($this->enableLogging) {
1109  $this->newlog('Stage for versioning root point and users access level did not allow for editing', 1);
1110  }
1111  }
1112  } else {
1113  // So, if no live records were allowed, we have to create a new version of this record:
1114  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1115  $createNewVersion = true;
1116  } else {
1117  $recordAccess = false;
1118  if ($this->enableLogging) {
1119  $this->newlog('Record could not be created in this workspace in this branch', 1);
1120  }
1121  }
1122  }
1123  }
1124  }
1125  } else {
1126  debug('Internal ERROR: pid should not be less than zero!');
1127  }
1128  // Yes new record, change $record_status to 'insert'
1129  $status = 'new';
1130  } else {
1131  // Nope... $id is a number
1132  $fieldArray = [];
1133  $recordAccess = $this->checkRecordUpdateAccess($table, $id, $incomingFieldArray, $hookObjectsArr);
1134  if (!$recordAccess) {
1135  if ($this->enableLogging) {
1136  $propArr = $this->getRecordProperties($table, $id);
1137  $this->log($table, $id, 2, 0, 1, 'Attempt to modify record \'%s\' (%s) without permission. Or non-existing page.', 2, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
1138  }
1139  continue;
1140  }
1141  // Next check of the record permissions (internals)
1142  $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $id);
1143  if (!$recordAccess) {
1144  if ($this->enableLogging) {
1145  $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', 1);
1146  }
1147  } else {
1148  // Here we fetch the PID of the record that we point to...
1149  $tempdata = $this->recordInfo($table, $id, 'pid' . ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? ',t3ver_wsid,t3ver_stage' : ''));
1150  $theRealPid = $tempdata['pid'] ?? null;
1151  // Use the new id of the versionized record we're trying to write to:
1152  // (This record is a child record of a parent and has already been versionized.)
1153  if ($this->autoVersionIdMap[$table][$id]) {
1154  // For the reason that creating a new version of this record, automatically
1155  // created related child records (e.g. "IRRE"), update the accordant field:
1156  $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1157  // Use the new id of the copied/versionized record:
1158  $id = $this->autoVersionIdMap[$table][$id];
1159  $recordAccess = true;
1160  $this->autoVersioningUpdate = true;
1161  } elseif (!$this->bypassWorkspaceRestrictions && ($errorCode = $this->BE_USER->workspaceCannotEditRecord($table, $tempdata))) {
1162  $recordAccess = false;
1163  // Versioning is required and it must be offline version!
1164  // Check if there already is a workspace version
1165  $WSversion = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid,t3ver_oid');
1166  if ($WSversion) {
1167  $id = $WSversion['uid'];
1168  $recordAccess = true;
1169  } elseif ($this->BE_USER->workspaceAllowAutoCreation($table, $id, $theRealPid)) {
1170  // new version of a record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1171  $this->pagetreeNeedsRefresh = true;
1172 
1174  $tce = GeneralUtility::makeInstance(__CLASS__);
1175  $tce->enableLogging = $this->enableLogging;
1176  // Setting up command for creating a new version of the record:
1177  $cmd = [];
1178  $cmd[$table][$id]['version'] = [
1179  'action' => 'new',
1180  // Default is to create a version of the individual records... element versioning that is.
1181  'label' => 'Auto-created for WS #' . $this->BE_USER->workspace
1182  ];
1183  $tce->start([], $cmd);
1184  $tce->process_cmdmap();
1185  $this->errorLog = array_merge($this->errorLog, $tce->errorLog);
1186  // If copying was successful, share the new uids (also of related children):
1187  if ($tce->copyMappingArray[$table][$id]) {
1188  foreach ($tce->copyMappingArray as $origTable => $origIdArray) {
1189  foreach ($origIdArray as $origId => $newId) {
1190  $this->uploadedFileArray[$origTable][$newId] = $this->uploadedFileArray[$origTable][$origId];
1191  $this->autoVersionIdMap[$origTable][$origId] = $newId;
1192  }
1193  }
1194  ArrayUtility::mergeRecursiveWithOverrule($this->RTEmagic_copyIndex, $tce->RTEmagic_copyIndex);
1195  // See where RTEmagic_copyIndex is used inside fillInFieldArray() for more information...
1196  // Update registerDBList, that holds the copied relations to child records:
1197  $registerDBList = array_merge($registerDBList, $tce->registerDBList);
1198  // For the reason that creating a new version of this record, automatically
1199  // created related child records (e.g. "IRRE"), update the accordant field:
1200  $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1201  // Use the new id of the copied/versionized record:
1202  $id = $this->autoVersionIdMap[$table][$id];
1203  $recordAccess = true;
1204  $this->autoVersioningUpdate = true;
1205  } elseif ($this->enableLogging) {
1206  $this->newlog('Could not be edited in offline workspace in the branch where found (failure state: \'' . $errorCode . '\'). Auto-creation of version failed!', 1);
1207  }
1208  } elseif ($this->enableLogging) {
1209  $this->newlog('Could not be edited in offline workspace in the branch where found (failure state: \'' . $errorCode . '\'). Auto-creation of version not allowed in workspace!', 1);
1210  }
1211  }
1212  }
1213  // The default is 'update'
1214  $status = 'update';
1215  }
1216  // If access was granted above, proceed to create or update record:
1217  if (!$recordAccess) {
1218  continue;
1219  }
1220 
1221  // Here the "pid" is set IF NOT the old pid was a string pointing to a place in the subst-id array.
1222  list($tscPID) = BackendUtility::getTSCpid($table, $id, $old_pid_value ? $old_pid_value : $fieldArray['pid']);
1223  if ($status === 'new' && $table === 'pages') {
1224  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
1225  if (isset($TSConfig['permissions.']) && is_array($TSConfig['permissions.'])) {
1226  $fieldArray = $this->setTSconfigPermissions($fieldArray, $TSConfig['permissions.']);
1227  }
1228  }
1229  // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1230  $fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1231  $newVersion_placeholderFieldArray = [];
1232  if ($createNewVersion) {
1233  // create a placeholder array with already processed field content
1234  $newVersion_placeholderFieldArray = $fieldArray;
1235  }
1236  // NOTICE! All manipulation beyond this point bypasses both "excludeFields" AND possible "MM" relations / file uploads to field!
1237  // Forcing some values unto field array:
1238  // NOTICE: This overriding is potentially dangerous; permissions per field is not checked!!!
1239  $fieldArray = $this->overrideFieldArray($table, $fieldArray);
1240  if ($createNewVersion) {
1241  $newVersion_placeholderFieldArray = $this->overrideFieldArray($table, $newVersion_placeholderFieldArray);
1242  }
1243  // Setting system fields
1244  if ($status === 'new') {
1245  if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1246  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1247  if ($createNewVersion) {
1248  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1249  }
1250  }
1251  if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1252  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1253  if ($createNewVersion) {
1254  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1255  }
1256  }
1257  } elseif ($this->checkSimilar) {
1258  // Removing fields which are equal to the current value:
1259  $fieldArray = $this->compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1260  }
1261  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] && !empty($fieldArray)) {
1262  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1263  if ($createNewVersion) {
1264  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1265  }
1266  }
1267  // Set stage to "Editing" to make sure we restart the workflow
1268  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1269  $fieldArray['t3ver_stage'] = 0;
1270  }
1271  // Hook: processDatamap_postProcessFieldArray
1272  foreach ($hookObjectsArr as $hookObj) {
1273  if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1274  $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1275  }
1276  }
1277  // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1278  // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already and files could have been uploaded that are now "lost"
1279  if (is_array($fieldArray)) {
1280  if ($status === 'new') {
1281  if ($table === 'pages') {
1282  // for new pages always a refresh is needed
1283  $this->pagetreeNeedsRefresh = true;
1284  }
1285 
1286  // This creates a new version of the record with online placeholder and offline version
1287  if ($createNewVersion) {
1288  // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1289  $this->pagetreeNeedsRefresh = true;
1290 
1291  $newVersion_placeholderFieldArray['t3ver_label'] = 'INITIAL PLACEHOLDER';
1292  // Setting placeholder state value for temporary record
1293  $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER);
1294  // Setting workspace - only so display of place holders can filter out those from other workspaces.
1295  $newVersion_placeholderFieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1296  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $this->getPlaceholderTitleForTableLabel($table);
1297  // Saving placeholder as 'original'
1298  $this->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1299  // For the actual new offline version, set versioning values to point to placeholder:
1300  $fieldArray['pid'] = -1;
1301  $fieldArray['t3ver_oid'] = $this->substNEWwithIDs[$id];
1302  $fieldArray['t3ver_id'] = 1;
1303  // Setting placeholder state value for version (so it can know it is currently a new version...)
1304  $fieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER_VERSION);
1305  $fieldArray['t3ver_label'] = 'First draft version';
1306  $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1307  // When inserted, $this->substNEWwithIDs[$id] will be changed to the uid of THIS version and so the interface will pick it up just nice!
1308  $phShadowId = $this->insertDB($table, $id, $fieldArray, true, 0, true);
1309  if ($phShadowId) {
1310  // Processes fields of the placeholder record:
1311  $this->triggerRemapAction($table, $id, [$this, 'placeholderShadowing'], [$table, $phShadowId]);
1312  // Hold auto-versionized ids of placeholders:
1313  $this->autoVersionIdMap[$table][$this->substNEWwithIDs[$id]] = $phShadowId;
1314  }
1315  } else {
1316  $this->insertDB($table, $id, $fieldArray, false, $incomingFieldArray['uid']);
1317  }
1318  } else {
1319  if ($table === 'pages') {
1320  // only a certain number of fields needs to be checked for updates
1321  // if $this->checkSimilar is TRUE, fields with unchanged values are already removed here
1322  $fieldsToCheck = array_intersect($this->pagetreeRefreshFieldsFromPages, array_keys($fieldArray));
1323  if (!empty($fieldsToCheck)) {
1324  $this->pagetreeNeedsRefresh = true;
1325  }
1326  }
1327  $this->updateDB($table, $id, $fieldArray);
1328  $this->placeholderShadowing($table, $id);
1329  }
1330  }
1331  // Hook: processDatamap_afterDatabaseOperations
1332  // Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
1333  // but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
1334  $this->hook_processDatamap_afterDatabaseOperations($hookObjectsArr, $status, $table, $id, $fieldArray);
1335  }
1336  }
1337  // Process the stack of relations to remap/correct
1338  $this->processRemapStack();
1339  $this->dbAnalysisStoreExec();
1340  $this->removeRegisteredFiles();
1341  // Hook: processDatamap_afterAllOperations
1342  // Note: When this hook gets called, all operations on the submitted data have been finished.
1343  foreach ($hookObjectsArr as $hookObj) {
1344  if (method_exists($hookObj, 'processDatamap_afterAllOperations')) {
1345  $hookObj->processDatamap_afterAllOperations($this);
1346  }
1347  }
1348  if ($this->isOuterMostInstance()) {
1349  $this->processClearCacheQueue();
1350  $this->resetElementsToBeDeleted();
1351  }
1352  }
1353 
1360  public function placeholderShadowing($table, $id)
1361  {
1362  if ($liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, '*')) {
1363  if (VersionState::cast($liveRec['t3ver_state'])->indicatesPlaceholder()) {
1364  $justStoredRecord = BackendUtility::getRecord($table, $id);
1365  $newRecord = [];
1366  $shadowCols = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'];
1367  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1368  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1369  if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
1370  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
1371  }
1372  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['type'];
1373  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label'];
1374  $shadowColumns = array_unique(GeneralUtility::trimExplode(',', $shadowCols, true));
1375  foreach ($shadowColumns as $fieldName) {
1376  if ((string)$justStoredRecord[$fieldName] !== (string)$liveRec[$fieldName] && isset($GLOBALS['TCA'][$table]['columns'][$fieldName]) && $fieldName !== 'uid' && $fieldName !== 'pid') {
1377  $newRecord[$fieldName] = $justStoredRecord[$fieldName];
1378  }
1379  }
1380  if (!empty($newRecord)) {
1381  if ($this->enableLogging) {
1382  $this->newlog2('Shadowing done on fields <i>' . implode(',', array_keys($newRecord)) . '</i> in placeholder record ' . $table . ':' . $liveRec['uid'] . ' (offline version UID=' . $id . ')', $table, $liveRec['uid'], $liveRec['pid']);
1383  }
1384  $this->updateDB($table, $liveRec['uid'], $newRecord);
1385  }
1386  }
1387  }
1388  }
1389 
1397  public function getPlaceholderTitleForTableLabel($table, $placeholderContent = null)
1398  {
1399  if ($placeholderContent === null) {
1400  $placeholderContent = 'PLACEHOLDER';
1401  }
1402 
1403  $labelPlaceholder = '[' . $placeholderContent . ', WS#' . $this->BE_USER->workspace . ']';
1404  $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1405  if (!isset($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'])) {
1406  return $labelPlaceholder;
1407  }
1408  $evalCodesArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'], true);
1409  $transformedLabel = $this->checkValue_input_Eval($labelPlaceholder, $evalCodesArray, '');
1410  return isset($transformedLabel['value']) ? $transformedLabel['value'] : $labelPlaceholder;
1411  }
1412 
1426  public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID)
1427  {
1428  // Initialize:
1429  $originalLanguageRecord = null;
1430  $originalLanguage_diffStorage = null;
1431  $diffStorageFlag = false;
1432  // Setting 'currentRecord' and 'checkValueRecord':
1433  if (strstr($id, 'NEW')) {
1434  // Must have the 'current' array - not the values after processing below...
1435  $currentRecord = ($checkValueRecord = $fieldArray);
1436  // IF $incomingFieldArray is an array, overlay it.
1437  // 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...
1438  if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {
1439  ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);
1440  }
1441  } else {
1442  // We must use the current values as basis for this!
1443  $currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*'));
1444  // This is done to make the pid positive for offline versions; Necessary to have diff-view for pages_language_overlay in workspaces.
1445  BackendUtility::fixVersioningPid($table, $currentRecord);
1446  // Get original language record if available:
1447  if (is_array($currentRecord) && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'] && $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] && (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0) {
1448  $lookUpTable = $table === 'pages_language_overlay' ? 'pages' : $table;
1449  $originalLanguageRecord = $this->recordInfo($lookUpTable, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*');
1450  BackendUtility::workspaceOL($lookUpTable, $originalLanguageRecord);
1451  $originalLanguage_diffStorage = unserialize(
1452  $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']],
1453  ['allowed_classes' => false]
1454  );
1455  }
1456  }
1457  $this->checkValue_currentRecord = $checkValueRecord;
1458  // In the following all incoming value-fields are tested:
1459  // - Are the user allowed to change the field?
1460  // - Is the field uid/pid (which are already set)
1461  // - perms-fields for pages-table, then do special things...
1462  // - If the field is nothing of the above and the field is configured in TCA, the fieldvalues are evaluated by ->checkValue
1463  // If everything is OK, the field is entered into $fieldArray[]
1464  foreach ($incomingFieldArray as $field => $fieldValue) {
1465  if (isset($this->excludedTablesAndFields[$table . '-' . $field]) || $this->data_disableFields[$table][$id][$field]) {
1466  continue;
1467  }
1468 
1469  // The field must be editable.
1470  // Checking if a value for language can be changed:
1471  $languageDeny = $GLOBALS['TCA'][$table]['ctrl']['languageField'] && (string)$GLOBALS['TCA'][$table]['ctrl']['languageField'] === (string)$field && !$this->BE_USER->checkLanguageAccess($fieldValue);
1472  if ($languageDeny) {
1473  continue;
1474  }
1475 
1476  switch ($field) {
1477  case 'uid':
1478  case 'pid':
1479  // Nothing happens, already set
1480  break;
1481  case 'perms_userid':
1482  case 'perms_groupid':
1483  case 'perms_user':
1484  case 'perms_group':
1485  case 'perms_everybody':
1486  // Permissions can be edited by the owner or the administrator
1487  if ($table === 'pages' && ($this->admin || $status === 'new' || $this->pageInfo($id, 'perms_userid') == $this->userid)) {
1488  $value = (int)$fieldValue;
1489  switch ($field) {
1490  case 'perms_userid':
1491  $fieldArray[$field] = $value;
1492  break;
1493  case 'perms_groupid':
1494  $fieldArray[$field] = $value;
1495  break;
1496  default:
1497  if ($value >= 0 && $value < pow(2, 5)) {
1498  $fieldArray[$field] = $value;
1499  }
1500  }
1501  }
1502  break;
1503  case 't3ver_oid':
1504  case 't3ver_id':
1505  case 't3ver_wsid':
1506  case 't3ver_state':
1507  case 't3ver_count':
1508  case 't3ver_stage':
1509  case 't3ver_tstamp':
1510  // t3ver_label is not here because it CAN be edited as a regular field!
1511  break;
1512  case 'l10n_state':
1513  $fieldArray[$field] = $fieldValue;
1514  break;
1515  default:
1516  if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
1517  // Evaluating the value
1518  $res = $this->checkValue($table, $field, $fieldValue, $id, $status, $realPid, $tscPID);
1519  if (array_key_exists('value', $res)) {
1520  $fieldArray[$field] = $res['value'];
1521  }
1522  // Add the value of the original record to the diff-storage content:
1523  if ($this->updateModeL10NdiffData && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
1524  $originalLanguage_diffStorage[$field] = $this->updateModeL10NdiffDataClear ? '' : $originalLanguageRecord[$field];
1525  $diffStorageFlag = true;
1526  }
1527  // If autoversioning is happening we need to perform a nasty hack. The case is parallel to a similar hack inside checkValue_group_select_file().
1528  // When a copy or version is made of a record, a search is made for any RTEmagic* images in fields having the "images" soft reference parser applied.
1529  // That should be TRUE for RTE fields. If any are found they are duplicated to new names and the file reference in the bodytext is updated accordingly.
1530  // However, with auto-versioning the submitted content of the field will just overwrite the corrected values. This leaves a) lost RTEmagic files and b) creates a double reference to the old files.
1531  // The only solution I can come up with is detecting when auto versioning happens, then see if any RTEmagic images was copied and if so make a stupid string-replace of the content !
1532  if ($this->autoVersioningUpdate === true) {
1533  if (is_array($this->RTEmagic_copyIndex[$table][$id][$field])) {
1534  foreach ($this->RTEmagic_copyIndex[$table][$id][$field] as $oldRTEmagicName => $newRTEmagicName) {
1535  $fieldArray[$field] = str_replace(' src="' . $oldRTEmagicName . '"', ' src="' . $newRTEmagicName . '"', $fieldArray[$field]);
1536  }
1537  }
1538  }
1539  } elseif ($GLOBALS['TCA'][$table]['ctrl']['origUid'] === $field) {
1540  // Allow value for original UID to pass by...
1541  $fieldArray[$field] = $fieldValue;
1542  }
1543  }
1544  }
1545  // Add diff-storage information:
1546  if ($diffStorageFlag && !isset($fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']])) {
1547  // If the field is set it would probably be because of an undo-operation - in which case we should not update the field of course...
1548  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = serialize($originalLanguage_diffStorage);
1549  }
1550  // Return fieldArray
1551  return $fieldArray;
1552  }
1553 
1554  /*********************************************
1555  *
1556  * Evaluation of input values
1557  *
1558  ********************************************/
1573  public function checkValue($table, $field, $value, $id, $status, $realPid, $tscPID)
1574  {
1575  // Result array
1576  $res = [];
1577 
1578  // Processing special case of field pages.doktype
1579  if (($table === 'pages' || $table === 'pages_language_overlay') && $field === 'doktype') {
1580  // If the user may not use this specific doktype, we issue a warning
1581  if (!($this->admin || GeneralUtility::inList($this->BE_USER->groupData['pagetypes_select'], $value))) {
1582  if ($this->enableLogging) {
1583  $propArr = $this->getRecordProperties($table, $id);
1584  $this->log($table, $id, 5, 0, 1, 'You cannot change the \'doktype\' of page \'%s\' to the desired value.', 1, [$propArr['header']], $propArr['event_pid']);
1585  }
1586  return $res;
1587  }
1588  if ($status === 'update') {
1589  // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1590  $onlyAllowedTables = isset($GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables']) ? $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] : $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1591  if ($onlyAllowedTables) {
1592  // use the real page id (default language)
1593  if ($table !== 'pages_language_overlay') {
1594  $recordId = $id;
1595  } elseif ($realPid >= 0) {
1596  $recordId = $realPid;
1597  } else {
1598  $recordId = $tscPID;
1599  }
1600  $theWrongTables = $this->doesPageHaveUnallowedTables($recordId, $value);
1601  if ($theWrongTables) {
1602  if ($this->enableLogging) {
1603  $propArr = $this->getRecordProperties($table, $id);
1604  $this->log($table, $id, 5, 0, 1, '\'doktype\' of page \'%s\' could not be changed because the page contains records from disallowed tables; %s', 2, [$propArr['header'], $theWrongTables], $propArr['event_pid']);
1605  }
1606  return $res;
1607  }
1608  }
1609  }
1610  }
1611 
1612  $curValue = null;
1613  if ((int)$id !== 0) {
1614  // Get current value:
1615  $curValueRec = $this->recordInfo($table, $id, $field);
1616  // isset() won't work here, since values can be NULL
1617  if ($curValueRec !== null && array_key_exists($field, $curValueRec)) {
1618  $curValue = $curValueRec[$field];
1619  }
1620  }
1621 
1622  // Getting config for the field
1623  $tcaFieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1624 
1625  // Create $recFID only for those types that need it
1626  if (
1627  $tcaFieldConf['type'] === 'flex'
1628  || $tcaFieldConf['type'] === 'group' && ($tcaFieldConf['internal_type'] === 'file' || $tcaFieldConf['internal_type'] === 'file_reference')
1629  ) {
1630  $recFID = $table . ':' . $id . ':' . $field;
1631  } else {
1632  $recFID = null;
1633  }
1634 
1635  // Perform processing:
1636  $res = $this->checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $this->uploadedFileArray[$table][$id][$field], $tscPID);
1637  return $res;
1638  }
1639 
1659  public function checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $uploadedFiles, $tscPID, array $additionalData = null)
1660  {
1661  // Convert to NULL value if defined in TCA
1662  if ($value === null && !empty($tcaFieldConf['eval']) && GeneralUtility::inList($tcaFieldConf['eval'], 'null')) {
1663  $res = ['value' => null];
1664  return $res;
1665  }
1666 
1667  switch ($tcaFieldConf['type']) {
1668  case 'text':
1669  $res = $this->checkValueForText($value, $tcaFieldConf, $table, $id, $realPid, $field);
1670  break;
1671  case 'passthrough':
1672  case 'imageManipulation':
1673  case 'user':
1674  $res['value'] = $value;
1675  break;
1676  case 'input':
1677  $res = $this->checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field);
1678  break;
1679  case 'check':
1680  $res = $this->checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1681  break;
1682  case 'radio':
1683  $res = $this->checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1684  break;
1685  case 'group':
1686  case 'select':
1687  $res = $this->checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field);
1688  break;
1689  case 'inline':
1690  $res = $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
1691  break;
1692  case 'flex':
1693  // FlexForms are only allowed for real fields.
1694  if ($field) {
1695  $res = $this->checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field);
1696  }
1697  break;
1698  default:
1699  // Do nothing
1700  }
1701  $res = $this->checkValueForInternalReferences($res, $value, $tcaFieldConf, $table, $id, $field);
1702  return $res;
1703  }
1704 
1723  protected function checkValueForInternalReferences(array $res, $value, $tcaFieldConf, $table, $id, $field)
1724  {
1725  $relevantFieldNames = [
1726  $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null,
1727  $GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null,
1728  ];
1729 
1730  if (
1731  // in case field is empty
1732  empty($field)
1733  // in case the field is not relevant
1734  || !in_array($field, $relevantFieldNames)
1735  // in case the 'value' index has been unset already
1736  || !array_key_exists('value', $res)
1737  // in case it's not a NEW-identifier
1738  || strpos($value, 'NEW') === false
1739  ) {
1740  return $res;
1741  }
1742 
1743  $valueArray = [$value];
1744  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
1745  $this->addNewValuesToRemapStackChildIds($valueArray);
1746  $this->remapStack[] = [
1747  'args' => [$valueArray, $tcaFieldConf, $id, $table, $field],
1748  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 3],
1749  'field' => $field
1750  ];
1751  unset($res['value']);
1752 
1753  return $res;
1754  }
1755 
1767  protected function checkValueForText($value, $tcaFieldConf, $table, $id, $realPid, $field)
1768  {
1769  if (isset($tcaFieldConf['eval']) && $tcaFieldConf['eval'] !== '') {
1770  $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1771  if ($this->runtimeCache->has($cacheId)) {
1772  $evalCodesArray = $this->runtimeCache->get($cacheId);
1773  } else {
1774  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1775  $this->runtimeCache->set($cacheId, $evalCodesArray);
1776  }
1777  $valueArray = $this->checkValue_text_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1778  } else {
1779  $valueArray = ['value' => $value];
1780  }
1781 
1782  // Handle richtext transformations
1783  if ($this->dontProcessTransformations) {
1784  return $valueArray;
1785  }
1786  $recordType = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
1787  $columnsOverridesConfigOfField = $GLOBALS['TCA'][$table]['types'][$recordType]['columnsOverrides'][$field]['config'] ?? null;
1788  if ($columnsOverridesConfigOfField) {
1789  ArrayUtility::mergeRecursiveWithOverrule($tcaFieldConf, $columnsOverridesConfigOfField);
1790  }
1791  if (isset($tcaFieldConf['enableRichtext']) && (bool)$tcaFieldConf['enableRichtext'] === true) {
1792  $richtextConfigurationProvider = GeneralUtility::makeInstance(Richtext::class);
1793  $richtextConfiguration = $richtextConfigurationProvider->getConfiguration($table, $field, $realPid, $recordType, $tcaFieldConf);
1794  $parseHTML = GeneralUtility::makeInstance(RteHtmlParser::class);
1795  $parseHTML->init($table . ':' . $field, $realPid);
1796  $valueArray['value'] = $parseHTML->RTE_transform($value, [], 'db', $richtextConfiguration);
1797  }
1798 
1799  return $valueArray;
1800  }
1801 
1813  protected function checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field)
1814  {
1815  // Handle native date/time fields
1816  $isDateOrDateTimeField = false;
1817  $format = '';
1818  $emptyValue = '';
1819  // normal integer "date" fields (timestamps) are handled in checkValue_input_Eval
1820  if (isset($tcaFieldConf['dbType']) && ($tcaFieldConf['dbType'] === 'date' || $tcaFieldConf['dbType'] === 'datetime')) {
1821  if (empty($value)) {
1822  $value = null;
1823  } else {
1824  $isDateOrDateTimeField = true;
1825  $dateTimeFormats = QueryHelper::getDateTimeFormats();
1826  $format = $dateTimeFormats[$tcaFieldConf['dbType']]['format'];
1827 
1828  // Convert the date/time into a timestamp for the sake of the checks
1829  $emptyValue = $dateTimeFormats[$tcaFieldConf['dbType']]['empty'];
1830  // We store UTC timestamps in the database, which is what getTimestamp() returns.
1831  $dateTime = new \DateTime($value);
1832  $value = $value === $emptyValue ? null : $dateTime->getTimestamp();
1833  }
1834  }
1835  // Secures the string-length to be less than max.
1836  if ((int)$tcaFieldConf['max'] > 0) {
1837  $value = mb_substr((string)$value, 0, (int)$tcaFieldConf['max'], 'utf-8');
1838  }
1839 
1840  if (empty($tcaFieldConf['eval'])) {
1841  $res = ['value' => $value];
1842  } else {
1843  // Process evaluation settings:
1844  $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1845  if ($this->runtimeCache->has($cacheId)) {
1846  $evalCodesArray = $this->runtimeCache->get($cacheId);
1847  } else {
1848  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1849  $this->runtimeCache->set($cacheId, $evalCodesArray);
1850  }
1851 
1852  $res = $this->checkValue_input_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1853  if (isset($tcaFieldConf['dbType']) && isset($res['value']) && !$res['value']) {
1854  // set the value to null if we have an empty value for a native field
1855  $res['value'] = null;
1856  }
1857 
1858  // Process UNIQUE settings:
1859  // 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 and if PID is -1 ($realPid<0) then versioning is happening...
1860  if ($field && $realPid >= 0 && !empty($res['value'])) {
1861  if (in_array('uniqueInPid', $evalCodesArray, true)) {
1862  $res['value'] = $this->getUnique($table, $field, $res['value'], $id, $realPid);
1863  }
1864  if ($res['value'] && in_array('unique', $evalCodesArray, true)) {
1865  $res['value'] = $this->getUnique($table, $field, $res['value'], $id);
1866  }
1867  }
1868  }
1869 
1870  // Checking range of value:
1871  // @todo: The "checkbox" option was removed for type=input, this check could be probably relaxed?
1872  if ($tcaFieldConf['range'] && $res['value'] != $tcaFieldConf['checkbox'] && (int)$res['value'] !== (int)$tcaFieldConf['default']) {
1873  if (isset($tcaFieldConf['range']['upper']) && (int)$res['value'] > (int)$tcaFieldConf['range']['upper']) {
1874  $res['value'] = (int)$tcaFieldConf['range']['upper'];
1875  }
1876  if (isset($tcaFieldConf['range']['lower']) && (int)$res['value'] < (int)$tcaFieldConf['range']['lower']) {
1877  $res['value'] = (int)$tcaFieldConf['range']['lower'];
1878  }
1879  }
1880 
1881  // Handle native date/time fields
1882  if ($isDateOrDateTimeField) {
1883  // Convert the timestamp back to a date/time
1884  $res['value'] = $res['value'] ? gmdate($format, $res['value']) : $emptyValue;
1885  }
1886  return $res;
1887  }
1888 
1901  protected function checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field)
1902  {
1903  $items = $tcaFieldConf['items'];
1904  if ($tcaFieldConf['itemsProcFunc']) {
1906  $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
1907  $items = $processingService->getProcessingItems(
1908  $table,
1909  $realPid,
1910  $field,
1911  $this->checkValue_currentRecord,
1912  $tcaFieldConf,
1913  $tcaFieldConf['items']
1914  );
1915  }
1916 
1917  $itemC = 0;
1918  if ($items !== null) {
1919  $itemC = count($items);
1920  }
1921  if (!$itemC) {
1922  $itemC = 1;
1923  }
1924  $maxV = pow(2, $itemC) - 1;
1925  if ($value < 0) {
1926  // @todo: throw LogicException here? Negative values for checkbox items do not make sense and indicate a coding error.
1927  $value = 0;
1928  }
1929  if ($value > $maxV) {
1930  // @todo: This case is pretty ugly: If there is an itemsProcFunc registered, and if it returns a dynamic,
1931  // @todo: changing list of items, then it may happen that a value is transformed and vanished checkboxes
1932  // @todo: are permanently removed from the value.
1933  // @todo: Suggestion: Throw an exception instead? Maybe a specific, catchable exception that generates a
1934  // @todo: error message to the user - dynamic item sets via itemProcFunc on check would be a bad idea anyway.
1935  $value = $value & $maxV;
1936  }
1937  if ($field && $realPid >= 0 && $value > 0 && !empty($tcaFieldConf['eval'])) {
1938  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1939  $otherRecordsWithSameValue = [];
1940  $maxCheckedRecords = 0;
1941  if (in_array('maximumRecordsCheckedInPid', $evalCodesArray, true)) {
1942  $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value, $realPid);
1943  $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsCheckedInPid'];
1944  }
1945  if (in_array('maximumRecordsChecked', $evalCodesArray, true)) {
1946  $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value);
1947  $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsChecked'];
1948  }
1949 
1950  // there are more than enough records with value "1" in the DB
1951  // if so, set this value to "0" again
1952  if ($maxCheckedRecords && count($otherRecordsWithSameValue) >= $maxCheckedRecords) {
1953  $value = 0;
1954  if ($this->enableLogging) {
1955  $this->log($table, $id, 5, 0, 1, 'Could not activate checkbox for field "%s". A total of %s record(s) can have this checkbox activated. Uncheck other records first in order to activate the checkbox of this record.', -1, [$GLOBALS['LANG']->sL(BackendUtility::getItemLabel($table, $field)), $maxCheckedRecords]);
1956  }
1957  }
1958  }
1959  $res['value'] = $value;
1960  return $res;
1961  }
1962 
1975  protected function checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $pid, $field)
1976  {
1977  if (is_array($tcaFieldConf['items'])) {
1978  foreach ($tcaFieldConf['items'] as $set) {
1979  if ((string)$set[1] === (string)$value) {
1980  $res['value'] = $value;
1981  break;
1982  }
1983  }
1984  }
1985 
1986  // if no value was found and an itemsProcFunc is defined, check that for the value
1987  if ($tcaFieldConf['itemsProcFunc'] && empty($res['value'])) {
1988  $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
1989  $processedItems = $processingService->getProcessingItems(
1990  $table,
1991  $pid,
1992  $field,
1993  $this->checkValue_currentRecord,
1994  $tcaFieldConf,
1995  $tcaFieldConf['items']
1996  );
1997 
1998  foreach ($processedItems as $set) {
1999  if ((string)$set[1] === (string)$value) {
2000  $res['value'] = $value;
2001  break;
2002  }
2003  }
2004  }
2005 
2006  return $res;
2007  }
2008 
2024  protected function checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field)
2025  {
2026  // Detecting if value sent is an array and if so, implode it around a comma:
2027  if (is_array($value)) {
2028  $value = implode(',', $value);
2029  }
2030  // This converts all occurrences of '&#123;' to the byte 123 in the string - this is needed in very rare cases where file names with special characters (e.g. ???, umlaut) gets sent to the server as HTML entities instead of bytes. The error is done only by MSIE, not Mozilla and Opera.
2031  // Anyway, this should NOT disturb anything else:
2032  $value = $this->convNumEntityToByteValue($value);
2033  // When values are sent as group or select they come as comma-separated values which are exploded by this function:
2034  $valueArray = $this->checkValue_group_select_explodeSelectGroupValue($value);
2035  // If multiple is not set, remove duplicates:
2036  if (!$tcaFieldConf['multiple']) {
2037  $valueArray = array_unique($valueArray);
2038  }
2039  // If an exclusive key is found, discard all others:
2040  if ($tcaFieldConf['type'] === 'select' && $tcaFieldConf['exclusiveKeys']) {
2041  $exclusiveKeys = GeneralUtility::trimExplode(',', $tcaFieldConf['exclusiveKeys']);
2042  foreach ($valueArray as $index => $key) {
2043  if (in_array($key, $exclusiveKeys, true)) {
2044  $valueArray = [$index => $key];
2045  break;
2046  }
2047  }
2048  }
2049  // 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?)
2050  // 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!!
2051  $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
2052  // Checking for select / authMode, removing elements from $valueArray if any of them is not allowed!
2053  if ($tcaFieldConf['type'] === 'select' && $tcaFieldConf['authMode']) {
2054  $preCount = count($valueArray);
2055  foreach ($valueArray as $index => $key) {
2056  if (!$this->BE_USER->checkAuthMode($table, $field, $key, $tcaFieldConf['authMode'])) {
2057  unset($valueArray[$index]);
2058  }
2059  }
2060  // 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.
2061  if ($preCount && empty($valueArray)) {
2062  return [];
2063  }
2064  }
2065  // For group types:
2066  if ($tcaFieldConf['type'] === 'group') {
2067  switch ($tcaFieldConf['internal_type']) {
2068  case 'file_reference':
2069  case 'file':
2070  $valueArray = $this->checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFiles, $status, $table, $id, $recFID);
2071  break;
2072  }
2073  }
2074  // For select types which has a foreign table attached:
2075  $unsetResult = false;
2076  if (
2077  $tcaFieldConf['type'] === 'group' && $tcaFieldConf['internal_type'] === 'db'
2078  || $tcaFieldConf['type'] === 'select' && ($tcaFieldConf['foreign_table'] || isset($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages')
2079  ) {
2080  // check, if there is a NEW... id in the value, that should be substituted later
2081  if (strpos($value, 'NEW') !== false) {
2082  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2083  $this->addNewValuesToRemapStackChildIds($valueArray);
2084  $this->remapStack[] = [
2085  'func' => 'checkValue_group_select_processDBdata',
2086  'args' => [$valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field],
2087  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 5],
2088  'field' => $field
2089  ];
2090  $unsetResult = true;
2091  } else {
2092  $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field);
2093  }
2094  }
2095  if (!$unsetResult) {
2096  $newVal = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
2097  $res['value'] = $this->castReferenceValue(implode(',', $newVal), $tcaFieldConf);
2098  } else {
2099  unset($res['value']);
2100  }
2101  return $res;
2102  }
2103 
2112  protected function applyFiltersToValues(array $tcaFieldConfiguration, array $values)
2113  {
2114  if (empty($tcaFieldConfiguration['filter']) || !is_array($tcaFieldConfiguration['filter'])) {
2115  return $values;
2116  }
2117  foreach ($tcaFieldConfiguration['filter'] as $filter) {
2118  if (empty($filter['userFunc'])) {
2119  continue;
2120  }
2121  $parameters = $filter['parameters'] ?: [];
2122  $parameters['values'] = $values;
2123  $parameters['tcaFieldConfig'] = $tcaFieldConfiguration;
2124  $values = GeneralUtility::callUserFunction($filter['userFunc'], $parameters, $this);
2125  if (!is_array($values)) {
2126  throw new \RuntimeException('Failed calling filter userFunc.', 1336051942);
2127  }
2128  }
2129  return $values;
2130  }
2131 
2147  public function checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFileArray, $status, $table, $id, $recFID)
2148  {
2149  // If file handling should NOT be bypassed, do processing:
2150  if (!$this->bypassFileHandling) {
2151  // If any files are uploaded, add them to value array
2152  // Numeric index means that there are multiple files
2153  if (isset($uploadedFileArray[0])) {
2154  $uploadedFiles = $uploadedFileArray;
2155  } else {
2156  // There is only one file
2157  $uploadedFiles = [$uploadedFileArray];
2158  }
2159  foreach ($uploadedFiles as $uploadedFileArray) {
2160  if (!empty($uploadedFileArray['name']) && $uploadedFileArray['tmp_name'] !== 'none') {
2161  $valueArray[] = $uploadedFileArray['tmp_name'];
2162  $this->alternativeFileName[$uploadedFileArray['tmp_name']] = $uploadedFileArray['name'];
2163  }
2164  }
2165  // Creating fileFunc object.
2166  if (!$this->fileFunc) {
2167  $this->fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
2168  }
2169  // Setting permitted extensions.
2170  $this->fileFunc->setFileExtensionPermissions($tcaFieldConf['allowed'], $tcaFieldConf['disallowed'] ?: '*');
2171  }
2172  // If there is an upload folder defined:
2173  if ($tcaFieldConf['uploadfolder'] && $tcaFieldConf['internal_type'] === 'file') {
2174  $currentFilesForHistory = null;
2175  // If filehandling should NOT be bypassed, do processing:
2176  if (!$this->bypassFileHandling) {
2177  // For logging..
2178  $propArr = $this->getRecordProperties($table, $id);
2179  // Get destination path:
2180  $dest = PATH_site . $tcaFieldConf['uploadfolder'];
2181  // If we are updating:
2182  if ($status === 'update') {
2183  // Traverse the input values and convert to absolute filenames in case the update happens to an autoVersionized record.
2184  // Background: This is a horrible workaround! The problem is that when a record is auto-versionized the files of the record get copied and therefore get new names which is overridden with the names from the original record in the incoming data meaning both lost files and double-references!
2185  // The only solution I could come up with (except removing support for managing files when autoversioning) was to convert all relative files to absolute names so they are copied again (and existing files deleted). This should keep references intact but means that some files are copied, then deleted after being copied _again_.
2186  // Actually, the same problem applies to database references in case auto-versioning would include sub-records since in such a case references are remapped - and they would be overridden due to the same principle then.
2187  // Illustration of the problem comes here:
2188  // We have a record 123 with a file logo.gif. We open and edit the files header in a workspace. So a new version is automatically made.
2189  // The versions uid is 456 and the file is copied to "logo_01.gif". But the form data that we sent was based on uid 123 and hence contains the filename "logo.gif" from the original.
2190  // The file management code below will do two things: First it will blindly accept "logo.gif" as a file attached to the record (thus creating a double reference) and secondly it will find that "logo_01.gif" was not in the incoming filelist and therefore should be deleted.
2191  // If we prefix the incoming file "logo.gif" with its absolute path it will be seen as a new file added. Thus it will be copied to "logo_02.gif". "logo_01.gif" will still be deleted but since the files are the same the difference is zero - only more processing and file copying for no reason. But it will work.
2192  if ($this->autoVersioningUpdate === true) {
2193  foreach ($valueArray as $key => $theFile) {
2194  // If it is an already attached file...
2195  if ($theFile === basename($theFile)) {
2196  $valueArray[$key] = PATH_site . $tcaFieldConf['uploadfolder'] . '/' . $theFile;
2197  }
2198  }
2199  }
2200  // Finding the CURRENT files listed, either from MM or from the current record.
2201  $theFileValues = [];
2202  // If MM relations for the files also!
2203  if ($tcaFieldConf['MM']) {
2204  $dbAnalysis = $this->createRelationHandlerInstance();
2206  $dbAnalysis->start('', 'files', $tcaFieldConf['MM'], $id);
2207  foreach ($dbAnalysis->itemArray as $item) {
2208  if ($item['id']) {
2209  $theFileValues[] = $item['id'];
2210  }
2211  }
2212  } else {
2213  $theFileValues = GeneralUtility::trimExplode(',', $curValue, true);
2214  }
2215  $currentFilesForHistory = implode(',', $theFileValues);
2216  // DELETE files: If existing files were found, traverse those and register files for deletion which has been removed:
2217  if (!empty($theFileValues)) {
2218  // Traverse the input values and for all input values which match an EXISTING value, remove the existing from $theFileValues array (this will result in an array of all the existing files which should be deleted!)
2219  foreach ($valueArray as $key => $theFile) {
2220  if ($theFile && !strstr(GeneralUtility::fixWindowsFilePath($theFile), '/')) {
2221  $theFileValues = ArrayUtility::removeArrayEntryByValue($theFileValues, $theFile);
2222  }
2223  }
2224  // This array contains the filenames in the uploadfolder that should be deleted:
2225  foreach ($theFileValues as $key => $theFile) {
2226  $theFile = trim($theFile);
2227  if (@is_file(($dest . '/' . $theFile))) {
2228  $this->removeFilesStore[] = $dest . '/' . $theFile;
2229  } elseif ($this->enableLogging && $theFile) {
2230  $this->log($table, $id, 5, 0, 1, 'Could not delete file \'%s\' (does not exist). (%s)', 10, [$dest . '/' . $theFile, $recFID], $propArr['event_pid']);
2231  }
2232  }
2233  }
2234  }
2235  // Traverse the submitted values:
2236  foreach ($valueArray as $key => $theFile) {
2237  // Init:
2238  $maxSize = (int)$tcaFieldConf['max_size'];
2239  // Must be cleared. Else a faulty fileref may be inserted if the below code returns an error!
2240  $theDestFile = '';
2241  // a FAL file was added, now resolve the file object and get the absolute path
2242  // @todo in future versions this needs to be modified to handle FAL objects natively
2243  if (!empty($theFile) && MathUtility::canBeInterpretedAsInteger($theFile)) {
2244  $fileObject = ResourceFactory::getInstance()->getFileObject($theFile);
2245  $theFile = $fileObject->getForLocalProcessing(false);
2246  }
2247  // NEW FILES? If the value contains '/' it indicates, that the file
2248  // is new and should be added to the uploadsdir (whether its absolute or relative does not matter here)
2249  if (strstr(GeneralUtility::fixWindowsFilePath($theFile), '/')) {
2250  // Check various things before copying file:
2251  // File and destination must exist
2252  if (@is_dir($dest) && (@is_file($theFile) || @is_uploaded_file($theFile))) {
2253  // Finding size.
2254  if (is_uploaded_file($theFile) && $theFile == $uploadedFileArray['tmp_name']) {
2255  $fileSize = $uploadedFileArray['size'];
2256  } else {
2257  $fileSize = filesize($theFile);
2258  }
2259  // Check file size:
2260  if (!$maxSize || $fileSize <= $maxSize * 1024) {
2261  // Prepare filename:
2262  $theEndFileName = isset($this->alternativeFileName[$theFile]) ? $this->alternativeFileName[$theFile] : $theFile;
2263  $fI = GeneralUtility::split_fileref($theEndFileName);
2264  // Check for allowed extension:
2265  if ($this->fileFunc->checkIfAllowed($fI['fileext'], $dest, $theEndFileName)) {
2266  $theDestFile = $this->fileFunc->getUniqueName($this->fileFunc->cleanFileName($fI['file']), $dest);
2267  // If we have a unique destination filename, then write the file:
2268  if ($theDestFile) {
2269  GeneralUtility::upload_copy_move($theFile, $theDestFile);
2270  // Hook for post-processing the upload action
2271  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processUpload'])) {
2272  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processUpload'] as $classRef) {
2273  $hookObject = GeneralUtility::getUserObj($classRef);
2274  if (!$hookObject instanceof DataHandlerProcessUploadHookInterface) {
2275  throw new \UnexpectedValueException($classRef . ' must implement interface ' . DataHandlerProcessUploadHookInterface::class, 1279962349);
2276  }
2277  $hookObject->processUpload_postProcessAction($theDestFile, $this);
2278  }
2279  }
2280  $this->copiedFileMap[$theFile] = $theDestFile;
2281  clearstatcache();
2282  if ($this->enableLogging && !@is_file($theDestFile)) {
2283  $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: The destination path (%s) may be write protected. Please make it write enabled!. (%s)', 16, [$theFile, dirname($theDestFile), $recFID], $propArr['event_pid']);
2284  }
2285  } elseif ($this->enableLogging) {
2286  $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: No destination file (%s) possible!. (%s)', 11, [$theFile, $theDestFile, $recFID], $propArr['event_pid']);
2287  }
2288  } elseif ($this->enableLogging) {
2289  $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, [$fI['fileext'], $recFID], $propArr['event_pid']);
2290  }
2291  } elseif ($this->enableLogging) {
2292  $this->log($table, $id, 5, 0, 1, 'Filesize (%s) of file \'%s\' exceeds limit (%s). (%s)', 13, [GeneralUtility::formatSize($fileSize), $theFile, GeneralUtility::formatSize($maxSize * 1024), $recFID], $propArr['event_pid']);
2293  }
2294  } elseif ($this->enableLogging) {
2295  $this->log($table, $id, 5, 0, 1, 'The destination (%s) or the source file (%s) does not exist. (%s)', 14, [$dest, $theFile, $recFID], $propArr['event_pid']);
2296  }
2297  // If the destination file was created, we will set the new filename in the value array, otherwise unset the entry in the value array!
2298  if (@is_file($theDestFile)) {
2299  $info = GeneralUtility::split_fileref($theDestFile);
2300  // The value is set to the new filename
2301  $valueArray[$key] = $info['file'];
2302  } else {
2303  // The value is set to the new filename
2304  unset($valueArray[$key]);
2305  }
2306  }
2307  }
2308  }
2309  // If MM relations for the files, we will set the relations as MM records and change the valuearray to contain a single entry with a count of the number of files!
2310  if ($tcaFieldConf['MM']) {
2312  $dbAnalysis = $this->createRelationHandlerInstance();
2313  // Dummy
2314  $dbAnalysis->tableArray['files'] = [];
2315  foreach ($valueArray as $key => $theFile) {
2316  // Explode files
2317  $dbAnalysis->itemArray[]['id'] = $theFile;
2318  }
2319  if ($status === 'update') {
2320  $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, 0);
2321  $newFiles = implode(',', $dbAnalysis->getValueArray());
2322  list(, , $recFieldName) = explode(':', $recFID);
2323  if ($currentFilesForHistory != $newFiles) {
2324  $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$recFieldName] = $currentFilesForHistory;
2325  $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$recFieldName] = $newFiles;
2326  } else {
2327  $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$recFieldName] = '';
2328  $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$recFieldName] = '';
2329  }
2330  } else {
2331  $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, 0];
2332  }
2333  $valueArray = $dbAnalysis->countItems();
2334  }
2335  } else {
2336  if (!empty($valueArray)) {
2337  // If filehandling should NOT be bypassed, do processing:
2338  if (!$this->bypassFileHandling) {
2339  // For logging..
2340  $propArr = $this->getRecordProperties($table, $id);
2341  foreach ($valueArray as &$theFile) {
2342  // FAL handling: it's a UID, thus it is resolved to the absolute path
2343  if (!empty($theFile) && MathUtility::canBeInterpretedAsInteger($theFile)) {
2344  $fileObject = ResourceFactory::getInstance()->getFileObject($theFile);
2345  $theFile = $fileObject->getForLocalProcessing(false);
2346  }
2347  if ($this->alternativeFilePath[$theFile]) {
2348  // If alternative File Path is set for the file, then it was an import
2349  // don't import the file if it already exists
2350  if (@is_file((PATH_site . $this->alternativeFilePath[$theFile]))) {
2351  $theFile = PATH_site . $this->alternativeFilePath[$theFile];
2352  } elseif (@is_file($theFile)) {
2353  $dest = dirname(PATH_site . $this->alternativeFilePath[$theFile]);
2354  if (!@is_dir($dest)) {
2355  GeneralUtility::mkdir_deep(PATH_site, dirname($this->alternativeFilePath[$theFile]) . '/');
2356  }
2357  // Init:
2358  $maxSize = (int)$tcaFieldConf['max_size'];
2359  // Must be cleared. Else a faulty fileref may be inserted if the below code returns an error!
2360  $theDestFile = '';
2361  $fileSize = filesize($theFile);
2362  // Check file size:
2363  if (!$maxSize || $fileSize <= $maxSize * 1024) {
2364  // Prepare filename:
2365  $theEndFileName = isset($this->alternativeFileName[$theFile]) ? $this->alternativeFileName[$theFile] : $theFile;
2366  $fI = GeneralUtility::split_fileref($theEndFileName);
2367  // Check for allowed extension:
2368  if ($this->fileFunc->checkIfAllowed($fI['fileext'], $dest, $theEndFileName)) {
2369  $theDestFile = PATH_site . $this->alternativeFilePath[$theFile];
2370  // Write the file:
2371  if ($theDestFile) {
2372  GeneralUtility::upload_copy_move($theFile, $theDestFile);
2373  $this->copiedFileMap[$theFile] = $theDestFile;
2374  clearstatcache();
2375  if ($this->enableLogging && !@is_file($theDestFile)) {
2376  $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: The destination path (%s) may be write protected. Please make it write enabled!. (%s)', 16, [$theFile, dirname($theDestFile), $recFID], $propArr['event_pid']);
2377  }
2378  } elseif ($this->enableLogging) {
2379  $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: No destination file (%s) possible!. (%s)', 11, [$theFile, $theDestFile, $recFID], $propArr['event_pid']);
2380  }
2381  } elseif ($this->enableLogging) {
2382  $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, [$fI['fileext'], $recFID], $propArr['event_pid']);
2383  }
2384  } elseif ($this->enableLogging) {
2385  $this->log($table, $id, 5, 0, 1, 'Filesize (%s) of file \'%s\' exceeds limit (%s). (%s)', 13, [GeneralUtility::formatSize($fileSize), $theFile, GeneralUtility::formatSize($maxSize * 1024), $recFID], $propArr['event_pid']);
2386  }
2387  // If the destination file was created, we will set the new filename in the value array, otherwise unset the entry in the value array!
2388  if (@is_file($theDestFile)) {
2389  // The value is set to the new filename
2390  $theFile = $theDestFile;
2391  } else {
2392  // The value is set to the new filename
2393  unset($theFile);
2394  }
2395  }
2396  }
2397  if (!empty($theFile)) {
2398  $theFile = GeneralUtility::fixWindowsFilePath($theFile);
2399  if (GeneralUtility::isFirstPartOfStr($theFile, PATH_site)) {
2400  $theFile = PathUtility::stripPathSitePrefix($theFile);
2401  }
2402  }
2403  }
2404  unset($theFile);
2405  }
2406  }
2407  }
2408  return $valueArray;
2409  }
2410 
2428  protected function checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field)
2429  {
2430  if (is_array($value)) {
2431  // This value is necessary for flex form processing to happen on flexform fields in page records when they are copied.
2432  // Problem: when copying a page, flexform XML comes along in the array for the new record - but since $this->checkValue_currentRecord
2433  // does not have a uid or pid for that sake, the FlexFormTools->getDataStructureIdentifier() function returns no good DS. For new
2434  // records we do know the expected PID so therefore we send that with this special parameter. Only active when larger than zero.
2436  if ($status === 'new') {
2437  $row['pid'] = $realPid;
2438  }
2439 
2440  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
2441 
2442  // Get data structure. The methods may throw various exceptions, with some of them being
2443  // ok in certain scenarios, for instance on new record rows. Those are ok to "eat" here
2444  // and substitute with a dummy DS.
2445  $dataStructureArray = [ 'sheets' => [ 'sDEF' => [] ] ];
2446  try {
2447  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
2448  [ 'config' => $tcaFieldConf ],
2449  $table,
2450  $field,
2451  $row
2452  );
2453 
2454  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
2455  } catch (InvalidParentRowException $e) {
2456  } catch (InvalidParentRowLoopException $e) {
2457  } catch (InvalidParentRowRootException $e) {
2458  } catch (InvalidPointerFieldValueException $e) {
2459  } catch (InvalidIdentifierException $e) {
2460  }
2461 
2462  // Get current value array:
2463  $currentValueArray = (string)$curValue !== '' ? GeneralUtility::xml2array($curValue) : [];
2464  if (!is_array($currentValueArray)) {
2465  $currentValueArray = [];
2466  }
2467  // Remove all old meta for languages...
2468  // Evaluation of input values:
2469  $value['data'] = $this->checkValue_flex_procInData($value['data'], $currentValueArray['data'], $uploadedFiles['data'], $dataStructureArray, [$table, $id, $curValue, $status, $realPid, $recFID, $tscPID]);
2470  // Create XML from input value:
2471  $xmlValue = $this->checkValue_flexArray2Xml($value, true);
2472 
2473  // 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
2474  // (provided that the current value was already stored IN the charset that the new value is converted to).
2475  $arrValue = GeneralUtility::xml2array($xmlValue);
2476 
2477  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'])) {
2478  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'] as $classRef) {
2479  $hookObject = GeneralUtility::getUserObj($classRef);
2480  if (method_exists($hookObject, 'checkFlexFormValue_beforeMerge')) {
2481  $hookObject->checkFlexFormValue_beforeMerge($this, $currentValueArray, $arrValue);
2482  }
2483  }
2484  }
2485 
2486  ArrayUtility::mergeRecursiveWithOverrule($currentValueArray, $arrValue);
2487  $xmlValue = $this->checkValue_flexArray2Xml($currentValueArray, true);
2488 
2489  // Action commands (sorting order and removals of elements) for flexform sections,
2490  // see FormEngine for the use of this GP parameter
2491  $actionCMDs = GeneralUtility::_GP('_ACTION_FLEX_FORMdata');
2492  if (is_array($actionCMDs[$table][$id][$field]['data'] ?? null)) {
2493  $arrValue = GeneralUtility::xml2array($xmlValue);
2494  $this->_ACTION_FLEX_FORMdata($arrValue['data'], $actionCMDs[$table][$id][$field]['data']);
2495  $xmlValue = $this->checkValue_flexArray2Xml($arrValue, true);
2496  }
2497  // Create the value XML:
2498  $res['value'] = '';
2499  $res['value'] .= $xmlValue;
2500  } else {
2501  // Passthrough...:
2502  $res['value'] = $value;
2503  }
2504 
2505  return $res;
2506  }
2507 
2515  public function checkValue_flexArray2Xml($array, $addPrologue = false)
2516  {
2518  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
2519  return $flexObj->flexArray2Xml($array, $addPrologue);
2520  }
2521 
2529  protected function _ACTION_FLEX_FORMdata(&$valueArray, $actionCMDs)
2530  {
2531  if (!is_array($valueArray) || !is_array($actionCMDs)) {
2532  return;
2533  }
2534 
2535  foreach ($actionCMDs as $key => $value) {
2536  if ($key === '_ACTION') {
2537  // First, check if there are "commands":
2538  if (current($actionCMDs[$key]) === '') {
2539  continue;
2540  }
2541 
2542  asort($actionCMDs[$key]);
2543  $newValueArray = [];
2544  foreach ($actionCMDs[$key] as $idx => $order) {
2545  // Just one reflection here: It is clear that when removing elements from a flexform, then we will get lost
2546  // files unless we act on this delete operation by traversing and deleting files that were referred to.
2547  if ($order !== 'DELETE') {
2548  $newValueArray[$idx] = $valueArray[$idx];
2549  }
2550  unset($valueArray[$idx]);
2551  }
2552  $valueArray = $valueArray + $newValueArray;
2553  } elseif (is_array($actionCMDs[$key]) && isset($valueArray[$key])) {
2554  $this->_ACTION_FLEX_FORMdata($valueArray[$key], $actionCMDs[$key]);
2555  }
2556  }
2557  }
2558 
2571  public function checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData = null)
2572  {
2573  list($table, $id, , $status) = $PP;
2574  $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
2575  }
2576 
2591  public function checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData = null)
2592  {
2593  if (!$tcaFieldConf['foreign_table']) {
2594  // Fatal error, inline fields should always have a foreign_table defined
2595  return false;
2596  }
2597  // When values are sent they come as comma-separated values which are exploded by this function:
2598  $valueArray = GeneralUtility::trimExplode(',', $value);
2599  // Remove duplicates: (should not be needed)
2600  $valueArray = array_unique($valueArray);
2601  // Example for received data:
2602  // $value = 45,NEW4555fdf59d154,12,123
2603  // We need to decide whether we use the stack or can save the relation directly.
2604  if (!empty($value) && (strpos($value, 'NEW') !== false || !MathUtility::canBeInterpretedAsInteger($id))) {
2605  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2606  $this->addNewValuesToRemapStackChildIds($valueArray);
2607  $this->remapStack[] = [
2608  'func' => 'checkValue_inline_processDBdata',
2609  'args' => [$valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData],
2610  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 4],
2611  'additionalData' => $additionalData,
2612  'field' => $field,
2613  ];
2614  unset($res['value']);
2615  } elseif ($value || MathUtility::canBeInterpretedAsInteger($id)) {
2616  $res['value'] = $this->checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData);
2617  }
2618  return $res;
2619  }
2620 
2629  public function checkValue_checkMax($tcaFieldConf, $valueArray)
2630  {
2631  // BTW, checking for min and max items here does NOT make any sense when MM is used because the above function
2632  // calls will just return an array with a single item (the count) if MM is used... Why didn't I perform the check
2633  // before? Probably because we could not evaluate the validity of record uids etc... Hmm...
2634  // NOTE to the comment: It's not really possible to check for too few items, because you must then determine first,
2635  // if the field is actual used regarding the CType.
2636  $maxitems = isset($tcaFieldConf['maxitems']) ? (int)$tcaFieldConf['maxitems'] : 99999;
2637  return array_slice($valueArray, 0, $maxitems);
2638  }
2639 
2640  /*********************************************
2641  *
2642  * Helper functions for evaluation functions.
2643  *
2644  ********************************************/
2655  public function getUnique($table, $field, $value, $id, $newPid = 0)
2656  {
2657  if (!is_array($GLOBALS['TCA'][$table]) || !is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
2658  // Field is not configured in TCA
2659  return $value;
2660  }
2661 
2662  if ((string)$GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode'] === 'exclude') {
2663  $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
2664  $l10nParent = (int)$this->checkValue_currentRecord[$transOrigPointerField];
2665  if ($l10nParent > 0) {
2666  // Current record is a translation and l10n_mode "exclude" just copies the value from source language
2667  return $value;
2668  }
2669  }
2670 
2671  $newValue = $value;
2672  $statement = $this->getUniqueCountStatement($newValue, $table, $field, (int)$id, (int)$newPid);
2673  // For as long as records with the test-value existing, try again (with incremented numbers appended)
2674  if ($statement->fetchColumn()) {
2675  for ($counter = 0; $counter <= 100; $counter++) {
2676  $newValue = $value . $counter;
2677  $statement->bindValue(1, $newValue);
2678  $statement->execute();
2679  if (!$statement->fetchColumn()) {
2680  break;
2681  }
2682  }
2683  }
2684 
2685  return $newValue;
2686  }
2687 
2698  protected function getUniqueCountStatement(
2699  string $value,
2700  string $table,
2701  string $field,
2702  int $uid,
2703  int $pid
2704  ): Statement {
2705  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2706  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
2707  $queryBuilder
2708  ->count('uid')
2709  ->from($table)
2710  ->where(
2711  $queryBuilder->expr()->eq($field, $queryBuilder->createPositionalParameter($value, \PDO::PARAM_STR)),
2712  $queryBuilder->expr()->neq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT))
2713  );
2714  if ($pid !== 0) {
2715  $queryBuilder->andWhere(
2716  $queryBuilder->expr()->eq('pid', $queryBuilder->createPositionalParameter($pid, \PDO::PARAM_INT))
2717  );
2718  } else {
2719  // pid>=0 for versioning
2720  $queryBuilder->andWhere(
2721  $queryBuilder->expr()->gte('pid', $queryBuilder->createPositionalParameter(0, \PDO::PARAM_INT))
2722  );
2723  }
2724 
2725  return $queryBuilder->execute();
2726  }
2727 
2739  public function getRecordsWithSameValue($tableName, $uid, $fieldName, $value, $pageId = 0)
2740  {
2741  $result = [];
2742  if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
2743  return $result;
2744  }
2745 
2746  $uid = (int)$uid;
2747  $pageId = (int)$pageId;
2748 
2749  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
2750  $queryBuilder->getRestrictions()
2751  ->removeAll()
2752  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2753  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2754 
2755  $queryBuilder->select('*')
2756  ->from($tableName)
2757  ->where(
2758  $queryBuilder->expr()->eq(
2759  $fieldName,
2760  $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)
2761  ),
2762  $queryBuilder->expr()->neq(
2763  'uid',
2764  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
2765  )
2766  );
2767 
2768  if ($pageId) {
2769  $queryBuilder->andWhere(
2770  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT))
2771  );
2772  } else {
2773  $queryBuilder->andWhere(
2774  $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
2775  );
2776  }
2777 
2778  $result = $queryBuilder->execute()->fetchAll();
2779 
2780  return $result;
2781  }
2782 
2789  public function checkValue_text_Eval($value, $evalArray, $is_in)
2790  {
2791  $res = [];
2792  $set = true;
2793  foreach ($evalArray as $func) {
2794  switch ($func) {
2795  case 'trim':
2796  $value = trim($value);
2797  break;
2798  case 'required':
2799  if (!$value) {
2800  $set = false;
2801  }
2802  break;
2803  default:
2804  if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2805  if (class_exists($func)) {
2806  $evalObj = GeneralUtility::makeInstance($func);
2807  if (method_exists($evalObj, 'evaluateFieldValue')) {
2808  $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2809  }
2810  }
2811  }
2812  }
2813  }
2814  if ($set) {
2815  $res['value'] = $value;
2816  }
2817  return $res;
2818  }
2819 
2828  public function checkValue_input_Eval($value, $evalArray, $is_in)
2829  {
2830  $res = [];
2831  $set = true;
2832  foreach ($evalArray as $func) {
2833  switch ($func) {
2834  case 'int':
2835  case 'year':
2836  $value = (int)$value;
2837  break;
2838  case 'time':
2839  case 'timesec':
2840  // If $value is a pure integer we have the number of seconds, we can store that directly
2841  if ($value !== '' && !MathUtility::canBeInterpretedAsInteger($value)) {
2842  // $value is an ISO 8601 date
2843  $value = (new \DateTime($value))->getTimestamp();
2844  }
2845  break;
2846  case 'date':
2847  case 'datetime':
2848  // If $value is a pure integer we have the number of seconds, we can store that directly
2849  if ($value !== null && $value !== '' && !MathUtility::canBeInterpretedAsInteger($value)) {
2850  // The value we receive from JS is an ISO 8601 date, which is always in UTC. (the JS code works like that, on purpose!)
2851  // For instance "1999-11-11T11:11:11Z"
2852  // Since the user actually specifies the time in the server's local time, we need to mangle this
2853  // to reflect the server TZ. So we make this 1999-11-11T11:11:11+0200 (assuming Europe/Vienna here)
2854  // In the database we store the date in UTC (1999-11-11T09:11:11Z), hence we take the timestamp of this converted value.
2855  // For achieving this we work with timestamps only (which are UTC) and simply adjust it for the
2856  // TZ difference.
2857  try {
2858  // Make the date from JS a timestamp
2859  $value = (new \DateTime($value))->getTimestamp();
2860  } catch (\Exception $e) {
2861  // set the default timezone value to achieve the value of 0 as a result
2862  $value = (int)date('Z', 0);
2863  }
2864 
2865  // @todo this hacky part is problematic when it comes to times around DST switch! Add test to prove that this is broken.
2866  $value -= date('Z', $value);
2867  }
2868  break;
2869  case 'double2':
2870  $value = preg_replace('/[^0-9,\\.-]/', '', $value);
2871  $negative = $value[0] === '-';
2872  $value = strtr($value, [',' => '.', '-' => '']);
2873  if (strpos($value, '.') === false) {
2874  $value .= '.0';
2875  }
2876  $valueArray = explode('.', $value);
2877  $dec = array_pop($valueArray);
2878  $value = implode('', $valueArray) . '.' . $dec;
2879  if ($negative) {
2880  $value *= -1;
2881  }
2882  $value = number_format($value, 2, '.', '');
2883  break;
2884  case 'md5':
2885  if (strlen($value) != 32) {
2886  $set = false;
2887  }
2888  break;
2889  case 'trim':
2890  $value = trim($value);
2891  break;
2892  case 'upper':
2893  $value = mb_strtoupper($value, 'utf-8');
2894  break;
2895  case 'lower':
2896  $value = mb_strtolower($value, 'utf-8');
2897  break;
2898  case 'required':
2899  if (!isset($value) || $value === '') {
2900  $set = false;
2901  }
2902  break;
2903  case 'is_in':
2904  $c = mb_strlen($value);
2905  if ($c) {
2906  $newVal = '';
2907  for ($a = 0; $a < $c; $a++) {
2908  $char = mb_substr($value, $a, 1);
2909  if (mb_strpos($is_in, $char) !== false) {
2910  $newVal .= $char;
2911  }
2912  }
2913  $value = $newVal;
2914  }
2915  break;
2916  case 'nospace':
2917  $value = str_replace(' ', '', $value);
2918  break;
2919  case 'alpha':
2920  $value = preg_replace('/[^a-zA-Z]/', '', $value);
2921  break;
2922  case 'num':
2923  $value = preg_replace('/[^0-9]/', '', $value);
2924  break;
2925  case 'alphanum':
2926  $value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
2927  break;
2928  case 'alphanum_x':
2929  $value = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
2930  break;
2931  case 'domainname':
2932  if (!preg_match('/^[a-z0-9.\\-]*$/i', $value)) {
2933  $value = GeneralUtility::idnaEncode($value);
2934  }
2935  break;
2936  case 'email':
2937  if ((string)$value !== '') {
2938  $this->checkValue_input_ValidateEmail($value, $set);
2939  }
2940  break;
2941  default:
2942  if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2943  if (class_exists($func)) {
2944  $evalObj = GeneralUtility::makeInstance($func);
2945  if (method_exists($evalObj, 'evaluateFieldValue')) {
2946  $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2947  }
2948  }
2949  }
2950  }
2951  }
2952  if ($set) {
2953  $res['value'] = $value;
2954  }
2955  return $res;
2956  }
2957 
2968  protected function checkValue_input_ValidateEmail($value, &$set)
2969  {
2970  if (GeneralUtility::validEmail($value)) {
2971  return;
2972  }
2973 
2974  $set = false;
2976  $message = GeneralUtility::makeInstance(
2977  FlashMessage::class,
2978  sprintf($GLOBALS['LANG']->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:error.invalidEmail'), $value),
2979  '', // header is optional
2981  true // whether message should be stored in session
2982  );
2984  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
2985  $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
2986  }
2987 
3000  public function checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
3001  {
3002  if ($type === 'group') {
3003  $tables = $tcaFieldConf['allowed'];
3004  } elseif (!empty($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages') {
3005  $tables = 'sys_language';
3006  } else {
3007  $tables = $tcaFieldConf['foreign_table'];
3008  }
3009  $prep = $type === 'group' ? $tcaFieldConf['prepend_tname'] : '';
3010  $newRelations = implode(',', $valueArray);
3012  $dbAnalysis = $this->createRelationHandlerInstance();
3013  $dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
3014  $dbAnalysis->start($newRelations, $tables, '', 0, $currentTable, $tcaFieldConf);
3015  if ($tcaFieldConf['MM']) {
3016  // convert submitted items to use version ids instead of live ids
3017  // (only required for MM relations in a workspace context)
3018  $dbAnalysis->convertItemArray();
3019  if ($status === 'update') {
3021  $oldRelations_dbAnalysis = $this->createRelationHandlerInstance();
3022  $oldRelations_dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
3023  // Db analysis with $id will initialize with the existing relations
3024  $oldRelations_dbAnalysis->start('', $tables, $tcaFieldConf['MM'], $id, $currentTable, $tcaFieldConf);
3025  $oldRelations = implode(',', $oldRelations_dbAnalysis->getValueArray());
3026  $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, $prep);
3027  if ($oldRelations != $newRelations) {
3028  $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
3029  $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
3030  } else {
3031  $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
3032  $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
3033  }
3034  } else {
3035  $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
3036  }
3037  $valueArray = $dbAnalysis->countItems();
3038  } else {
3039  $valueArray = $dbAnalysis->getValueArray($prep);
3040  }
3041  // 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.
3042  return $valueArray;
3043  }
3044 
3052  {
3053  $valueArray = GeneralUtility::trimExplode(',', $value, true);
3054  foreach ($valueArray as &$newVal) {
3055  $temp = explode('|', $newVal, 2);
3056  $newVal = str_replace(',', '', str_replace('|', '', rawurldecode($temp[0])));
3057  }
3058  unset($newVal);
3059  return $valueArray;
3060  }
3061 
3077  public function checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructure, $pParams, $callBackFunc = '', array $workspaceOptions = [])
3078  {
3079  if (is_array($dataPart)) {
3080  foreach ($dataPart as $sKey => $sheetDef) {
3081  if (isset($dataStructure['sheets'][$sKey]) && is_array($dataStructure['sheets'][$sKey]) && is_array($sheetDef)) {
3082  foreach ($sheetDef as $lKey => $lData) {
3084  $dataPart[$sKey][$lKey],
3085  $dataPart_current[$sKey][$lKey],
3086  $uploadedFiles[$sKey][$lKey],
3087  $dataStructure['sheets'][$sKey]['ROOT']['el'],
3088  $pParams,
3089  $callBackFunc,
3090  $sKey . '/' . $lKey . '/',
3091  $workspaceOptions
3092  );
3093  }
3094  }
3095  }
3096  }
3097  return $dataPart;
3098  }
3099 
3114  public function checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions = [])
3115  {
3116  if (!is_array($DSelements)) {
3117  return;
3118  }
3119 
3120  // For each DS element:
3121  foreach ($DSelements as $key => $dsConf) {
3122  // Array/Section:
3123  if ($DSelements[$key]['type'] === 'array') {
3124  if (!is_array($dataValues[$key]['el'])) {
3125  continue;
3126  }
3127 
3128  if ($DSelements[$key]['section']) {
3129  $newIndexCounter = 0;
3130  foreach ($dataValues[$key]['el'] as $ik => $el) {
3131  if (!is_array($el)) {
3132  continue;
3133  }
3134 
3135  if (!is_array($dataValues_current[$key]['el'])) {
3136  $dataValues_current[$key]['el'] = [];
3137  }
3138  $theKey = key($el);
3139  if (!is_array($dataValues[$key]['el'][$ik][$theKey]['el'])) {
3140  continue;
3141  }
3142 
3143  $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'][$ik][$theKey]['el'], is_array($dataValues_current[$key]['el'][$ik]) ? $dataValues_current[$key]['el'][$ik][$theKey]['el'] : [], $uploadedFiles[$key]['el'][$ik][$theKey]['el'], $DSelements[$key]['el'][$theKey]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/' . $ik . '/' . $theKey . '/el/', $workspaceOptions);
3144  }
3145  } else {
3146  if (!isset($dataValues[$key]['el'])) {
3147  $dataValues[$key]['el'] = [];
3148  }
3149  $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $uploadedFiles[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/', $workspaceOptions);
3150  }
3151  } else {
3152  // When having no specific sheets, it's "TCEforms.config", when having a sheet, it's just "config"
3153  $fieldConfiguration = $dsConf['TCEforms']['config'] ?? $dsConf['config'] ?? null;
3154  // init with value from config for passthrough fields
3155  if (!empty($fieldConfiguration['type']) && $fieldConfiguration['type'] === 'passthrough') {
3156  if (!empty($dataValues_current[$key]['vDEF'])) {
3157  // If there is existing value, keep it
3158  $dataValues[$key]['vDEF'] = $dataValues_current[$key]['vDEF'];
3159  } elseif (
3160  !empty($fieldConfiguration['default'])
3161  && isset($pParams[1])
3163  ) {
3164  // If is new record and a default is specified for field, use it.
3165  $dataValues[$key]['vDEF'] = $fieldConfiguration['default'];
3166  }
3167  }
3168  if (!is_array($fieldConfiguration) || !is_array($dataValues[$key])) {
3169  continue;
3170  }
3171 
3172  foreach ($dataValues[$key] as $vKey => $data) {
3173  if ($callBackFunc) {
3174  if (is_object($this->callBackObj)) {
3175  $res = $this->callBackObj->{$callBackFunc}($pParams, $fieldConfiguration, $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
3176  } else {
3177  $res = $this->{$callBackFunc}($pParams, $fieldConfiguration, $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
3178  }
3179  } else {
3180  // Default
3181  list($CVtable, $CVid, $CVcurValue, $CVstatus, $CVrealPid, $CVrecFID, $CVtscPID) = $pParams;
3182 
3183  $additionalData = [
3184  'flexFormId' => $CVrecFID,
3185  'flexFormPath' => trim(rtrim($structurePath, '/') . '/' . $key . '/' . $vKey, '/'),
3186  ];
3187 
3188  $res = $this->checkValue_SW([], $dataValues[$key][$vKey], $fieldConfiguration, $CVtable, $CVid, $dataValues_current[$key][$vKey], $CVstatus, $CVrealPid, $CVrecFID, '', $uploadedFiles[$key][$vKey], $CVtscPID, $additionalData);
3189  }
3190  // Adding the value:
3191  if (isset($res['value'])) {
3192  $dataValues[$key][$vKey] = $res['value'];
3193  }
3194  // Finally, check if new and old values are different (or no .vDEFbase value is found) and if so, we record the vDEF value for diff'ing.
3195  // We do this after $dataValues has been updated since I expect that $dataValues_current holds evaluated values from database (so this must be the right value to compare with).
3196  if (mb_substr($vKey, -9) !== '.vDEFbase') {
3197  if ($this->updateModeL10NdiffData && $GLOBALS['TYPO3_CONF_VARS']['BE']['flexFormXMLincludeDiffBase'] && $vKey !== 'vDEF' && ((string)$dataValues[$key][$vKey] !== (string)$dataValues_current[$key][$vKey] || !isset($dataValues_current[$key][$vKey . '.vDEFbase']) || $this->updateModeL10NdiffData === 'FORCE_FFUPD')) {
3198  // Now, check if a vDEF value is submitted in the input data, if so we expect this has been processed prior to this operation (normally the case since those fields are higher in the form) and we can use that:
3199  if (isset($dataValues[$key]['vDEF'])) {
3200  $diffValue = $dataValues[$key]['vDEF'];
3201  } else {
3202  // If not found (for translators with no access to the default language) we use the one from the current-value data set:
3203  $diffValue = $dataValues_current[$key]['vDEF'];
3204  }
3205  // Setting the reference value for vDEF for this translation. This will be used for translation tools to make a diff between the vDEF and vDEFbase to see if an update would be fitting.
3206  $dataValues[$key][$vKey . '.vDEFbase'] = $this->updateModeL10NdiffDataClear ? '' : $diffValue;
3207  }
3208  }
3209  }
3210  }
3211  }
3212  }
3213 
3226  protected function checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, array $additionalData = null)
3227  {
3228  $newValue = '';
3229  $foreignTable = $tcaFieldConf['foreign_table'];
3230  $transOrigPointer = 0;
3231  $keepTranslation = false;
3232  $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
3233  // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3235  $dbAnalysis = $this->createRelationHandlerInstance();
3236  $dbAnalysis->start(implode(',', $valueArray), $foreignTable, '', 0, $table, $tcaFieldConf);
3237  // If the localizationMode is set to 'keep', the children for the localized parent are kept as in the original untranslated record:
3238  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $tcaFieldConf);
3239  if ($localizationMode === 'keep' && $status === 'update') {
3240  // Fetch the current record and determine the original record:
3241  $row = BackendUtility::getRecordWSOL($table, $id);
3242  if (is_array($row)) {
3243  $language = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
3244  $transOrigPointer = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
3245  // If language is set (e.g. 1) and also transOrigPointer (e.g. 123), use transOrigPointer as uid:
3246  if ($language > 0 && $transOrigPointer) {
3247  $id = $transOrigPointer;
3248  // If we're in active localizationMode 'keep', prevent from writing data to the field of the parent record:
3249  // (on removing the localized parent, the original (untranslated) children would then also be removed)
3250  $keepTranslation = true;
3251  }
3252  }
3253  }
3254  // IRRE with a pointer field (database normalization):
3255  if ($tcaFieldConf['foreign_field']) {
3256  // if the record was imported, sorting was also imported, so skip this
3257  $skipSorting = (bool)$this->callFromImpExp;
3258  // update record in intermediate table (sorting & pointer uid to parent record)
3259  $dbAnalysis->writeForeignField($tcaFieldConf, $id, 0, $skipSorting);
3260  $newValue = $keepTranslation ? 0 : $dbAnalysis->countItems(false);
3261  } else {
3262  if ($this->getInlineFieldType($tcaFieldConf) === 'mm') {
3263  // In order to fully support all the MM stuff, directly call checkValue_group_select_processDBdata instead of repeating the needed code here
3264  $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
3265  $newValue = $keepTranslation ? 0 : $valueArray[0];
3266  } else {
3267  $valueArray = $dbAnalysis->getValueArray();
3268  // Checking that the number of items is correct:
3269  $valueArray = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
3270  $valueData = $this->castReferenceValue(implode(',', $valueArray), $tcaFieldConf);
3271  // If a valid translation of the 'keep' mode is active, update relations in the original(!) record:
3272  if ($keepTranslation) {
3273  $this->updateDB($table, $transOrigPointer, [$field => $valueData]);
3274  } else {
3275  $newValue = $valueData;
3276  }
3277  }
3278  }
3279  return $newValue;
3280  }
3281 
3282  /*********************************************
3283  *
3284  * PROCESSING COMMANDS
3285  *
3286  ********************************************/
3293  public function process_cmdmap()
3294  {
3295  // Editing frozen:
3296  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
3297  if ($this->enableLogging) {
3298  $this->newlog('All editing in this workspace has been frozen!', 1);
3299  }
3300  return false;
3301  }
3302  // Hook initialization:
3303  $hookObjectsArr = [];
3304  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'])) {
3305  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] as $classRef) {
3306  $hookObj = GeneralUtility::getUserObj($classRef);
3307  if (method_exists($hookObj, 'processCmdmap_beforeStart')) {
3308  $hookObj->processCmdmap_beforeStart($this);
3309  }
3310  $hookObjectsArr[] = $hookObj;
3311  }
3312  }
3313  $pasteDatamap = [];
3314  // Traverse command map:
3315  foreach ($this->cmdmap as $table => $_) {
3316  // Check if the table may be modified!
3317  $modifyAccessList = $this->checkModifyAccessList($table);
3318  if ($this->enableLogging && !$modifyAccessList) {
3319  $this->log($table, 0, 2, 0, 1, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
3320  }
3321  // Check basic permissions and circumstances:
3322  if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->cmdmap[$table]) || !$modifyAccessList) {
3323  continue;
3324  }
3325 
3326  // Traverse the command map:
3327  foreach ($this->cmdmap[$table] as $id => $incomingCmdArray) {
3328  if (!is_array($incomingCmdArray)) {
3329  continue;
3330  }
3331 
3332  if ($table === 'pages') {
3333  // for commands on pages do a pagetree-refresh
3334  $this->pagetreeNeedsRefresh = true;
3335  }
3336 
3337  foreach ($incomingCmdArray as $command => $value) {
3338  $pasteUpdate = false;
3339  if (is_array($value) && isset($value['action']) && $value['action'] === 'paste') {
3340  // Extended paste command: $command is set to "move" or "copy"
3341  // $value['update'] holds field/value pairs which should be updated after copy/move operation
3342  // $value['target'] holds original $value (target of move/copy)
3343  $pasteUpdate = $value['update'];
3344  $value = $value['target'];
3345  }
3346  foreach ($hookObjectsArr as $hookObj) {
3347  if (method_exists($hookObj, 'processCmdmap_preProcess')) {
3348  $hookObj->processCmdmap_preProcess($command, $table, $id, $value, $this, $pasteUpdate);
3349  }
3350  }
3351  // Init copyMapping array:
3352  // Must clear this array before call from here to those functions:
3353  // Contains mapping information between new and old id numbers.
3354  $this->copyMappingArray = [];
3355  // process the command
3356  $commandIsProcessed = false;
3357  foreach ($hookObjectsArr as $hookObj) {
3358  if (method_exists($hookObj, 'processCmdmap')) {
3359  $hookObj->processCmdmap($command, $table, $id, $value, $commandIsProcessed, $this, $pasteUpdate);
3360  }
3361  }
3362  // Only execute default commands if a hook hasn't been processed the command already
3363  if (!$commandIsProcessed) {
3364  $procId = $id;
3365  $backupUseTransOrigPointerField = $this->useTransOrigPointerField;
3366  // Branch, based on command
3367  switch ($command) {
3368  case 'move':
3369  $this->moveRecord($table, $id, $value);
3370  break;
3371  case 'copy':
3372  if ($table === 'pages') {
3373  $this->copyPages($id, $value);
3374  } else {
3375  $this->copyRecord($table, $id, $value, true);
3376  }
3377  $procId = $this->copyMappingArray[$table][$id];
3378  break;
3379  case 'localize':
3380  $this->useTransOrigPointerField = true;
3381  $this->localize($table, $id, $value);
3382  break;
3383  case 'copyToLanguage':
3384  $this->useTransOrigPointerField = false;
3385  $this->localize($table, $id, $value);
3386  break;
3387  case 'inlineLocalizeSynchronize':
3388  $this->inlineLocalizeSynchronize($table, $id, $value);
3389  break;
3390  case 'delete':
3391  $this->deleteAction($table, $id);
3392  break;
3393  case 'undelete':
3394  $this->undeleteRecord($table, $id);
3395  break;
3396  }
3397  $this->useTransOrigPointerField = $backupUseTransOrigPointerField;
3398  if (is_array($pasteUpdate)) {
3399  $pasteDatamap[$table][$procId] = $pasteUpdate;
3400  }
3401  }
3402  foreach ($hookObjectsArr as $hookObj) {
3403  if (method_exists($hookObj, 'processCmdmap_postProcess')) {
3404  $hookObj->processCmdmap_postProcess($command, $table, $id, $value, $this, $pasteUpdate, $pasteDatamap);
3405  }
3406  }
3407  // Merging the copy-array info together for remapping purposes.
3408  ArrayUtility::mergeRecursiveWithOverrule($this->copyMappingArray_merged, $this->copyMappingArray);
3409  }
3410  }
3411  }
3413  $copyTCE = $this->getLocalTCE();
3414  $copyTCE->start($pasteDatamap, '', $this->BE_USER);
3415  $copyTCE->process_datamap();
3416  $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3417  unset($copyTCE);
3418 
3419  // Finally, before exit, check if there are ID references to remap.
3420  // This might be the case if versioning or copying has taken place!
3421  $this->remapListedDBRecords();
3422  $this->processRemapStack();
3423  foreach ($hookObjectsArr as $hookObj) {
3424  if (method_exists($hookObj, 'processCmdmap_afterFinish')) {
3425  $hookObj->processCmdmap_afterFinish($this);
3426  }
3427  }
3428  if ($this->isOuterMostInstance()) {
3429  $this->processClearCacheQueue();
3430  $this->resetNestedElementCalls();
3431  }
3432  }
3433 
3434  /*********************************************
3435  *
3436  * Cmd: Copying
3437  *
3438  ********************************************/
3452  public function copyRecord($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '', $language = 0, $ignoreLocalization = false)
3453  {
3454  $uid = ($origUid = (int)$uid);
3455  // Only copy if the table is defined in $GLOBALS['TCA'], a uid is given and the record wasn't copied before:
3456  if (empty($GLOBALS['TCA'][$table]) || $uid === 0) {
3457  return null;
3458  }
3459  if ($this->isRecordCopied($table, $uid)) {
3460  return null;
3461  }
3462 
3463  // Fetch record with permission check
3464  $row = $this->recordInfoWithPermissionCheck($table, $uid, 'show');
3465 
3466  // This checks if the record can be selected which is all that a copy action requires.
3467  if ($row === false) {
3468  if ($this->enableLogging) {
3469  $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record "%s:%s" which does not exist or you do not have permission to read', -1, [$table, $uid]);
3470  }
3471  return null;
3472  }
3473 
3474  // Check if table is allowed on destination page
3475  if ($destPid >= 0 && !$this->isTableAllowedForThisPage($destPid, $table)) {
3476  if ($this->enableLogging) {
3477  $this->log($table, $uid, 1, 0, 1, 'Attempt to insert record "%s:%s" on a page (%s) that can\'t store record type.', -1, [$table, $uid, $destPid]);
3478  }
3479  return null;
3480  }
3481 
3482  $fullLanguageCheckNeeded = $table !== 'pages';
3483  //Used to check language and general editing rights
3484  if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded)) {
3485  if ($this->enableLogging) {
3486  $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record "%s:%s" without having permissions to do so. [' . $this->BE_USER->errorMsg . '].', -1, [$table, $uid]);
3487  }
3488  return null;
3489  }
3490 
3491  $data = [];
3492  $nonFields = array_unique(GeneralUtility::trimExplode(',', 'uid,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,t3ver_oid,t3ver_wsid,t3ver_id,t3ver_label,t3ver_state,t3ver_count,t3ver_stage,t3ver_tstamp,' . $excludeFields, true));
3493  BackendUtility::workspaceOL($table, $row, -99, false);
3495 
3496  // Initializing:
3497  $theNewID = StringUtility::getUniqueId('NEW');
3498  $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3499  $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3500  // Getting default data:
3501  $defaultData = $this->newFieldArray($table);
3502  // Getting "copy-after" fields if applicable:
3503  $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, abs($destPid), 0) : [];
3504  // Page TSconfig related:
3505  // 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...
3506  $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
3507  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
3508  $tE = $this->getTableEntries($table, $TSConfig);
3509  // Traverse ALL fields of the selected record:
3510  $setDefaultOnCopyArray = array_flip(GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['setToDefaultOnCopy']));
3511  foreach ($row as $field => $value) {
3512  if (!in_array($field, $nonFields, true)) {
3513  // Get TCA configuration for the field:
3514  $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3515  // Preparation/Processing of the value:
3516  // "pid" is hardcoded of course:
3517  // isset() won't work here, since values can be NULL in each of the arrays
3518  // except setDefaultOnCopyArray, since we exploded that from a string
3519  if ($field === 'pid') {
3520  $value = $destPid;
3521  } elseif (array_key_exists($field, $overrideValues)) {
3522  // Override value...
3523  $value = $overrideValues[$field];
3524  } elseif (array_key_exists($field, $copyAfterFields)) {
3525  // Copy-after value if available:
3526  $value = $copyAfterFields[$field];
3527  } elseif ($GLOBALS['TCA'][$table]['ctrl']['setToDefaultOnCopy'] && isset($setDefaultOnCopyArray[$field])) {
3528  $value = $defaultData[$field];
3529  } else {
3530  // Hide at copy may override:
3531  if ($first && $field == $enableField && $GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] && !$this->neverHideAtCopy && !$tE['disableHideAtCopy']) {
3532  $value = 1;
3533  }
3534  // Prepend label on copy:
3535  if ($first && $field == $headerField && $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy'] && !$tE['disablePrependAtCopy']) {
3536  $value = $this->getCopyHeader($table, $this->resolvePid($table, $destPid), $field, $this->clearPrefixFromValue($table, $value), 0);
3537  }
3538  // Processing based on the TCA config field type (files, references, flexforms...)
3539  $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $tscPID, $language);
3540  }
3541  // Add value to array.
3542  $data[$table][$theNewID][$field] = $value;
3543  }
3544  }
3545  // Overriding values:
3546  if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
3547  $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3548  }
3549  // Setting original UID:
3550  if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3551  $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3552  }
3553  // Do the copy by simply submitting the array through DataHandler:
3555  $copyTCE = $this->getLocalTCE();
3556  $copyTCE->start($data, '', $this->BE_USER);
3557  $copyTCE->process_datamap();
3558  // Getting the new UID:
3559  $theNewSQLID = $copyTCE->substNEWwithIDs[$theNewID];
3560  if ($theNewSQLID) {
3561  $this->copyRecord_fixRTEmagicImages($table, BackendUtility::wsMapId($table, $theNewSQLID));
3562  $this->copyMappingArray[$table][$origUid] = $theNewSQLID;
3563  // Keep automatically versionized record information:
3564  if (isset($copyTCE->autoVersionIdMap[$table][$theNewSQLID])) {
3565  $this->autoVersionIdMap[$table][$theNewSQLID] = $copyTCE->autoVersionIdMap[$table][$theNewSQLID];
3566  }
3567  }
3568  // Copy back the cached TSconfig
3569  $this->cachedTSconfig = $copyTCE->cachedTSconfig;
3570  $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3571  unset($copyTCE);
3572  if (!$ignoreLocalization && $language == 0) {
3573  //repointing the new translation records to the parent record we just created
3574  $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
3575  $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = 0;
3576  $this->copyL10nOverlayRecords($table, $uid, $destPid, $first, $overrideValues, $excludeFields);
3577  }
3578 
3579  return $theNewSQLID;
3580  }
3581 
3589  public function copyPages($uid, $destPid)
3590  {
3591  // Initialize:
3592  $uid = (int)$uid;
3593  $destPid = (int)$destPid;
3594  // Finding list of tables to copy.
3595  // These are the tables, the user may modify
3596  $copyTablesArray = $this->admin ? $this->compileAdminTables() : explode(',', $this->BE_USER->groupData['tables_modify']);
3597  // If not all tables are allowed then make a list of allowed tables: That is the tables that figure in both allowed tables AND the copyTable-list
3598  if (!strstr($this->copyWhichTables, '*')) {
3599  $copyWhichTablesArray = array_flip(GeneralUtility::trimExplode(',', $this->copyWhichTables . ',pages'));
3600  foreach ($copyTablesArray as $k => $table) {
3601  // Pages are always going...
3602  if (!$table || !isset($copyWhichTablesArray[$table])) {
3603  unset($copyTablesArray[$k]);
3604  }
3605  }
3606  }
3607  $copyTablesArray = array_unique($copyTablesArray);
3608  // Begin to copy pages if we're allowed to:
3609  if ($this->admin || in_array('pages', $copyTablesArray, true)) {
3610  // Copy this page we're on. And set first-flag (this will trigger that the record is hidden if that is configured)!
3611  $theNewRootID = $this->copySpecificPage($uid, $destPid, $copyTablesArray, 1);
3612  // If we're going to copy recursively...:
3613  if ($theNewRootID && $this->copyTree) {
3614  // Get ALL subpages to copy (read-permissions are respected!):
3615  $CPtable = $this->int_pageTreeInfo([], $uid, (int)$this->copyTree, $theNewRootID);
3616  // Now copying the subpages:
3617  foreach ($CPtable as $thePageUid => $thePagePid) {
3618  $newPid = $this->copyMappingArray['pages'][$thePagePid];
3619  if (isset($newPid)) {
3620  $this->copySpecificPage($thePageUid, $newPid, $copyTablesArray);
3621  } else {
3622  if ($this->enableLogging) {
3623  $this->log('pages', $uid, 5, 0, 1, 'Something went wrong during copying branch');
3624  }
3625  break;
3626  }
3627  }
3628  }
3629  } elseif ($this->enableLogging) {
3630  $this->log('pages', $uid, 5, 0, 1, 'Attempt to copy page without permission to this table');
3631  }
3632  }
3633 
3643  public function copySpecificPage($uid, $destPid, $copyTablesArray, $first = false)
3644  {
3645  // Copy the page itself:
3646  $theNewRootID = $this->copyRecord('pages', $uid, $destPid, $first);
3647  // If a new page was created upon the copy operation we will proceed with all the tables ON that page:
3648  if ($theNewRootID) {
3649  foreach ($copyTablesArray as $table) {
3650  // All records under the page is copied.
3651  if ($table && is_array($GLOBALS['TCA'][$table]) && $table !== 'pages') {
3652  $fields = ['uid'];
3653  $languageField = null;
3654  $transOrigPointerField = null;
3655  $translationSourceField = null;
3656  if (BackendUtility::isTableLocalizable($table)) {
3657  $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
3658  $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3659  $fields[] = $languageField;
3660  $fields[] = $transOrigPointerField;
3661  if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3662  $translationSourceField = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
3663  $fields[] = $translationSourceField;
3664  }
3665  }
3666  $isTableWorkspaceEnabled = BackendUtility::isTableWorkspaceEnabled($table);
3667  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3668  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
3669  $queryBuilder
3670  ->select(...$fields)
3671  ->from($table)
3672  ->where(
3673  $queryBuilder->expr()->eq(
3674  'pid',
3675  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3676  )
3677  );
3678  if ($isTableWorkspaceEnabled && (int)$this->BE_USER->workspace === 0) {
3679  // Table is workspace enabled, user is in default ws -> add t3ver_wsid=0 restriction
3680  $queryBuilder->andWhere(
3681  $queryBuilder->expr()->eq(
3682  't3ver_wsid',
3683  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3684  )
3685  );
3686  } elseif ($isTableWorkspaceEnabled) {
3687  // Table is workspace enabled, user has a ws selected -> select wsid=0 and selected wsid rows
3688  $queryBuilder->andWhere($queryBuilder->expr()->in(
3689  't3ver_wsid',
3690  $queryBuilder->createNamedParameter(
3691  [0, $this->BE_USER->workspace],
3692  Connection::PARAM_INT_ARRAY
3693  )
3694  ));
3695  }
3696  if (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby'])) {
3697  $queryBuilder->orderBy($GLOBALS['TCA'][$table]['ctrl']['sortby'], 'DESC');
3698  }
3699  $queryBuilder->addOrderBy('uid');
3700  try {
3701  $result = $queryBuilder->execute();
3702  $rows = [];
3703  while ($row = $result->fetch()) {
3704  $rows[$row['uid']] = $row;
3705  }
3706  // Resolve placeholders of workspace versions
3707  if (!empty($rows) && (int)$this->BE_USER->workspace !== 0 && $isTableWorkspaceEnabled) {
3708  $rows = array_reverse(
3709  $this->resolveVersionedRecords(
3710  $table,
3711  implode(',', $fields),
3712  $GLOBALS['TCA'][$table]['ctrl']['sortby'],
3713  array_keys($rows)
3714  ),
3715  true
3716  );
3717  }
3718  if (is_array($rows)) {
3719  $languageSourceMap = [];
3720  $overrideValues = $translationSourceField ? [$translationSourceField => 0] : [];
3721  $doRemap = false;
3722  foreach ($rows as $row) {
3723  // Skip localized records that will be processed in
3724  // copyL10nOverlayRecords() on copying the default language record
3725  $transOrigPointer = $row[$transOrigPointerField];
3726  if ($row[$languageField] > 0 && $transOrigPointer > 0 && isset($rows[$transOrigPointer])) {
3727  continue;
3728  }
3729  // Copying each of the underlying records...
3730  $newUid = $this->copyRecord($table, $row['uid'], $theNewRootID, false, $overrideValues);
3731  if ($translationSourceField) {
3732  $languageSourceMap[$row['uid']] = $newUid;
3733  if ($row[$languageField] > 0) {
3734  $doRemap = true;
3735  }
3736  }
3737  }
3738  if ($doRemap) {
3739  //remap is needed for records in non-default language records in the "free mode"
3740  $this->copy_remapTranslationSourceField($table, $rows, $languageSourceMap);
3741  }
3742  }
3743  } catch (DBALException $e) {
3744  if ($this->enableLogging) {
3745  $databaseErrorMessage = $e->getPrevious()->getMessage();
3746  $this->log($table, $uid, 5, 0, 1, 'An SQL error occurred: ' . $databaseErrorMessage);
3747  }
3748  }
3749  }
3750  }
3751  $this->processRemapStack();
3752  return $theNewRootID;
3753  }
3754  return null;
3755  }
3756 
3772  public function copyRecord_raw($table, $uid, $pid, $overrideArray = [], array $workspaceOptions = [])
3773  {
3774  $uid = (int)$uid;
3775  // Stop any actions if the record is marked to be deleted:
3776  // (this can occur if IRRE elements are versionized and child elements are removed)
3777  if ($this->isElementToBeDeleted($table, $uid)) {
3778  return null;
3779  }
3780  // Only copy if the table is defined in TCA, a uid is given and the record wasn't copied before:
3781  if (!$GLOBALS['TCA'][$table] || !$uid || $this->isRecordCopied($table, $uid)) {
3782  return null;
3783  }
3784 
3785  // Fetch record with permission check
3786  $row = $this->recordInfoWithPermissionCheck($table, $uid, 'show');
3787 
3788  // This checks if the record can be selected which is all that a copy action requires.
3789  if ($row === false) {
3790  if ($this->enableLogging) {
3791  $this->log(
3792  $table,
3793  $uid,
3794  3,
3795  0,
3796  1,
3797  'Attempt to rawcopy/versionize record which either does not exist or you don\'t have permission to read'
3798  );
3799  }
3800  return null;
3801  }
3802 
3803  // Set up fields which should not be processed. They are still written - just passed through no-questions-asked!
3804  $nonFields = ['uid', 'pid', 't3ver_id', 't3ver_oid', 't3ver_wsid', 't3ver_label', 't3ver_state', 't3ver_count', 't3ver_stage', 't3ver_tstamp', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody'];
3805 
3806  // Merge in override array.
3807  $row = array_merge($row, $overrideArray);
3808  // Traverse ALL fields of the selected record:
3809  foreach ($row as $field => $value) {
3810  if (!in_array($field, $nonFields, true)) {
3811  // Get TCA configuration for the field:
3812  $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3813  if (is_array($conf)) {
3814  // Processing based on the TCA config field type (files, references, flexforms...)
3815  $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3816  }
3817  // Add value to array.
3818  $row[$field] = $value;
3819  }
3820  }
3821  // Force versioning related fields:
3822  $row['pid'] = $pid;
3823  // Setting original UID:
3824  if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3825  $row[$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3826  }
3827  // Do the copy by internal function
3828  $theNewSQLID = $this->insertNewCopyVersion($table, $row, $pid);
3829  if ($theNewSQLID) {
3830  $this->dbAnalysisStoreExec();
3831  $this->dbAnalysisStore = [];
3832  $this->copyRecord_fixRTEmagicImages($table, BackendUtility::wsMapId($table, $theNewSQLID));
3833  return $this->copyMappingArray[$table][$uid] = $theNewSQLID;
3834  }
3835  return null;
3836  }
3837 
3847  public function insertNewCopyVersion($table, $fieldArray, $realPid)
3848  {
3849  $id = StringUtility::getUniqueId('NEW');
3850  // $fieldArray is set as current record.
3851  // 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...
3852  $this->checkValue_currentRecord = $fieldArray;
3853  // Makes sure that transformations aren't processed on the copy.
3854  $backupDontProcessTransformations = $this->dontProcessTransformations;
3855  $this->dontProcessTransformations = true;
3856  // Traverse record and input-process each value:
3857  foreach ($fieldArray as $field => $fieldValue) {
3858  if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
3859  // Evaluating the value.
3860  $res = $this->checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0);
3861  if (isset($res['value'])) {
3862  $fieldArray[$field] = $res['value'];
3863  }
3864  }
3865  }
3866  // System fields being set:
3867  if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3868  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
3869  }
3870  if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3871  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
3872  }
3873  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3874  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3875  }
3876  // Finally, insert record:
3877  $this->insertDB($table, $id, $fieldArray, true);
3878  // Resets dontProcessTransformations to the previous state.
3879  $this->dontProcessTransformations = $backupDontProcessTransformations;
3880  // Return new id:
3881  return $this->substNEWwithIDs[$id];
3882  }
3883 
3900  public function copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language = 0, array $workspaceOptions = [])
3901  {
3902  // Process references and files, currently that means only the files, prepending absolute paths (so the DataHandler engine will detect the file as new and one that should be made into a copy)
3903  $value = $this->copyRecord_procFilesRefs($conf, $uid, $value);
3904  $inlineSubType = $this->getInlineFieldType($conf);
3905  // Get the localization mode for the current (parent) record (keep|select):
3906  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $field);
3907  // Register if there are references to take care of or MM is used on an inline field (no change to value):
3908  if ($this->isReferenceField($conf) || $inlineSubType === 'mm') {
3909  $value = $this->copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language, $localizationMode, $inlineSubType);
3910  } elseif ($inlineSubType !== false) {
3911  $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions, $localizationMode, $inlineSubType);
3912  }
3913  // 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())
3914  if ($conf['type'] === 'flex') {
3915  // Get current value array:
3916  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
3917  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
3918  [ 'config' => $conf ],
3919  $table,
3920  $field,
3921  $row
3922  );
3923  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
3924  $currentValueArray = GeneralUtility::xml2array($value);
3925  // Traversing the XML structure, processing files:
3926  if (is_array($currentValueArray)) {
3927  $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $uid, $field, $realDestPid], 'copyRecord_flexFormCallBack', $workspaceOptions);
3928  // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
3929  $value = $currentValueArray;
3930  }
3931  }
3932  return $value;
3933  }
3934 
3948  protected function copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language, $localizationMode, $inlineSubType)
3949  {
3950  $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
3951  $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
3952  $mmTable = isset($conf['MM']) && $conf['MM'] ? $conf['MM'] : '';
3953  $localizeForeignTable = isset($conf['foreign_table']) && BackendUtility::isTableLocalizable($conf['foreign_table']);
3954  $localizeReferences = $localizeForeignTable && isset($conf['localizeReferencesAtParentLocalization']) && $conf['localizeReferencesAtParentLocalization'];
3955  $localizeChildren = $localizeForeignTable && isset($conf['behaviour']['localizeChildrenAtParentLocalization']) && $conf['behaviour']['localizeChildrenAtParentLocalization'];
3957  $dbAnalysis = $this->createRelationHandlerInstance();
3958  $dbAnalysis->start($value, $allowedTables, $mmTable, $uid, $table, $conf);
3959  // Localize referenced records of select fields:
3960  $localizingNonManyToManyFieldReferences = $localizeReferences && empty($mmTable);
3961  $isInlineFieldInSelectMode = $localizationMode === 'select' && $inlineSubType === 'mm';
3962  $purgeItems = false;
3963  if ($language > 0 && ($localizingNonManyToManyFieldReferences || $isInlineFieldInSelectMode)) {
3964  foreach ($dbAnalysis->itemArray as $index => $item) {
3965  // Since select fields can reference many records, check whether there's already a localization:
3966  $recordLocalization = BackendUtility::getRecordLocalization($item['table'], $item['id'], $language);
3967  if ($recordLocalization) {
3968  $dbAnalysis->itemArray[$index]['id'] = $recordLocalization[0]['uid'];
3969  } elseif ($this->isNestedElementCallRegistered($item['table'], $item['id'], 'localize-' . (string)$language) === false) {
3970  if ($localizingNonManyToManyFieldReferences || $localizeChildren) {
3971  $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], $language);
3972  } else {
3973  unset($dbAnalysis->itemArray[$index]);
3974  }
3975  }
3976  }
3977  $purgeItems = true;
3978  }
3979 
3980  if ($purgeItems || $mmTable) {
3981  $dbAnalysis->purgeItemArray();
3982  $value = implode(',', $dbAnalysis->getValueArray($prependName));
3983  }
3984  // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected
3985  if ($value) {
3986  $this->registerDBList[$table][$uid][$field] = $value;
3987  }
3988 
3989  return $value;
3990  }
3991 
4008  protected function copyRecord_processInline(
4009  $table,
4010  $uid,
4011  $field,
4012  $value,
4013  $row,
4014  $conf,
4015  $realDestPid,
4016  $language,
4017  array $workspaceOptions,
4018  $localizationMode,
4019  $inlineSubType
4020  ) {
4021  // Localization in mode 'keep', isn't a real localization, but keeps the children of the original parent record:
4022  if ($language > 0 && $localizationMode === 'keep') {
4023  $value = $inlineSubType === 'field' ? 0 : '';
4024  } else {
4025  // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
4027  $dbAnalysis = $this->createRelationHandlerInstance();
4028  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
4029  // Walk through the items, copy them and remember the new id:
4030  foreach ($dbAnalysis->itemArray as $k => $v) {
4031  $newId = null;
4032  // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
4033  if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
4034  // If children should be localized when the parent gets localized the first time, just do it:
4035  if ($localizationMode != false && isset($conf['behaviour']['localizeChildrenAtParentLocalization']) && $conf['behaviour']['localizeChildrenAtParentLocalization']) {
4036  $newId = $this->localize($v['table'], $v['id'], $language);
4037  }
4038  } else {
4039  if (!MathUtility::canBeInterpretedAsInteger($realDestPid)) {
4040  $newId = $this->copyRecord($v['table'], $v['id'], -$v['id']);
4041  // If the destination page id is a NEW string, keep it on the same page
4042  } elseif ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($v['table'])) {
4043  // A filled $workspaceOptions indicated that this call
4044  // has it's origin in previous versionizeRecord() processing
4045  if (!empty($workspaceOptions)) {
4046  // Versions use live default id, thus the "new"
4047  // id is the original live default child record
4048  $newId = $v['id'];
4049  $this->versionizeRecord(
4050  $v['table'],
4051  $v['id'],
4052  (isset($workspaceOptions['label']) ? $workspaceOptions['label'] : 'Auto-created for WS #' . $this->BE_USER->workspace),
4053  (isset($workspaceOptions['delete']) ? $workspaceOptions['delete'] : false)
4054  );
4055  // Otherwise just use plain copyRecord() to create placeholders etc.
4056  } else {
4057  // If a record has been copied already during this request,
4058  // prevent superfluous duplication and use the existing copy
4059  if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
4060  $newId = $this->copyMappingArray[$v['table']][$v['id']];
4061  } else {
4062  $newId = $this->copyRecord($v['table'], $v['id'], $realDestPid);
4063  }
4064  }
4065  } else {
4066  // If a record has been copied already during this request,
4067  // prevent superfluous duplication and use the existing copy
4068  if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
4069  $newId = $this->copyMappingArray[$v['table']][$v['id']];
4070  } else {
4071  $newId = $this->copyRecord_raw($v['table'], $v['id'], $realDestPid, [], $workspaceOptions);
4072  }
4073  }
4074  }
4075  // If the current field is set on a page record, update the pid of related child records:
4076  if ($table === 'pages') {
4077  $this->registerDBPids[$v['table']][$v['id']] = $uid;
4078  } elseif (isset($this->registerDBPids[$table][$uid])) {
4079  $this->registerDBPids[$v['table']][$v['id']] = $this->registerDBPids[$table][$uid];
4080  }
4081  $dbAnalysis->itemArray[$k]['id'] = $newId;
4082  }
4083  // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4084  $value = implode(',', $dbAnalysis->getValueArray());
4085  $this->registerDBList[$table][$uid][$field] = $value;
4086  }
4087 
4088  return $value;
4089  }
4090 
4104  public function copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $_3, $workspaceOptions)
4105  {
4106  // Extract parameters:
4107  list($table, $uid, $field, $realDestPid) = $pParams;
4108  // Process references and files, currently that means only the files, prepending absolute paths:
4109  $dataValue = $this->copyRecord_procFilesRefs($dsConf, $uid, $dataValue);
4110  // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
4111  if (($this->isReferenceField($dsConf) || $this->getInlineFieldType($dsConf) !== false) && (string)$dataValue !== '') {
4112  $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, [], $dsConf, $realDestPid, 0, $workspaceOptions);
4113  $this->registerDBList[$table][$uid][$field] = 'FlexForm_reference';
4114  }
4115  // Return
4116  return ['value' => $dataValue];
4117  }
4118 
4130  public function copyRecord_procFilesRefs($conf, $uid, $value)
4131  {
4132  // Prepend absolute paths to files:
4133  if ($conf['type'] !== 'group' || ($conf['internal_type'] !== 'file' && $conf['internal_type'] !== 'file_reference')) {
4134  return $value;
4135  }
4136 
4137  // Get an array with files as values:
4138  if ($conf['MM']) {
4139  $theFileValues = [];
4141  $dbAnalysis = $this->createRelationHandlerInstance();
4142  $dbAnalysis->start('', 'files', $conf['MM'], $uid);
4143  foreach ($dbAnalysis->itemArray as $somekey => $someval) {
4144  if ($someval['id']) {
4145  $theFileValues[] = $someval['id'];
4146  }
4147  }
4148  } else {
4149  $theFileValues = GeneralUtility::trimExplode(',', $value, true);
4150  }
4151  // Traverse this array of files:
4152  $uploadFolder = $conf['internal_type'] === 'file' ? $conf['uploadfolder'] : '';
4153  $dest = PATH_site . $uploadFolder;
4154  $newValue = [];
4155  foreach ($theFileValues as $file) {
4156  if (trim($file)) {
4157  $realFile = str_replace('//', '/', $dest . '/' . trim($file));
4158  if (@is_file($realFile)) {
4159  $newValue[] = $realFile;
4160  }
4161  }
4162  }
4163  // Implode the new filelist into the new value (all files have absolute paths now which means they will get copied when entering DataHandler as new values...)
4164  $value = implode(',', $newValue);
4165 
4166  // Return the new value:
4167  return $value;
4168  }
4169 
4178  public function copyRecord_fixRTEmagicImages($table, $theNewSQLID)
4179  {
4180  // Creating fileFunc object.
4181  if (!$this->fileFunc) {
4182  $this->fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
4183  }
4184  // Select all RTEmagic files in the reference table from the table/ID
4185  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
4186  $queryBuilder->getRestrictions()->removeAll();
4187  $rteFileRecords = $queryBuilder
4188  ->select('*')
4189  ->from('sys_refindex')
4190  ->where(
4191  $queryBuilder->expr()->eq(
4192  'ref_table',
4193  $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)
4194  ),
4195  $queryBuilder->expr()->like(
4196  'ref_string',
4197  $queryBuilder->createNamedParameter('%/RTEmagic%', \PDO::PARAM_STR)
4198  ),
4199  $queryBuilder->expr()->eq(
4200  'softref_key',
4201  $queryBuilder->createNamedParameter('images', \PDO::PARAM_STR)
4202  ),
4203  $queryBuilder->expr()->eq(
4204  'tablename',
4205  $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
4206  ),
4207  $queryBuilder->expr()->eq(
4208  'recuid',
4209  $queryBuilder->createNamedParameter($theNewSQLID, \PDO::PARAM_INT)
4210  )
4211  )
4212  ->orderBy('sorting', 'DESC')
4213  ->execute()
4214  ->fetchAll();
4215  // Traverse the files found and copy them:
4216  if (!is_array($rteFileRecords)) {
4217  return;
4218  }
4219  foreach ($rteFileRecords as $rteFileRecord) {
4220  $filename = basename($rteFileRecord['ref_string']);
4221  if (!GeneralUtility::isFirstPartOfStr($filename, 'RTEmagicC_')) {
4222  continue;
4223  }
4224  $fileInfo = [];
4225  $fileInfo['exists'] = @is_file((PATH_site . $rteFileRecord['ref_string']));
4226  $fileInfo['original'] = mb_substr($rteFileRecord['ref_string'], 0, -mb_strlen($filename)) . 'RTEmagicP_' . preg_replace('/\\.[[:alnum:]]+$/', '', mb_substr($filename, 10));
4227  $fileInfo['original_exists'] = @is_file((PATH_site . $fileInfo['original']));
4228  // CODE from tx_impexp and class.rte_images.php adapted for use here:
4229  if (!$fileInfo['exists'] || !$fileInfo['original_exists']) {
4230  if ($this->enableLogging) {
4231  $this->newlog('Trying to copy RTEmagic files (' . $rteFileRecord['ref_string'] . ' / ' . $fileInfo['original'] . ') but one or both were missing', 1);
4232  }
4233  continue;
4234  }
4235  // Initialize; Get directory prefix for file and set the original name:
4236  $dirPrefix = dirname($rteFileRecord['ref_string']) . '/';
4237  $rteOrigName = basename($fileInfo['original']);
4238  // If filename looks like an RTE file, and the directory is in "uploads/", then process as a RTE file!
4239  if ($rteOrigName && GeneralUtility::isFirstPartOfStr($dirPrefix, 'uploads/') && @is_dir(PATH_site . $dirPrefix)) {
4240  // RTE:
4241  // From the "original" RTE filename, produce a new "original" destination filename which is unused.
4242  $origDestName = $this->fileFunc->getUniqueName($rteOrigName, PATH_site . $dirPrefix);
4243  // Create copy file name:
4244  $pI = pathinfo($rteFileRecord['ref_string']);
4245  $copyDestName = dirname($origDestName) . '/RTEmagicC_' . mb_substr(basename($origDestName), 10) . '.' . $pI['extension'];
4246  if (!@is_file($copyDestName) && !@is_file($origDestName) && $origDestName === GeneralUtility::getFileAbsFileName($origDestName) && $copyDestName === GeneralUtility::getFileAbsFileName($copyDestName)) {
4247  // Making copies:
4248  GeneralUtility::upload_copy_move(PATH_site . $fileInfo['original'], $origDestName);
4249  GeneralUtility::upload_copy_move(PATH_site . $rteFileRecord['ref_string'], $copyDestName);
4250  clearstatcache();
4251  // Register this:
4252  $this->RTEmagic_copyIndex[$rteFileRecord['tablename']][$rteFileRecord['recuid']][$rteFileRecord['field']][$rteFileRecord['ref_string']] = PathUtility::stripPathSitePrefix($copyDestName);
4253  // Check and update the record using \TYPO3\CMS\Core\Database\ReferenceIndex
4254  if (@is_file($copyDestName)) {
4256  $sysRefObj = GeneralUtility::makeInstance(ReferenceIndex::class);
4257  $error = $sysRefObj->setReferenceValue($rteFileRecord['hash'], PathUtility::stripPathSitePrefix($copyDestName), false, true);
4258  if ($this->enableLogging && $error) {
4259  echo $this->newlog(ReferenceIndex::class . '::setReferenceValue(): ' . $error, 1);
4260  }
4261  } elseif ($this->enableLogging) {
4262  $this->newlog('File "' . $copyDestName . '" was not created!', 1);
4263  }
4264  } elseif ($this->enableLogging) {
4265  $this->newlog('Could not construct new unique names for file!', 1);
4266  }
4267  } elseif ($this->enableLogging) {
4268  $this->newlog('Maybe directory of file was not within "uploads/"?', 1);
4269  }
4270  }
4271  }
4272 
4283  public function copyL10nOverlayRecords($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '')
4284  {
4285  // There's no need to perform this for page-records or for tables that are not localizable
4286  if (!BackendUtility::isTableLocalizable($table) || $table === 'pages' || $table === 'pages_language_overlay') {
4287  return;
4288  }
4289 
4290  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4291  $queryBuilder->getRestrictions()
4292  ->removeAll()
4293  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4294  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
4295 
4296  $queryBuilder->select('*')
4297  ->from($table)
4298  ->where(
4299  $queryBuilder->expr()->eq(
4300  $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
4301  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
4302  )
4303  );
4304 
4305  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
4306  $queryBuilder->andWhere(
4307  $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
4308  );
4309  }
4310  // If $destPid is < 0, get the pid of the record with uid equal to abs($destPid)
4311  $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
4312  // Get the localized records to be copied
4313  $l10nRecords = $queryBuilder->execute()->fetchAll();
4314  if (is_array($l10nRecords)) {
4315  $localizedDestPids = [];
4316  // If $destPid < 0, then it is the uid of the original language record we are inserting after
4317  if ($destPid < 0) {
4318  // Get the localized records of the record we are inserting after
4319  $queryBuilder->setParameter('pointer', abs($destPid), \PDO::PARAM_INT);
4320  $destL10nRecords = $queryBuilder->execute()->fetchAll();
4321  // Index the localized record uids by language
4322  if (is_array($destL10nRecords)) {
4323  foreach ($destL10nRecords as $record) {
4324  $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4325  }
4326  }
4327  }
4328  $languageSourceMap = [
4329  $uid => $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
4330  ];
4331  // Copy the localized records after the corresponding localizations of the destination record
4332  foreach ($l10nRecords as $record) {
4333  $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4334  if ($localizedDestPid < 0) {
4335  $newUid = $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4336  } else {
4337  $newUid = $this->copyRecord($table, $record['uid'], $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4338  }
4339  $languageSourceMap[$record['uid']] = $newUid;
4340  }
4341  $this->copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap);
4342  }
4343  }
4344 
4352  protected function copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap)
4353  {
4354  if (empty($GLOBALS['TCA'][$table]['ctrl']['translationSource']) || empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
4355  return;
4356  }
4357  $translationSourceFieldName = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
4358  $translationParentFieldName = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
4359 
4360  //We can avoid running these update queries by sorting the $l10nRecords by languageSource dependency (in copyL10nOverlayRecords)
4361  //and first copy records depending on default record (and map the field).
4362  foreach ($l10nRecords as $record) {
4363  $oldSourceUid = $record[$translationSourceFieldName];
4364  if ($oldSourceUid <= 0 && $record[$translationParentFieldName] > 0) {
4365  //BC fix - in connected mode 'translationSource' field should not be 0
4366  $oldSourceUid = $record[$translationParentFieldName];
4367  }
4368  if ($oldSourceUid > 0) {
4369  if (empty($languageSourceMap[$oldSourceUid])) {
4370  // we don't have mapping information available e.g when copyRecord returned null
4371  continue;
4372  }
4373  $newFieldValue = $languageSourceMap[$oldSourceUid];
4374  $updateFields = [
4375  $translationSourceFieldName => $newFieldValue
4376  ];
4377  GeneralUtility::makeInstance(ConnectionPool::class)
4378  ->getConnectionForTable($table)
4379  ->update($table, $updateFields, ['uid' => (int)$languageSourceMap[$record['uid']]]);
4380  if ($this->BE_USER->workspace > 0) {
4381  GeneralUtility::makeInstance(ConnectionPool::class)
4382  ->getConnectionForTable($table)
4383  ->update($table, $updateFields, ['t3ver_oid' => (int)$languageSourceMap[$record['uid']], 't3ver_wsid' => $this->BE_USER->workspace]);
4384  }
4385  }
4386  }
4387  }
4388 
4389  /*********************************************
4390  *
4391  * Cmd: Moving, Localizing
4392  *
4393  ********************************************/
4401  public function moveRecord($table, $uid, $destPid)
4402  {
4403  if (!$GLOBALS['TCA'][$table]) {
4404  return;
4405  }
4406 
4407  // In case the record to be moved turns out to be an offline version,
4408  // we have to find the live version and work on that one (this case
4409  // happens for pages with "branch" versioning type)
4410  // @deprecated note: as "branch" versioning is deprecated since TYPO3 4.2, this
4411  // functionality will be removed in TYPO3 4.7 (note by benni: a hook could replace this)
4412  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $uid, 'uid')) {
4413  $uid = $lookForLiveVersion['uid'];
4414  }
4415  // Initialize:
4416  $destPid = (int)$destPid;
4417  // Get this before we change the pid (for logging)
4418  $propArr = $this->getRecordProperties($table, $uid);
4419  $moveRec = $this->getRecordProperties($table, $uid, true);
4420  // This is the actual pid of the moving to destination
4421  $resolvedPid = $this->resolvePid($table, $destPid);
4422  // 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.
4423  // If the record is a page, then there are two options: If the page is moved within itself,
4424  // (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.
4425  if ($table !== 'pages' || $resolvedPid == $moveRec['pid']) {
4426  // Edit rights for the record...
4427  $mayMoveAccess = $this->checkRecordUpdateAccess($table, $uid);
4428  } else {
4429  $mayMoveAccess = $this->doesRecordExist($table, $uid, 'delete');
4430  }
4431  // Finding out, if the record may be moved TO another place. Here we check insert-rights (non-pages = edit, pages = new),
4432  // unless the pages are moved on the same pid, then edit-rights are checked
4433  if ($table !== 'pages' || $resolvedPid != $moveRec['pid']) {
4434  // Insert rights for the record...
4435  $mayInsertAccess = $this->checkRecordInsertAccess($table, $resolvedPid, 4);
4436  } else {
4437  $mayInsertAccess = $this->checkRecordUpdateAccess($table, $uid);
4438  }
4439  // Checking if there is anything else disallowing moving the record by checking if editing is allowed
4440  $fullLanguageCheckNeeded = $table !== 'pages';
4441  $mayEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded);
4442  // If moving is allowed, begin the processing:
4443  if (!$mayEditAccess) {
4444  if ($this->enableLogging) {
4445  $this->log($table, $uid, 4, 0, 1, 'Attempt to move record "%s" (%s) without having permissions to do so. [' . $this->BE_USER->errorMsg . ']', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4446  }
4447  return;
4448  }
4449 
4450  if (!$mayMoveAccess) {
4451  if ($this->enableLogging) {
4452  $this->log($table, $uid, 4, 0, 1, 'Attempt to move record \'%s\' (%s) without having permissions to do so.', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4453  }
4454  return;
4455  }
4456 
4457  if (!$mayInsertAccess) {
4458  if ($this->enableLogging) {
4459  $this->log($table, $uid, 4, 0, 1, 'Attempt to move record \'%s\' (%s) without having permissions to insert.', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4460  }
4461  return;
4462  }
4463 
4464  $recordWasMoved = false;
4465  // Move the record via a hook, used e.g. for versioning
4466  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'])) {
4467  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] as $classRef) {
4468  $hookObj = GeneralUtility::getUserObj($classRef);
4469  if (method_exists($hookObj, 'moveRecord')) {
4470  $hookObj->moveRecord($table, $uid, $destPid, $propArr, $moveRec, $resolvedPid, $recordWasMoved, $this);
4471  }
4472  }
4473  }
4474  // Move the record if a hook hasn't moved it yet
4475  if (!$recordWasMoved) {
4476  $this->moveRecord_raw($table, $uid, $destPid);
4477  }
4478  }
4479 
4489  public function moveRecord_raw($table, $uid, $destPid)
4490  {
4491  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
4492  $origDestPid = $destPid;
4493  // This is the actual pid of the moving to destination
4494  $resolvedPid = $this->resolvePid($table, $destPid);
4495  // Checking if the pid is negative, but no sorting row is defined. In that case, find the correct pid. Basically this check make the error message 4-13 meaning less... But you can always remove this check if you prefer the error instead of a no-good action (which is to move the record to its own page...)
4496  // $destPid>=0 because we must correct pid in case of versioning "page" types.
4497  if ($destPid < 0 && !$sortRow || $destPid >= 0) {
4498  $destPid = $resolvedPid;
4499  }
4500  // Get this before we change the pid (for logging)
4501  $propArr = $this->getRecordProperties($table, $uid);
4502  $moveRec = $this->getRecordProperties($table, $uid, true);
4503  // Prepare user defined objects (if any) for hooks which extend this function:
4504  $hookObjectsArr = [];
4505  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'])) {
4506  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] as $classRef) {
4507  $hookObjectsArr[] = GeneralUtility::getUserObj($classRef);
4508  }
4509  }
4510  // Timestamp field:
4511  $updateFields = [];
4512  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4513  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4514  }
4515  // Insert as first element on page (where uid = $destPid)
4516  if ($destPid >= 0) {
4517  if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4518  // Clear cache before moving
4519  list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
4520  $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4521  // Setting PID
4522  $updateFields['pid'] = $destPid;
4523  // Table is sorted by 'sortby'
4524  if ($sortRow) {
4525  $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4526  $updateFields[$sortRow] = $sortNumber;
4527  }
4528  // Check for child records that have also to be moved
4529  $this->moveRecord_procFields($table, $uid, $destPid);
4530  // Create query for update:
4531  GeneralUtility::makeInstance(ConnectionPool::class)
4532  ->getConnectionForTable($table)
4533  ->update($table, $updateFields, ['uid' => (int)$uid]);
4534  // Check for the localizations of that element
4535  $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4536  // Call post processing hooks:
4537  foreach ($hookObjectsArr as $hookObj) {
4538  if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4539  $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4540  }
4541  }
4542  if ($this->enableLogging) {
4543  // Logging...
4544  $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4545  if ($destPid != $propArr['pid']) {
4546  // Logged to old page
4547  $newPropArr = $this->getRecordProperties($table, $uid);
4548  $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4549  $this->log($table, $uid, 4, $destPid, 0, 'Moved record \'%s\' (%s) to page \'%s\' (%s)', 2, [$propArr['header'], $table . ':' . $uid, $newpagePropArr['header'], $newPropArr['pid']], $propArr['pid']);
4550  // Logged to new page
4551  $this->log($table, $uid, 4, $destPid, 0, 'Moved record \'%s\' (%s) from page \'%s\' (%s)', 3, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4552  } else {
4553  // Logged to new page
4554  $this->log($table, $uid, 4, $destPid, 0, 'Moved record \'%s\' (%s) on page \'%s\' (%s)', 4, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4555  }
4556  }
4557  // Clear cache after moving
4558  $this->registerRecordIdForPageCacheClearing($table, $uid);
4559  $this->fixUniqueInPid($table, $uid);
4560  // fixCopyAfterDuplFields
4561  if ($origDestPid < 0) {
4562  $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
4563  }
4564  } elseif ($this->enableLogging) {
4565  $destPropArr = $this->getRecordProperties('pages', $destPid);
4566  $this->log($table, $uid, 4, 0, 1, 'Attempt to move page \'%s\' (%s) to inside of its own rootline (at page \'%s\' (%s))', 10, [$propArr['header'], $uid, $destPropArr['header'], $destPid], $propArr['pid']);
4567  }
4568  } else {
4569  // Put after another record
4570  // Table is being sorted
4571  if ($sortRow) {
4572  // Save the position to which the original record is requested to be moved
4573  $originalRecordDestinationPid = $destPid;
4574  $sortInfo = $this->getSortNumber($table, $uid, $destPid);
4575  // Setting the destPid to the new pid of the record.
4576  $destPid = $sortInfo['pid'];
4577  // If not an array, there was an error (which is already logged)
4578  if (is_array($sortInfo)) {
4579  if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4580  // clear cache before moving
4581  $this->registerRecordIdForPageCacheClearing($table, $uid);
4582  // We now update the pid and sortnumber
4583  $updateFields['pid'] = $destPid;
4584  $updateFields[$sortRow] = $sortInfo['sortNumber'];
4585  // Check for child records that have also to be moved
4586  $this->moveRecord_procFields($table, $uid, $destPid);
4587  // Create query for update:
4588  GeneralUtility::makeInstance(ConnectionPool::class)
4589  ->getConnectionForTable($table)
4590  ->update($table, $updateFields, ['uid' => (int)$uid]);
4591  // Check for the localizations of that element
4592  $this->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
4593  // Call post processing hooks:
4594  foreach ($hookObjectsArr as $hookObj) {
4595  if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4596  $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4597  }
4598  }
4599  if ($this->enableLogging) {
4600  // Logging...
4601  $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4602  if ($destPid != $propArr['pid']) {
4603  // Logged to old page
4604  $newPropArr = $this->getRecordProperties($table, $uid);
4605  $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4606  $this->log($table, $uid, 4, 0, 0, 'Moved record \'%s\' (%s) to page \'%s\' (%s)', 2, [$propArr['header'], $table . ':' . $uid, $newpagePropArr['header'], $newPropArr['pid']], $propArr['pid']);
4607  // Logged to old page
4608  $this->log($table, $uid, 4, 0, 0, 'Moved record \'%s\' (%s) from page \'%s\' (%s)', 3, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4609  } else {
4610  // Logged to old page
4611  $this->log($table, $uid, 4, 0, 0, 'Moved record \'%s\' (%s) on page \'%s\' (%s)', 4, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4612  }
4613  }
4614  // Clear cache after moving
4615  $this->registerRecordIdForPageCacheClearing($table, $uid);
4616  // fixUniqueInPid
4617  $this->fixUniqueInPid($table, $uid);
4618  // fixCopyAfterDuplFields
4619  if ($origDestPid < 0) {
4620  $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
4621  }
4622  } elseif ($this->enableLogging) {
4623  $destPropArr = $this->getRecordProperties('pages', $destPid);
4624  $this->log($table, $uid, 4, 0, 1, 'Attempt to move page \'%s\' (%s) to inside of its own rootline (at page \'%s\' (%s))', 10, [$propArr['header'], $uid, $destPropArr['header'], $destPid], $propArr['pid']);
4625  }
4626  }
4627  } elseif ($this->enableLogging) {
4628  $this->log($table, $uid, 4, 0, 1, 'Attempt to move record \'%s\' (%s) to after another record, although the table has no sorting row.', 13, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4629  }
4630  }
4631  }
4632 
4641  public function moveRecord_procFields($table, $uid, $destPid)
4642  {
4643  $conf = $GLOBALS['TCA'][$table]['columns'];
4644  $row = BackendUtility::getRecordWSOL($table, $uid);
4645  if (is_array($row) && (int)$destPid !== (int)$row['pid']) {
4646  foreach ($row as $field => $value) {
4647  $this->moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf[$field]['config']);
4648  }
4649  }
4650  }
4651 
4662  public function moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf)
4663  {
4664  if ($conf['type'] === 'inline') {
4665  $foreign_table = $conf['foreign_table'];
4666  $moveChildrenWithParent = !isset($conf['behaviour']['disableMovingChildrenWithParent']) || !$conf['behaviour']['disableMovingChildrenWithParent'];
4667  if ($foreign_table && $moveChildrenWithParent) {
4668  $inlineType = $this->getInlineFieldType($conf);
4669  if ($inlineType === 'list' || $inlineType === 'field') {
4670  if ($table === 'pages') {
4671  // If the inline elements are related to a page record,
4672  // make sure they reside at that page and not at its parent
4673  $destPid = $uid;
4674  }
4675  $dbAnalysis = $this->createRelationHandlerInstance();
4676  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
4677  }
4678  }
4679  }
4680  // Move the records
4681  if (isset($dbAnalysis)) {
4682  // Moving records to a positive destination will insert each
4683  // record at the beginning, thus the order is reversed here:
4684  foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4685  $this->moveRecord($v['table'], $v['id'], $destPid);
4686  }
4687  }
4688  }
4689 
4698  public function moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
4699  {
4700  // There's no need to perform this for page-records or not localizable tables
4701  if (!BackendUtility::isTableLocalizable($table) || $table === 'pages' || $table === 'pages_language_overlay') {
4702  return;
4703  }
4704 
4705  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4706  $queryBuilder->getRestrictions()
4707  ->removeAll()
4708  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4709  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
4710 
4711  $queryBuilder->select('*')
4712  ->from($table)
4713  ->where(
4714  $queryBuilder->expr()->eq(
4715  $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
4716  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
4717  )
4718  );
4719 
4720  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
4721  $queryBuilder->andWhere(
4722  $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
4723  );
4724  }
4725 
4726  $l10nRecords = $queryBuilder->execute()->fetchAll();
4727  if (is_array($l10nRecords)) {
4728  $localizedDestPids = [];
4729  // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4730  if ($originalRecordDestinationPid < 0) {
4731  // Get the localized records of the record we are inserting after
4732  $queryBuilder->setParameter('pointer', abs($originalRecordDestinationPid), \PDO::PARAM_INT);
4733  $destL10nRecords = $queryBuilder->execute()->fetchAll();
4734  // Index the localized record uids by language
4735  if (is_array($destL10nRecords)) {
4736  foreach ($destL10nRecords as $record) {
4737  $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4738  }
4739  }
4740  }
4741  // Move the localized records after the corresponding localizations of the destination record
4742  foreach ($l10nRecords as $record) {
4743  $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4744  if ($localizedDestPid < 0) {
4745  $this->moveRecord($table, $record['uid'], $localizedDestPid);
4746  } else {
4747  $this->moveRecord($table, $record['uid'], $destPid);
4748  }
4749  }
4750  }
4751  }
4752 
4761  public function localize($table, $uid, $language)
4762  {
4763  $newId = false;
4764  $uid = (int)$uid;
4765  if (!$GLOBALS['TCA'][$table] || !$uid || $this->isNestedElementCallRegistered($table, $uid, 'localize-' . (string)$language) !== false) {
4766  return false;
4767  }
4768 
4769  $this->registerNestedElementCall($table, $uid, 'localize-' . (string)$language);
4770  if ((!$GLOBALS['TCA'][$table]['ctrl']['languageField']
4771  || !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
4772  || $table === 'pages_language_overlay')
4773  && $table !== 'pages') {
4774  if ($this->enableLogging) {
4775  $this->newlog('Localization failed; "languageField" and "transOrigPointerField" must be defined for the table ' . $table, 1);
4776  }
4777  return false;
4778  }
4779  $langRec = BackendUtility::getRecord('sys_language', (int)$language, 'uid,title');
4780  if (!$langRec) {
4781  if ($this->enableLogging) {
4782  $this->newlog('Sys language UID "' . $language . '" not found valid!', 1);
4783  }
4784  return false;
4785  }
4786 
4787  if (!$this->doesRecordExist($table, $uid, 'show')) {
4788  if ($this->enableLogging) {
4789  $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' without permission.', 1);
4790  }
4791  return false;
4792  }
4793 
4794  // Getting workspace overlay if possible - this will localize versions in workspace if any
4795  $row = BackendUtility::getRecordWSOL($table, $uid);
4796  if (!is_array($row)) {
4797  if ($this->enableLogging) {
4798  $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' that did not exist!', 1);
4799  }
4800  return false;
4801  }
4802 
4803  // Make sure that records which are translated from another language than the default language have a correct
4804  // localization source set themselves, before translating them to another language.
4805  if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4806  && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
4807  && $table !== 'pages') {
4808  $localizationParentRecord = BackendUtility::getRecord(
4809  $table,
4810  $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
4811  );
4812  if ((int)$localizationParentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== 0) {
4813  if ($this->enableLogging) {
4814  $this->newlog('Localization failed; Source record ' . $table . ':' . $localizationParentRecord['uid'] . ' contained a reference to an original record that is not a default record (which is strange)!', 1);
4815  }
4816  return false;
4817  }
4818  }
4819 
4820  // Default language records must never have a localization parent as they are the origin of any translation.
4821  if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4822  && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0
4823  && $table !== 'pages') {
4824  if ($this->enableLogging) {
4825  $this->newlog('Localization failed; Source record ' . $table . ':' . $row['uid'] . ' contained a reference to an original default record but is a default record itself (which is strange)!', 1);
4826  }
4827  return false;
4828  }
4829 
4830  if ($table === 'pages') {
4831  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
4832  ->getQueryBuilderForTable('pages_language_overlay');
4833  $queryBuilder->getRestrictions()
4834  ->removeAll()
4835  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4836  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
4837 
4838  $recordCount = $queryBuilder->count('*')
4839  ->from('pages_language_overlay')
4840  ->where(
4841  $queryBuilder->expr()->eq(
4842  'pid',
4843  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
4844  ),
4845  $queryBuilder->expr()->eq(
4846  $GLOBALS['TCA']['pages_language_overlay']['ctrl']['languageField'],
4847  $queryBuilder->createNamedParameter((int)$langRec['uid'], \PDO::PARAM_INT)
4848  )
4849  )
4850  ->execute()
4851  ->fetchColumn(0);
4852 
4853  $pass = !$recordCount;
4854  $Ttable = 'pages_language_overlay';
4855  } else {
4856  $pass = !BackendUtility::getRecordLocalization($table, $uid, $langRec['uid'], 'AND pid=' . (int)$row['pid']);
4857  $Ttable = $table;
4858  }
4859 
4860  if (!$pass) {
4861  if ($this->enableLogging) {
4862  $this->newlog('Localization failed; There already was a localization for this language of the record ' . $table . ':' . $uid . '!', 1);
4863  }
4864  return false;
4865  }
4866 
4867  // Initialize:
4868  $overrideValues = [];
4869  $excludeFields = [];
4870  // Set override values:
4871  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['languageField']] = $langRec['uid'];
4872  // If the translated record is a default language record, set it's uid as localization parent of the new record.
4873  // If translating from any other language, no override is needed; we just can copy the localization parent of
4874  // the original record (which is pointing to the correspondent default language record) to the new record.
4875  // In copy / free mode the TransOrigPointer field is always set to 0, as no connection to the localization parent is wanted in that case.
4876  if (($this->useTransOrigPointerField && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0)
4877  || $table === 'pages') {
4878  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['transOrigPointerField']] = $uid;
4879  } elseif (!$this->useTransOrigPointerField) {
4880  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['transOrigPointerField']] = 0;
4881  }
4882  if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
4883  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['translationSource']] = $uid;
4884  }
4885  // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4886  if (isset($GLOBALS['TCA'][$table]['ctrl']['type']) && isset($GLOBALS['TCA'][$Ttable]['ctrl']['type'])) {
4887  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['type']] = $row[$GLOBALS['TCA'][$table]['ctrl']['type']];
4888  }
4889  // Set exclude Fields:
4890  foreach ($GLOBALS['TCA'][$Ttable]['columns'] as $fN => $fCfg) {
4891  $translateToMsg = '';
4892  // Check if we are just prefixing:
4893  if ($fCfg['l10n_mode'] === 'prefixLangTitle') {
4894  if (($fCfg['config']['type'] === 'text' || $fCfg['config']['type'] === 'input') && (string)$row[$fN] !== '') {
4895  list($tscPID) = BackendUtility::getTSCpid($table, $uid, '');
4896  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
4897  $tE = $this->getTableEntries($table, $TSConfig);
4898  if (!empty($TSConfig['translateToMessage']) && !$tE['disablePrependAtCopy']) {
4899  $translateToMsg = $GLOBALS['LANG'] ? $GLOBALS['LANG']->sL($TSConfig['translateToMessage']) : $TSConfig['translateToMessage'];
4900  $translateToMsg = @sprintf($translateToMsg, $langRec['title']);
4901  }
4902  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processTranslateToClass'])) {
4903  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processTranslateToClass'] as $classRef) {
4904  $hookObj = GeneralUtility::getUserObj($classRef);
4905  if (method_exists($hookObj, 'processTranslateTo_copyAction')) {
4906  $hookObj->processTranslateTo_copyAction($row[$fN], $langRec, $this, $fN);
4907  }
4908  }
4909  }
4910  if (!empty($translateToMsg)) {
4911  $overrideValues[$fN] = '[' . $translateToMsg . '] ' . $row[$fN];
4912  } else {
4913  $overrideValues[$fN] = $row[$fN];
4914  }
4915  }
4916  } elseif (
4917  ($fCfg['l10n_mode'] === 'exclude')
4918  && $fN != $GLOBALS['TCA'][$Ttable]['ctrl']['languageField']
4919  && $fN != $GLOBALS['TCA'][$Ttable]['ctrl']['transOrigPointerField']
4920  ) {
4921  // Otherwise, do not copy field (unless it is the language field or
4922  // pointer to the original language)
4923  $excludeFields[] = $fN;
4924  }
4925  }
4926  if ($Ttable === $table) {
4927  // Get the uid of record after which this localized record should be inserted
4928  $previousUid = $this->getPreviousLocalizedRecordUid($table, $uid, $row['pid'], $language);
4929  // Execute the copy:
4930  $newId = $this->copyRecord($table, $uid, -$previousUid, true, $overrideValues, implode(',', $excludeFields), $language);
4931  $autoVersionNewId = $this->getAutoVersionId($table, $newId);
4932  if (is_null($autoVersionNewId) === false) {
4933  $this->triggerRemapAction($table, $newId, [$this, 'placeholderShadowing'], [$table, $autoVersionNewId], true);
4934  }
4935  } else {
4936  // Create new record:
4937  $temporaryId = StringUtility::getUniqueId('NEW');
4938  $copyTCE = $this->getLocalTCE();
4939  $copyTCE->start([$Ttable => [$temporaryId => $overrideValues]], [], $this->BE_USER);
4940  $copyTCE->process_datamap();
4941  // Getting the new UID as if it had been copied:
4942  $theNewSQLID = $copyTCE->substNEWwithIDs[$temporaryId];
4943  if ($theNewSQLID) {
4944  // If is by design that $Ttable is used and not $table! See "l10nmgr" extension. Could be debated, but this is what I chose for this "pseudo case"
4945  $this->copyMappingArray[$Ttable][$uid] = $theNewSQLID;
4946  $newId = $theNewSQLID;
4947  }
4948  }
4949 
4950  return $newId;
4951  }
4952 
4969  protected function inlineLocalizeSynchronize($table, $id, $command)
4970  {
4971  $parentRecord = BackendUtility::getRecordWSOL($table, $id);
4972 
4973  // Backward-compatibility handling
4974  if (!is_array($command)) {
4975  // <field>, (localize | synchronize | <uid>):
4976  $parts = GeneralUtility::trimExplode(',', $command);
4977  $command = [];
4978  $command['field'] = $parts[0];
4979  // The previous process expected $id to point to the localized record already
4980  $command['language'] = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
4981 
4982  if (!MathUtility::canBeInterpretedAsInteger($parts[1])) {
4983  $command['action'] = $parts[1];
4984  } else {
4985  $command['ids'] = [$parts[1]];
4986  }
4987  }
4988 
4989  // In case the parent record is the default language record, fetch the localization
4990  if (empty($parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
4991  // Fetch the live record
4992  $parentRecordLocalization = BackendUtility::getRecordLocalization($table, $id, $command['language'], 'AND pid<>-1');
4993  if (empty($parentRecordLocalization)) {
4994  $this->newlog2('Localization for parent record ' . $table . ':' . $id . '" cannot be fetched', $table, $id, $parentRecord['pid']);
4995  return;
4996  }
4997  $parentRecord = $parentRecordLocalization[0];
4998  $id = $parentRecord['uid'];
4999  // Process overlay for current selected workspace
5000  BackendUtility::workspaceOL($table, $parentRecord);
5001  }
5002 
5003  $field = $command['field'];
5004  $language = $command['language'];
5005  $action = $command['action'];
5006  $ids = $command['ids'];
5007 
5008  if (!$field || !($action === 'localize' || $action === 'synchronize') && empty($ids) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
5009  return;
5010  }
5011 
5012  $config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
5013  $foreignTable = $config['foreign_table'];
5014  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $config);
5015  if ($localizationMode !== 'select') {
5016  return;
5017  }
5018 
5019  $transOrigPointer = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
5020  $transOrigTable = BackendUtility::getOriginalTranslationTable($table);
5021  $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
5022 
5023  if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) {
5024  return;
5025  }
5026 
5027  $inlineSubType = $this->getInlineFieldType($config);
5028  $transOrigRecord = BackendUtility::getRecordWSOL($transOrigTable, $transOrigPointer);
5029 
5030  if ($inlineSubType === false) {
5031  return;
5032  }
5033 
5034  $removeArray = [];
5035  $mmTable = $inlineSubType === 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : '';
5036  // Fetch children from original language parent:
5038  $dbAnalysisOriginal = $this->createRelationHandlerInstance();
5039  $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $transOrigTable, $config);
5040  $elementsOriginal = [];
5041  foreach ($dbAnalysisOriginal->itemArray as $item) {
5042  $elementsOriginal[$item['id']] = $item;
5043  }
5044  unset($dbAnalysisOriginal);
5045  // Fetch children from current localized parent:
5047  $dbAnalysisCurrent = $this->createRelationHandlerInstance();
5048  $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config);
5049  // Perform synchronization: Possibly removal of already localized records:
5050  if ($action === 'synchronize') {
5051  foreach ($dbAnalysisCurrent->itemArray as $index => $item) {
5052  $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']);
5053  if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) {
5054  $childTransOrigPointer = $childRecord[$childTransOrigPointerField];
5055  // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it:
5056  if (!isset($elementsOriginal[$childTransOrigPointer])) {
5057  unset($dbAnalysisCurrent->itemArray[$index]);
5058  $removeArray[$item['table']][$item['id']]['delete'] = 1;
5059  }
5060  }
5061  }
5062  }
5063  // Perform synchronization/localization: Possibly add unlocalized records for original language:
5064  if ($action === 'localize' || $action === 'synchronize') {
5065  foreach ($elementsOriginal as $originalId => $item) {
5066  $item['id'] = $this->localize($item['table'], $item['id'], $language);
5067  $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
5068  $dbAnalysisCurrent->itemArray[] = $item;
5069  }
5070  } elseif (!empty($ids)) {
5071  foreach ($ids as $childId) {
5072  if (!MathUtility::canBeInterpretedAsInteger($childId) || !isset($elementsOriginal[$childId])) {
5073  continue;
5074  }
5075  $item = $elementsOriginal[$childId];
5076  $item['id'] = $this->localize($item['table'], $item['id'], $language);
5077  $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
5078  $dbAnalysisCurrent->itemArray[] = $item;
5079  }
5080  }
5081  // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
5082  $value = implode(',', $dbAnalysisCurrent->getValueArray());
5083  $this->registerDBList[$table][$id][$field] = $value;
5084  // Remove child records (if synchronization requested it):
5085  if (is_array($removeArray) && !empty($removeArray)) {
5087  $tce = GeneralUtility::makeInstance(__CLASS__);
5088  $tce->enableLogging = $this->enableLogging;
5089  $tce->start([], $removeArray);
5090  $tce->process_cmdmap();
5091  unset($tce);
5092  }
5093  $updateFields = [];
5094  // Handle, reorder and store relations:
5095  if ($inlineSubType === 'list') {
5096  $updateFields = [$field => $value];
5097  } elseif ($inlineSubType === 'field') {
5098  $dbAnalysisCurrent->writeForeignField($config, $id);
5099  $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
5100  } elseif ($inlineSubType === 'mm') {
5101  $dbAnalysisCurrent->writeMM($config['MM'], $id);
5102  $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
5103  }
5104  // Update field referencing to child records of localized parent record:
5105  if (!empty($updateFields)) {
5106  $this->updateDB($table, $id, $updateFields);
5107  }
5108  }
5109 
5110  /*********************************************
5111  *
5112  * Cmd: Deleting
5113  *
5114  ********************************************/
5121  public function deleteAction($table, $id)
5122  {
5123  $recordToDelete = BackendUtility::getRecord($table, $id);
5124  // Record asked to be deleted was found:
5125  if (is_array($recordToDelete)) {
5126  $recordWasDeleted = false;
5127  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'])) {
5128  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] as $classRef) {
5129  $hookObj = GeneralUtility::getUserObj($classRef);
5130  if (method_exists($hookObj, 'processCmdmap_deleteAction')) {
5131  $hookObj->processCmdmap_deleteAction($table, $id, $recordToDelete, $recordWasDeleted, $this);
5132  }
5133  }
5134  }
5135  // Delete the record if a hook hasn't deleted it yet
5136  if (!$recordWasDeleted) {
5137  $this->deleteEl($table, $id);
5138  }
5139  }
5140  }
5141 
5150  public function deleteEl($table, $uid, $noRecordCheck = false, $forceHardDelete = false)
5151  {
5152  if ($table === 'pages') {
5153  $this->deletePages($uid, $noRecordCheck, $forceHardDelete);
5154  } else {
5155  $this->deleteVersionsForRecord($table, $uid, $forceHardDelete);
5156  $this->deleteRecord($table, $uid, $noRecordCheck, $forceHardDelete);
5157  }
5158  }
5159 
5167  public function deleteVersionsForRecord($table, $uid, $forceHardDelete)
5168  {
5169  $versions = BackendUtility::selectVersionsOfRecord($table, $uid, 'uid,pid,t3ver_wsid,t3ver_state', $this->BE_USER->workspace ?: null);
5170  if (is_array($versions)) {
5171  foreach ($versions as $verRec) {
5172  if (!$verRec['_CURRENT_VERSION']) {
5173  if ($table === 'pages') {
5174  $this->deletePages($verRec['uid'], true, $forceHardDelete);
5175  } else {
5176  $this->deleteRecord($table, $verRec['uid'], true, $forceHardDelete);
5177  }
5178 
5179  // Delete move-placeholder
5180  $versionState = VersionState::cast($verRec['t3ver_state']);
5181  if ($versionState->equals(VersionState::MOVE_POINTER)) {
5182  $versionMovePlaceholder = BackendUtility::getMovePlaceholder($table, $uid, 'uid', $verRec['t3ver_wsid']);
5183  if (!empty($versionMovePlaceholder)) {
5184  $this->deleteEl($table, $versionMovePlaceholder['uid'], true, $forceHardDelete);
5185  }
5186  }
5187  }
5188  }
5189  }
5190  }
5191 
5198  public function undeleteRecord($table, $uid)
5199  {
5200  if ($this->isRecordUndeletable($table, $uid)) {
5201  $this->deleteRecord($table, $uid, true, false, true);
5202  }
5203  }
5204 
5217  public function deleteRecord($table, $uid, $noRecordCheck = false, $forceHardDelete = false, $undeleteRecord = false)
5218  {
5219  $uid = (int)$uid;
5220  if (!$GLOBALS['TCA'][$table] || !$uid) {
5221  if ($this->enableLogging) {
5222  $this->log($table, $uid, 3, 0, 1, 'Attempt to delete record without delete-permissions. [' . $this->BE_USER->errorMsg . ']');
5223  }
5224  return;
5225  }
5226 
5227  // Checking if there is anything else disallowing deleting the record by checking if editing is allowed
5228  $deletedRecord = $forceHardDelete || $undeleteRecord;
5229  $hasEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, $deletedRecord, true);
5230  if (!$hasEditAccess) {
5231  if ($this->enableLogging) {
5232  $this->log($table, $uid, 3, 0, 1, 'Attempt to delete record without delete-permissions');
5233  }
5234  return;
5235  }
5236  if (!$noRecordCheck && !$this->doesRecordExist($table, $uid, 'delete')) {
5237  return;
5238  }
5239 
5240  // Clear cache before deleting the record, else the correct page cannot be identified by clear_cache
5241  list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
5242  $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
5243  $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
5244  $databaseErrorMessage = '';
5245  if ($deleteField && !$forceHardDelete) {
5246  $updateFields = [
5247  $deleteField => $undeleteRecord ? 0 : 1
5248  ];
5249  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
5250  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
5251  }
5252  // If the table is sorted, then the sorting number is set very high
5253  if ($GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$undeleteRecord) {
5254  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = 1000000000;
5255  }
5256  // before (un-)deleting this record, check for child records or references
5257  $this->deleteRecord_procFields($table, $uid, $undeleteRecord);
5258  try {
5259  GeneralUtility::makeInstance(ConnectionPool::class)
5260  ->getConnectionForTable($table)
5261  ->update($table, $updateFields, ['uid' => (int)$uid]);
5262  // Delete all l10n records as well, impossible during undelete because it might bring too many records back to life
5263  if (!$undeleteRecord) {
5264  $this->deletedRecords[$table][] = (int)$uid;
5265  $this->deleteL10nOverlayRecords($table, $uid);
5266  }
5267  } catch (DBALException $e) {
5268  $databaseErrorMessage = $e->getPrevious()->getMessage();
5269  }
5270  } else {
5271  // Fetches all fields with flexforms and look for files to delete:
5272  foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $cfg) {
5273  $conf = $cfg['config'];
5274  switch ($conf['type']) {
5275  case 'flex':
5276  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
5277 
5278  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5279  ->getQueryBuilderForTable($table);
5280  $queryBuilder->getRestrictions()->removeAll();
5281 
5282  $files = $queryBuilder
5283  ->select('*')
5284  ->from($table)
5285  ->where(
5286  $queryBuilder->expr()->eq(
5287  'uid',
5288  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5289  )
5290  )
5291  ->execute()
5292  ->fetch();
5293 
5294  $flexObj->traverseFlexFormXMLData($table, $fieldName, $files, $this, 'deleteRecord_flexFormCallBack');
5295  break;
5296  }
5297  }
5298  // Fetches all fields that holds references to files
5299  $fileFieldArr = $this->extFileFields($table);
5300  if (!empty($fileFieldArr)) {
5301  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5302  $queryBuilder->getRestrictions()->removeAll();
5303  $result = $queryBuilder
5304  ->select(...$fileFieldArr)
5305  ->from($table)
5306  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
5307  ->execute();
5308  if ($row = $result->fetch()) {
5309  $fArray = $fileFieldArr;
5310  // MISSING: Support for MM file relations!
5311  foreach ($fArray as $theField) {
5312  // This deletes files that belonged to this record.
5313  $this->extFileFunctions($table, $theField, $row[$theField]);
5314  }
5315  } elseif ($this->enableLogging) {
5316  $this->log($table, $uid, 3, 0, 100, 'Delete: Zero rows in result when trying to read filenames from record which should be deleted');
5317  }
5318  }
5319  // Delete the hard way...:
5320  try {
5321  GeneralUtility::makeInstance(ConnectionPool::class)
5322  ->getConnectionForTable($table)
5323  ->delete($table, ['uid' => (int)$uid]);
5324  $this->deletedRecords[$table][] = (int)$uid;
5325  $this->deleteL10nOverlayRecords($table, $uid);
5326  } catch (DBALException $e) {
5327  $databaseErrorMessage = $e->getPrevious()->getMessage();
5328  }
5329  }
5330  if ($this->enableLogging) {
5331  // 1 means insert, 3 means delete
5332  $state = $undeleteRecord ? 1 : 3;
5333  if ($databaseErrorMessage === '') {
5334  if ($forceHardDelete) {
5335  $message = 'Record \'%s\' (%s) was deleted unrecoverable from page \'%s\' (%s)';
5336  } else {
5337  $message = $state == 1 ? 'Record \'%s\' (%s) was restored on page \'%s\' (%s)' : 'Record \'%s\' (%s) was deleted from page \'%s\' (%s)';
5338  }
5339  $propArr = $this->getRecordProperties($table, $uid);
5340  $pagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
5341 
5342  $this->log($table, $uid, $state, 0, 0, $message, 0, [
5343  $propArr['header'],
5344  $table . ':' . $uid,
5345  $pagePropArr['header'],
5346  $propArr['pid']
5347  ], $propArr['event_pid']);
5348  } else {
5349  $this->log($table, $uid, $state, 0, 100, $databaseErrorMessage);
5350  }
5351  }
5352  // Update reference index:
5353  $this->updateRefIndex($table, $uid);
5354 
5355  // We track calls to update the reference index as to avoid calling it twice
5356  // with the same arguments. This is done because reference indexing is quite
5357  // costly and the update reference index stack usually contain duplicates.
5358  // NB: also filled and checked in loop below. The initialisation prevents
5359  // running the "root" record twice if it appears in the stack twice.
5360  $updateReferenceIndexCalls = [[$table, $uid]];
5361 
5362  // If there are entries in the updateRefIndexStack
5363  if (is_array($this->updateRefIndexStack[$table]) && is_array($this->updateRefIndexStack[$table][$uid])) {
5364  while ($args = array_pop($this->updateRefIndexStack[$table][$uid])) {
5365  if (!in_array($args, $updateReferenceIndexCalls, true)) {
5366  // $args[0]: table, $args[1]: uid
5367  $this->updateRefIndex($args[0], $args[1]);
5368  $updateReferenceIndexCalls[] = $args;
5369  }
5370  }
5371  unset($this->updateRefIndexStack[$table][$uid]);
5372  }
5373  }
5374 
5384  public function deleteRecord_flexFormCallBack($dsArr, $dataValue, $PA, $structurePath, $pObj)
5385  {
5386  // Use reference index object to find files in fields:
5388  $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
5389  $files = $refIndexObj->getRelations_procFiles($dataValue, $dsArr['TCEforms']['config'], $PA['uid']);
5390  // Traverse files and delete them if the field is a regular file field (and not a file_reference field)
5391  if (is_array($files) && $dsArr['TCEforms']['config']['internal_type'] === 'file') {
5392  foreach ($files as $dat) {
5393  if (@is_file($dat['ID_absFile'])) {
5394  $file = $this->getResourceFactory()->retrieveFileOrFolderObject($dat['ID_absFile']);
5395  $file->delete();
5396  } elseif ($this->enableLogging) {
5397  $this->log('', 0, 3, 0, 100, 'Delete: Referenced file \'' . $dat['ID_absFile'] . '\' that was supposed to be deleted together with its record which didn\'t exist');
5398  }
5399  }
5400  }
5401  }
5402 
5410  public function deletePages($uid, $force = false, $forceHardDelete = false)
5411  {
5412  $uid = (int)$uid;
5413  if ($uid === 0) {
5414  $this->newlog2('Deleting all pages starting from the root-page is disabled.', 'pages', 0, 0, 2);
5415  return;
5416  }
5417  // Getting list of pages to delete:
5418  if ($force) {
5419  // Returns the branch WITHOUT permission checks (0 secures that), so it cannot return -1
5420  $pageIdsInBranch = $this->doesBranchExist('', $uid, 0, true);
5421  $res = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
5422  } else {
5423  $res = $this->canDeletePage($uid);
5424  }
5425  // Perform deletion if not error:
5426  if (is_array($res)) {
5427  foreach ($res as $deleteId) {
5428  $this->deleteSpecificPage($deleteId, $forceHardDelete);
5429  }
5430  } else {
5432  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, true);
5434  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
5435  $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
5436 
5437  if ($this->enableLogging) {
5438  $this->newlog($res, 1);
5439  }
5440  }
5441  }
5442 
5451  public function deleteSpecificPage($uid, $forceHardDelete = false)
5452  {
5453  $uid = (int)$uid;
5454  if ($uid) {
5455  foreach ($GLOBALS['TCA'] as $table => $_) {
5456  if ($table !== 'pages') {
5457  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5458  ->getQueryBuilderForTable($table);
5459 
5460  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5461 
5462  $statement = $queryBuilder
5463  ->select('uid')
5464  ->from($table)
5465  ->where($queryBuilder->expr()->eq(
5466  'pid',
5467  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5468  ))
5469  ->execute();
5470 
5471  while ($row = $statement->fetch()) {
5472  $this->copyMovedRecordToNewLocation($table, $row['uid']);
5473  $this->deleteVersionsForRecord($table, $row['uid'], $forceHardDelete);
5474  $this->deleteRecord($table, $row['uid'], true, $forceHardDelete);
5475  }
5476  }
5477  }
5478  $this->copyMovedRecordToNewLocation('pages', $uid);
5479  $this->deleteVersionsForRecord('pages', $uid, $forceHardDelete);
5480  $this->deleteRecord('pages', $uid, true, $forceHardDelete);
5481  }
5482  }
5483 
5496  protected function copyMovedRecordToNewLocation($table, $uid)
5497  {
5498  if ($this->BE_USER->workspace > 0) {
5499  $originalRecord = BackendUtility::getRecord($table, $uid);
5500  $movePlaceholder = BackendUtility::getMovePlaceholder($table, $uid);
5501  // Check whether target page to copied to is different to current page
5502  // Cloning on the same page is superfluous and does not help at all
5503  if (!empty($originalRecord) && !empty($movePlaceholder) && (int)$originalRecord['pid'] !== (int)$movePlaceholder['pid']) {
5504  // If move placeholder exists, copy to new location
5505  // This will create a New placeholder on the new location
5506  // and a version for this new placeholder
5507  $command = [
5508  $table => [
5509  $uid => [
5510  'copy' => '-' . $movePlaceholder['uid']
5511  ]
5512  ]
5513  ];
5515  $dataHandler = GeneralUtility::makeInstance(__CLASS__);
5516  $dataHandler->enableLogging = $this->enableLogging;
5517  $dataHandler->neverHideAtCopy = true;
5518  $dataHandler->start([], $command);
5519  $dataHandler->process_cmdmap();
5520  unset($dataHandler);
5521 
5522  // Delete move placeholder
5523  $this->deleteRecord($table, $movePlaceholder['uid'], true, true);
5524  }
5525  }
5526  }
5527 
5534  public function canDeletePage($uid)
5535  {
5536  $uid = (int)$uid;
5537 
5538  // If we may at all delete this page
5539  if (!$this->doesRecordExist('pages', $uid, 'delete')) {
5540  return 'Attempt to delete page without permissions';
5541  }
5542 
5543  $pageIdsInBranch = $this->doesBranchExist('', $uid, $this->pMap['delete'], true);
5544 
5545  if ($this->deleteTree) {
5546  if ($pageIdsInBranch === -1) {
5547  return 'Attempt to delete pages in branch without permissions';
5548  }
5549 
5550  $pagesInBranch = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
5551  } else {
5552  if ($pageIdsInBranch === -1) {
5553  return 'Attempt to delete page without permissions';
5554  }
5555  if ($pageIdsInBranch !== '') {
5556  return 'Attempt to delete page which has subpages';
5557  }
5558 
5559  $pagesInBranch = [$uid];
5560  }
5561 
5562  if (!$this->checkForRecordsFromDisallowedTables($pagesInBranch)) {
5563  return 'Attempt to delete records from disallowed tables';
5564  }
5565 
5566  foreach ($pagesInBranch as $pageInBranch) {
5567  if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, true)) {
5568  return 'Attempt to delete page which has prohibited localizations.';
5569  }
5570  }
5571  return $pagesInBranch;
5572  }
5573 
5581  public function cannotDeleteRecord($table, $id)
5582  {
5583  if ($table === 'pages') {
5584  $res = $this->canDeletePage($id);
5585  return is_array($res) ? false : $res;
5586  }
5587  return $this->doesRecordExist($table, $id, 'delete') ? false : 'No permission to delete record';
5588  }
5589 
5597  public function isRecordUndeletable($table, $uid)
5598  {
5599  $result = false;
5600  $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5601  if ($record['pid']) {
5602  $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5603  // The page containing the record is not deleted, thus the record can be undeleted:
5604  if (!$page['deleted']) {
5605  $result = true;
5606  } elseif ($this->enableLogging) {
5607  $this->log($table, $uid, 'isRecordUndeletable', '', 1, 'Record cannot be undeleted since the page containing it is deleted! Undelete page "' . $page['title'] . ' (UID: ' . $page['uid'] . ')" first');
5608  }
5609  } else {
5610  // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5611  $result = true;
5612  }
5613  return $result;
5614  }
5615 
5625  public function deleteRecord_procFields($table, $uid, $undeleteRecord = false)
5626  {
5627  $conf = $GLOBALS['TCA'][$table]['columns'];
5628  $row = BackendUtility::getRecord($table, $uid, '*', '', false);
5629  if (empty($row)) {
5630  return;
5631  }
5632  foreach ($row as $field => $value) {
5633  $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5634  }
5635  }
5636 
5649  public function deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf, $undeleteRecord = false)
5650  {
5651  if ($conf['type'] === 'inline') {
5652  $foreign_table = $conf['foreign_table'];
5653  if ($foreign_table) {
5654  $inlineType = $this->getInlineFieldType($conf);
5655  if ($inlineType === 'list' || $inlineType === 'field') {
5657  $dbAnalysis = $this->createRelationHandlerInstance();
5658  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
5659  $dbAnalysis->undeleteRecord = true;
5660 
5661  $enableCascadingDelete = true;
5662  // non type save comparison is intended!
5663  if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5664  $enableCascadingDelete = false;
5665  }
5666 
5667  // Walk through the items and remove them
5668  foreach ($dbAnalysis->itemArray as $v) {
5669  if (!$undeleteRecord) {
5670  if ($enableCascadingDelete) {
5671  $this->deleteAction($v['table'], $v['id']);
5672  }
5673  } else {
5674  $this->undeleteRecord($v['table'], $v['id']);
5675  }
5676  }
5677  }
5678  }
5679  } elseif ($this->isReferenceField($conf)) {
5680  $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5681  $dbAnalysis = $this->createRelationHandlerInstance();
5682  $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5683  foreach ($dbAnalysis->itemArray as $v) {
5684  $this->updateRefIndexStack[$table][$uid][] = [$v['table'], $v['id']];
5685  }
5686  }
5687  }
5688 
5695  public function deleteL10nOverlayRecords($table, $uid)
5696  {
5697  // Check whether table can be localized or has a different table defined to store localizations:
5698  if (!BackendUtility::isTableLocalizable($table) || $table === 'pages' || $table === 'pages_language_overlay') {
5699  return;
5700  }
5701 
5702  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5703  $queryBuilder->getRestrictions()
5704  ->removeAll()
5705  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
5706  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
5707 
5708  $queryBuilder->select('*')
5709  ->from($table)
5710  ->where(
5711  $queryBuilder->expr()->eq(
5712  $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5713  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5714  )
5715  );
5716 
5717  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
5718  $queryBuilder->andWhere(
5719  $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
5720  );
5721  }
5722 
5723  $result = $queryBuilder->execute();
5724  while ($record = $result->fetch()) {
5725  // Ignore workspace delete placeholders. Those records have been marked for
5726  // deletion before - deleting them again in a workspace would revert that state.
5727  if ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5728  BackendUtility::workspaceOL($table, $record);
5729  if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5730  continue;
5731  }
5732  }
5733  $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5734  }
5735  }
5736 
5737  /*********************************************
5738  *
5739  * Cmd: Versioning
5740  *
5741  ********************************************/
5753  public function versionizeRecord($table, $id, $label, $delete = false)
5754  {
5755  $id = (int)$id;
5756  // Stop any actions if the record is marked to be deleted:
5757  // (this can occur if IRRE elements are versionized and child elements are removed)
5758  if ($this->isElementToBeDeleted($table, $id)) {
5759  return null;
5760  }
5761  if (!$GLOBALS['TCA'][$table] || !$GLOBALS['TCA'][$table]['ctrl']['versioningWS'] || $id <= 0) {
5762  if ($this->enableLogging) {
5763  $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, 1);
5764  }
5765  return null;
5766  }
5767 
5768  // Fetch record with permission check
5769  $row = $this->recordInfoWithPermissionCheck($table, $id, 'show');
5770 
5771  // This checks if the record can be selected which is all that a copy action requires.
5772  if ($row === false) {
5773  if ($this->enableLogging) {
5774  $this->newlog(
5775  'The record does not exist or you don\'t have correct permissions to make a new version (copy) of this record "' . $table . ':' . $id . '"',
5776  1
5777  );
5778  }
5779  return null;
5780  }
5781 
5782  // Record must be online record
5783  if ($row['pid'] < 0) {
5784  if ($this->enableLogging) {
5785  $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (pid=-1)!', 1);
5786  }
5787  return null;
5788  }
5789 
5790  // Record must not be placeholder for moving.
5791  if (VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)) {
5792  if ($this->enableLogging) {
5793  $this->newlog('Record cannot be versioned because it is a placeholder for a moving operation', 1);
5794  }
5795  return null;
5796  }
5797 
5798  if ($delete && $this->cannotDeleteRecord($table, $id)) {
5799  if ($this->enableLogging) {
5800  $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), 1);
5801  }
5802  return null;
5803  }
5804 
5805  // Look for next version number:
5806  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5807  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5808  $highestVerNumber = $queryBuilder
5809  ->select('t3ver_id')
5810  ->from($table)
5811  ->where($queryBuilder->expr()->orX(
5812  $queryBuilder->expr()->andX(
5813  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
5814  $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
5815  ),
5816  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
5817  ))
5818  ->orderBy('t3ver_id', 'DESC')
5819  ->setMaxResults(1)
5820  ->execute()
5821  ->fetchColumn(0);
5822  // Look for version number of the current:
5823  $subVer = $row['t3ver_id'] . '.' . ($highestVerNumber + 1);
5824  // Set up the values to override when making a raw-copy:
5825  $overrideArray = [
5826  't3ver_id' => $highestVerNumber + 1,
5827  't3ver_oid' => $id,
5828  't3ver_label' => $label ?: $subVer . ' / ' . date('d-m-Y H:m:s'),
5829  't3ver_wsid' => $this->BE_USER->workspace,
5830  't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5831  't3ver_count' => 0,
5832  't3ver_stage' => 0,
5833  't3ver_tstamp' => 0
5834  ];
5835  if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5836  $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5837  }
5838  // Checking if the record already has a version in the current workspace of the backend user
5839  if ($this->BE_USER->workspace !== 0) {
5840  // Look for version already in workspace:
5841  $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5842  }
5843  // Create new version of the record and return the new uid
5844  if (empty($versionRecord['uid'])) {
5845  // Create raw-copy and return result:
5846  // The information of the label to be used for the workspace record
5847  // as well as the information whether the record shall be removed
5848  // must be forwarded (creating remove placeholders on a workspace are
5849  // done by copying the record and override several fields).
5850  $workspaceOptions = [
5851  'delete' => $delete,
5852  'label' => $label,
5853  ];
5854  return $this->copyRecord_raw($table, $id, -1, $overrideArray, $workspaceOptions);
5855  }
5856  // Reuse the existing record and return its uid
5857  // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5858  // did not make much sense since the information is available)
5859  return $versionRecord['uid'];
5860  }
5861 
5870  public function version_remapMMForVersionSwap($table, $id, $swapWith)
5871  {
5872  // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5873  $currentRec = BackendUtility::getRecord($table, $id);
5874  $swapRec = BackendUtility::getRecord($table, $swapWith);
5875  $this->version_remapMMForVersionSwap_reg = [];
5876  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5877  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5878  $conf = $fConf['config'];
5879  if ($this->isReferenceField($conf)) {
5880  $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5881  $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5882  if ($conf['MM']) {
5884  $dbAnalysis = $this->createRelationHandlerInstance();
5885  $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5886  if (!empty($dbAnalysis->getValueArray($prependName))) {
5887  $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5888  }
5890  $dbAnalysis = $this->createRelationHandlerInstance();
5891  $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5892  if (!empty($dbAnalysis->getValueArray($prependName))) {
5893  $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5894  }
5895  }
5896  } elseif ($conf['type'] === 'flex') {
5897  // Current record
5898  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5899  $fConf,
5900  $table,
5901  $field,
5902  $currentRec
5903  );
5904  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5905  $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5906  if (is_array($currentValueArray)) {
5907  $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5908  }
5909  // Swap record
5910  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5911  $fConf,
5912  $table,
5913  $field,
5914  $swapRec
5915  );
5916  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5917  $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5918  if (is_array($currentValueArray)) {
5919  $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5920  }
5921  }
5922  }
5923  // Execute:
5924  $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5925  }
5926 
5939  public function version_remapMMForVersionSwap_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
5940  {
5941  // Extract parameters:
5942  list($table, $uid, $field) = $pParams;
5943  if ($this->isReferenceField($dsConf)) {
5944  $allowedTables = $dsConf['type'] === 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5945  $prependName = $dsConf['type'] === 'group' ? $dsConf['prepend_tname'] : '';
5946  if ($dsConf['MM']) {
5948  $dbAnalysis = $this->createRelationHandlerInstance();
5949  $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5950  $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5951  }
5952  }
5953  }
5954 
5964  public function version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
5965  {
5966  if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5967  foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5968  $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5969  }
5970  }
5971  if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5972  foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5973  $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5974  }
5975  }
5976  if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5977  foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5978  $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5979  }
5980  }
5981  }
5982 
5983  /*********************************************
5984  *
5985  * Cmd: Helper functions
5986  *
5987  ********************************************/
5988 
5994  protected function getLocalTCE()
5995  {
5996  $copyTCE = GeneralUtility::makeInstance(__CLASS__);
5997  $copyTCE->copyTree = $this->copyTree;
5998  $copyTCE->enableLogging = $this->enableLogging;
5999  // Copy forth the cached TSconfig
6000  $copyTCE->cachedTSconfig = $this->cachedTSconfig;
6001  // Transformations should NOT be carried out during copy
6002  $copyTCE->dontProcessTransformations = true;
6003  // make sure the isImporting flag is transferred, so all hooks know if
6004  // the current process is an import process
6005  $copyTCE->isImporting = $this->isImporting;
6006  return $copyTCE;
6007  }
6008 
6012  public function remapListedDBRecords()
6013  {
6014  if (!empty($this->registerDBList)) {
6015  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
6016  foreach ($this->registerDBList as $table => $records) {
6017  foreach ($records as $uid => $fields) {
6018  $newData = [];
6019  $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
6020  $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
6021  foreach ($fields as $fieldName => $value) {
6022  $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
6023  switch ($conf['type']) {
6024  case 'group':
6025 
6026  case 'select':
6027  $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
6028  if (is_array($vArray)) {
6029  $newData[$fieldName] = implode(',', $vArray);
6030  }
6031  break;
6032  case 'flex':
6033  if ($value === 'FlexForm_reference') {
6034  // This will fetch the new row for the element
6035  $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
6036  if (is_array($origRecordRow)) {
6037  BackendUtility::workspaceOL($table, $origRecordRow);
6038  // Get current data structure and value array:
6039  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
6040  [ 'config' => $conf ],
6041  $table,
6042  $fieldName,
6043  $origRecordRow
6044  );
6045  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
6046  $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
6047  // Do recursive processing of the XML data:
6048  $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
6049  // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
6050  if (is_array($currentValueArray['data'])) {
6051  $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
6052  }
6053  }
6054  }
6055  break;
6056  case 'inline':
6057  $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
6058  break;
6059  default:
6060  debug('Field type should not appear here: ' . $conf['type']);
6061  }
6062  }
6063  // If any fields were changed, those fields are updated!
6064  if (!empty($newData)) {
6065  $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
6066  }
6067  }
6068  }
6069  }
6070  }
6071 
6083  public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
6084  {
6085  // Extract parameters:
6086  list($table, $uid, $field) = $pParams;
6087  // If references are set for this field, set flag so they can be corrected later:
6088  if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
6089  $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
6090  if (is_array($vArray)) {
6091  $dataValue = implode(',', $vArray);
6092  }
6093  }
6094  // Return
6095  return ['value' => $dataValue];
6096  }
6097 
6108  public function remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
6109  {
6110  // Initialize variables
6111  // Will be set TRUE if an upgrade should be done...
6112  $set = false;
6113  // Allowed tables for references.
6114  $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
6115  // Table name to prepend the UID
6116  $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
6117  // Which tables that should possibly not be remapped
6118  $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
6119  // Convert value to list of references:
6120  $dbAnalysis = $this->createRelationHandlerInstance();
6121  $dbAnalysis->registerNonTableValues = $conf['type'] === 'select' && $conf['allowNonIdValues'];
6122  $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
6123  // Traverse those references and map IDs:
6124  foreach ($dbAnalysis->itemArray as $k => $v) {
6125  $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
6126  if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
6127  $dbAnalysis->itemArray[$k]['id'] = $mapID;
6128  $set = true;
6129  }
6130  }
6131  if (!empty($conf['MM'])) {
6132  // Purge invalid items (live/version)
6133  $dbAnalysis->purgeItemArray();
6134  if ($dbAnalysis->isPurged()) {
6135  $set = true;
6136  }
6137 
6138  // If record has been versioned/copied in this process, handle invalid relations of the live record
6139  $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
6140  $originalId = 0;
6141  if (!empty($this->copyMappingArray_merged[$table])) {
6142  $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
6143  }
6144  if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
6145  $liveRelations = $this->createRelationHandlerInstance();
6146  $liveRelations->setWorkspaceId(0);
6147  $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
6148  // Purge invalid relations in the live workspace ("0")
6149  $liveRelations->purgeItemArray(0);
6150  if ($liveRelations->isPurged()) {
6151  $liveRelations->writeMM($conf['MM'], $liveId, $prependName);
6152  }
6153  }
6154  }
6155  // If a change has been done, set the new value(s)
6156  if ($set) {
6157  if ($conf['MM']) {
6158  $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
6159  } else {
6160  return $dbAnalysis->getValueArray($prependName);
6161  }
6162  }
6163  return null;
6164  }
6165 
6174  public function remapListedDBRecords_procInline($conf, $value, $uid, $table)
6175  {
6176  $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
6177  if ($conf['foreign_table']) {
6178  $inlineType = $this->getInlineFieldType($conf);
6179  if ($inlineType === 'mm') {
6180  $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
6181  } elseif ($inlineType !== false) {
6183  $dbAnalysis = $this->createRelationHandlerInstance();
6184  $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
6185 
6186  // Keep original (live) item array and update values for specific versioned records
6187  $originalItemArray = $dbAnalysis->itemArray;
6188  foreach ($dbAnalysis->itemArray as &$item) {
6189  $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
6190  if (!empty($versionedId)) {
6191  $item['id'] = $versionedId;
6192  }
6193  }
6194 
6195  // Update child records if using pointer fields ('foreign_field'):
6196  if ($inlineType === 'field') {
6197  $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
6198  }
6199  $thePidToUpdate = null;
6200  // If the current field is set on a page record, update the pid of related child records:
6201  if ($table === 'pages') {
6202  $thePidToUpdate = $theUidToUpdate;
6203  } elseif (isset($this->registerDBPids[$table][$uid])) {
6204  $thePidToUpdate = $this->registerDBPids[$table][$uid];
6205  $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
6206  }
6207  // Update child records if change to pid is required (only if the current record is not on a workspace):
6208  if ($thePidToUpdate) {
6209  // ensure, only live page ids are used as 'pid' values
6210  $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
6211  if ($liveId !== null) {
6212  $thePidToUpdate = $liveId;
6213  }
6214  $updateValues = ['pid' => $thePidToUpdate];
6215  foreach ($originalItemArray as $v) {
6216  if ($v['id'] && $v['table'] && is_null(BackendUtility::getLiveVersionIdOfRecord($v['table'], $v['id']))) {
6217  GeneralUtility::makeInstance(ConnectionPool::class)
6218  ->getConnectionForTable($v['table'])
6219  ->update($v['table'], $updateValues, ['uid' => (int)$v['id']]);
6220  }
6221  }
6222  }
6223  }
6224  }
6225  }
6226 
6231  public function processRemapStack()
6232  {
6233  // Processes the remap stack:
6234  if (is_array($this->remapStack)) {
6235  $remapFlexForms = [];
6236  $hookPayload = [];
6237 
6238  foreach ($this->remapStack as $remapAction) {
6239  // If no position index for the arguments was set, skip this remap action:
6240  if (!is_array($remapAction['pos'])) {
6241  continue;
6242  }
6243  // Load values from the argument array in remapAction:
6244  $field = $remapAction['field'];
6245  $id = $remapAction['args'][$remapAction['pos']['id']];
6246  $rawId = $id;
6247  $table = $remapAction['args'][$remapAction['pos']['table']];
6248  $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
6249  $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
6250  $additionalData = $remapAction['additionalData'];
6251  // The record is new and has one or more new ids (in case of versioning/workspaces):
6252  if (strpos($id, 'NEW') !== false) {
6253  // Replace NEW...-ID with real uid:
6254  $id = $this->substNEWwithIDs[$id];
6255  // If the new parent record is on a non-live workspace or versionized, it has another new id:
6256  if (isset($this->autoVersionIdMap[$table][$id])) {
6257  $id = $this->autoVersionIdMap[$table][$id];
6258  }
6259  $remapAction['args'][$remapAction['pos']['id']] = $id;
6260  }
6261  // Replace relations to NEW...-IDs in field value (uids of child records):
6262  if (is_array($valueArray)) {
6263  foreach ($valueArray as $key => $value) {
6264  if (strpos($value, 'NEW') !== false) {
6265  if (strpos($value, '_') === false) {
6266  $affectedTable = $tcaFieldConf['foreign_table'];
6267  $prependTable = false;
6268  } else {
6269  $parts = explode('_', $value);
6270  $value = array_pop($parts);
6271  $affectedTable = implode('_', $parts);
6272  $prependTable = true;
6273  }
6274  $value = $this->substNEWwithIDs[$value];
6275  // The record is new, but was also auto-versionized and has another new id:
6276  if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
6277  $value = $this->autoVersionIdMap[$affectedTable][$value];
6278  }
6279  if ($prependTable) {
6280  $value = $affectedTable . '_' . $value;
6281  }
6282  // Set a hint that this was a new child record:
6283  $this->newRelatedIDs[$affectedTable][] = $value;
6284  $valueArray[$key] = $value;
6285  }
6286  }
6287  $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
6288  }
6289  // Process the arguments with the defined function:
6290  if (!empty($remapAction['func'])) {
6291  $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
6292  }
6293  // If array is returned, check for maxitems condition, if string is returned this was already done:
6294  if (is_array($newValue)) {
6295  $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
6296  // The reference casting is only required if
6297  // checkValue_group_select_processDBdata() returns an array
6298  $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
6299  }
6300  // Update in database (list of children (csv) or number of relations (foreign_field)):
6301  if (!empty($field)) {
6302  $fieldArray = [$field => $newValue];
6303  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
6304  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
6305  }
6306  $this->updateDB($table, $id, $fieldArray);
6307  } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
6308  // Collect data to update FlexForms
6309  $flexFormId = $additionalData['flexFormId'];
6310  $flexFormPath = $additionalData['flexFormPath'];
6311 
6312  if (!isset($remapFlexForms[$flexFormId])) {
6313  $remapFlexForms[$flexFormId] = [];
6314  }
6315 
6316  $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
6317  }
6318 
6319  // Collect elements that shall trigger processDatamap_afterDatabaseOperations
6320  if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
6321  $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
6322  if (!isset($hookPayload[$table][$rawId])) {
6323  $hookPayload[$table][$rawId] = [
6324  'status' => $hookArgs['status'],
6325  'fieldArray' => $hookArgs['fieldArray'],
6326  'hookObjects' => $hookArgs['hookObjectsArr'],
6327  ];
6328  }
6329  $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
6330  }
6331  }
6332 
6333  if ($remapFlexForms) {
6334  foreach ($remapFlexForms as $flexFormId => $modifications) {
6335  $this->updateFlexFormData($flexFormId, $modifications);
6336  }
6337  }
6338 
6339  foreach ($hookPayload as $tableName => $rawIdPayload) {
6340  foreach ($rawIdPayload as $rawId => $payload) {
6341  foreach ($payload['hookObjects'] as $hookObject) {
6342  if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
6343  continue;
6344  }
6345  $hookObject->processDatamap_afterDatabaseOperations(
6346  $payload['status'],
6347  $tableName,
6348  $rawId,
6349  $payload['fieldArray'],
6350  $this
6351  );
6352  }
6353  }
6354  }
6355  }
6356  // Processes the remap stack actions:
6357  if ($this->remapStackActions) {
6358  foreach ($this->remapStackActions as $action) {
6359  if (isset($action['callback']) && isset($action['arguments'])) {
6360  call_user_func_array($action['callback'], $action['arguments']);
6361  }
6362  }
6363  }
6364  // Processes the reference index updates of the remap stack:
6365  foreach ($this->remapStackRefIndex as $table => $idArray) {
6366  foreach ($idArray as $id) {
6367  $this->updateRefIndex($table, $id);
6368  unset($this->remapStackRefIndex[$table][$id]);
6369  }
6370  }
6371  // Reset:
6372  $this->remapStack = [];
6373  $this->remapStackRecords = [];
6374  $this->remapStackActions = [];
6375  $this->remapStackRefIndex = [];
6376  }
6377 
6384  protected function updateFlexFormData($flexFormId, array $modifications)
6385  {
6386  list($table, $uid, $field) = explode(':', $flexFormId, 3);
6387 
6388  if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
6389  $uid = $this->substNEWwithIDs[$uid];
6390  }
6391 
6392  $record = $this->recordInfo($table, $uid, '*');
6393 
6394  if (!$table || !$uid || !$field || !is_array($record)) {
6395  return;
6396  }
6397 
6398  BackendUtility::workspaceOL($table, $record);
6399 
6400  // Get current data structure and value array:
6401  $valueStructure = GeneralUtility::xml2array($record[$field]);
6402 
6403  // Do recursive processing of the XML data:
6404  foreach ($modifications as $path => $value) {
6405  $valueStructure['data'] = ArrayUtility::setValueByPath(
6406  $valueStructure['data'],
6407  $path,
6408  $value
6409  );
6410  }
6411 
6412  if (is_array($valueStructure['data'])) {
6413  // The return value should be compiled back into XML
6414  $values = [
6415  $field => $this->checkValue_flexArray2Xml($valueStructure, true),
6416  ];
6417 
6418  $this->updateDB($table, $uid, $values);
6419  }
6420  }
6421 
6436  protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
6437  {
6438  // Check whether the affected record is marked to be remapped:
6439  if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
6440  call_user_func_array($callback, $arguments);
6441  } else {
6442  $this->addRemapAction($table, $id, $callback, $arguments);
6443  }
6444  }
6445 
6454  public function addRemapAction($table, $id, array $callback, array $arguments)
6455  {
6456  $this->remapStackActions[] = [
6457  'affects' => [
6458  'table' => $table,
6459  'id' => $id
6460  ],
6461  'callback' => $callback,
6462  'arguments' => $arguments
6463  ];
6464  }
6465 
6472  public function addRemapStackRefIndex($table, $id)
6473  {
6474  $this->remapStackRefIndex[$table][$id] = $id;
6475  }
6476 
6488  public function getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
6489  {
6490  if (is_array($registerDBList[$table][$id])) {
6491  foreach ($incomingFieldArray as $field => $value) {
6492  $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
6493  if ($registerDBList[$table][$id][$field] && ($foreignTable = $fieldConf['foreign_table'])) {
6494  $newValueArray = [];
6495  $origValueArray = is_array($value) ? $value : explode(',', $value);
6496  // Update the uids of the copied records, but also take care about new records:
6497  foreach ($origValueArray as $childId) {
6498  $newValueArray[] = $this->autoVersionIdMap[$foreignTable][$childId] ? $this->autoVersionIdMap[$foreignTable][$childId] : $childId;
6499  }
6500  // Set the changed value to the $incomingFieldArray
6501  $incomingFieldArray[$field] = implode(',', $newValueArray);
6502  }
6503  }
6504  // Clean up the $registerDBList array:
6505  unset($registerDBList[$table][$id]);
6506  if (empty($registerDBList[$table])) {
6507  unset($registerDBList[$table]);
6508  }
6509  }
6510  }
6511 
6512  /*****************************
6513  *
6514  * Access control / Checking functions
6515  *
6516  *****************************/
6523  public function checkModifyAccessList($table)
6524  {
6525  $res = $this->admin || !$this->tableAdminOnly($table) && GeneralUtility::inList($this->BE_USER->groupData['tables_modify'], $table);
6526  // Hook 'checkModifyAccessList': Post-processing of the state of access
6527  foreach ($this->getCheckModifyAccessListHookObjects() as $hookObject) {
6529  $hookObject->checkModifyAccessList($res, $table, $this);
6530  }
6531  return $res;
6532  }
6533 
6541  public function isRecordInWebMount($table, $id)
6542  {
6543  if (!isset($this->isRecordInWebMount_Cache[$table . ':' . $id])) {
6544  $recP = $this->getRecordProperties($table, $id);
6545  $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->isInWebMount($recP['event_pid']);
6546  }
6547  return $this->isRecordInWebMount_Cache[$table . ':' . $id];
6548  }
6549 
6556  public function isInWebMount($pid)
6557  {
6558  if (!isset($this->isInWebMount_Cache[$pid])) {
6559  $this->isInWebMount_Cache[$pid] = $this->BE_USER->isInWebMount($pid);
6560  }
6561  return $this->isInWebMount_Cache[$pid];
6562  }
6563 
6573  public function checkRecordUpdateAccess($table, $id, $data = false, $hookObjectsArr = null)
6574  {
6575  $res = null;
6576  if (is_array($hookObjectsArr)) {
6577  foreach ($hookObjectsArr as $hookObj) {
6578  if (method_exists($hookObj, 'checkRecordUpdateAccess')) {
6579  $res = $hookObj->checkRecordUpdateAccess($table, $id, $data, $res, $this);
6580  }
6581  }
6582  }
6583  if ($res === 1 || $res === 0) {
6584  return $res;
6585  }
6586  $res = 0;
6587 
6588  if ($GLOBALS['TCA'][$table] && (int)$id > 0) {
6589  // If information is cached, return it
6590  if (isset($this->recUpdateAccessCache[$table][$id])) {
6591  return $this->recUpdateAccessCache[$table][$id];
6592  }
6593  if ($this->doesRecordExist($table, $id, 'edit')) {
6594  $res = 1;
6595  }
6596  // Cache the result
6597  $this->recUpdateAccessCache[$table][$id] = $res;
6598  }
6599  return $res;
6600  }
6601 
6611  public function checkRecordInsertAccess($insertTable, $pid, $action = 1)
6612  {
6613  $pid = (int)$pid;
6614  if ($pid < 0) {
6615  return false;
6616  }
6617  // If information is cached, return it
6618  if (isset($this->recInsertAccessCache[$insertTable][$pid])) {
6619  return $this->recInsertAccessCache[$insertTable][$pid];
6620  }
6621 
6622  $res = false;
6623  if ($insertTable === 'pages') {
6624  $perms = $this->pMap['new'];
6625  } elseif (($insertTable === 'sys_file_reference') && array_key_exists('pages', $this->datamap)) {
6626  // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6627  $perms = $this->pMap['edit'];
6628  } else {
6629  $perms = $this->pMap['editcontent'];
6630  }
6631  $pageExists = (bool)$this->doesRecordExist('pages', $pid, $perms);
6632  // 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
6633  if ($pageExists || $pid === 0 && ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($insertTable))) {
6634  // Check permissions
6635  if ($this->isTableAllowedForThisPage($pid, $insertTable)) {
6636  $res = true;
6637  // Cache the result
6638  $this->recInsertAccessCache[$insertTable][$pid] = $res;
6639  } elseif ($this->enableLogging) {
6640  $propArr = $this->getRecordProperties('pages', $pid);
6641  $this->log($insertTable, $pid, $action, 0, 1, 'Attempt to insert record on page \'%s\' (%s) where this table, %s, is not allowed', 11, [$propArr['header'], $pid, $insertTable], $propArr['event_pid']);
6642  }
6643  } elseif ($this->enableLogging) {
6644  $propArr = $this->getRecordProperties('pages', $pid);
6645  $this->log($insertTable, $pid, $action, 0, 1, 'Attempt to insert a record on page \'%s\' (%s) from table \'%s\' without permissions. Or non-existing page.', 12, [$propArr['header'], $pid, $insertTable], $propArr['event_pid']);
6646  }
6647  return $res;
6648  }
6649 
6657  public function isTableAllowedForThisPage($page_uid, $checkTable)
6658  {
6659  $page_uid = (int)$page_uid;
6660  $rootLevelSetting = (int)$GLOBALS['TCA'][$checkTable]['ctrl']['rootLevel'];
6661  // 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.
6662  if ($checkTable !== 'pages' && $rootLevelSetting !== -1 && ($rootLevelSetting xor !$page_uid)) {
6663  return false;
6664  }
6665  $allowed = false;
6666  // Check root-level
6667  if (!$page_uid) {
6668  if ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($checkTable)) {
6669  $allowed = true;
6670  }
6671  } else {
6672  // Check non-root-level
6673  $doktype = $this->pageInfo($page_uid, 'doktype');
6674  $allowedTableList = isset($GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'])
6675  ? $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables']
6676  : $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6677  $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6678  // If all tables or the table is listed as an allowed type, return TRUE
6679  if (strpos($allowedTableList, '*') !== false || in_array($checkTable, $allowedArray, true)) {
6680  $allowed = true;
6681  }
6682  }
6683  return $allowed;
6684  }
6685 
6696  public function doesRecordExist($table, $id, $perms)
6697  {
6698  return $this->recordInfoWithPermissionCheck($table, $id, $perms, 'uid, pid') !== false;
6699  }
6700 
6711  protected function doesRecordExist_pageLookUp($id, $perms, $columns = ['uid'])
6712  {
6713  $cacheId = md5('doesRecordExist_pageLookUp' . '_' . $id . '_' . $perms . '_' . implode(
6714  '_',
6715  $columns
6716  ) . '_' . (string)$this->admin);
6717 
6718  // If result is cached, return it
6719  $cachedResult = $this->runtimeCache->get($cacheId);
6720  if (!empty($cachedResult)) {
6721  return $cachedResult;
6722  }
6723 
6724  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6725  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6726  $queryBuilder
6727  ->select(...$columns)
6728  ->from('pages')
6729  ->where($queryBuilder->expr()->eq(
6730  'uid',
6731  $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
6732  ));
6733  if ($perms && !$this->admin) {
6734  $queryBuilder->andWhere($this->BE_USER->getPagePermsClause($perms));
6735  }
6736  if (!$this->admin && $GLOBALS['TCA']['pages']['ctrl']['editlock'] &&
6738  ) {
6739  $queryBuilder->andWhere($queryBuilder->expr()->eq(
6740  $GLOBALS['TCA']['pages']['ctrl']['editlock'],
6741  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
6742  ));
6743  }
6744 
6745  $row = $queryBuilder->execute()->fetch();
6746  $this->runtimeCache->set($cacheId, $row);
6747 
6748  return $row;
6749  }
6750 
6763  public function doesBranchExist($inList, $pid, $perms, $recurse)
6764  {
6765  $pid = (int)$pid;
6766  $perms = (int)$perms;
6767  if ($pid >= 0) {
6768  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6769  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6770  $result = $queryBuilder
6771  ->select('uid', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody')
6772  ->from('pages')
6773  ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
6774  ->orderBy('sorting')
6775  ->execute();
6776  while ($row = $result->fetch()) {
6777  // IF admin, then it's OK
6778  if ($this->admin || $this->BE_USER->doesUserHaveAccess($row, $perms)) {
6779  $inList .= $row['uid'] . ',';
6780  if ($recurse) {
6781  // Follow the subpages recursively...
6782  $inList = $this->doesBranchExist($inList, $row['uid'], $perms, $recurse);
6783  if ($inList === -1) {
6784  return -1;
6785  }
6786  }
6787  } else {
6788  // No permissions
6789  return -1;
6790  }
6791  }
6792  }
6793  return $inList;
6794  }
6795 
6802  public function tableReadOnly($table)
6803  {
6804  // Returns TRUE if table is readonly
6805  return (bool)$GLOBALS['TCA'][$table]['ctrl']['readOnly'];
6806  }
6807 
6814  public function tableAdminOnly($table)
6815  {
6816  // Returns TRUE if table is admin-only
6817  return (bool)$GLOBALS['TCA'][$table]['ctrl']['adminOnly'];
6818  }
6819 
6828  public function destNotInsideSelf($destinationId, $id)
6829  {
6830  $loopCheck = 100;
6831  $destinationId = (int)$destinationId;
6832  $id = (int)$id;
6833  if ($destinationId === $id) {
6834  return false;
6835  }
6836  while ($destinationId !== 0 && $loopCheck > 0) {
6837  $loopCheck--;
6838  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6839  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6840  $result = $queryBuilder
6841  ->select('pid', 'uid', 't3ver_oid', 't3ver_wsid')
6842  ->from('pages')
6843  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($destinationId, \PDO::PARAM_INT)))
6844  ->execute();
6845  if ($row = $result->fetch()) {
6846  BackendUtility::fixVersioningPid('pages', $row);
6847  if ($row['pid'] == $id) {
6848  return false;
6849  }
6850  $destinationId = (int)$row['pid'];
6851  } else {
6852  return false;
6853  }
6854  }
6855  return true;
6856  }
6857 
6864  public function getExcludeListArray()
6865  {
6866  $list = [];
6867  $nonExcludeFieldsArray = array_flip(GeneralUtility::trimExplode(',', $this->BE_USER->groupData['non_exclude_fields']));
6868  foreach ($GLOBALS['TCA'] as $table => $_) {
6869  if (isset($GLOBALS['TCA'][$table]['columns'])) {
6870  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $config) {
6871  if ($config['exclude'] && !isset($nonExcludeFieldsArray[$table . ':' . $field])) {
6872  $list[] = $table . '-' . $field;
6873  }
6874  }
6875  }
6876  }
6877  return $list;
6878  }
6879 
6887  public function doesPageHaveUnallowedTables($page_uid, $doktype)
6888  {
6889  $page_uid = (int)$page_uid;
6890  if (!$page_uid) {
6891  // Not a number. Probably a new page
6892  return false;
6893  }
6894  $allowedTableList = isset($GLOBALS['PAGES_TYPES'][$doktype]['allowedTables']) ? $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] : $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6895  $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6896  // If all tables is OK the return TRUE
6897  if (strstr($allowedTableList, '*')) {
6898  // OK...
6899  return false;
6900  }
6901  $tableList = [];
6902  foreach ($GLOBALS['TCA'] as $table => $_) {
6903  // If the table is not in the allowed list, check if there are records...
6904  if (!in_array($table, $allowedArray, true)) {
6905  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6906  $queryBuilder->getRestrictions()->removeAll();
6907  $count = $queryBuilder
6908  ->count('uid')
6909  ->from($table)
6910  ->where($queryBuilder->expr()->eq(
6911  'pid',
6912  $queryBuilder->createNamedParameter($page_uid, \PDO::PARAM_INT)
6913  ))
6914  ->execute()
6915  ->fetchColumn(0);
6916  if ($count) {
6917  $tableList[] = $table;
6918  }
6919  }
6920  }
6921  return implode(',', $tableList);
6922  }
6923 
6924  /*****************************
6925  *
6926  * Information lookup
6927  *
6928  *****************************/
6937  public function pageInfo($id, $field)
6938  {
6939  if (!isset($this->pageCache[$id])) {
6940  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6941  $queryBuilder->getRestrictions()->removeAll();
6942  $row = $queryBuilder
6943  ->select('*')
6944  ->from('pages')
6945  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6946  ->execute()
6947  ->fetch();
6948  if ($row) {
6949  $this->pageCache[$id] = $row;
6950  }
6951  }
6952  return $this->pageCache[$id][$field];
6953  }
6954 
6964  public function recordInfo($table, $id, $fieldList)
6965  {
6966  // Skip, if searching for NEW records or there's no TCA table definition
6967  if ((int)$id === 0 || !isset($GLOBALS['TCA'][$table])) {
6968  return null;
6969  }
6970  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6971  $queryBuilder->getRestrictions()->removeAll();
6972  $result = $queryBuilder
6973  ->select(...GeneralUtility::trimExplode(',', $fieldList))
6974  ->from($table)
6975  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6976  ->execute()
6977  ->fetch();
6978  return $result ?: null;
6979  }
6980 
6991  protected function recordInfoWithPermissionCheck(string $table, int $id, $perms, string $fieldList = '*')
6992  {
6993  $id = (int)$id;
6994  if ($this->bypassAccessCheckForRecords) {
6995  $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6996 
6997  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6998  $queryBuilder->getRestrictions()->removeAll();
6999 
7000  $record = $queryBuilder->select(...$columns)
7001  ->from($table)
7002  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7003  ->execute()
7004  ->fetch();
7005 
7006  return $record ?: false;
7007  }
7008  // Processing the incoming $perms (from possible string to integer that can be AND'ed)
7010  if ($table !== 'pages') {
7011  switch ($perms) {
7012  case 'edit':
7013 
7014  case 'delete':
7015 
7016  case 'new':
7017  // This holds it all in case the record is not page!!
7018  if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
7019  $perms = 'edit';
7020  } else {
7021  $perms = 'editcontent';
7022  }
7023  break;
7024  }
7025  }
7026  $perms = (int)$this->pMap[$perms];
7027  } else {
7028  $perms = (int)$perms;
7029  }
7030  if (!$perms) {
7031  throw new \RuntimeException('Internal ERROR: no permissions to check for non-admin user', 1270853920);
7032  }
7033  // For all tables: Check if record exists:
7034  $isWebMountRestrictionIgnored = BackendUtility::isWebMountRestrictionIgnored($table);
7035  if (is_array($GLOBALS['TCA'][$table]) && $id > 0 && ($this->admin || $isWebMountRestrictionIgnored || $this->isRecordInWebMount($table, $id))) {
7036  $columns = GeneralUtility::trimExplode(',', $fieldList, true);
7037  if ($table !== 'pages') {
7038  // Find record without checking page
7039  // @todo: This should probably check for editlock
7040  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7041  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7042  $output = $queryBuilder
7043  ->select(...$columns)
7044  ->from($table)
7045  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7046  ->execute()
7047  ->fetch();
7048  BackendUtility::fixVersioningPid($table, $output, true);
7049  // If record found, check page as well:
7050  if (is_array($output)) {
7051  // Looking up the page for record:
7052  $pageRec = $this->doesRecordExist_pageLookUp($output['pid'], $perms);
7053  // 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):
7054  $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
7055  if (is_array($pageRec) || !$output['pid'] && ($this->admin || $isRootLevelRestrictionIgnored)) {
7056  return $output;
7057  }
7058  }
7059  return false;
7060  }
7061  return $this->doesRecordExist_pageLookUp($id, $perms, $columns);
7062  }
7063  return false;
7064  }
7065 
7077  public function getRecordProperties($table, $id, $noWSOL = false)
7078  {
7079  $row = $table === 'pages' && !$id ? ['title' => '[root-level]', 'uid' => 0, 'pid' => 0] : $this->recordInfo($table, $id, '*');
7080  if (!$noWSOL) {
7081  BackendUtility::workspaceOL($table, $row);
7082  }
7083  return $this->getRecordPropertiesFromRow($table, $row);
7084  }
7085 
7093  public function getRecordPropertiesFromRow($table, $row)
7094  {
7095  if ($GLOBALS['TCA'][$table]) {
7096  BackendUtility::fixVersioningPid($table, $row);
7097  $out = [
7098  'header' => BackendUtility::getRecordTitle($table, $row),
7099  'pid' => $row['pid'],
7100  'event_pid' => $this->eventPid($table, isset($row['_ORIG_pid']) ? $row['t3ver_oid'] : $row['uid'], $row['pid']),
7101  't3ver_state' => $GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? $row['t3ver_state'] : '',
7102  '_ORIG_pid' => $row['_ORIG_pid']
7103  ];
7104  return $out;
7105  }
7106  return null;
7107  }
7108 
7115  public function eventPid($table, $uid, $pid)
7116  {
7117  return $table === 'pages' ? $uid : $pid;
7118  }
7119 
7120  /*********************************************
7121  *
7122  * Storing data to Database Layer
7123  *
7124  ********************************************/
7133  public function updateDB($table, $id, $fieldArray)
7134  {
7135  if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && (int)$id) {
7136  // Do NOT update the UID field, ever!
7137  unset($fieldArray['uid']);
7138  if (!empty($fieldArray)) {
7139  $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
7140 
7141  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7142 
7143  $types = [];
7144  $platform = $connection->getDatabasePlatform();
7145  if ($platform instanceof SQLServerPlatform) {
7146  // mssql needs to set proper PARAM_LOB and others to update fields
7147  $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
7148  foreach ($fieldArray as $columnName => $columnValue) {
7149  $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
7150  }
7151  }
7152 
7153  // Execute the UPDATE query:
7154  $updateErrorMessage = '';
7155  try {
7156  $connection->update($table, $fieldArray, ['uid' => (int)$id], $types);
7157  } catch (DBALException $e) {
7158  $updateErrorMessage = $e->getPrevious()->getMessage();
7159  }
7160  // If succeeds, do...:
7161  if ($updateErrorMessage === '') {
7162  // Update reference index:
7163  $this->updateRefIndex($table, $id);
7164  if ($this->enableLogging) {
7165  if ($this->checkStoredRecords) {
7166  $newRow = $this->checkStoredRecord($table, $id, $fieldArray, 2);
7167  } else {
7168  $newRow = $fieldArray;
7169  $newRow['uid'] = $id;
7170  }
7171  // Set log entry:
7172  $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7173  $theLogId = $this->log($table, $id, 2, $propArr['pid'], 0, 'Record \'%s\' (%s) was updated.' . ($propArr['_ORIG_pid'] == -1 ? ' (Offline version).' : ' (Online).'), 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
7174  // Set History data:
7175  $this->setHistory($table, $id, $theLogId);
7176  }
7177  // Clear cache for relevant pages:
7178  $this->registerRecordIdForPageCacheClearing($table, $id);
7179  // Unset the pageCache for the id if table was page.
7180  if ($table === 'pages') {
7181  unset($this->pageCache[$id]);
7182  }
7183  } elseif ($this->enableLogging) {
7184  $this->log($table, $id, 2, 0, 2, 'SQL error: \'%s\' (%s)', 12, [$updateErrorMessage, $table . ':' . $id]);
7185  }
7186  }
7187  }
7188  }
7189 
7202  public function insertDB($table, $id, $fieldArray, $newVersion = false, $suggestedUid = 0, $dontSetNewIdIndex = false)
7203  {
7204  if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && isset($fieldArray['pid'])) {
7205  // Do NOT insert the UID field, ever!
7206  unset($fieldArray['uid']);
7207  if (!empty($fieldArray)) {
7208  // Check for "suggestedUid".
7209  // This feature is used by the import functionality to force a new record to have a certain UID value.
7210  // This is only recommended for use when the destination server is a passive mirror of another server.
7211  // As a security measure this feature is available only for Admin Users (for now)
7212  $suggestedUid = (int)$suggestedUid;
7213  if ($this->BE_USER->isAdmin() && $suggestedUid && $this->suggestedInsertUids[$table . ':' . $suggestedUid]) {
7214  // When the value of ->suggestedInsertUids[...] is "DELETE" it will try to remove the previous record
7215  if ($this->suggestedInsertUids[$table . ':' . $suggestedUid] === 'DELETE') {
7216  // DELETE:
7217  GeneralUtility::makeInstance(ConnectionPool::class)
7218  ->getConnectionForTable($table)
7219  ->delete($table, ['uid' => (int)$suggestedUid]);
7220  }
7221  $fieldArray['uid'] = $suggestedUid;
7222  }
7223  $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
7224  $typeArray = [];
7225  if (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])
7226  && array_key_exists($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'], $fieldArray)
7227  ) {
7228  $typeArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = Connection::PARAM_LOB;
7229  }
7230  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7231  $insertErrorMessage = '';
7232  try {
7233  // Execute the INSERT query:
7234  $connection->insert(
7235  $table,
7236  $fieldArray,
7237  $typeArray
7238  );
7239  } catch (DBALException $e) {
7240  $insertErrorMessage = $e->getPrevious()->getMessage();
7241  }
7242  // If succees, do...:
7243  if ($insertErrorMessage === '') {
7244  // Set mapping for NEW... -> real uid:
7245  // the NEW_id now holds the 'NEW....' -id
7246  $NEW_id = $id;
7247  $id = $this->postProcessDatabaseInsert($connection, $table, $suggestedUid);
7248 
7249  if (!$dontSetNewIdIndex) {
7250  $this->substNEWwithIDs[$NEW_id] = $id;
7251  $this->substNEWwithIDs_table[$NEW_id] = $table;
7252  }
7253  $newRow = [];
7254  if ($this->enableLogging) {
7255  // Checking the record is properly saved if configured
7256  if ($this->checkStoredRecords) {
7257  $newRow = $this->checkStoredRecord($table, $id, $fieldArray, 1);
7258  } else {
7259  $newRow = $fieldArray;
7260  $newRow['uid'] = $id;
7261  }
7262  }
7263  // Update reference index:
7264  $this->updateRefIndex($table, $id);
7265  if ($newVersion) {
7266  if ($this->enableLogging) {
7267  $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7268  $this->log($table, $id, 1, 0, 0, 'New version created of table \'%s\', uid \'%s\'. UID of new version is \'%s\'', 10, [$table, $fieldArray['t3ver_oid'], $id], $propArr['event_pid'], $NEW_id);
7269  }
7270  } else {
7271  if ($this->enableLogging) {
7272  $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7273  $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
7274  $this->log($table, $id, 1, 0, 0, 'Record \'%s\' (%s) was inserted on page \'%s\' (%s)', 10, [$propArr['header'], $table . ':' . $id, $page_propArr['header'], $newRow['pid']], $newRow['pid'], $NEW_id);
7275  }
7276  // Clear cache for relevant pages:
7277  $this->registerRecordIdForPageCacheClearing($table, $id);
7278  }
7279  return $id;
7280  }
7281  if ($this->enableLogging) {
7282  $this->log($table, $id, 1, 0, 2, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
7283  }
7284  }
7285  }
7286  return null;
7287  }
7288 
7299  public function checkStoredRecord($table, $id, $fieldArray, $action)
7300  {
7301  $id = (int)$id;
7302  if (is_array($GLOBALS['TCA'][$table]) && $id) {
7303  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7304  $queryBuilder->getRestrictions()->removeAll();
7305 
7306  $row = $queryBuilder
7307  ->select('*')
7308  ->from($table)
7309  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7310  ->execute()
7311  ->fetch();
7312 
7313  if (!empty($row)) {
7314  // Traverse array of values that was inserted into the database and compare with the actually stored value:
7315  $errors = [];
7316  foreach ($fieldArray as $key => $value) {
7317  if (!$this->checkStoredRecords_loose || $value || $row[$key]) {
7318  if (is_float($row[$key])) {
7319  // if the database returns the value as double, compare it as double
7320  if ((double)$value !== (double)$row[$key]) {
7321  $errors[] = $key;
7322  }
7323  } else {
7324  if ((string)$value !== (string)$row[$key]) {
7325  // The is_numeric check catches cases where we want to store a float/double value
7326  // and database returns the field as a string with the least required amount of
7327  // significant digits, i.e. "0.00" being saved and "0" being read back.
7328  if (is_numeric($value) && is_numeric($row[$key])) {
7329  if ((double)$value === (double)$row[$key]) {
7330  continue;
7331  }
7332  }
7333  $errors[] = $key;
7334  }
7335  }
7336  }
7337  }
7338  // Set log message if there were fields with unmatching values:
7339  if ($this->enableLogging && !empty($errors)) {
7340  $message = sprintf(
7341  'These fields of record %d in table "%s" have not been saved correctly: %s! The values might have changed due to type casting of the database.',
7342  $id,
7343  $table,
7344  implode(', ', $errors)
7345  );
7346  $this->log($table, $id, $action, 0, 1, $message);
7347  }
7348  // Return selected rows:
7349  return $row;
7350  }
7351  }
7352  return null;
7353  }
7354 
7362  public function setHistory($table, $id, $logId)
7363  {
7364  if (isset($this->historyRecords[$table . ':' . $id]) && (int)$logId > 0) {
7365  $fields_values = [];
7366  $fields_values['history_data'] = serialize($this->historyRecords[$table . ':' . $id]);
7367  $fields_values['fieldlist'] = implode(',', array_keys($this->historyRecords[$table . ':' . $id]['newRecord']));
7368  $fields_values['tstamp'] = $GLOBALS['EXEC_TIME'];
7369  $fields_values['tablename'] = $table;
7370  $fields_values['recuid'] = $id;
7371  $fields_values['sys_log_uid'] = $logId;
7372  GeneralUtility::makeInstance(ConnectionPool::class)
7373  ->getConnectionForTable('sys_history')
7374  ->insert('sys_history', $fields_values);
7375  }
7376  }
7377 
7385  public function updateRefIndex($table, $id)
7386  {
7388  $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
7390  $refIndexObj->setWorkspaceId($this->BE_USER->workspace);
7391  }
7392  $refIndexObj->updateRefIndexTable($table, $id);
7393  }
7394 
7395  /*********************************************
7396  *
7397  * Misc functions
7398  *
7399  ********************************************/
7409  public function getSortNumber($table, $uid, $pid)
7410  {
7411  if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
7412  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
7413  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
7414  $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7415  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7416 
7417  $queryBuilder
7418  ->select($sortRow, 'pid', 'uid')
7419  ->from($table);
7420 
7421  // Sorting number is in the top
7422  if ($pid >= 0) {
7423  // Fetches the first record under this pid
7424  $row = $queryBuilder
7425  ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7426  ->orderBy($sortRow, 'ASC')
7427  ->setMaxResults(1)
7428  ->execute()
7429  ->fetch();
7430  // There was an element
7431  if (!empty($row)) {
7432  // The top record was the record it self, so we return its current sortnumber
7433  if ($row['uid'] == $uid) {
7434  return $row[$sortRow];
7435  }
7436  // If the pages sortingnumber < 1 we must resort the records under this pid
7437  if ($row[$sortRow] < 1) {
7438  $this->resorting($table, $pid, $sortRow, 0);
7439  // First sorting number after resorting
7440  return $this->sortIntervals;
7441  }
7442  // Sorting number between current top element and zero
7443  return floor($row[$sortRow] / 2);
7444  }
7445  // No pages, so we choose the default value as sorting-number
7446  // First sorting number if no elements.
7447  return $this->sortIntervals;
7448  }
7449  // Sorting number is inside the list
7450  // Fetches the record which is supposed to be the prev record
7451  $row = $queryBuilder
7452  ->where($queryBuilder->expr()->eq(
7453  'uid',
7454  $queryBuilder->createNamedParameter(abs($pid), \PDO::PARAM_INT)
7455  ))
7456  ->execute()
7457  ->fetch();
7458 
7459  // There was a record
7460  if (!empty($row)) {
7461  // Look, if the record UID happens to be an offline record. If so, find its live version. Offline uids will be used when a page is versionized as "branch" so this is when we must correct - otherwise a pid of "-1" and a wrong sort-row number is returned which we don't want.
7462  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $row['uid'], $sortRow . ',pid,uid')) {
7463  $row = $lookForLiveVersion;
7464  }
7465  // Fetch move placeholder, since it might point to a new page in the current workspace
7466  if ($movePlaceholder = BackendUtility::getMovePlaceholder($table, $row['uid'], 'uid,pid,' . $sortRow)) {
7467  $row = $movePlaceholder;
7468  }
7469  // If the record should be inserted after itself, keep the current sorting information:
7470  if ((int)$row['uid'] === (int)$uid) {
7471  $sortNumber = $row[$sortRow];
7472  } else {
7473  $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7474  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7475 
7476  $subResults = $queryBuilder
7477  ->select($sortRow, 'pid', 'uid')
7478  ->from($table)
7479  ->where(
7480  $queryBuilder->expr()->eq(
7481  'pid',
7482  $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT)
7483  ),
7484  $queryBuilder->expr()->gte(
7485  $sortRow,
7486  $queryBuilder->createNamedParameter($row[$sortRow], \PDO::PARAM_INT)
7487  )
7488  )
7489  ->orderBy($sortRow, 'ASC')
7490  ->setMaxResults(2)
7491  ->execute()
7492  ->fetchAll();
7493  // Fetches the next record in order to calculate the in-between sortNumber
7494  // There was a record afterwards
7495  if (count($subResults) === 2) {
7496  // There was a record afterwards, fetch that
7497  $subrow = array_pop($subResults);
7498  // The sortNumber is found in between these values
7499  $sortNumber = $row[$sortRow] + floor(($subrow[$sortRow] - $row[$sortRow]) / 2);
7500  // The sortNumber happened NOT to be between the two surrounding numbers, so we'll have to resort the list
7501  if ($sortNumber <= $row[$sortRow] || $sortNumber >= $subrow[$sortRow]) {
7502  // By this special param, resorting reserves and returns the sortnumber after the uid
7503  $sortNumber = $this->resorting($table, $row['pid'], $sortRow, $row['uid']);
7504  }
7505  } else {
7506  // If after the last record in the list, we just add the sortInterval to the last sortvalue
7507  $sortNumber = $row[$sortRow] + $this->sortIntervals;
7508  }
7509  }
7510  return ['pid' => $row['pid'], 'sortNumber' => $sortNumber];
7511  }
7512  if ($this->enableLogging) {
7513  $propArr = $this->getRecordProperties($table, $uid);
7514  // OK, don't insert $propArr['event_pid'] here...
7515  $this->log($table, $uid, 4, 0, 1, 'Attempt to move record \'%s\' (%s) to after a non-existing record (uid=%s)', 1, [$propArr['header'], $table . ':' . $uid, abs($pid)], $propArr['pid']);
7516  }
7517  // There MUST be a page or else this cannot work
7518  return false;
7519  }
7520  return null;
7521  }
7522 
7535  public function resorting($table, $pid, $sortRow, $return_SortNumber_After_This_Uid)
7536  {
7537  if ($GLOBALS['TCA'][$table] && $sortRow && $GLOBALS['TCA'][$table]['ctrl']['sortby'] == $sortRow) {
7538  $returnVal = 0;
7539  $intervals = $this->sortIntervals;
7540  $i = $intervals * 2;
7541  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7542  $queryBuilder = $connection->createQueryBuilder();
7543  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7544 
7545  $result = $queryBuilder
7546  ->select('uid')
7547  ->from($table)
7548  ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7549  ->orderBy($sortRow, 'ASC')
7550  ->addOrderBy('uid', 'ASC')
7551  ->execute();
7552  while ($row = $result->fetch()) {
7553  $uid = (int)$row['uid'];
7554  if ($uid) {
7555  $connection->update($table, [$sortRow => $i], ['uid' => (int)$uid]);
7556  // This is used to return a sortingValue if the list is resorted because of inserting records inside the list and not in the top
7557  if ($uid == $return_SortNumber_After_This_Uid) {
7558  $i = $i + $intervals;
7559  $returnVal = $i;
7560  }
7561  } else {
7562  die('Fatal ERROR!! No Uid at resorting.');
7563  }
7564  $i = $i + $intervals;
7565  }
7566  return $returnVal;
7567  }
7568  return null;
7569  }
7570 
7581  protected function getPreviousLocalizedRecordUid($table, $uid, $pid, $language)
7582  {
7583  $previousLocalizedRecordUid = $uid;
7584  if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
7585  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
7586  $select = [$sortRow, 'pid', 'uid'];
7587  // For content elements, we also need the colPos
7588  if ($table === 'tt_content') {
7589  $select[] = 'colPos';
7590  }
7591  // Get the sort value of the default language record
7592  $row = BackendUtility::getRecord($table, $uid, implode(',', $select));
7593  if (is_array($row)) {
7594  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7595  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7596 
7597  $queryBuilder
7598  ->select(...$select)
7599  ->from($table)
7600  ->where(
7601  $queryBuilder->expr()->eq(
7602  'pid',
7603  $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
7604  ),
7605  $queryBuilder->expr()->eq(
7606  'sys_language_uid',
7607  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
7608  ),
7609  $queryBuilder->expr()->lt(
7610  $sortRow,
7611  $queryBuilder->createNamedParameter($row[$sortRow], \PDO::PARAM_INT)
7612  )
7613  )
7614  ->orderBy($sortRow, 'DESC')
7615  ->setMaxResults(1);
7616  if ($table === 'tt_content') {
7617  $queryBuilder
7618  ->andWhere(
7619  $queryBuilder->expr()->eq(
7620  'colPos',
7621  $queryBuilder->createNamedParameter($row['colPos'], \PDO::PARAM_INT)
7622  )
7623  );
7624  }
7625  // If there is an element, find its localized record in specified localization language
7626  if ($previousRow = $queryBuilder->execute()->fetch()) {
7627  $previousLocalizedRecord = BackendUtility::getRecordLocalization($table, $previousRow['uid'], $language);
7628  if (is_array($previousLocalizedRecord[0])) {
7629  $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
7630  }
7631  }
7632  }
7633  }
7634  return $previousLocalizedRecordUid;
7635  }
7636 
7645  public function setTSconfigPermissions($fieldArray, $TSConfig_p)
7646  {
7647  if ((string)$TSConfig_p['userid'] !== '') {
7648  $fieldArray['perms_userid'] = (int)$TSConfig_p['userid'];
7649  }
7650  if ((string)$TSConfig_p['groupid'] !== '') {
7651  $fieldArray['perms_groupid'] = (int)$TSConfig_p['groupid'];
7652  }
7653  if ((string)$TSConfig_p['user'] !== '') {
7654  $fieldArray['perms_user'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['user']) ? $TSConfig_p['user'] : $this->assemblePermissions($TSConfig_p['user']);
7655  }
7656  if ((string)$TSConfig_p['group'] !== '') {
7657  $fieldArray['perms_group'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['group']) ? $TSConfig_p['group'] : $this->assemblePermissions($TSConfig_p['group']);
7658  }
7659  if ((string)$TSConfig_p['everybody'] !== '') {
7660  $fieldArray['perms_everybody'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['everybody']) ? $TSConfig_p['everybody'] : $this->assemblePermissions($TSConfig_p['everybody']);
7661  }
7662  return $fieldArray;
7663  }
7664 
7672  public function newFieldArray($table)
7673  {
7674  $fieldArray = [];
7675  if (is_array($GLOBALS['TCA'][$table]['columns'])) {
7676  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $content) {
7677  if (isset($this->defaultValues[$table][$field])) {
7678  $fieldArray[$field] = $this->defaultValues[$table][$field];
7679  } elseif (isset($content['config']['default'])) {
7680  $fieldArray[$field] = $content['config']['default'];
7681  }
7682  }
7683  }
7684  // Set default permissions for a page.
7685  if ($table === 'pages') {
7686  $fieldArray['perms_userid'] = $this->userid;
7687  $fieldArray['perms_groupid'] = (int)$this->BE_USER->firstMainGroup;
7688  $fieldArray['perms_user'] = $this->assemblePermissions($this->defaultPermissions['user']);
7689  $fieldArray['perms_group'] = $this->assemblePermissions($this->defaultPermissions['group']);
7690  $fieldArray['perms_everybody'] = $this->assemblePermissions($this->defaultPermissions['everybody']);
7691  }
7692  return $fieldArray;
7693  }
7694 
7701  public function addDefaultPermittedLanguageIfNotSet($table, &$incomingFieldArray)
7702  {
7703  // Checking languages:
7704  if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
7705  if (!isset($incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
7706  // Language field must be found in input row - otherwise it does not make sense.
7707  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
7708  ->getQueryBuilderForTable('sys_language');
7709  $queryBuilder->getRestrictions()
7710  ->removeAll()
7711  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7712  $queryBuilder
7713  ->select('uid')
7714  ->from('sys_language')
7715  ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)));
7716  $rows = array_merge([['uid' => 0]], $queryBuilder->execute()->fetchAll(), [['uid' => -1]]);
7717  foreach ($rows as $r) {
7718  if ($this->BE_USER->checkLanguageAccess($r['uid'])) {
7719  $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $r['uid'];
7720  break;
7721  }
7722  }
7723  }
7724  }
7725  }
7726 
7734  public function overrideFieldArray($table, $data)
7735  {
7736  if (is_array($this->overrideValues[$table])) {
7737  $data = array_merge($data, $this->overrideValues[$table]);
7738  }
7739  return $data;
7740  }
7741 
7751  public function compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
7752  {
7753  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7754  $queryBuilder = $connection->createQueryBuilder();
7755  $queryBuilder->getRestrictions()->removeAll();
7756  $currentRecord = $queryBuilder->select('*')
7757  ->from($table)
7758  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7759  ->execute()
7760  ->fetch();
7761  // If the current record exists (which it should...), begin comparison:
7762  if (is_array($currentRecord)) {
7763  $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
7764  $columnRecordTypes = [];
7765  foreach ($currentRecord as $columnName => $_) {
7766  $columnRecordTypes[$columnName] = '';
7767  $type = $tableDetails->getColumn($columnName)->getType();
7768  if ($type instanceof IntegerType) {
7769  $columnRecordTypes[$columnName] = 'int';
7770  }
7771  }
7772  // Unset the fields which are similar:
7773  foreach ($fieldArray as $col => $val) {
7774  $fieldConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
7775  $isNullField = (!empty($fieldConfiguration['eval']) && GeneralUtility::inList($fieldConfiguration['eval'], 'null'));
7776 
7777  // Unset fields if stored and submitted values are equal - except the current field holds MM relations.
7778  // In general this avoids to store superfluous data which also will be visualized in the editing history.
7779  if (!$fieldConfiguration['MM'] && $this->isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $columnRecordTypes[$col], $isNullField)) {
7780  unset($fieldArray[$col]);
7781  } else {
7782  if (!isset($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col])) {
7783  $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
7784  } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
7785  $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
7786  }
7787  if (!isset($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col])) {
7788  $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
7789  } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
7790  $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
7791  }
7792  }
7793  }
7794  } else {
7795  // If the current record does not exist this is an error anyways and we just return an empty array here.
7796  $fieldArray = [];
7797  }
7798  return $fieldArray;
7799  }
7800 
7813  protected function isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull = false)
7814  {
7815  // No NULL values are allowed, this is the regular behaviour.
7816  // Thus, check whether strings are the same or whether integer values are empty ("0" or "").
7817  if (!$allowNull) {
7818  $result = (string)$submittedValue === (string)$storedValue || $storedType === 'int' && (int)$storedValue === (int)$submittedValue;
7819  // Null values are allowed, but currently there's a real (not NULL) value.
7820  // Thus, ensure no NULL value was submitted and fallback to the regular behaviour.
7821  } elseif ($storedValue !== null) {
7822  $result = (
7823  $submittedValue !== null
7824  && $this->isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, false)
7825  );
7826  // Null values are allowed, and currently there's a NULL value.
7827  // Thus, check whether a NULL value was submitted.
7828  } else {
7829  $result = ($submittedValue === null);
7830  }
7831 
7832  return $result;
7833  }
7834 
7842  public function assemblePermissions($string)
7843  {
7844  $keyArr = GeneralUtility::trimExplode(',', $string, true);
7845  $value = 0;
7846  foreach ($keyArr as $key) {
7847  if ($key && isset($this->pMap[$key])) {
7848  $value |= $this->pMap[$key];
7849  }
7850  }
7851  return $value;
7852  }
7853 
7861  public function rmComma($input)
7862  {
7864  return rtrim($input, ',');
7865  }
7866 
7873  public function convNumEntityToByteValue($input)
7874  {
7875  $token = md5(microtime());
7876  $parts = explode($token, preg_replace('/(&#([0-9]+);)/', $token . '\\2' . $token, $input));
7877  foreach ($parts as $k => $v) {
7878  if ($k % 2) {
7879  $v = (int)$v;
7880  // Just to make sure that control bytes are not converted.
7881  if ($v > 32) {
7882  $parts[$k] = chr((int)$v);
7883  }
7884  }
7885  }
7886  return implode('', $parts);
7887  }
7888 
7896  public function destPathFromUploadFolder($folder)
7897  {
7899  return PATH_site . $folder;
7900  }
7901 
7907  public function disableDeleteClause()
7908  {
7909  $this->disableDeleteClause = true;
7910  }
7911 
7918  public function deleteClause($table)
7919  {
7920  // Returns the proper delete-clause if any for a table from TCA
7921  if (!$this->disableDeleteClause && $GLOBALS['TCA'][$table]['ctrl']['delete']) {
7922  return ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0';
7923  }
7924  return '';
7925  }
7926 
7933  {
7934  if (!$this->disableDeleteClause) {
7935  $restrictions->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7936  }
7937  }
7938 
7947  protected function getOriginalParentOfRecord($table, $uid)
7948  {
7949  if (isset(self::$recordPidsForDeletedRecords[$table][$uid])) {
7950  return self::$recordPidsForDeletedRecords[$table][$uid];
7951  }
7952  list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
7953  return [$parentUid];
7954  }
7955 
7962  public function getTCEMAIN_TSconfig($tscPID)
7963  {
7964  if (!isset($this->cachedTSconfig[$tscPID])) {
7965  $this->cachedTSconfig[$tscPID] = $this->BE_USER->getTSConfig('TCEMAIN', BackendUtility::getPagesTSconfig($tscPID));
7966  }
7967  return $this->cachedTSconfig[$tscPID]['properties'];
7968  }
7969 
7978  public function getTableEntries($table, $TSconfig)
7979  {
7980  $tA = is_array($TSconfig['table.'][$table . '.']) ? $TSconfig['table.'][$table . '.'] : [];
7981  $dA = is_array($TSconfig['default.']) ? $TSconfig['default.'] : [];
7983  return $dA;
7984  }
7985 
7993  public function getPID($table, $uid)
7994  {
7995  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7996  $queryBuilder->getRestrictions()
7997  ->removeAll();
7998  $queryBuilder->select('pid')
7999  ->from($table)
8000  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
8001  if ($row = $queryBuilder->execute()->fetch()) {
8002  return $row['pid'];
8003  }
8004  return false;
8005  }
8006 
8011  public function dbAnalysisStoreExec()
8012  {
8013  foreach ($this->dbAnalysisStore as $action) {
8014  $id = BackendUtility::wsMapId($action[4], MathUtility::canBeInterpretedAsInteger($action[2]) ? $action[2] : $this->substNEWwithIDs[$action[2]]);
8015  if ($id) {
8016  $action[0]->writeMM($action[1], $id, $action[3]);
8017  }
8018  }
8019  }
8020 
8024  public function removeRegisteredFiles()
8025  {
8026  foreach ($this->removeFilesStore as $file) {
8027  if (@is_file($file)) {
8028  $file = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
8029  $file->delete();
8030  }
8031  }
8032  }
8033 
8044  public function int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
8045  {
8046  if ($counter) {
8047  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
8048  $restrictions = $queryBuilder->getRestrictions()->removeAll();
8049  $this->addDeleteRestriction($restrictions);
8050  $queryBuilder
8051  ->select('uid')
8052  ->from('pages')
8053  ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
8054  ->orderBy('sorting', 'DESC');
8055  if (!$this->admin) {
8056  $queryBuilder->andWhere($this->BE_USER->getPagePermsClause($this->pMap['show']));
8057  }
8058  if ((int)$this->BE_USER->workspace === 0) {
8059  $queryBuilder->andWhere(
8060  $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8061  );
8062  } else {
8063  $queryBuilder->andWhere($queryBuilder->expr()->in(
8064  't3ver_wsid',
8065  $queryBuilder->createNamedParameter([0, $this->BE_USER->workspace], Connection::PARAM_INT_ARRAY)
8066  ));
8067  }
8068  $result = $queryBuilder->execute();
8069 
8070  $pages = [];
8071  while ($row = $result->fetch()) {
8072  $pages[$row['uid']] = $row;
8073  }
8074 
8075  // Resolve placeholders of workspace versions
8076  if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
8077  $pages = array_reverse(
8078  $this->resolveVersionedRecords(
8079  'pages',
8080  'uid',
8081  'sorting',
8082  array_keys($pages)
8083  ),
8084  true
8085  );
8086  }
8087 
8088  foreach ($pages as $page) {
8089  if ($page['uid'] != $rootID) {
8090  $CPtable[$page['uid']] = $pid;
8091  // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
8092  if ($counter - 1) {
8093  $CPtable = $this->int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
8094  }
8095  }
8096  }
8097  }
8098  return $CPtable;
8099  }
8100 
8106  public function compileAdminTables()
8107  {
8108  return array_keys($GLOBALS['TCA']);
8109  }
8110 
8117  public function fixUniqueInPid($table, $uid)
8118  {
8119  if (empty($GLOBALS['TCA'][$table])) {
8120  return;
8121  }
8122 
8123  $curData = $this->recordInfo($table, $uid, '*');
8124  $newData = [];
8125  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
8126  if ($conf['config']['type'] === 'input' && (string)$curData[$field] !== '') {
8127  $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
8128  if (in_array('uniqueInPid', $evalCodesArray, true)) {
8129  $newV = $this->getUnique($table, $field, $curData[$field], $uid, $curData['pid']);
8130  if ((string)$newV !== (string)$curData[$field]) {
8131  $newData[$field] = $newV;
8132  }
8133  }
8134  }
8135  }
8136  // IF there are changed fields, then update the database
8137  if (!empty($newData)) {
8138  $this->updateDB($table, $uid, $newData);
8139  }
8140  }
8141 
8153  public function fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData = [])
8154  {
8155  if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields']) {
8156  $prevData = $this->recordInfo($table, $prevUid, '*');
8157  $theFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true);
8158  foreach ($theFields as $field) {
8159  if ($GLOBALS['TCA'][$table]['columns'][$field] && ($update || !isset($newData[$field]))) {
8160  $newData[$field] = $prevData[$field];
8161  }
8162  }
8163  if ($update && !empty($newData)) {
8164  $this->updateDB($table, $uid, $newData);
8165  }
8166  }
8167  return $newData;
8168  }
8169 
8176  public function extFileFields($table)
8177  {
8178  $listArr = [];
8179  if (isset($GLOBALS['TCA'][$table]['columns'])) {
8180  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) {
8181  if ($configArr['config']['type'] === 'group' && ($configArr['config']['internal_type'] === 'file' || $configArr['config']['internal_type'] === 'file_reference')) {
8182  $listArr[] = $field;
8183  }
8184  }
8185  }
8186  return $listArr;
8187  }
8188 
8201  protected function castReferenceValue($value, array $configuration)
8202  {
8203  if ((string)$value !== '') {
8204  return $value;
8205  }
8206 
8207  if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) {
8208  return 0;
8209  }
8210 
8211  if (array_key_exists('default', $configuration)) {
8212  return $configuration['default'];
8213  }
8214 
8215  return $value;
8216  }
8217 
8224  public function isReferenceField($conf)
8225  {
8226  return $conf['type'] === 'group' && $conf['internal_type'] === 'db' || $conf['type'] === 'select' && $conf['foreign_table'];
8227  }
8228 
8236  public function getInlineFieldType($conf)
8237  {
8238  if ($conf['type'] !== 'inline' || !$conf['foreign_table']) {
8239  return false;
8240  }
8241  if ($conf['foreign_field']) {
8242  // The reference to the parent is stored in a pointer field in the child record
8243  return 'field';
8244  }
8245  if ($conf['MM']) {
8246  // Regular MM intermediate table is used to store data
8247  return 'mm';
8248  }
8249  // An item list (separated by comma) is stored (like select type is doing)
8250  return 'list';
8251  }
8252 
8264  public function getCopyHeader($table, $pid, $field, $value, $count, $prevTitle = '')
8265  {
8266  // Set title value to check for:
8267  if ($count) {
8268  $checkTitle = $value . rtrim(' ' . sprintf($this->prependLabel($table), $count));
8269  } else {
8270  $checkTitle = $value;
8271  }
8272  // Do check:
8273  if ($prevTitle != $checkTitle || $count < 100) {
8274  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8275  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
8276  $rowCount = $queryBuilder
8277  ->count('uid')
8278  ->from($table)
8279  ->where(
8280  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
8281  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($checkTitle, \PDO::PARAM_STR))
8282  )
8283  ->execute()
8284  ->fetchColumn(0);
8285  if ($rowCount) {
8286  return $this->getCopyHeader($table, $pid, $field, $value, $count + 1, $checkTitle);
8287  }
8288  }
8289  // Default is to just return the current input title if no other was returned before:
8290  return $checkTitle;
8291  }
8292 
8300  public function prependLabel($table)
8301  {
8302  if (is_object($GLOBALS['LANG'])) {
8303  $label = $GLOBALS['LANG']->sL($GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
8304  } else {
8305  list($label) = explode('|', $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
8306  }
8307  return $label;
8308  }
8309 
8317  public function resolvePid($table, $pid)
8318  {
8319  $pid = (int)$pid;
8320  if ($pid < 0) {
8321  $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8322  $query->getRestrictions()
8323  ->removeAll();
8324  $row = $query
8325  ->select('pid')
8326  ->from($table)
8327  ->where($query->expr()->eq('uid', $query->createNamedParameter(abs($pid), \PDO::PARAM_INT)))
8328  ->execute()
8329  ->fetch();
8330  // Look, if the record UID happens to be an offline record. If so, find its live version.
8331  // Offline uids will be used when a page is versionized as "branch" so this is when we
8332  // must correct - otherwise a pid of "-1" and a wrong sort-row number
8333  // is returned which we don't want.
8334  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, abs($pid), 'pid')) {
8335  $row = $lookForLiveVersion;
8336  }
8337  $pid = (int)$row['pid'];
8338  }
8339  return $pid;
8340  }
8341 
8349  public function clearPrefixFromValue($table, $value)
8350  {
8351  $regex = '/' . sprintf(quotemeta($this->prependLabel($table)), '[0-9]*') . '$/';
8352  return @preg_replace($regex, '', $value);
8353  }
8354 
8363  public function extFileFunctions($table, $field, $filelist, $func = null)
8364  {
8365  if ($func !== null) {
8366  GeneralUtility::deprecationLog('Parameter 4 of DataHandler::extFileFunctions() has been removed in TYPO3 v8, and will be removed in TYPO3 v9.');
8367  if ($func !== 'deleteAll') {
8368  return;
8369  }
8370  }
8371  $uploadFolder = $GLOBALS['TCA'][$table]['columns'][$field]['config']['uploadfolder'];
8372  if ($uploadFolder && trim($filelist) && $GLOBALS['TCA'][$table]['columns'][$field]['config']['internal_type'] === 'file') {
8373  $uploadPath = PATH_site . $uploadFolder;
8374  $fileArray = GeneralUtility::trimExplode(',', $filelist, true);
8375  foreach ($fileArray as $theFile) {
8376  $theFileFullPath = $uploadPath . '/' . $theFile;
8377  if (@is_file($theFileFullPath)) {
8378  $this->getResourceFactory()->retrieveFileOrFolderObject($theFileFullPath)->delete();
8379  } elseif ($this->enableLogging) {
8380  $this->log($table, 0, 3, 0, 100, 'Delete: Referenced file that was supposed to be deleted together with it\'s record didn\'t exist');
8381  }
8382  }
8383  }
8384  }
8385 
8393  public function noRecordsFromUnallowedTables($inList)
8394  {
8396  return $this->checkForRecordsFromDisallowedTables(GeneralUtility::intExplode(',', $inList, true));
8397  }
8398 
8406  protected function checkForRecordsFromDisallowedTables(array $pageIds)
8407  {
8408  if ($this->admin) {
8409  return true;
8410  }
8411 
8412  if (!empty($pageIds)) {
8413  foreach ($GLOBALS['TCA'] as $table => $_) {
8414  $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8415  $query->getRestrictions()
8416  ->removeAll()
8417  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8418  $count = $query->count('uid')
8419  ->from($table)
8420  ->where($query->expr()->in(
8421  'pid',
8422  $query->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
8423  ))
8424  ->execute()
8425  ->fetchColumn(0);
8426  if ($count && ($this->tableReadOnly($table) || !$this->checkModifyAccessList($table))) {
8427  return false;
8428  }
8429  }
8430  }
8431  return true;
8432  }
8433 
8441  public function isRecordCopied($table, $uid)
8442  {
8443  // If the record was copied:
8444  if (isset($this->copyMappingArray[$table][$uid])) {
8445  return true;
8446  }
8447  if (isset($this->copyMappingArray[$table]) && in_array($uid, array_values($this->copyMappingArray[$table]))) {
8448  return true;
8449  }
8450  return false;
8451  }
8452 
8453  /******************************
8454  *
8455  * Clearing cache
8456  *
8457  ******************************/
8458 
8469  public function registerRecordIdForPageCacheClearing($table, $uid, $pid = null)
8470  {
8471  if (!is_array(static::$recordsToClearCacheFor[$table])) {
8472  static::$recordsToClearCacheFor[$table] = [];
8473  }
8474  static::$recordsToClearCacheFor[$table][] = (int)$uid;
8475  if ($pid !== null) {
8476  if (!is_array(static::$recordPidsForDeletedRecords[$table])) {
8477  static::$recordPidsForDeletedRecords[$table] = [];
8478  }
8479  static::$recordPidsForDeletedRecords[$table][$uid][] = (int)$pid;
8480  }
8481  }
8482 
8486  protected function processClearCacheQueue()
8487  {
8488  $tagsToClear = [];
8489  $clearCacheCommands = [];
8490 
8491  foreach (static::$recordsToClearCacheFor as $table => $uids) {
8492  foreach (array_unique($uids) as $uid) {
8493  if (!isset($GLOBALS['TCA'][$table]) || $uid <= 0) {
8494  return;
8495  }
8496  // For move commands we may get more then 1 parent.
8497  $pageUids = $this->getOriginalParentOfRecord($table, $uid);
8498  foreach ($pageUids as $originalParent) {
8499  list($tagsToClearFromPrepare, $clearCacheCommandsFromPrepare)
8500  = $this->prepareCacheFlush($table, $uid, $originalParent);
8501  $tagsToClear = array_merge($tagsToClear, $tagsToClearFromPrepare);
8502  $clearCacheCommands = array_merge($clearCacheCommands, $clearCacheCommandsFromPrepare);
8503  }
8504  }
8505  }
8506 
8508  $cacheManager = $this->getCacheManager();
8509  $cacheManager->flushCachesInGroupByTags('pages', array_keys($tagsToClear));
8510 
8511  // Filter duplicate cache commands from cacheQueue
8512  $clearCacheCommands = array_unique($clearCacheCommands);
8513  // Execute collected clear cache commands from page TSConfig
8514  foreach ($clearCacheCommands as $command) {
8515  $this->clear_cacheCmd($command);
8516  }
8517 
8518  // Reset the cache clearing array
8519  static::$recordsToClearCacheFor = [];
8520 
8521  // Reset the original pid array
8522  static::$recordPidsForDeletedRecords = [];
8523  }
8524 
8534  protected function prepareCacheFlush($table, $uid, $pid)
8535  {
8536  $tagsToClear = [];
8537  $clearCacheCommands = [];
8538  $pageUid = 0;
8539  // Get Page TSconfig relevant:
8540  $TSConfig = $this->getTCEMAIN_TSconfig($pid);
8541  if (empty($TSConfig['clearCache_disable'])) {
8542  // If table is "pages":
8543  $pageIdsThatNeedCacheFlush = [];
8544  if ($table === 'pages' || $table === 'pages_language_overlay') {
8545  if ($table === 'pages_language_overlay') {
8546  $pageUid = $this->getPID($table, $uid);
8547  } else {
8548  $pageUid = $uid;
8549  }
8550 
8551  // Builds list of pages on the SAME level as this page (siblings)
8552  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
8553  $queryBuilder = $connectionPool->getQueryBuilderForTable('pages');
8554  $queryBuilder->getRestrictions()
8555  ->removeAll()
8556  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8557  $siblings = $queryBuilder
8558  ->select('A.pid AS pid', 'B.uid AS uid')
8559  ->from('pages', 'A')
8560  ->from('pages', 'B')
8561  ->where(
8562  $queryBuilder->expr()->eq('A.uid', $queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)),
8563  $queryBuilder->expr()->eq('B.pid', $queryBuilder->quoteIdentifier('A.pid')),
8564  $queryBuilder->expr()->gte('A.pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8565  )
8566  ->execute();
8567 
8568  $pid_tmp = 0;
8569  while ($row_tmp = $siblings->fetch()) {
8570  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
8571  $pid_tmp = (int)$row_tmp['pid'];
8572  // Add children as well:
8573  if ($TSConfig['clearCache_pageSiblingChildren']) {
8574  $siblingChildrenQuery = $connectionPool->getQueryBuilderForTable('pages');
8575  $siblingChildrenQuery->getRestrictions()
8576  ->removeAll()
8577  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8578  $siblingChildren = $siblingChildrenQuery
8579  ->select('uid')
8580  ->from('pages')
8581  ->where($siblingChildrenQuery->expr()->eq(
8582  'pid',
8583  $siblingChildrenQuery->createNamedParameter($row_tmp['uid'], \PDO::PARAM_INT)
8584  ))
8585  ->execute();
8586  while ($row_tmp2 = $siblingChildren->fetch()) {
8587  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
8588  }
8589  }
8590  }
8591  // Finally, add the parent page as well:
8592  if ($pid_tmp > 0) {
8593  $pageIdsThatNeedCacheFlush[] = $pid_tmp;
8594  }
8595  // Add grand-parent as well:
8596  if ($TSConfig['clearCache_pageGrandParent']) {
8597  $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8598  $parentQuery->getRestrictions()
8599  ->removeAll()
8600  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8601  $row_tmp = $parentQuery
8602  ->select('pid')
8603  ->from('pages')
8604  ->where($parentQuery->expr()->eq(
8605  'uid',
8606  $parentQuery->createNamedParameter($pid_tmp, \PDO::PARAM_INT)
8607  ))
8608  ->execute()
8609  ->fetch();
8610  if (!empty($row_tmp)) {
8611  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
8612  }
8613  }
8614  } else {
8615  // For other tables than "pages", delete cache for the records "parent page".
8616  $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
8617  }
8618  // Call pre-processing function for clearing of cache for page ids:
8619  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'])) {
8620  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] as $funcName) {
8621  $_params = ['pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()'];
8622  // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8623  GeneralUtility::callUserFunction($funcName, $_params, $this);
8624  }
8625  }
8626  // Delete cache for selected pages:
8627  foreach ($pageIdsThatNeedCacheFlush as $pageId) {
8628  // Workspaces always use "-1" as the page id which do not
8629  // point to real pages and caches at all. Flushing caches for
8630  // those records does not make sense and decreases performance
8631  if ($pageId >= 0) {
8632  $tagsToClear['pageId_' . $pageId] = true;
8633  }
8634  }
8635  // Queue delete cache for current table and record
8636  $tagsToClear[$table] = true;
8637  $tagsToClear[$table . '_' . $uid] = true;
8638  }
8639  // Clear cache for pages entered in TSconfig:
8640  if (!empty($TSConfig['clearCacheCmd'])) {
8641  $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
8642  $clearCacheCommands = array_unique($commands);
8643  }
8644  // Call post processing function for clear-cache:
8645  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'])) {
8646  $_params = ['table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig];
8647  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] as $_funcRef) {
8648  GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8649  }
8650  }
8651  return [
8652  $tagsToClear,
8653  $clearCacheCommands
8654  ];
8655  }
8656 
8695  public function clear_cacheCmd($cacheCmd)
8696  {
8697  if (is_object($this->BE_USER)) {
8698  $this->BE_USER->writelog(3, 1, 0, 0, 'User %s has cleared the cache (cacheCmd=%s)', [$this->BE_USER->user['username'], $cacheCmd]);
8699  }
8700  switch (strtolower($cacheCmd)) {
8701  case 'pages':
8702  if ($this->admin || $this->BE_USER->getTSConfigVal('options.clearCache.pages')) {
8703  $this->getCacheManager()->flushCachesInGroup('pages');
8704  }
8705  break;
8706  case 'all':
8707  // allow to clear all caches if the TS config option is enabled or the option is not explicitly
8708  // disabled for admins (which could clear all caches by default). The latter option is useful
8709  // for big production sites where it should be possible to restrict the cache clearing for some admins.
8710  if ($this->BE_USER->getTSConfigVal('options.clearCache.all') || ($this->admin && $this->BE_USER->getTSConfigVal('options.clearCache.all') !== '0')) {
8711  $this->getCacheManager()->flushCaches();
8712  GeneralUtility::makeInstance(ConnectionPool::class)
8713  ->getConnectionForTable('cache_treelist')
8714  ->truncate('cache_treelist');
8715 
8716  // Delete Opcode Cache
8717  GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
8718  }
8719  break;
8720  case 'temp_cached':
8721  case 'system':
8723  'Calling clear_cacheCmd() with arguments \'temp_cached\' or \'system\', using'
8724  . ' the TS config option \'options.clearCache.system\' has been deprecated.'
8725  );
8726  if ($this->admin || $this->BE_USER->getTSConfigVal('options.clearCache.system')) {
8727  $this->getCacheManager()->flushCachesInGroup('system');
8728  }
8729  break;
8730  }
8731 
8732  $tagsToFlush = [];
8733  // Clear cache for a page ID!
8734  if (MathUtility::canBeInterpretedAsInteger($cacheCmd)) {
8735  $list_cache = [$cacheCmd];
8736  // Call pre-processing function for clearing of cache for page ids:
8737  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'])) {
8738  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] as $funcName) {
8739  $_params = ['pageIdArray' => &$list_cache, 'cacheCmd' => $cacheCmd, 'functionID' => 'clear_cacheCmd()'];
8740  // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8741  GeneralUtility::callUserFunction($funcName, $_params, $this);
8742  }
8743  }
8744  // Delete cache for selected pages:
8745  if (is_array($list_cache)) {
8746  foreach ($list_cache as $pageId) {
8747  $tagsToFlush[] = 'pageId_' . (int)$pageId;
8748  }
8749  }
8750  }
8751  // flush cache by tag
8752  if (GeneralUtility::isFirstPartOfStr(strtolower($cacheCmd), 'cachetag:')) {
8753  $cacheTag = substr($cacheCmd, 9);
8754  $tagsToFlush[] = $cacheTag;
8755  }
8756  // process caching framwork operations
8757  if (!empty($tagsToFlush)) {
8758  $this->getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush);
8759  }
8760 
8761  // Call post processing function for clear-cache:
8762  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'])) {
8763  $_params = ['cacheCmd' => strtolower($cacheCmd)];
8764  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] as $_funcRef) {
8765  GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8766  }
8767  }
8768  }
8769 
8770  /*****************************
8771  *
8772  * Logging
8773  *
8774  *****************************/
8790  public function log($table, $recuid, $action, $recpid, $error, $details, $details_nr = -1, $data = [], $event_pid = -1, $NEWid = '')
8791  {
8792  if (!$this->enableLogging) {
8793  return 0;
8794  }
8795  // Type value for DataHandler
8796  $type = 1;
8797  if (!$this->storeLogMessages) {
8798  $details = '';
8799  }
8800  if ($error > 0) {
8801  $detailMessage = $details;
8802  if (is_array($data)) {
8803  $detailMessage = vsprintf($details, $data);
8804  }
8805  $this->errorLog[] = '[' . $type . '.' . $action . '.' . $details_nr . ']: ' . $detailMessage;
8806  }
8807  return $this->BE_USER->writelog($type, $action, $error, $details_nr, $details, $data, $table, $recuid, $recpid, $event_pid, $NEWid);
8808  }
8809 
8818  public function newlog($message, $error = 0)
8819  {
8820  return $this->log('', 0, 0, 0, $error, $message, -1);
8821  }
8822 
8834  public function newlog2($message, $table, $uid, $pid = null, $error = 0)
8835  {
8836  if (is_null($pid)) {
8837  $propArr = $this->getRecordProperties($table, $uid);
8838  $pid = $propArr['pid'];
8839  }
8840  return $this->log($table, $uid, 0, 0, $error, $message, -1, [], $this->eventPid($table, $uid, $pid));
8841  }
8842 
8846  public function printLogErrorMessages()
8847  {
8848  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
8849  $queryBuilder->getRestrictions()->removeAll();
8850  $result = $queryBuilder
8851  ->select('*')
8852  ->from('sys_log')
8853  ->where(
8854  $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
8855  $queryBuilder->expr()->lt('action', $queryBuilder->createNamedParameter(256, \PDO::PARAM_INT)),
8856  $queryBuilder->expr()->eq(
8857  'userid',
8858  $queryBuilder->createNamedParameter($this->BE_USER->user['uid'], \PDO::PARAM_INT)
8859  ),
8860  $queryBuilder->expr()->eq(
8861  'tstamp',
8862  $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
8863  ),
8864  $queryBuilder->expr()->neq('error', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8865  )
8866  ->execute();
8867 
8868  while ($row = $result->fetch()) {
8869  $log_data = unserialize($row['log_data']);
8870  $msg = $row['error'] . ': ' . sprintf($row['details'], $log_data[0], $log_data[1], $log_data[2], $log_data[3], $log_data[4]);
8872  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', FlashMessage::ERROR, true);
8874  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
8875  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
8876  $defaultFlashMessageQueue->enqueue($flashMessage);
8877  }
8878  }
8879 
8880  /*****************************
8881  *
8882  * Internal (do not use outside Core!)
8883  *
8884  *****************************/
8885 
8895  public function insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
8896  {
8897  $result = $fieldArray;
8898  foreach ($fieldArray as $field => $value) {
8899  switch ($GLOBALS['TCA'][$table]['columns'][$field]['config']['type']) {
8900  case 'inline':
8901  if ($GLOBALS['TCA'][$table]['columns'][$field]['config']['foreign_field']) {
8903  $result[$field] = count(GeneralUtility::trimExplode(',', $value, true));
8904  }
8905  }
8906  break;
8907  }
8908  }
8909  return $result;
8910  }
8911 
8920  public function hasDeletedRecord($tableName, $uid)
8921  {
8922  return
8923  !empty($this->deletedRecords[$tableName])
8924  && in_array($uid, $this->deletedRecords[$tableName])
8925  ;
8926  }
8927 
8935  public function getAutoVersionId($table, $id)
8936  {
8937  $result = null;
8938  if (isset($this->autoVersionIdMap[$table][$id])) {
8939  $result = $this->autoVersionIdMap[$table][$id];
8940  }
8941  return $result;
8942  }
8943 
8951  protected function overlayAutoVersionId($table, $id)
8952  {
8953  $autoVersionId = $this->getAutoVersionId($table, $id);
8954  if (is_null($autoVersionId) === false) {
8955  $id = $autoVersionId;
8956  }
8957  return $id;
8958  }
8959 
8965  protected function addNewValuesToRemapStackChildIds(array $idValues)
8966  {
8967  foreach ($idValues as $idValue) {
8968  if (strpos($idValue, 'NEW') === 0) {
8969  $this->remapStackChildIds[$idValue] = true;
8970  }
8971  }
8972  }
8973 
8984  protected function resolveVersionedRecords($tableName, $fieldNames, $sortingField, array $liveIds)
8985  {
8986  $connection = GeneralUtility::makeInstance(ConnectionPool::class)
8987  ->getConnectionForTable($tableName);
8988  $sortingStatement = !empty($sortingField)
8989  ? [$connection->quoteIdentifier($sortingField)]
8990  : null;
8992  $resolver = GeneralUtility::makeInstance(
8993  PlainDataResolver::class,
8994  $tableName,
8995  $liveIds,
8996  $sortingStatement
8997  );
8998 
8999  $resolver->setWorkspaceId($this->BE_USER->workspace);
9000  $resolver->setKeepDeletePlaceholder(false);
9001  $resolver->setKeepMovePlaceholder(false);
9002  $resolver->setKeepLiveIds(true);
9003  $recordIds = $resolver->get();
9004 
9005  $records = [];
9006  foreach ($recordIds as $recordId) {
9007  $records[$recordId] = BackendUtility::getRecord($tableName, $recordId, $fieldNames);
9008  }
9009 
9010  return $records;
9011  }
9012 
9020  protected function getOuterMostInstance()
9021  {
9022  if (!isset($this->outerMostInstance)) {
9023  $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS));
9024  foreach ($stack as $stackItem) {
9025  if (isset($stackItem['object']) && $stackItem['object'] instanceof self) {
9026  $this->outerMostInstance = $stackItem['object'];
9027  break;
9028  }
9029  }
9030  }
9031  return $this->outerMostInstance;
9032  }
9033 
9041  public function isOuterMostInstance()
9042  {
9043  return $this->getOuterMostInstance() === $this;
9044  }
9045 
9051  protected function getRuntimeCache()
9052  {
9053  return $this->getCacheManager()->getCache('cache_runtime');
9054  }
9055 
9064  protected function isNestedElementCallRegistered($table, $id, $identifier)
9065  {
9066  $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
9067  return isset($nestedElementCalls[$identifier][$table][$id]);
9068  }
9069 
9078  protected function registerNestedElementCall($table, $id, $identifier)
9079  {
9080  $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
9081  $nestedElementCalls[$identifier][$table][$id] = true;
9082  $this->runtimeCache->set($this->cachePrefixNestedElementCalls, $nestedElementCalls);
9083  }
9084 
9088  protected function resetNestedElementCalls()
9089  {
9090  $this->runtimeCache->remove($this->cachePrefixNestedElementCalls);
9091  }
9092 
9104  protected function isElementToBeDeleted($table, $id)
9105  {
9106  $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
9107  return isset($elementsToBeDeleted[$table][$id]);
9108  }
9109 
9115  protected function registerElementsToBeDeleted()
9116  {
9117  $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
9118  $this->runtimeCache->set('core-datahandler-elementsToBeDeleted', array_merge($elementsToBeDeleted, $this->getCommandMapElements('delete')));
9119  }
9120 
9126  protected function resetElementsToBeDeleted()
9127  {
9128  $this->runtimeCache->remove('core-datahandler-elementsToBeDeleted');
9129  }
9130 
9138  protected function unsetElementsToBeDeleted(array $elements)
9139  {
9140  $elements = ArrayUtility::arrayDiffAssocRecursive($elements, $this->getCommandMapElements('delete'));
9141  foreach ($elements as $key => $value) {
9142  if (empty($value)) {
9143  unset($elements[$key]);
9144  }
9145  }
9146  return $elements;
9147  }
9148 
9155  protected function getCommandMapElements($needle)
9156  {
9157  $elements = [];
9158  foreach ($this->cmdmap as $tableName => $idArray) {
9159  foreach ($idArray as $id => $commandArray) {
9160  foreach ($commandArray as $command => $value) {
9161  if ($value && $command == $needle) {
9162  $elements[$tableName][$id] = true;
9163  }
9164  }
9165  }
9166  }
9167  return $elements;
9168  }
9169 
9174  protected function controlActiveElements()
9175  {
9176  if (!empty($this->control['active'])) {
9177  $this->setNullValues(
9178  $this->control['active'],
9179  $this->datamap
9180  );
9181  }
9182  }
9183 
9192  protected function setNullValues(array $active, array &$haystack)
9193  {
9194  foreach ($active as $key => $value) {
9195  // Nested data is processes recursively
9196  if (is_array($value)) {
9197  $this->setNullValues(
9198  $value,
9199  $haystack[$key]
9200  );
9201  } elseif ($value == 0) {
9202  // Field has not been activated in the user interface,
9203  // thus a NULL value shall be stored in the database
9204  $haystack[$key] = null;
9205  }
9206  }
9207  }
9208 
9218  protected function postProcessDatabaseInsert(Connection $connection, string $tableName, int $suggestedUid): int
9219  {
9220  if ($suggestedUid !== 0 && $connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
9221  $this->postProcessPostgresqlInsert($connection, $tableName);
9222  // The last inserted id on postgresql is actually the last value generated by the sequence.
9223  // On a forced UID insert this might not be the actual value or the sequence might not even
9224  // have generated a value yet.
9225  // Return the actual ID we forced on insert as a surrogate.
9226  return $suggestedUid;
9227  }
9228  if ($connection->getDatabasePlatform() instanceof SQLServerPlatform) {
9229  return $this->postProcessSqlServerInsert($connection, $tableName);
9230  }
9231  $id = $connection->lastInsertId($tableName);
9232  return (int)$id;
9233  }
9234 
9249  protected function postProcessSqlServerInsert(Connection $connection, string $tableName): int
9250  {
9251  $id = $connection->lastInsertId($tableName);
9252  if (!((int)$id > 0)) {
9253  $table = $connection->quoteIdentifier($tableName);
9254  $result = $connection->executeQuery('SELECT IDENT_CURRENT(\'' . $table . '\') AS id')->fetch();
9255  if (isset($result['id']) && $result['id'] > 0) {
9256  $id = $result['id'];
9257  }
9258  }
9259  return (int)$id;
9260  }
9261 
9270  protected function postProcessPostgresqlInsert(Connection $connection, string $tableName)
9271  {
9272  $queryBuilder = $connection->createQueryBuilder();
9273  $queryBuilder->getRestrictions()->removeAll();
9274  $row = $queryBuilder->select('PGT.schemaname', 'S.relname', 'C.attname', 'T.relname AS tablename')
9275  ->from('pg_class', 'S')
9276  ->from('pg_depend', 'D')
9277  ->from('pg_class', 'T')
9278  ->from('pg_attribute', 'C')
9279  ->from('pg_tables', 'PGT')
9280  ->where(
9281  $queryBuilder->expr()->eq('S.relkind', $queryBuilder->quote('S')),
9282  $queryBuilder->expr()->eq('S.oid', $queryBuilder->quoteIdentifier('D.objid')),
9283  $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('T.oid')),
9284  $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('C.attrelid')),
9285  $queryBuilder->expr()->eq('D.refobjsubid', $queryBuilder->quoteIdentifier('C.attnum')),
9286  $queryBuilder->expr()->eq('T.relname', $queryBuilder->quoteIdentifier('PGT.tablename')),
9287  $queryBuilder->expr()->eq('PGT.tablename', $queryBuilder->quote($tableName))
9288  )
9289  ->setMaxResults(1)
9290  ->execute()
9291  ->fetch();
9292 
9293  if ($row !== false) {
9294  $connection->exec(
9295  sprintf(
9296  'SELECT SETVAL(%s, COALESCE(MAX(%s), 0)+1, FALSE) FROM %s',
9297  $connection->quote($row['schemaname'] . '.' . $row['relname']),
9298  $connection->quoteIdentifier($row['attname']),
9299  $connection->quoteIdentifier($row['schemaname'] . '.' . $row['tablename'])
9300  )
9301  );
9302  }
9303  }
9304 
9311  protected function getFieldEvalCacheIdentifier($additionalIdentifier)
9312  {
9313  return 'core-datahandler-eval-' . md5($additionalIdentifier);
9314  }
9315 
9319  protected function createRelationHandlerInstance()
9320  {
9321  $isVersionLoaded = ExtensionManagementUtility::isLoaded('version');
9322  $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
9323  $relationHandler->setWorkspaceId($this->BE_USER->workspace);
9324  $relationHandler->setUseLiveReferenceIds($isVersionLoaded);
9325  $relationHandler->setUseLiveParentIds($isVersionLoaded);
9326  return $relationHandler;
9327  }
9328 
9334  protected function getCacheManager()
9335  {
9336  return GeneralUtility::makeInstance(CacheManager::class);
9337  }
9338 
9344  protected function getResourceFactory()
9345  {
9346  return ResourceFactory::getInstance();
9347  }
9348 }
version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
copySpecificPage($uid, $destPid, $copyTablesArray, $first=false)
updateFlexFormData($flexFormId, array $modifications)
getPlaceholderTitleForTableLabel($table, $placeholderContent=null)
static getTSconfig_pidValue($table, $uid, $pid)
extFileFunctions($table, $field, $filelist, $func=null)
static getPagesTSconfig($id, $rootLine=null, $returnPartArray=false)
int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
checkValueForText($value, $tcaFieldConf, $table, $id, $realPid, $field)
static getRecordWSOL( $table, $uid, $fields=' *', $where='', $useDeleteClause=true, $unsetMovePointers=false)
moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf)
checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $uploadedFiles, $tscPID, array $additionalData=null)
applyFiltersToValues(array $tcaFieldConfiguration, array $values)
copyL10nOverlayRecords($table, $uid, $destPid, $first=false, $overrideValues=[], $excludeFields='')
insertDB($table, $id, $fieldArray, $newVersion=false, $suggestedUid=0, $dontSetNewIdIndex=false)
doesBranchExist($inList, $pid, $perms, $recurse)
static intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
static mkdir_deep($directory, $deepDirectory='')
checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field)
static getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields=' *')
checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $pid, $field)
hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray)
getPreviousLocalizedRecordUid($table, $uid, $pid, $language)
getUnique($table, $field, $value, $id, $newPid=0)
static setValueByPath(array $array, $path, $value, $delimiter='/')
checkValue_text_Eval($value, $evalArray, $is_in)
getUniqueCountStatement(string $value, string $table, string $field, int $uid, int $pid)
insertNewCopyVersion($table, $fieldArray, $realPid)
doesRecordExist_pageLookUp($id, $perms, $columns=['uid'])
checkStoredRecord($table, $id, $fieldArray, $action)
checkValue($table, $field, $value, $id, $status, $realPid, $tscPID)
debug($variable='', $name=' *variable *', $line=' *line *', $file=' *file *', $recursiveDepth=3, $debugLevel='E_DEBUG')
static isFirstPartOfStr($str, $partStr)
addDeleteRestriction(QueryRestrictionContainerInterface $restrictions)
postProcessSqlServerInsert(Connection $connection, string $tableName)
isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull=false)
static callUserFunction($funcName, &$params, &$ref, $_='', $errorMode=0)
setTSconfigPermissions($fieldArray, $TSConfig_p)
moveRecord_procFields($table, $uid, $destPid)
castReferenceValue($value, array $configuration)
lastInsertId($tableName=null, string $fieldName='uid')
Definition: Connection.php:439
setNullValues(array $active, array &$haystack)
deleteVersionsForRecord($table, $uid, $forceHardDelete)
registerNestedElementCall($table, $id, $identifier)
checkValueForInternalReferences(array $res, $value, $tcaFieldConf, $table, $id, $field)
static purgeComputedPropertiesFromRecord(array $record)
static getFileAbsFileName($filename, $_=null, $_2=null)
moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
deleteSpecificPage($uid, $forceHardDelete=false)
getRecordsWithSameValue($tableName, $uid, $fieldName, $value, $pageId=0)
checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData=null)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
start($data, $cmd, $altUserObject=null)
deleteRecord($table, $uid, $noRecordCheck=false, $forceHardDelete=false, $undeleteRecord=false)
static workspaceOL($table, &$row, $wsid=-99, $unsetMovePointers=false)
copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language=0, array $workspaceOptions=[])
resorting($table, $pid, $sortRow, $return_SortNumber_After_This_Uid)
copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $_3, $workspaceOptions)
static makeInstance($className,... $constructorArguments)
addRemapAction($table, $id, array $callback, array $arguments)
insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
$fields
Definition: pages.php:4
fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData=[])
updateDB($table, $id, $fieldArray)
static selectVersionsOfRecord( $table, $uid, $fields=' *', $workspace=0, $includeDeletedRecords=false, $row=null)
static getInlineLocalizationMode($table, $fieldOrConfig)
static split_fileref($fileNameWithPath)
checkValue_input_Eval($value, $evalArray, $is_in)
copyRecord_raw($table, $uid, $pid, $overrideArray=[], array $workspaceOptions=[])
static fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch=false)
getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
isTableAllowedForThisPage($page_uid, $checkTable)
static instance(array $dataMap, BackendUserAuthentication $backendUser)
compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
registerRecordIdForPageCacheClearing($table, $uid, $pid=null)
deleteEl($table, $uid, $noRecordCheck=false, $forceHardDelete=false)
checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field)
getRecordProperties($table, $id, $noWSOL=false)
static getRecordLocalization($table, $uid, $language, $andWhereClause='')
checkRecordInsertAccess($insertTable, $pid, $action=1)
log($table, $recuid, $action, $recpid, $error, $details, $details_nr=-1, $data=[], $event_pid=-1, $NEWid='')
checkValue_checkMax($tcaFieldConf, $valueArray)
static removeArrayEntryByValue(array $array, $cmpValue)
addDefaultPermittedLanguageIfNotSet($table, &$incomingFieldArray)
static getRecordTitle($table, $row, $prep=false, $forceResult=true)
triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions=false)
deleteRecord_procFields($table, $uid, $undeleteRecord=false)
doesPageHaveUnallowedTables($page_uid, $doktype)
static mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys=true, $includeEmptyValues=true, $enableUnsetFeature=true)
static xml2array($string, $NSprefix='', $reportDocTag=false)
getCopyHeader($table, $pid, $field, $value, $count, $prevTitle='')
checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData=null)
_ACTION_FLEX_FORMdata(&$valueArray, $actionCMDs)
static formatSize($sizeInBytes, $labels='', $base=0)
newlog2($message, $table, $uid, $pid=null, $error=0)
static getLiveVersionOfRecord($table, $uid, $fields=' *')
static getMovePlaceholder($table, $uid, $fields=' *', $workspace=null)
process_uploads_traverseArray(&$outputArr, $inputArr, $keyToSet)
checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructure, $pParams, $callBackFunc='', array $workspaceOptions=[])
moveRecord_raw($table, $uid, $destPid)
static getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
checkRecordUpdateAccess($table, $id, $data=false, $hookObjectsArr=null)
checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field)
recordInfo($table, $id, $fieldList)
fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID)
checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions=[])
copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap)
postProcessDatabaseInsert(Connection $connection, string $tableName, int $suggestedUid)
recordInfoWithPermissionCheck(string $table, int $id, $perms, string $fieldList=' *')
remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
versionizeRecord($table, $id, $label, $delete=false)
isNestedElementCallRegistered($table, $id, $identifier)
static upload_copy_move($source, $destination)
static arrayDiffAssocRecursive(array $array1, array $array2)
postProcessPostgresqlInsert(Connection $connection, string $tableName)
getFieldEvalCacheIdentifier($additionalIdentifier)