TYPO3 CMS  TYPO3_7-6
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 
37 
52 {
53  // *********************
54  // Public variables you can configure before using the class:
55  // *********************
62  public $storeLogMessages = true;
63 
69  public $enableLogging = true;
70 
77  public $reverseOrder = false;
78 
85  public $checkSimilar = true;
86 
94  public $stripslashes_values = true;
95 
102  public $checkStoredRecords = true;
103 
110 
117  public $deleteTree = false;
118 
124  public $neverHideAtCopy = false;
125 
131  public $isImporting = false;
132 
139 
147 
155  protected $useTransOrigPointerField = true;
156 
164  public $updateModeL10NdiffData = true;
165 
173 
181 
188  public $bypassFileHandling = false;
189 
197 
204  public $copyWhichTables = '*';
205 
213  public $copyTree = 0;
214 
223  public $defaultValues = [];
224 
232  public $overrideValues = [];
233 
241  public $alternativeFileName = [];
242 
248  public $alternativeFilePath = [];
249 
257  public $data_disableFields = [];
258 
268  public $suggestedInsertUids = [];
269 
276  public $callBackObj;
277 
278  // *********************
279  // Internal variables (mapping arrays) which can be used (read-only) from outside
280  // *********************
286  public $autoVersionIdMap = [];
287 
293  public $substNEWwithIDs = [];
294 
301 
307  public $newRelatedIDs = [];
308 
315 
321  protected $deletedRecords = [];
322 
328  public $copiedFileMap = [];
329 
335  public $RTEmagic_copyIndex = [];
336 
342  public $errorLog = [];
343 
349  public $pagetreeRefreshFieldsFromPages = ['pid', 'sorting', 'deleted', 'hidden', 'title', 'doktype', 'is_siteroot', 'fe_group', 'nav_hide', 'nav_title', 'module', 'starttime', 'endtime', 'content_from_pid'];
350 
356  public $pagetreeNeedsRefresh = false;
357 
358  // *********************
359  // Internal Variables, do not touch.
360  // *********************
361 
362  // Variables set in init() function:
363 
369  public $BE_USER;
370 
376  public $userid;
377 
383  public $username;
384 
390  public $admin;
391 
398  'user' => 'show,edit,delete,new,editcontent',
399  'group' => 'show,edit,new,editcontent',
400  'everybody' => ''
401  ];
402 
408  protected $excludedTablesAndFields = [];
409 
415 
422  protected $control = [];
423 
429  public $datamap = [];
430 
436  public $cmdmap = [];
437 
443  protected $mmHistoryRecords = [];
444 
450  protected $historyRecords = [];
451 
452  // Internal static:
458  public $pMap = [
459  'show' => 1,
460  // 1st bit
461  'edit' => 2,
462  // 2nd bit
463  'delete' => 4,
464  // 3rd bit
465  'new' => 8,
466  // 4th bit
467  'editcontent' => 16
468  ];
469 
475  public $sortIntervals = 256;
476 
477  // Internal caching arrays
484 
491 
498 
504  public $isInWebMount_Cache = [];
505 
511  public $cachedTSconfig = [];
512 
518  public $pageCache = [];
519 
525  public $checkWorkspaceCache = [];
526 
527  // Other arrays:
533  public $dbAnalysisStore = [];
534 
540  public $removeFilesStore = [];
541 
547  public $uploadedFileArray = [];
548 
554  public $registerDBList = [];
555 
561  public $registerDBPids = [];
562 
573  public $copyMappingArray = [];
574 
580  public $remapStack = [];
581 
588  public $remapStackRecords = [];
589 
595  protected $remapStackChildIds = [];
596 
602  protected $remapStackActions = [];
603 
609  protected $remapStackRefIndex = [];
610 
616  public $updateRefIndexStack = [];
617 
624  public $callFromImpExp = false;
625 
631  public $newIndexMap = [];
632 
633  // Various
640  public $fileFunc;
641 
648 
654  public $autoVersioningUpdate = false;
655 
661  protected $disableDeleteClause = false;
662 
667 
672 
679  protected $outerMostInstance = null;
680 
686  protected static $recordsToClearCacheFor = [];
687 
694  protected static $recordPidsForDeletedRecords = [];
695 
702 
708  protected $runtimeCache = null;
709 
715  protected $cachePrefixNestedElementCalls = 'core-datahandler-nestedElementCalls-';
716 
720  public function __construct()
721  {
722  $this->databaseConnection = $GLOBALS['TYPO3_DB'];
723  $this->runtimeCache = $this->getRuntimeCache();
724  }
725 
729  public function setControl(array $control)
730  {
731  $this->control = $control;
732  }
733 
744  public function start($data, $cmd, $altUserObject = null)
745  {
746  // Initializing BE_USER
747  $this->BE_USER = is_object($altUserObject) ? $altUserObject : $GLOBALS['BE_USER'];
748  $this->userid = $this->BE_USER->user['uid'];
749  $this->username = $this->BE_USER->user['username'];
750  $this->admin = $this->BE_USER->user['admin'];
751  if ($this->BE_USER->uc['recursiveDelete']) {
752  $this->deleteTree = 1;
753  }
754  if ($GLOBALS['TYPO3_CONF_VARS']['BE']['explicitConfirmationOfTranslation'] && $this->updateModeL10NdiffData === true) {
755  $this->updateModeL10NdiffData = false;
756  }
757  // Initializing default permissions for pages
758  $defaultPermissions = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions'];
759  if (isset($defaultPermissions['user'])) {
760  $this->defaultPermissions['user'] = $defaultPermissions['user'];
761  }
762  if (isset($defaultPermissions['group'])) {
763  $this->defaultPermissions['group'] = $defaultPermissions['group'];
764  }
765  if (isset($defaultPermissions['everybody'])) {
766  $this->defaultPermissions['everybody'] = $defaultPermissions['everybody'];
767  }
768  // generates the excludelist, based on TCA/exclude-flag and non_exclude_fields for the user:
769  if (!$this->admin) {
770  $this->excludedTablesAndFields = array_flip($this->getExcludeListArray());
771  }
772  // Setting the data and cmd arrays
773  if (is_array($data)) {
774  reset($data);
775  $this->datamap = $data;
776  }
777  if (is_array($cmd)) {
778  reset($cmd);
779  $this->cmdmap = $cmd;
780  }
781  }
782 
790  public function setMirror($mirror)
791  {
792  if (!is_array($mirror)) {
793  return;
794  }
795 
796  foreach ($mirror as $table => $uid_array) {
797  if (!isset($this->datamap[$table])) {
798  continue;
799  }
800 
801  foreach ($uid_array as $id => $uidList) {
802  if (!isset($this->datamap[$table][$id])) {
803  continue;
804  }
805 
806  $theIdsInArray = GeneralUtility::trimExplode(',', $uidList, true);
807  foreach ($theIdsInArray as $copyToUid) {
808  $this->datamap[$table][$copyToUid] = $this->datamap[$table][$id];
809  }
810  }
811  }
812  }
813 
820  public function setDefaultsFromUserTS($userTS)
821  {
822  if (!is_array($userTS)) {
823  return;
824  }
825 
826  foreach ($userTS as $k => $v) {
827  $k = substr($k, 0, -1);
828  if (!$k || !is_array($v) || !isset($GLOBALS['TCA'][$k])) {
829  continue;
830  }
831 
832  if (is_array($this->defaultValues[$k])) {
833  $this->defaultValues[$k] = array_merge($this->defaultValues[$k], $v);
834  } else {
835  $this->defaultValues[$k] = $v;
836  }
837  }
838  }
839 
847  public function process_uploads($postFiles)
848  {
849  if (!is_array($postFiles)) {
850  return;
851  }
852 
853  // Editing frozen:
854  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
855  if ($this->enableLogging) {
856  $this->newlog('All editing in this workspace has been frozen!', 1);
857  }
858  return;
859  }
860  $subA = reset($postFiles);
861  if (is_array($subA)) {
862  if (is_array($subA['name']) && is_array($subA['type']) && is_array($subA['tmp_name']) && is_array($subA['size'])) {
863  // Initialize the uploadedFilesArray:
864  $this->uploadedFileArray = [];
865  // For each entry:
866  foreach ($subA as $key => $values) {
867  $this->process_uploads_traverseArray($this->uploadedFileArray, $values, $key);
868  }
869  } else {
870  $this->uploadedFileArray = $subA;
871  }
872  }
873  }
874 
885  public function process_uploads_traverseArray(&$outputArr, $inputArr, $keyToSet)
886  {
887  if (is_array($inputArr)) {
888  foreach ($inputArr as $key => $value) {
889  $this->process_uploads_traverseArray($outputArr[$key], $inputArr[$key], $keyToSet);
890  }
891  } else {
892  $outputArr[$keyToSet] = $inputArr;
893  }
894  }
895 
896  /*********************************************
897  *
898  * HOOKS
899  *
900  *********************************************/
915  public function hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray)
916  {
917  // Process hook directly:
918  if (!isset($this->remapStackRecords[$table][$id])) {
919  foreach ($hookObjectsArr as $hookObj) {
920  if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
921  $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);
922  }
923  }
924  } else {
925  $this->remapStackRecords[$table][$id]['processDatamap_afterDatabaseOperations'] = [
926  'status' => $status,
927  'fieldArray' => $fieldArray,
928  'hookObjectsArr' => $hookObjectsArr
929  ];
930  }
931  }
932 
941  {
942  if (!isset($this->checkModifyAccessListHookObjects)) {
943  $this->checkModifyAccessListHookObjects = [];
944  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'])) {
945  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] as $classData) {
946  $hookObject = GeneralUtility::getUserObj($classData);
947  if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
948  throw new \UnexpectedValueException($classData . ' must implement interface ' . DataHandlerCheckModifyAccessListHookInterface::class, 1251892472);
949  }
950  $this->checkModifyAccessListHookObjects[] = $hookObject;
951  }
952  }
953  }
955  }
956 
957  /*********************************************
958  *
959  * PROCESSING DATA
960  *
961  *********************************************/
968  public function process_datamap()
969  {
970  $this->controlActiveElements();
971 
972  // Keep versionized(!) relations here locally:
973  $registerDBList = [];
975  $this->datamap = $this->unsetElementsToBeDeleted($this->datamap);
976  // Editing frozen:
977  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
978  if ($this->enableLogging) {
979  $this->newlog('All editing in this workspace has been frozen!', 1);
980  }
981  return false;
982  }
983  // First prepare user defined objects (if any) for hooks which extend this function:
984  $hookObjectsArr = [];
985  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'])) {
986  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] as $classRef) {
987  $hookObject = GeneralUtility::getUserObj($classRef);
988  if (method_exists($hookObject, 'processDatamap_beforeStart')) {
989  $hookObject->processDatamap_beforeStart($this);
990  }
991  $hookObjectsArr[] = $hookObject;
992  }
993  }
994  // 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.
995  $orderOfTables = [];
996  // Set pages first.
997  if (isset($this->datamap['pages'])) {
998  $orderOfTables[] = 'pages';
999  }
1000  $orderOfTables = array_unique(array_merge($orderOfTables, array_keys($this->datamap)));
1001  // Process the tables...
1002  foreach ($orderOfTables as $table) {
1003  // Check if
1004  // - table is set in $GLOBALS['TCA'],
1005  // - table is NOT readOnly
1006  // - the table is set with content in the data-array (if not, there's nothing to process...)
1007  // - permissions for tableaccess OK
1008  $modifyAccessList = $this->checkModifyAccessList($table);
1009  if ($this->enableLogging && !$modifyAccessList) {
1010  $this->log($table, 0, 2, 0, 1, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
1011  }
1012  if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->datamap[$table]) || !$modifyAccessList) {
1013  continue;
1014  }
1015 
1016  if ($this->reverseOrder) {
1017  $this->datamap[$table] = array_reverse($this->datamap[$table], 1);
1018  }
1019  // For each record from the table, do:
1020  // $id is the record uid, may be a string if new records...
1021  // $incomingFieldArray is the array of fields
1022  foreach ($this->datamap[$table] as $id => $incomingFieldArray) {
1023  if (!is_array($incomingFieldArray)) {
1024  continue;
1025  }
1026  $theRealPid = null;
1027 
1028  // Handle native date/time fields
1029  $dateTimeFormats = $this->databaseConnection->getDateTimeFormats($table);
1030  foreach ($GLOBALS['TCA'][$table]['columns'] as $column => $config) {
1031  if (isset($incomingFieldArray[$column])) {
1032  if (isset($config['config']['dbType']) && ($config['config']['dbType'] === 'date' || $config['config']['dbType'] === 'datetime')) {
1033  $emptyValue = $dateTimeFormats[$config['config']['dbType']]['empty'];
1034  $format = $dateTimeFormats[$config['config']['dbType']]['format'];
1035  $incomingFieldArray[$column] = $incomingFieldArray[$column] && $incomingFieldArray[$column] !== $emptyValue ? gmdate($format, $incomingFieldArray[$column]) : $emptyValue;
1036  }
1037  }
1038  }
1039  // Hook: processDatamap_preProcessFieldArray
1040  foreach ($hookObjectsArr as $hookObj) {
1041  if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
1042  $hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
1043  }
1044  }
1045  // ******************************
1046  // Checking access to the record
1047  // ******************************
1048  $createNewVersion = false;
1049  $recordAccess = false;
1050  $old_pid_value = '';
1051  $this->autoVersioningUpdate = false;
1052  // Is it a new record? (Then Id is a string)
1054  // Get a fieldArray with default values
1055  $fieldArray = $this->newFieldArray($table);
1056  // A pid must be set for new records.
1057  if (isset($incomingFieldArray['pid'])) {
1058  // $value = the pid
1059  $pid_value = $incomingFieldArray['pid'];
1060  // Checking and finding numerical pid, it may be a string-reference to another value
1061  $OK = 1;
1062  // If a NEW... id
1063  if (strstr($pid_value, 'NEW')) {
1064  if ($pid_value[0] === '-') {
1065  $negFlag = -1;
1066  $pid_value = substr($pid_value, 1);
1067  } else {
1068  $negFlag = 1;
1069  }
1070  // Trying to find the correct numerical value as it should be mapped by earlier processing of another new record.
1071  if (isset($this->substNEWwithIDs[$pid_value])) {
1072  if ($negFlag === 1) {
1073  $old_pid_value = $this->substNEWwithIDs[$pid_value];
1074  }
1075  $pid_value = (int)($negFlag * $this->substNEWwithIDs[$pid_value]);
1076  } else {
1077  $OK = 0;
1078  }
1079  }
1080  $pid_value = (int)$pid_value;
1081  // The $pid_value is now the numerical pid at this point
1082  if ($OK) {
1083  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
1084  // Points to a page on which to insert the element, possibly in the top of the page
1085  if ($pid_value >= 0) {
1086  // If this table is sorted we better find the top sorting number
1087  if ($sortRow) {
1088  $fieldArray[$sortRow] = $this->getSortNumber($table, 0, $pid_value);
1089  }
1090  // The numerical pid is inserted in the data array
1091  $fieldArray['pid'] = $pid_value;
1092  } else {
1093  // points to another record before ifself
1094  // If this table is sorted we better find the top sorting number
1095  if ($sortRow) {
1096  // Because $pid_value is < 0, getSortNumber returns an array
1097  $tempArray = $this->getSortNumber($table, 0, $pid_value);
1098  $fieldArray['pid'] = $tempArray['pid'];
1099  $fieldArray[$sortRow] = $tempArray['sortNumber'];
1100  } else {
1101  // Here we fetch the PID of the record that we point to...
1102  $tempdata = $this->recordInfo($table, abs($pid_value), 'pid');
1103  $fieldArray['pid'] = $tempdata['pid'];
1104  }
1105  }
1106  }
1107  }
1108  $theRealPid = $fieldArray['pid'];
1109  // Now, check if we may insert records on this pid.
1110  if ($theRealPid >= 0) {
1111  // Checks if records can be inserted on this $pid.
1112  $recordAccess = $this->checkRecordInsertAccess($table, $theRealPid);
1113  if ($recordAccess) {
1114  $this->addDefaultPermittedLanguageIfNotSet($table, $incomingFieldArray);
1115  $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $incomingFieldArray, true);
1116  if (!$recordAccess) {
1117  if ($this->enableLogging) {
1118  $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', 1);
1119  }
1120  } elseif (!$this->bypassWorkspaceRestrictions) {
1121  // Workspace related processing:
1122  // If LIVE records cannot be created in the current PID due to workspace restrictions, prepare creation of placeholder-record
1123  if ($res = $this->BE_USER->workspaceAllowLiveRecordsInPID($theRealPid, $table)) {
1124  if ($res < 0) {
1125  $recordAccess = false;
1126  if ($this->enableLogging) {
1127  $this->newlog('Stage for versioning root point and users access level did not allow for editing', 1);
1128  }
1129  }
1130  } else {
1131  // So, if no live records were allowed, we have to create a new version of this record:
1132  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1133  $createNewVersion = true;
1134  } else {
1135  $recordAccess = false;
1136  if ($this->enableLogging) {
1137  $this->newlog('Record could not be created in this workspace in this branch', 1);
1138  }
1139  }
1140  }
1141  }
1142  }
1143  } else {
1144  debug('Internal ERROR: pid should not be less than zero!');
1145  }
1146  // Yes new record, change $record_status to 'insert'
1147  $status = 'new';
1148  } else {
1149  // Nope... $id is a number
1150  $fieldArray = [];
1151  $recordAccess = $this->checkRecordUpdateAccess($table, $id, $incomingFieldArray, $hookObjectsArr);
1152  if (!$recordAccess) {
1153  if ($this->enableLogging) {
1154  $propArr = $this->getRecordProperties($table, $id);
1155  $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']);
1156  }
1157  continue;
1158  }
1159  // Next check of the record permissions (internals)
1160  $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $id);
1161  if (!$recordAccess) {
1162  if ($this->enableLogging) {
1163  $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', 1);
1164  }
1165  } else {
1166  // Here we fetch the PID of the record that we point to...
1167  $tempdata = $this->recordInfo($table, $id, 'pid' . ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? ',t3ver_wsid,t3ver_stage' : ''));
1168  $theRealPid = $tempdata['pid'];
1169  // Use the new id of the versionized record we're trying to write to:
1170  // (This record is a child record of a parent and has already been versionized.)
1171  if ($this->autoVersionIdMap[$table][$id]) {
1172  // For the reason that creating a new version of this record, automatically
1173  // created related child records (e.g. "IRRE"), update the accordant field:
1174  $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1175  // Use the new id of the copied/versionized record:
1176  $id = $this->autoVersionIdMap[$table][$id];
1177  $recordAccess = true;
1178  $this->autoVersioningUpdate = true;
1179  } elseif (!$this->bypassWorkspaceRestrictions && ($errorCode = $this->BE_USER->workspaceCannotEditRecord($table, $tempdata))) {
1180  $recordAccess = false;
1181  // Versioning is required and it must be offline version!
1182  // Check if there already is a workspace version
1183  $WSversion = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid,t3ver_oid');
1184  if ($WSversion) {
1185  $id = $WSversion['uid'];
1186  $recordAccess = true;
1187  } elseif ($this->BE_USER->workspaceAllowAutoCreation($table, $id, $theRealPid)) {
1188  // new version of a record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1189  $this->pagetreeNeedsRefresh = true;
1190 
1192  $tce = GeneralUtility::makeInstance(__CLASS__);
1193  $tce->stripslashes_values = false;
1194  $tce->enableLogging = $this->enableLogging;
1195  // Setting up command for creating a new version of the record:
1196  $cmd = [];
1197  $cmd[$table][$id]['version'] = [
1198  'action' => 'new',
1199  'treeLevels' => -1,
1200  // Default is to create a version of the individual records... element versioning that is.
1201  'label' => 'Auto-created for WS #' . $this->BE_USER->workspace
1202  ];
1203  $tce->start([], $cmd);
1204  $tce->process_cmdmap();
1205  $this->errorLog = array_merge($this->errorLog, $tce->errorLog);
1206  // If copying was successful, share the new uids (also of related children):
1207  if ($tce->copyMappingArray[$table][$id]) {
1208  foreach ($tce->copyMappingArray as $origTable => $origIdArray) {
1209  foreach ($origIdArray as $origId => $newId) {
1210  $this->uploadedFileArray[$origTable][$newId] = $this->uploadedFileArray[$origTable][$origId];
1211  $this->autoVersionIdMap[$origTable][$origId] = $newId;
1212  }
1213  }
1214  ArrayUtility::mergeRecursiveWithOverrule($this->RTEmagic_copyIndex, $tce->RTEmagic_copyIndex);
1215  // See where RTEmagic_copyIndex is used inside fillInFieldArray() for more information...
1216  // Update registerDBList, that holds the copied relations to child records:
1217  $registerDBList = array_merge($registerDBList, $tce->registerDBList);
1218  // For the reason that creating a new version of this record, automatically
1219  // created related child records (e.g. "IRRE"), update the accordant field:
1220  $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1221  // Use the new id of the copied/versionized record:
1222  $id = $this->autoVersionIdMap[$table][$id];
1223  $recordAccess = true;
1224  $this->autoVersioningUpdate = true;
1225  } elseif ($this->enableLogging) {
1226  $this->newlog('Could not be edited in offline workspace in the branch where found (failure state: \'' . $errorCode . '\'). Auto-creation of version failed!', 1);
1227  }
1228  } elseif ($this->enableLogging) {
1229  $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);
1230  }
1231  }
1232  }
1233  // The default is 'update'
1234  $status = 'update';
1235  }
1236  // If access was granted above, proceed to create or update record:
1237  if (!$recordAccess) {
1238  continue;
1239  }
1240 
1241  // Here the "pid" is set IF NOT the old pid was a string pointing to a place in the subst-id array.
1242  list($tscPID) = BackendUtility::getTSCpid($table, $id, $old_pid_value ? $old_pid_value : $fieldArray['pid']);
1243  if ($status === 'new' && $table === 'pages') {
1244  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
1245  if (isset($TSConfig['permissions.']) && is_array($TSConfig['permissions.'])) {
1246  $fieldArray = $this->setTSconfigPermissions($fieldArray, $TSConfig['permissions.']);
1247  }
1248  }
1249  // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1250  $fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1251  $newVersion_placeholderFieldArray = [];
1252  if ($createNewVersion) {
1253  // create a placeholder array with already processed field content
1254  $newVersion_placeholderFieldArray = $fieldArray;
1255  }
1256  // NOTICE! All manipulation beyond this point bypasses both "excludeFields" AND possible "MM" relations / file uploads to field!
1257  // Forcing some values unto field array:
1258  // NOTICE: This overriding is potentially dangerous; permissions per field is not checked!!!
1259  $fieldArray = $this->overrideFieldArray($table, $fieldArray);
1260  if ($createNewVersion) {
1261  $newVersion_placeholderFieldArray = $this->overrideFieldArray($table, $newVersion_placeholderFieldArray);
1262  }
1263  // Setting system fields
1264  if ($status == 'new') {
1265  if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1266  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1267  if ($createNewVersion) {
1268  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1269  }
1270  }
1271  if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1272  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1273  if ($createNewVersion) {
1274  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1275  }
1276  }
1277  } elseif ($this->checkSimilar) {
1278  // Removing fields which are equal to the current value:
1279  $fieldArray = $this->compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1280  }
1281  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] && !empty($fieldArray)) {
1282  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1283  if ($createNewVersion) {
1284  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1285  }
1286  }
1287  // Set stage to "Editing" to make sure we restart the workflow
1288  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1289  $fieldArray['t3ver_stage'] = 0;
1290  }
1291  // Hook: processDatamap_postProcessFieldArray
1292  foreach ($hookObjectsArr as $hookObj) {
1293  if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1294  $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1295  }
1296  }
1297  // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1298  // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already and files could have been uploaded that are now "lost"
1299  if (is_array($fieldArray)) {
1300  if ($status == 'new') {
1301  if ($table === 'pages') {
1302  // for new pages always a refresh is needed
1303  $this->pagetreeNeedsRefresh = true;
1304  }
1305 
1306  // This creates a new version of the record with online placeholder and offline version
1307  if ($createNewVersion) {
1308  // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1309  $this->pagetreeNeedsRefresh = true;
1310 
1311  $newVersion_placeholderFieldArray['t3ver_label'] = 'INITIAL PLACEHOLDER';
1312  // Setting placeholder state value for temporary record
1313  $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER);
1314  // Setting workspace - only so display of place holders can filter out those from other workspaces.
1315  $newVersion_placeholderFieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1316  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $this->getPlaceholderTitleForTableLabel($table);
1317  // Saving placeholder as 'original'
1318  $this->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1319  // For the actual new offline version, set versioning values to point to placeholder:
1320  $fieldArray['pid'] = -1;
1321  $fieldArray['t3ver_oid'] = $this->substNEWwithIDs[$id];
1322  $fieldArray['t3ver_id'] = 1;
1323  // Setting placeholder state value for version (so it can know it is currently a new version...)
1324  $fieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER_VERSION);
1325  $fieldArray['t3ver_label'] = 'First draft version';
1326  $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1327  // When inserted, $this->substNEWwithIDs[$id] will be changed to the uid of THIS version and so the interface will pick it up just nice!
1328  $phShadowId = $this->insertDB($table, $id, $fieldArray, true, 0, true);
1329  if ($phShadowId) {
1330  // Processes fields of the placeholder record:
1331  $this->triggerRemapAction($table, $id, [$this, 'placeholderShadowing'], [$table, $phShadowId]);
1332  // Hold auto-versionized ids of placeholders:
1333  $this->autoVersionIdMap[$table][$this->substNEWwithIDs[$id]] = $phShadowId;
1334  }
1335  } else {
1336  $this->insertDB($table, $id, $fieldArray, false, $incomingFieldArray['uid']);
1337  }
1338  } else {
1339  if ($table === 'pages') {
1340  // only a certain number of fields needs to be checked for updates
1341  // if $this->checkSimilar is TRUE, fields with unchanged values are already removed here
1342  $fieldsToCheck = array_intersect($this->pagetreeRefreshFieldsFromPages, array_keys($fieldArray));
1343  if (!empty($fieldsToCheck)) {
1344  $this->pagetreeNeedsRefresh = true;
1345  }
1346  }
1347  $this->updateDB($table, $id, $fieldArray);
1348  $this->placeholderShadowing($table, $id);
1349  }
1350  }
1351  // Hook: processDatamap_afterDatabaseOperations
1352  // Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
1353  // but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
1354  $this->hook_processDatamap_afterDatabaseOperations($hookObjectsArr, $status, $table, $id, $fieldArray);
1355  }
1356  }
1357  // Process the stack of relations to remap/correct
1358  $this->processRemapStack();
1359  $this->dbAnalysisStoreExec();
1360  $this->removeRegisteredFiles();
1361  // Hook: processDatamap_afterAllOperations
1362  // Note: When this hook gets called, all operations on the submitted data have been finished.
1363  foreach ($hookObjectsArr as $hookObj) {
1364  if (method_exists($hookObj, 'processDatamap_afterAllOperations')) {
1365  $hookObj->processDatamap_afterAllOperations($this);
1366  }
1367  }
1368  if ($this->isOuterMostInstance()) {
1369  $this->processClearCacheQueue();
1370  $this->resetElementsToBeDeleted();
1371  }
1372  }
1373 
1381  public function placeholderShadowing($table, $id)
1382  {
1383  if ($liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, '*')) {
1384  if (VersionState::cast($liveRec['t3ver_state'])->indicatesPlaceholder()) {
1385  $justStoredRecord = BackendUtility::getRecord($table, $id);
1386  $newRecord = [];
1387  $shadowCols = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'];
1388  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1389  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1390  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['type'];
1391  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label'];
1392  $shadowColumns = array_unique(GeneralUtility::trimExplode(',', $shadowCols, true));
1393  foreach ($shadowColumns as $fieldName) {
1394  if ((string)$justStoredRecord[$fieldName] !== (string)$liveRec[$fieldName] && isset($GLOBALS['TCA'][$table]['columns'][$fieldName]) && $fieldName !== 'uid' && $fieldName !== 'pid') {
1395  $newRecord[$fieldName] = $justStoredRecord[$fieldName];
1396  }
1397  }
1398  if (!empty($newRecord)) {
1399  if ($this->enableLogging) {
1400  $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']);
1401  }
1402  $this->updateDB($table, $liveRec['uid'], $newRecord);
1403  }
1404  }
1405  }
1406  }
1407 
1415  public function getPlaceholderTitleForTableLabel($table, $placeholderContent = null)
1416  {
1417  if ($placeholderContent === null) {
1418  $placeholderContent = 'PLACEHOLDER';
1419  }
1420 
1421  $labelPlaceholder = '[' . $placeholderContent . ', WS#' . $this->BE_USER->workspace . ']';
1422  $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1423  if (!isset($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'])) {
1424  return $labelPlaceholder;
1425  }
1426  $evalCodesArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'], true);
1427  $transformedLabel = $this->checkValue_input_Eval($labelPlaceholder, $evalCodesArray, '');
1428  return isset($transformedLabel['value']) ? $transformedLabel['value'] : $labelPlaceholder;
1429  }
1430 
1444  public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID)
1445  {
1446  // Initialize:
1447  $originalLanguageRecord = null;
1448  $originalLanguage_diffStorage = null;
1449  $diffStorageFlag = false;
1450  // Setting 'currentRecord' and 'checkValueRecord':
1451  if (strstr($id, 'NEW')) {
1452  // Must have the 'current' array - not the values after processing below...
1453  $currentRecord = ($checkValueRecord = $fieldArray);
1454  // IF $incomingFieldArray is an array, overlay it.
1455  // 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...
1456  if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {
1457  ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);
1458  }
1459  } else {
1460  // We must use the current values as basis for this!
1461  $currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*'));
1462  // This is done to make the pid positive for offline versions; Necessary to have diff-view for pages_language_overlay in workspaces.
1463  BackendUtility::fixVersioningPid($table, $currentRecord);
1464  // Get original language record if available:
1465  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) {
1466  $lookUpTable = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable'] ?: $table;
1467  $originalLanguageRecord = $this->recordInfo($lookUpTable, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*');
1468  BackendUtility::workspaceOL($lookUpTable, $originalLanguageRecord);
1469  $originalLanguage_diffStorage = unserialize($currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]);
1470  }
1471  }
1472  $this->checkValue_currentRecord = $checkValueRecord;
1473  // In the following all incoming value-fields are tested:
1474  // - Are the user allowed to change the field?
1475  // - Is the field uid/pid (which are already set)
1476  // - perms-fields for pages-table, then do special things...
1477  // - If the field is nothing of the above and the field is configured in TCA, the fieldvalues are evaluated by ->checkValue
1478  // If everything is OK, the field is entered into $fieldArray[]
1479  foreach ($incomingFieldArray as $field => $fieldValue) {
1480  if (isset($this->excludedTablesAndFields[$table . '-' . $field]) || $this->data_disableFields[$table][$id][$field]) {
1481  continue;
1482  }
1483 
1484  // The field must be editable.
1485  // Checking if a value for language can be changed:
1486  $languageDeny = $GLOBALS['TCA'][$table]['ctrl']['languageField'] && (string)$GLOBALS['TCA'][$table]['ctrl']['languageField'] === (string)$field && !$this->BE_USER->checkLanguageAccess($fieldValue);
1487  if ($languageDeny) {
1488  continue;
1489  }
1490 
1491  // Stripping slashes - will probably be removed the day $this->stripslashes_values is removed as an option...
1492  // @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
1493  if ($this->stripslashes_values) {
1495  'The option stripslash_values is typically set to FALSE as data should be properly prepared before sending to DataHandler. Do not rely on DataHandler removing extra slashes. The option will be removed in TYPO3 CMS 8.'
1496  );
1497  if (is_array($fieldValue)) {
1499  } else {
1500  $fieldValue = stripslashes($fieldValue);
1501  }
1502  }
1503  switch ($field) {
1504  case 'uid':
1505  case 'pid':
1506  // Nothing happens, already set
1507  break;
1508  case 'perms_userid':
1509  case 'perms_groupid':
1510  case 'perms_user':
1511  case 'perms_group':
1512  case 'perms_everybody':
1513  // Permissions can be edited by the owner or the administrator
1514  if ($table == 'pages' && ($this->admin || $status == 'new' || $this->pageInfo($id, 'perms_userid') == $this->userid)) {
1515  $value = (int)$fieldValue;
1516  switch ($field) {
1517  case 'perms_userid':
1518  $fieldArray[$field] = $value;
1519  break;
1520  case 'perms_groupid':
1521  $fieldArray[$field] = $value;
1522  break;
1523  default:
1524  if ($value >= 0 && $value < pow(2, 5)) {
1525  $fieldArray[$field] = $value;
1526  }
1527  }
1528  }
1529  break;
1530  case 't3ver_oid':
1531  case 't3ver_id':
1532  case 't3ver_wsid':
1533  case 't3ver_state':
1534  case 't3ver_count':
1535  case 't3ver_stage':
1536  case 't3ver_tstamp':
1537  // t3ver_label is not here because it CAN be edited as a regular field!
1538  break;
1539  default:
1540  if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
1541  // Evaluating the value
1542  $res = $this->checkValue($table, $field, $fieldValue, $id, $status, $realPid, $tscPID);
1543  if (array_key_exists('value', $res)) {
1544  $fieldArray[$field] = $res['value'];
1545  }
1546  // Add the value of the original record to the diff-storage content:
1547  if ($this->updateModeL10NdiffData && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
1548  $originalLanguage_diffStorage[$field] = $this->updateModeL10NdiffDataClear ? '' : $originalLanguageRecord[$field];
1549  $diffStorageFlag = true;
1550  }
1551  // If autoversioning is happening we need to perform a nasty hack. The case is parallel to a similar hack inside checkValue_group_select_file().
1552  // 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.
1553  // 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.
1554  // 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.
1555  // 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 !
1556  if ($this->autoVersioningUpdate === true) {
1557  if (is_array($this->RTEmagic_copyIndex[$table][$id][$field])) {
1558  foreach ($this->RTEmagic_copyIndex[$table][$id][$field] as $oldRTEmagicName => $newRTEmagicName) {
1559  $fieldArray[$field] = str_replace(' src="' . $oldRTEmagicName . '"', ' src="' . $newRTEmagicName . '"', $fieldArray[$field]);
1560  }
1561  }
1562  }
1563  } elseif ($GLOBALS['TCA'][$table]['ctrl']['origUid'] === $field) {
1564  // Allow value for original UID to pass by...
1565  $fieldArray[$field] = $fieldValue;
1566  }
1567  }
1568  }
1569  // Add diff-storage information:
1570  if ($diffStorageFlag && !isset($fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']])) {
1571  // 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...
1572  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = serialize($originalLanguage_diffStorage);
1573  }
1574  // Checking for RTE-transformations of fields:
1575  $types_fieldConfig = BackendUtility::getTCAtypes($table, $this->checkValue_currentRecord);
1576  $theTypeString = null;
1577  if (is_array($types_fieldConfig)) {
1578  foreach ($types_fieldConfig as $vconf) {
1579  // RTE transformations:
1580  if ($this->dontProcessTransformations || !isset($fieldArray[$vconf['field']])) {
1581  continue;
1582  }
1583 
1584  // Look for transformation flag:
1585  if ((string)$incomingFieldArray['_TRANSFORM_' . $vconf['field']] === 'RTE') {
1586  if ($theTypeString === null) {
1587  $theTypeString = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
1588  }
1589  $RTEsetup = $this->BE_USER->getTSConfig('RTE', BackendUtility::getPagesTSconfig($tscPID));
1590  $thisConfig = BackendUtility::RTEsetup($RTEsetup['properties'], $table, $vconf['field'], $theTypeString);
1591  $fieldArray[$vconf['field']] = $this->transformRichtextContentToDatabase(
1592  $fieldArray[$vconf['field']], $table, $vconf['field'], $vconf['spec'], $thisConfig, $this->checkValue_currentRecord['pid']
1593  );
1594  }
1595  }
1596  }
1597  // Return fieldArray
1598  return $fieldArray;
1599  }
1600 
1612  protected function transformRichtextContentToDatabase($value, $table, $field, $defaultExtras, $thisConfig, $pid)
1613  {
1614  if ($defaultExtras['rte_transform']) {
1615  $parameters = BackendUtility::getSpecConfParametersFromArray($defaultExtras['rte_transform']['parameters']);
1616  // There must be a mode set for transformation, this is typically 'ts_css'
1617  if ($parameters['mode']) {
1618  // Initialize transformation:
1619  $parseHTML = GeneralUtility::makeInstance(RteHtmlParser::class);
1620  $parseHTML->init($table . ':' . $field, $pid);
1621  $parseHTML->setRelPath('');
1622  // Perform transformation:
1623  $value = $parseHTML->RTE_transform($value, $defaultExtras, 'db', $thisConfig);
1624  }
1625  }
1626  return $value;
1627  }
1628 
1629  /*********************************************
1630  *
1631  * Evaluation of input values
1632  *
1633  ********************************************/
1648  public function checkValue($table, $field, $value, $id, $status, $realPid, $tscPID)
1649  {
1650  // Result array
1651  $res = [];
1652 
1653  // Processing special case of field pages.doktype
1654  if (($table === 'pages' || $table === 'pages_language_overlay') && $field === 'doktype') {
1655  // If the user may not use this specific doktype, we issue a warning
1656  if (!($this->admin || GeneralUtility::inList($this->BE_USER->groupData['pagetypes_select'], $value))) {
1657  if ($this->enableLogging) {
1658  $propArr = $this->getRecordProperties($table, $id);
1659  $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']);
1660  }
1661  return $res;
1662  }
1663  if ($status == 'update') {
1664  // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1665  $onlyAllowedTables = isset($GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables']) ? $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] : $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1666  if ($onlyAllowedTables) {
1667  $theWrongTables = $this->doesPageHaveUnallowedTables($id, $value);
1668  if ($theWrongTables) {
1669  if ($this->enableLogging) {
1670  $propArr = $this->getRecordProperties($table, $id);
1671  $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']);
1672  }
1673  return $res;
1674  }
1675  }
1676  }
1677  }
1678 
1679  $curValue = null;
1680  if ((int)$id !== 0) {
1681  // Get current value:
1682  $curValueRec = $this->recordInfo($table, $id, $field);
1683  // isset() won't work here, since values can be NULL
1684  if ($curValueRec !== null && array_key_exists($field, $curValueRec)) {
1685  $curValue = $curValueRec[$field];
1686  }
1687  }
1688 
1689  // Getting config for the field
1690  $tcaFieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1691 
1692  // Create $recFID only for those types that need it
1693  if (
1694  $tcaFieldConf['type'] === 'flex'
1695  || $tcaFieldConf['type'] === 'group' && ($tcaFieldConf['internal_type'] === 'file' || $tcaFieldConf['internal_type'] === 'file_reference')
1696  ) {
1697  $recFID = $table . ':' . $id . ':' . $field;
1698  } else {
1699  $recFID = null;
1700  }
1701 
1702  // Perform processing:
1703  $res = $this->checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $this->uploadedFileArray[$table][$id][$field], $tscPID);
1704  return $res;
1705  }
1706 
1726  public function checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $uploadedFiles, $tscPID, array $additionalData = null)
1727  {
1728  // Convert to NULL value if defined in TCA
1729  if ($value === null && !empty($tcaFieldConf['eval']) && GeneralUtility::inList($tcaFieldConf['eval'], 'null')) {
1730  $res = ['value' => null];
1731  return $res;
1732  }
1733 
1734  switch ($tcaFieldConf['type']) {
1735  case 'text':
1736  $res = $this->checkValueForText($value, $tcaFieldConf);
1737  break;
1738  case 'passthrough':
1739  case 'imageManipulation':
1740  case 'user':
1741  $res['value'] = $value;
1742  break;
1743  case 'input':
1744  $res = $this->checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field);
1745  break;
1746  case 'check':
1747  $res = $this->checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1748  break;
1749  case 'radio':
1750  $res = $this->checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1751  break;
1752  case 'group':
1753  case 'select':
1754  $res = $this->checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field);
1755  break;
1756  case 'inline':
1757  $res = $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
1758  break;
1759  case 'flex':
1760  // FlexForms are only allowed for real fields.
1761  if ($field) {
1762  $res = $this->checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field);
1763  }
1764  break;
1765  default:
1766  // Do nothing
1767  }
1768  return $res;
1769  }
1770 
1782  public function checkValue_text($res, $value, $tcaFieldConf, $PP, $field = '')
1783  {
1785  return $this->checkValueForText($value, $tcaFieldConf);
1786  }
1787 
1795  protected function checkValueForText($value, $tcaFieldConf)
1796  {
1797  if (!isset($tcaFieldConf['eval']) || $tcaFieldConf['eval'] === '') {
1798  return ['value' => $value];
1799  }
1800  $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1801  if ($this->runtimeCache->has($cacheId)) {
1802  $evalCodesArray = $this->runtimeCache->get($cacheId);
1803  } else {
1804  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1805  $this->runtimeCache->set($cacheId, $evalCodesArray);
1806  }
1807  return $this->checkValue_text_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1808  }
1809 
1821  public function checkValue_input($res, $value, $tcaFieldConf, $PP, $field = '')
1822  {
1824  list($table, $id, , , $realPid) = $PP;
1825  return $this->checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field);
1826  }
1827 
1839  protected function checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field)
1840  {
1841  // Handle native date/time fields
1842  $isDateOrDateTimeField = false;
1843  $format = '';
1844  $emptyValue = '';
1845  if (isset($tcaFieldConf['dbType']) && ($tcaFieldConf['dbType'] === 'date' || $tcaFieldConf['dbType'] === 'datetime')) {
1846  if (empty($value)) {
1847  $value = 0;
1848  } else {
1849  $isDateOrDateTimeField = true;
1850  $dateTimeFormats = $this->databaseConnection->getDateTimeFormats($table);
1851  // Convert the date/time into a timestamp for the sake of the checks
1852  $emptyValue = $dateTimeFormats[$tcaFieldConf['dbType']]['empty'];
1853  $format = $dateTimeFormats[$tcaFieldConf['dbType']]['format'];
1854  // At this point in the processing, the timestamps are still based on UTC
1855  $timeZone = new \DateTimeZone('UTC');
1856  $dateTime = \DateTime::createFromFormat('!' . $format, $value, $timeZone);
1857  $value = $value === $emptyValue ? 0 : $dateTime->getTimestamp();
1858  }
1859  }
1860  // Secures the string-length to be less than max.
1861  if ((int)$tcaFieldConf['max'] > 0) {
1862  $value = $GLOBALS['LANG']->csConvObj->substr($GLOBALS['LANG']->charSet, (string)$value, 0, (int)$tcaFieldConf['max']);
1863  }
1864  // Checking range of value:
1865  // @todo: The "checkbox" option was removed for type=input, this check could be probably relaxed?
1866  if ($tcaFieldConf['range'] && $value != $tcaFieldConf['checkbox'] && (int)$value !== (int)$tcaFieldConf['default']) {
1867  if (isset($tcaFieldConf['range']['upper']) && (int)$value > (int)$tcaFieldConf['range']['upper']) {
1868  $value = $tcaFieldConf['range']['upper'];
1869  }
1870  if (isset($tcaFieldConf['range']['lower']) && (int)$value < (int)$tcaFieldConf['range']['lower']) {
1871  $value = $tcaFieldConf['range']['lower'];
1872  }
1873  }
1874 
1875  if (empty($tcaFieldConf['eval'])) {
1876  $res = ['value' => $value];
1877  } else {
1878  // Process evaluation settings:
1879  $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1880  if ($this->runtimeCache->has($cacheId)) {
1881  $evalCodesArray = $this->runtimeCache->get($cacheId);
1882  } else {
1883  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1884  $this->runtimeCache->set($cacheId, $evalCodesArray);
1885  }
1886 
1887  $res = $this->checkValue_input_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1888 
1889  // Process UNIQUE settings:
1890  // 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...
1891  if ($field && $realPid >= 0 && !empty($res['value'])) {
1892  if (in_array('uniqueInPid', $evalCodesArray, true)) {
1893  $res['value'] = $this->getUnique($table, $field, $res['value'], $id, $realPid);
1894  }
1895  if ($res['value'] && in_array('unique', $evalCodesArray, true)) {
1896  $res['value'] = $this->getUnique($table, $field, $res['value'], $id);
1897  }
1898  }
1899  }
1900 
1901  // Handle native date/time fields
1902  if ($isDateOrDateTimeField) {
1903  // Convert the timestamp back to a date/time
1904  $res['value'] = $res['value'] ? date($format, $res['value']) : $emptyValue;
1905  }
1906  return $res;
1907  }
1908 
1920  public function checkValue_check($res, $value, $tcaFieldConf, $PP, $field = '')
1921  {
1923  list($table, $id, , , $realPid) = $PP;
1924  return $this->checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1925  }
1926 
1939  protected function checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field)
1940  {
1941  $items = $tcaFieldConf['items'];
1942  if ($tcaFieldConf['itemsProcFunc']) {
1944  $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
1945  $items = $processingService->getProcessingItems($table, $realPid, $field,
1946  $this->checkValue_currentRecord,
1947  $tcaFieldConf, $tcaFieldConf['items']);
1948  }
1949 
1950  $itemC = 0;
1951  if ($items !== null) {
1952  $itemC = count($items);
1953  }
1954  if (!$itemC) {
1955  $itemC = 1;
1956  }
1957  $maxV = pow(2, $itemC) - 1;
1958  if ($value < 0) {
1959  // @todo: throw LogicException here? Negative values for checkbox items do not make sense and indicate a coding error.
1960  $value = 0;
1961  }
1962  if ($value > $maxV) {
1963  // @todo: This case is pretty ugly: If there is an itemsProcFunc registered, and if it returns a dynamic,
1964  // @todo: changing list of items, then it may happen that a value is transformed and vanished checkboxes
1965  // @todo: are permanently removed from the value.
1966  // @todo: Suggestion: Throw an exception instead? Maybe a specific, catchable exception that generates a
1967  // @todo: error message to the user - dynamic item sets via itemProcFunc on check would be a bad idea anyway.
1968  $value = $value & $maxV;
1969  }
1970  if ($field && $realPid >= 0 && $value > 0 && !empty($tcaFieldConf['eval'])) {
1971  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1972  $otherRecordsWithSameValue = [];
1973  $maxCheckedRecords = 0;
1974  if (in_array('maximumRecordsCheckedInPid', $evalCodesArray, true)) {
1975  $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value, $realPid);
1976  $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsCheckedInPid'];
1977  }
1978  if (in_array('maximumRecordsChecked', $evalCodesArray, true)) {
1979  $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value);
1980  $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsChecked'];
1981  }
1982 
1983  // there are more than enough records with value "1" in the DB
1984  // if so, set this value to "0" again
1985  if ($maxCheckedRecords && count($otherRecordsWithSameValue) >= $maxCheckedRecords) {
1986  $value = 0;
1987  if ($this->enableLogging) {
1988  $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]);
1989  }
1990  }
1991  }
1992  $res['value'] = $value;
1993  return $res;
1994  }
1995 
2006  public function checkValue_radio($res, $value, $tcaFieldConf, $PP)
2007  {
2009  // TODO find a way to get the field name; it should not be set in $recFID as this is only created for some record types, see checkValue()
2010  return $this->checkValueForRadio($res, $value, $tcaFieldConf, $PP[0], $PP[1], $PP[4], '');
2011  }
2012 
2025  protected function checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $pid, $field)
2026  {
2027  if (is_array($tcaFieldConf['items'])) {
2028  foreach ($tcaFieldConf['items'] as $set) {
2029  if ((string)$set[1] === (string)$value) {
2030  $res['value'] = $value;
2031  break;
2032  }
2033  }
2034  }
2035 
2036  // if no value was found and an itemsProcFunc is defined, check that for the value
2037  if ($tcaFieldConf['itemsProcFunc'] && empty($res['value'])) {
2038  $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
2039  $processedItems = $processingService->getProcessingItems($table, $pid, $field, $this->checkValue_currentRecord,
2040  $tcaFieldConf, $tcaFieldConf['items']);
2041 
2042  foreach ($processedItems as $set) {
2043  if ((string)$set[1] === (string)$value) {
2044  $res['value'] = $value;
2045  break;
2046  }
2047  }
2048  }
2049 
2050  return $res;
2051  }
2052 
2065  public function checkValue_group_select($res, $value, $tcaFieldConf, $PP, $uploadedFiles, $field)
2066  {
2068  list($table, $id, $curValue, $status, , $recFID) = $PP;
2069  return $this->checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field);
2070  }
2071 
2087  protected function checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field)
2088  {
2089  // Detecting if value sent is an array and if so, implode it around a comma:
2090  if (is_array($value)) {
2091  $value = implode(',', $value);
2092  }
2093  // 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.
2094  // Anyway, this should NOT disturb anything else:
2095  $value = $this->convNumEntityToByteValue($value);
2096  // When values are sent as group or select they come as comma-separated values which are exploded by this function:
2097  $valueArray = $this->checkValue_group_select_explodeSelectGroupValue($value);
2098  // If multiple is not set, remove duplicates:
2099  if (!$tcaFieldConf['multiple']) {
2100  $valueArray = array_unique($valueArray);
2101  }
2102  // If an exclusive key is found, discard all others:
2103  if ($tcaFieldConf['type'] == 'select' && $tcaFieldConf['exclusiveKeys']) {
2104  $exclusiveKeys = GeneralUtility::trimExplode(',', $tcaFieldConf['exclusiveKeys']);
2105  foreach ($valueArray as $index => $key) {
2106  if (in_array($key, $exclusiveKeys, true)) {
2107  $valueArray = [$index => $key];
2108  break;
2109  }
2110  }
2111  }
2112  // 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?)
2113  // 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!!
2114  $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
2115  // Checking for select / authMode, removing elements from $valueArray if any of them is not allowed!
2116  if ($tcaFieldConf['type'] == 'select' && $tcaFieldConf['authMode']) {
2117  $preCount = count($valueArray);
2118  foreach ($valueArray as $index => $key) {
2119  if (!$this->BE_USER->checkAuthMode($table, $field, $key, $tcaFieldConf['authMode'])) {
2120  unset($valueArray[$index]);
2121  }
2122  }
2123  // 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.
2124  if ($preCount && empty($valueArray)) {
2125  return [];
2126  }
2127  }
2128  // For group types:
2129  if ($tcaFieldConf['type'] == 'group') {
2130  switch ($tcaFieldConf['internal_type']) {
2131  case 'file_reference':
2132  case 'file':
2133  $valueArray = $this->checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFiles, $status, $table, $id, $recFID);
2134  break;
2135  }
2136  }
2137  // For select types which has a foreign table attached:
2138  $unsetResult = false;
2139  if (
2140  $tcaFieldConf['type'] === 'group' && $tcaFieldConf['internal_type'] === 'db'
2141  || $tcaFieldConf['type'] === 'select' && ($tcaFieldConf['foreign_table'] || isset($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages')
2142  ) {
2143  // check, if there is a NEW... id in the value, that should be substituted later
2144  if (strpos($value, 'NEW') !== false) {
2145  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2146  $this->addNewValuesToRemapStackChildIds($valueArray);
2147  $this->remapStack[] = [
2148  'func' => 'checkValue_group_select_processDBdata',
2149  'args' => [$valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field],
2150  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 5],
2151  'field' => $field
2152  ];
2153  $unsetResult = true;
2154  } else {
2155  $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field);
2156  }
2157  }
2158  if (!$unsetResult) {
2159  $newVal = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
2160  $res['value'] = $this->castReferenceValue(implode(',', $newVal), $tcaFieldConf);
2161  } else {
2162  unset($res['value']);
2163  }
2164  return $res;
2165  }
2166 
2175  protected function applyFiltersToValues(array $tcaFieldConfiguration, array $values)
2176  {
2177  if (empty($tcaFieldConfiguration['filter']) || !is_array($tcaFieldConfiguration['filter'])) {
2178  return $values;
2179  }
2180  foreach ($tcaFieldConfiguration['filter'] as $filter) {
2181  if (empty($filter['userFunc'])) {
2182  continue;
2183  }
2184  $parameters = $filter['parameters'] ?: [];
2185  $parameters['values'] = $values;
2186  $parameters['tcaFieldConfig'] = $tcaFieldConfiguration;
2187  $values = GeneralUtility::callUserFunction($filter['userFunc'], $parameters, $this);
2188  if (!is_array($values)) {
2189  throw new \RuntimeException('Failed calling filter userFunc.', 1336051942);
2190  }
2191  }
2192  return $values;
2193  }
2194 
2211  public function checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFileArray, $status, $table, $id, $recFID)
2212  {
2213  // If file handling should NOT be bypassed, do processing:
2214  if (!$this->bypassFileHandling) {
2215  // If any files are uploaded, add them to value array
2216  // Numeric index means that there are multiple files
2217  if (isset($uploadedFileArray[0])) {
2218  $uploadedFiles = $uploadedFileArray;
2219  } else {
2220  // There is only one file
2221  $uploadedFiles = [$uploadedFileArray];
2222  }
2223  foreach ($uploadedFiles as $uploadedFileArray) {
2224  if (!empty($uploadedFileArray['name']) && $uploadedFileArray['tmp_name'] !== 'none') {
2225  $valueArray[] = $uploadedFileArray['tmp_name'];
2226  $this->alternativeFileName[$uploadedFileArray['tmp_name']] = $uploadedFileArray['name'];
2227  }
2228  }
2229  // Creating fileFunc object.
2230  if (!$this->fileFunc) {
2231  $this->fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
2232  $this->include_filefunctions = 1;
2233  }
2234  // Setting permitted extensions.
2235  $all_files = [];
2236  $all_files['webspace']['allow'] = $tcaFieldConf['allowed'];
2237  $all_files['webspace']['deny'] = $tcaFieldConf['disallowed'] ?: '*';
2238  $all_files['ftpspace'] = $all_files['webspace'];
2239  $this->fileFunc->init('', $all_files);
2240  }
2241  // If there is an upload folder defined:
2242  if ($tcaFieldConf['uploadfolder'] && $tcaFieldConf['internal_type'] == 'file') {
2243  $currentFilesForHistory = null;
2244  // If filehandling should NOT be bypassed, do processing:
2245  if (!$this->bypassFileHandling) {
2246  // For logging..
2247  $propArr = $this->getRecordProperties($table, $id);
2248  // Get destrination path:
2249  $dest = $this->destPathFromUploadFolder($tcaFieldConf['uploadfolder']);
2250  // If we are updating:
2251  if ($status == 'update') {
2252  // Traverse the input values and convert to absolute filenames in case the update happens to an autoVersionized record.
2253  // 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!
2254  // 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_.
2255  // 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.
2256  // Illustration of the problem comes here:
2257  // 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.
2258  // 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.
2259  // 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.
2260  // 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.
2261  if ($this->autoVersioningUpdate === true) {
2262  foreach ($valueArray as $key => $theFile) {
2263  // If it is an already attached file...
2264  if ($theFile === basename($theFile)) {
2265  $valueArray[$key] = PATH_site . $tcaFieldConf['uploadfolder'] . '/' . $theFile;
2266  }
2267  }
2268  }
2269  // Finding the CURRENT files listed, either from MM or from the current record.
2270  $theFileValues = [];
2271  // If MM relations for the files also!
2272  if ($tcaFieldConf['MM']) {
2273  $dbAnalysis = $this->createRelationHandlerInstance();
2275  $dbAnalysis->start('', 'files', $tcaFieldConf['MM'], $id);
2276  foreach ($dbAnalysis->itemArray as $item) {
2277  if ($item['id']) {
2278  $theFileValues[] = $item['id'];
2279  }
2280  }
2281  } else {
2282  $theFileValues = GeneralUtility::trimExplode(',', $curValue, true);
2283  }
2284  $currentFilesForHistory = implode(',', $theFileValues);
2285  // DELETE files: If existing files were found, traverse those and register files for deletion which has been removed:
2286  if (!empty($theFileValues)) {
2287  // 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!)
2288  foreach ($valueArray as $key => $theFile) {
2289  if ($theFile && !strstr(GeneralUtility::fixWindowsFilePath($theFile), '/')) {
2290  $theFileValues = ArrayUtility::removeArrayEntryByValue($theFileValues, $theFile);
2291  }
2292  }
2293  // This array contains the filenames in the uploadfolder that should be deleted:
2294  foreach ($theFileValues as $key => $theFile) {
2295  $theFile = trim($theFile);
2296  if (@is_file(($dest . '/' . $theFile))) {
2297  $this->removeFilesStore[] = $dest . '/' . $theFile;
2298  } elseif ($this->enableLogging && $theFile) {
2299  $this->log($table, $id, 5, 0, 1, 'Could not delete file \'%s\' (does not exist). (%s)', 10, [$dest . '/' . $theFile, $recFID], $propArr['event_pid']);
2300  }
2301  }
2302  }
2303  }
2304  // Traverse the submitted values:
2305  foreach ($valueArray as $key => $theFile) {
2306  // Init:
2307  $maxSize = (int)$tcaFieldConf['max_size'];
2308  // Must be cleared. Else a faulty fileref may be inserted if the below code returns an error!
2309  $theDestFile = '';
2310  // a FAL file was added, now resolve the file object and get the absolute path
2311  // @todo in future versions this needs to be modified to handle FAL objects natively
2312  if (!empty($theFile) && MathUtility::canBeInterpretedAsInteger($theFile)) {
2313  $fileObject = ResourceFactory::getInstance()->getFileObject($theFile);
2314  $theFile = $fileObject->getForLocalProcessing(false);
2315  }
2316  // NEW FILES? If the value contains '/' it indicates, that the file
2317  // is new and should be added to the uploadsdir (whether its absolute or relative does not matter here)
2318  if (strstr(GeneralUtility::fixWindowsFilePath($theFile), '/')) {
2319  // Check various things before copying file:
2320  // File and destination must exist
2321  if (@is_dir($dest) && (@is_file($theFile) || @is_uploaded_file($theFile))) {
2322  // Finding size.
2323  if (is_uploaded_file($theFile) && $theFile == $uploadedFileArray['tmp_name']) {
2324  $fileSize = $uploadedFileArray['size'];
2325  } else {
2326  $fileSize = filesize($theFile);
2327  }
2328  // Check file size:
2329  if (!$maxSize || $fileSize <= $maxSize * 1024) {
2330  // Prepare filename:
2331  $theEndFileName = isset($this->alternativeFileName[$theFile]) ? $this->alternativeFileName[$theFile] : $theFile;
2332  $fI = GeneralUtility::split_fileref($theEndFileName);
2333  // Check for allowed extension:
2334  if ($this->fileFunc->checkIfAllowed($fI['fileext'], $dest, $theEndFileName)) {
2335  $theDestFile = $this->fileFunc->getUniqueName($this->fileFunc->cleanFileName($fI['file']), $dest);
2336  // If we have a unique destination filename, then write the file:
2337  if ($theDestFile) {
2338  GeneralUtility::upload_copy_move($theFile, $theDestFile);
2339  // Hook for post-processing the upload action
2340  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processUpload'])) {
2341  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processUpload'] as $classRef) {
2342  $hookObject = GeneralUtility::getUserObj($classRef);
2343  if (!$hookObject instanceof DataHandlerProcessUploadHookInterface) {
2344  throw new \UnexpectedValueException($classRef . ' must implement interface ' . DataHandlerProcessUploadHookInterface::class, 1279962349);
2345  }
2346  $hookObject->processUpload_postProcessAction($theDestFile, $this);
2347  }
2348  }
2349  $this->copiedFileMap[$theFile] = $theDestFile;
2350  clearstatcache();
2351  if ($this->enableLogging && !@is_file($theDestFile)) {
2352  $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']);
2353  }
2354  } elseif ($this->enableLogging) {
2355  $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: No destination file (%s) possible!. (%s)', 11, [$theFile, $theDestFile, $recFID], $propArr['event_pid']);
2356  }
2357  } elseif ($this->enableLogging) {
2358  $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, [$fI['fileext'], $recFID], $propArr['event_pid']);
2359  }
2360  } elseif ($this->enableLogging) {
2361  $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']);
2362  }
2363  } elseif ($this->enableLogging) {
2364  $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']);
2365  }
2366  // If the destination file was created, we will set the new filename in the value array, otherwise unset the entry in the value array!
2367  if (@is_file($theDestFile)) {
2368  $info = GeneralUtility::split_fileref($theDestFile);
2369  // The value is set to the new filename
2370  $valueArray[$key] = $info['file'];
2371  } else {
2372  // The value is set to the new filename
2373  unset($valueArray[$key]);
2374  }
2375  }
2376  }
2377  }
2378  // 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!
2379  if ($tcaFieldConf['MM']) {
2381  $dbAnalysis = $this->createRelationHandlerInstance();
2382  // Dummy
2383  $dbAnalysis->tableArray['files'] = [];
2384  foreach ($valueArray as $key => $theFile) {
2385  // Explode files
2386  $dbAnalysis->itemArray[]['id'] = $theFile;
2387  }
2388  if ($status == 'update') {
2389  $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, 0);
2390  $newFiles = implode(',', $dbAnalysis->getValueArray());
2391  list(, , $recFieldName) = explode(':', $recFID);
2392  if ($currentFilesForHistory != $newFiles) {
2393  $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$recFieldName] = $currentFilesForHistory;
2394  $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$recFieldName] = $newFiles;
2395  } else {
2396  $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$recFieldName] = '';
2397  $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$recFieldName] = '';
2398  }
2399  } else {
2400  $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, 0];
2401  }
2402  $valueArray = $dbAnalysis->countItems();
2403  }
2404  } else {
2405  if (!empty($valueArray)) {
2406  // If filehandling should NOT be bypassed, do processing:
2407  if (!$this->bypassFileHandling) {
2408  // For logging..
2409  $propArr = $this->getRecordProperties($table, $id);
2410  foreach ($valueArray as &$theFile) {
2411  // FAL handling: it's a UID, thus it is resolved to the absolute path
2412  if (!empty($theFile) && MathUtility::canBeInterpretedAsInteger($theFile)) {
2413  $fileObject = ResourceFactory::getInstance()->getFileObject($theFile);
2414  $theFile = $fileObject->getForLocalProcessing(false);
2415  }
2416  if ($this->alternativeFilePath[$theFile]) {
2417  // If alternative File Path is set for the file, then it was an import
2418  // don't import the file if it already exists
2419  if (@is_file((PATH_site . $this->alternativeFilePath[$theFile]))) {
2420  $theFile = PATH_site . $this->alternativeFilePath[$theFile];
2421  } elseif (@is_file($theFile)) {
2422  $dest = dirname(PATH_site . $this->alternativeFilePath[$theFile]);
2423  if (!@is_dir($dest)) {
2424  GeneralUtility::mkdir_deep(PATH_site, dirname($this->alternativeFilePath[$theFile]) . '/');
2425  }
2426  // Init:
2427  $maxSize = (int)$tcaFieldConf['max_size'];
2428  // Must be cleared. Else a faulty fileref may be inserted if the below code returns an error!
2429  $theDestFile = '';
2430  $fileSize = filesize($theFile);
2431  // Check file size:
2432  if (!$maxSize || $fileSize <= $maxSize * 1024) {
2433  // Prepare filename:
2434  $theEndFileName = isset($this->alternativeFileName[$theFile]) ? $this->alternativeFileName[$theFile] : $theFile;
2435  $fI = GeneralUtility::split_fileref($theEndFileName);
2436  // Check for allowed extension:
2437  if ($this->fileFunc->checkIfAllowed($fI['fileext'], $dest, $theEndFileName)) {
2438  $theDestFile = PATH_site . $this->alternativeFilePath[$theFile];
2439  // Write the file:
2440  if ($theDestFile) {
2441  GeneralUtility::upload_copy_move($theFile, $theDestFile);
2442  $this->copiedFileMap[$theFile] = $theDestFile;
2443  clearstatcache();
2444  if ($this->enableLogging && !@is_file($theDestFile)) {
2445  $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']);
2446  }
2447  } elseif ($this->enableLogging) {
2448  $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: No destination file (%s) possible!. (%s)', 11, [$theFile, $theDestFile, $recFID], $propArr['event_pid']);
2449  }
2450  } elseif ($this->enableLogging) {
2451  $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, [$fI['fileext'], $recFID], $propArr['event_pid']);
2452  }
2453  } elseif ($this->enableLogging) {
2454  $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']);
2455  }
2456  // If the destination file was created, we will set the new filename in the value array, otherwise unset the entry in the value array!
2457  if (@is_file($theDestFile)) {
2458  // The value is set to the new filename
2459  $theFile = $theDestFile;
2460  } else {
2461  // The value is set to the new filename
2462  unset($theFile);
2463  }
2464  }
2465  }
2466  if (!empty($theFile)) {
2467  $theFile = GeneralUtility::fixWindowsFilePath($theFile);
2468  if (GeneralUtility::isFirstPartOfStr($theFile, PATH_site)) {
2469  $theFile = PathUtility::stripPathSitePrefix($theFile);
2470  }
2471  }
2472  }
2473  unset($theFile);
2474  }
2475  }
2476  }
2477  return $valueArray;
2478  }
2479 
2492  public function checkValue_flex($res, $value, $tcaFieldConf, $PP, $uploadedFiles, $field)
2493  {
2495  list($table, $id, $curValue, $status, $realPid, $recFID, $tscPID) = $PP;
2496  $this->checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field);
2497  }
2498 
2516  protected function checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field)
2517  {
2518  if (is_array($value)) {
2519  // This value is necessary for flex form processing to happen on flexform fields in page records when they are copied.
2520  // Problem: when copying a page, flexform XML comes along in the array for the new record - but since $this->checkValue_currentRecord does not have a uid or pid for that
2521  // sake, the BackendUtility::getFlexFormDS() function returns no good DS. For new records we do know the expected PID so therefore we send that with this special parameter.
2522  // Only active when larger than zero.
2523  $newRecordPidValue = $status == 'new' ? $realPid : 0;
2524  // Get current value array:
2525  $dataStructArray = BackendUtility::getFlexFormDS($tcaFieldConf, $this->checkValue_currentRecord, $table, $field, true, $newRecordPidValue);
2526  $currentValueArray = (string)$curValue !== '' ? GeneralUtility::xml2array($curValue) : [];
2527  if (!is_array($currentValueArray)) {
2528  $currentValueArray = [];
2529  }
2530  if (isset($currentValueArray['meta']['currentLangId'])) {
2531  // @deprecated call since TYPO3 7, will be removed with TYPO3 8
2532  unset($currentValueArray['meta']['currentLangId']);
2533  }
2534  // Remove all old meta for languages...
2535  // Evaluation of input values:
2536  $value['data'] = $this->checkValue_flex_procInData($value['data'], $currentValueArray['data'], $uploadedFiles['data'], $dataStructArray, [$table, $id, $curValue, $status, $realPid, $recFID, $tscPID]);
2537  // Create XML from input value:
2538  $xmlValue = $this->checkValue_flexArray2Xml($value, true);
2539 
2540  // 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
2541  // (provided that the current value was already stored IN the charset that the new value is converted to).
2542  $arrValue = GeneralUtility::xml2array($xmlValue);
2543 
2544  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'])) {
2545  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'] as $classRef) {
2546  $hookObject = GeneralUtility::getUserObj($classRef);
2547  if (method_exists($hookObject, 'checkFlexFormValue_beforeMerge')) {
2548  $hookObject->checkFlexFormValue_beforeMerge($this, $currentValueArray, $arrValue);
2549  }
2550  }
2551  }
2552 
2553  ArrayUtility::mergeRecursiveWithOverrule($currentValueArray, $arrValue);
2554  $xmlValue = $this->checkValue_flexArray2Xml($currentValueArray, true);
2555 
2556  // Action commands (sorting order and removals of elements) for flexform sections,
2557  // see FormEngine for the use of this GP parameter
2558  $actionCMDs = GeneralUtility::_GP('_ACTION_FLEX_FORMdata');
2559  if (is_array($actionCMDs[$table][$id][$field]['data'])) {
2560  $arrValue = GeneralUtility::xml2array($xmlValue);
2561  $this->_ACTION_FLEX_FORMdata($arrValue['data'], $actionCMDs[$table][$id][$field]['data']);
2562  $xmlValue = $this->checkValue_flexArray2Xml($arrValue, true);
2563  }
2564  // Create the value XML:
2565  $res['value'] = '';
2566  $res['value'] .= $xmlValue;
2567  } else {
2568  // Passthrough...:
2569  $res['value'] = $value;
2570  }
2571 
2572  return $res;
2573  }
2574 
2582  public function checkValue_flexArray2Xml($array, $addPrologue = false)
2583  {
2585  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
2586  return $flexObj->flexArray2Xml($array, $addPrologue);
2587  }
2588 
2596  protected function _ACTION_FLEX_FORMdata(&$valueArray, $actionCMDs)
2597  {
2598  if (!is_array($valueArray) || !is_array($actionCMDs)) {
2599  return;
2600  }
2601 
2602  foreach ($actionCMDs as $key => $value) {
2603  if ($key == '_ACTION') {
2604  // First, check if there are "commands":
2605  if (current($actionCMDs[$key]) === '') {
2606  continue;
2607  }
2608 
2609  asort($actionCMDs[$key]);
2610  $newValueArray = [];
2611  foreach ($actionCMDs[$key] as $idx => $order) {
2612  if (substr($idx, 0, 3) == 'ID-') {
2613  $idx = $this->newIndexMap[$idx];
2614  }
2615  // Just one reflection here: It is clear that when removing elements from a flexform, then we will get lost files unless we act on this delete operation by traversing and deleting files that were referred to.
2616  if ($order != 'DELETE') {
2617  $newValueArray[$idx] = $valueArray[$idx];
2618  }
2619  unset($valueArray[$idx]);
2620  }
2621  $valueArray = $valueArray + $newValueArray;
2622  } elseif (is_array($actionCMDs[$key]) && isset($valueArray[$key])) {
2623  $this->_ACTION_FLEX_FORMdata($valueArray[$key], $actionCMDs[$key]);
2624  }
2625  }
2626  }
2627 
2640  public function checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData = null)
2641  {
2642  list($table, $id, , $status) = $PP;
2643  $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
2644  }
2645 
2660  public function checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData = null)
2661  {
2662  if (!$tcaFieldConf['foreign_table']) {
2663  // Fatal error, inline fields should always have a foreign_table defined
2664  return false;
2665  }
2666  // When values are sent they come as comma-separated values which are exploded by this function:
2667  $valueArray = GeneralUtility::trimExplode(',', $value);
2668  // Remove duplicates: (should not be needed)
2669  $valueArray = array_unique($valueArray);
2670  // Example for received data:
2671  // $value = 45,NEW4555fdf59d154,12,123
2672  // We need to decide whether we use the stack or can save the relation directly.
2673  if (!empty($value) && (strpos($value, 'NEW') !== false || !MathUtility::canBeInterpretedAsInteger($id))) {
2674  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2675  $this->addNewValuesToRemapStackChildIds($valueArray);
2676  $this->remapStack[] = [
2677  'func' => 'checkValue_inline_processDBdata',
2678  'args' => [$valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData],
2679  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 4],
2680  'additionalData' => $additionalData,
2681  'field' => $field,
2682  ];
2683  unset($res['value']);
2684  } elseif ($value || MathUtility::canBeInterpretedAsInteger($id)) {
2685  $res['value'] = $this->checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData);
2686  }
2687  return $res;
2688  }
2689 
2698  public function checkValue_checkMax($tcaFieldConf, $valueArray)
2699  {
2700  // BTW, checking for min and max items here does NOT make any sense when MM is used because the above function calls will just return an array with a single item (the count) if MM is used... Why didn't I perform the check before? Probably because we could not evaluate the validity of record uids etc... Hmm...
2701  $valueArrayC = count($valueArray);
2702  // NOTE to the comment: It's not really possible to check for too few items, because you must then determine first, if the field is actual used regarding the CType.
2703  $maxI = isset($tcaFieldConf['maxitems']) ? (int)$tcaFieldConf['maxitems'] : 1;
2704  if ($valueArrayC > $maxI) {
2705  $valueArrayC = $maxI;
2706  }
2707  // Checking for not too many elements
2708  // Dumping array to list
2709  $newVal = [];
2710  foreach ($valueArray as $nextVal) {
2711  if ($valueArrayC == 0) {
2712  break;
2713  }
2714  $valueArrayC--;
2715  $newVal[] = $nextVal;
2716  }
2717  return $newVal;
2718  }
2719 
2720  /*********************************************
2721  *
2722  * Helper functions for evaluation functions.
2723  *
2724  ********************************************/
2735  public function getUnique($table, $field, $value, $id, $newPid = 0)
2736  {
2737  // Initialize:
2738  $whereAdd = '';
2739  $newValue = '';
2740  if ((int)$newPid) {
2741  $whereAdd .= ' AND pid=' . (int)$newPid;
2742  } else {
2743  $whereAdd .= ' AND pid>=0';
2744  }
2745  // "AND pid>=0" for versioning
2746  $whereAdd .= $this->deleteClause($table);
2747  // If the field is configured in TCA, proceed:
2748  if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
2749  // Look for a record which might already have the value:
2750  $res = $this->databaseConnection->exec_SELECTquery('uid', $table, $field . '=' . $this->databaseConnection->fullQuoteStr($value, $table) . ' AND uid<>' . (int)$id . $whereAdd);
2751  $counter = 0;
2752  // For as long as records with the test-value existing, try again (with incremented numbers appended).
2753  while ($this->databaseConnection->sql_num_rows($res)) {
2754  $newValue = $value . $counter;
2755  $res = $this->databaseConnection->exec_SELECTquery('uid', $table, $field . '=' . $this->databaseConnection->fullQuoteStr($newValue, $table) . ' AND uid<>' . (int)$id . $whereAdd);
2756  $counter++;
2757  if ($counter > 100) {
2758  break;
2759  }
2760  }
2761  $this->databaseConnection->sql_free_result($res);
2762  // If the new value is there:
2763  $value = $newValue !== '' ? $newValue : $value;
2764  }
2765  return $value;
2766  }
2767 
2779  public function getRecordsWithSameValue($tableName, $uid, $fieldName, $value, $pageId = 0)
2780  {
2781  $result = [];
2782  if (!empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
2783  $uid = (int)$uid;
2784  $pageId = (int)$pageId;
2785  $whereStatement = ' AND uid <> ' . $uid . ' AND ' . ($pageId ? 'pid = ' . $pageId : 'pid >= 0');
2786  $result = BackendUtility::getRecordsByField($tableName, $fieldName, $value, $whereStatement);
2787  }
2788  return $result;
2789  }
2790 
2797  public function checkValue_text_Eval($value, $evalArray, $is_in)
2798  {
2799  $res = [];
2800  $set = true;
2801  foreach ($evalArray as $func) {
2802  switch ($func) {
2803  case 'trim':
2804  $value = trim($value);
2805  break;
2806  case 'required':
2807  if (!$value) {
2808  $set = false;
2809  }
2810  break;
2811  default:
2812  if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2813  if (class_exists($func)) {
2814  $evalObj = GeneralUtility::makeInstance($func);
2815  if (method_exists($evalObj, 'evaluateFieldValue')) {
2816  $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2817  }
2818  }
2819  }
2820  }
2821  }
2822  if ($set) {
2823  $res['value'] = $value;
2824  }
2825  return $res;
2826  }
2827 
2836  public function checkValue_input_Eval($value, $evalArray, $is_in)
2837  {
2838  $res = [];
2839  $set = true;
2840  foreach ($evalArray as $func) {
2841  switch ($func) {
2842  case 'int':
2843  case 'year':
2844  case 'time':
2845  case 'timesec':
2846  $value = (int)$value;
2847  break;
2848  case 'date':
2849  case 'datetime':
2850  $value = (int)$value;
2851  if ($value !== 0 && !$this->dontProcessTransformations) {
2852  $value -= date('Z', $value);
2853  }
2854  break;
2855  case 'double2':
2856  $value = preg_replace('/[^0-9,\\.-]/', '', $value);
2857  $negative = $value[0] === '-';
2858  $value = strtr($value, [',' => '.', '-' => '']);
2859  if (strpos($value, '.') === false) {
2860  $value .= '.0';
2861  }
2862  $valueArray = explode('.', $value);
2863  $dec = array_pop($valueArray);
2864  $value = implode('', $valueArray) . '.' . $dec;
2865  if ($negative) {
2866  $value *= -1;
2867  }
2868  $value = number_format($value, 2, '.', '');
2869  break;
2870  case 'md5':
2871  if (strlen($value) != 32) {
2872  $set = false;
2873  }
2874  break;
2875  case 'trim':
2876  $value = trim($value);
2877  break;
2878  case 'upper':
2879  $value = $GLOBALS['LANG']->csConvObj->conv_case($GLOBALS['LANG']->charSet, $value, 'toUpper');
2880  break;
2881  case 'lower':
2882  $value = $GLOBALS['LANG']->csConvObj->conv_case($GLOBALS['LANG']->charSet, $value, 'toLower');
2883  break;
2884  case 'required':
2885  if (!isset($value) || $value === '') {
2886  $set = false;
2887  }
2888  break;
2889  case 'is_in':
2890  $c = strlen($value);
2891  if ($c) {
2892  $newVal = '';
2893  for ($a = 0; $a < $c; $a++) {
2894  $char = substr($value, $a, 1);
2895  if (strpos($is_in, $char) !== false) {
2896  $newVal .= $char;
2897  }
2898  }
2899  $value = $newVal;
2900  }
2901  break;
2902  case 'nospace':
2903  $value = str_replace(' ', '', $value);
2904  break;
2905  case 'alpha':
2906  $value = preg_replace('/[^a-zA-Z]/', '', $value);
2907  break;
2908  case 'num':
2909  $value = preg_replace('/[^0-9]/', '', $value);
2910  break;
2911  case 'alphanum':
2912  $value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
2913  break;
2914  case 'alphanum_x':
2915  $value = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
2916  break;
2917  case 'domainname':
2918  if (!preg_match('/^[a-z0-9.\\-]*$/i', $value)) {
2919  $value = GeneralUtility::idnaEncode($value);
2920  }
2921  break;
2922  case 'email':
2923  if ((string)$value !== '') {
2924  $this->checkValue_input_ValidateEmail($value, $set);
2925  }
2926  break;
2927  default:
2928  if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2929  if (class_exists($func)) {
2930  $evalObj = GeneralUtility::makeInstance($func);
2931  if (method_exists($evalObj, 'evaluateFieldValue')) {
2932  $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2933  }
2934  }
2935  }
2936  }
2937  }
2938  if ($set) {
2939  $res['value'] = $value;
2940  }
2941  return $res;
2942  }
2943 
2955  protected function checkValue_input_ValidateEmail($value, &$set)
2956  {
2957  if (GeneralUtility::validEmail($value)) {
2958  return;
2959  }
2960 
2961  $set = false;
2963  $message = GeneralUtility::makeInstance(FlashMessage::class,
2964  sprintf($GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:error.invalidEmail'), $value),
2965  '', // header is optional
2967  true // whether message should be stored in session
2968  );
2970  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
2971  $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
2972  }
2973 
2986  public function checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
2987  {
2988  if ($type === 'group') {
2989  $tables = $tcaFieldConf['allowed'];
2990  } elseif (!empty($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages') {
2991  $tables = 'sys_language';
2992  } else {
2993  $tables = $tcaFieldConf['foreign_table'];
2994  }
2995  $prep = $type == 'group' ? $tcaFieldConf['prepend_tname'] : '';
2996  $newRelations = implode(',', $valueArray);
2998  $dbAnalysis = $this->createRelationHandlerInstance();
2999  $dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
3000  $dbAnalysis->start($newRelations, $tables, '', 0, $currentTable, $tcaFieldConf);
3001  if ($tcaFieldConf['MM']) {
3002  // convert submitted items to use version ids instead of live ids
3003  // (only required for MM relations in a workspace context)
3004  $dbAnalysis->convertItemArray();
3005  if ($status == 'update') {
3007  $oldRelations_dbAnalysis = $this->createRelationHandlerInstance();
3008  $oldRelations_dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
3009  // Db analysis with $id will initialize with the existing relations
3010  $oldRelations_dbAnalysis->start('', $tables, $tcaFieldConf['MM'], $id, $currentTable, $tcaFieldConf);
3011  $oldRelations = implode(',', $oldRelations_dbAnalysis->getValueArray());
3012  $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, $prep);
3013  if ($oldRelations != $newRelations) {
3014  $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
3015  $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
3016  } else {
3017  $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
3018  $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
3019  }
3020  } else {
3021  $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
3022  }
3023  $valueArray = $dbAnalysis->countItems();
3024  } else {
3025  $valueArray = $dbAnalysis->getValueArray($prep);
3026  }
3027  // 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.
3028  return $valueArray;
3029  }
3030 
3038  {
3039  $valueArray = GeneralUtility::trimExplode(',', $value, true);
3040  foreach ($valueArray as &$newVal) {
3041  $temp = explode('|', $newVal, 2);
3042  $newVal = str_replace(',', '', str_replace('|', '', rawurldecode($temp[0])));
3043  }
3044  unset($newVal);
3045  return $valueArray;
3046  }
3047 
3063  public function checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructArray, $pParams, $callBackFunc = '', array $workspaceOptions = [])
3064  {
3065  if (is_array($dataPart)) {
3066  foreach ($dataPart as $sKey => $sheetDef) {
3067  list($dataStruct, $actualSheet) = GeneralUtility::resolveSheetDefInDS($dataStructArray, $sKey);
3068  if (is_array($dataStruct) && $actualSheet == $sKey && is_array($sheetDef)) {
3069  foreach ($sheetDef as $lKey => $lData) {
3070  $this->checkValue_flex_procInData_travDS($dataPart[$sKey][$lKey], $dataPart_current[$sKey][$lKey], $uploadedFiles[$sKey][$lKey], $dataStruct['ROOT']['el'], $pParams, $callBackFunc, $sKey . '/' . $lKey . '/', $workspaceOptions);
3071  }
3072  }
3073  }
3074  }
3075  return $dataPart;
3076  }
3077 
3093  public function checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions = [])
3094  {
3095  if (!is_array($DSelements)) {
3096  return;
3097  }
3098 
3099  // For each DS element:
3100  foreach ($DSelements as $key => $dsConf) {
3101  // Array/Section:
3102  if ($DSelements[$key]['type'] == 'array') {
3103  if (!is_array($dataValues[$key]['el'])) {
3104  continue;
3105  }
3106 
3107  if ($DSelements[$key]['section']) {
3108  $newIndexCounter = 0;
3109  foreach ($dataValues[$key]['el'] as $ik => $el) {
3110  if (!is_array($el)) {
3111  continue;
3112  }
3113 
3114  if (!is_array($dataValues_current[$key]['el'])) {
3115  $dataValues_current[$key]['el'] = [];
3116  }
3117  $theKey = key($el);
3118  if (!is_array($dataValues[$key]['el'][$ik][$theKey]['el'])) {
3119  continue;
3120  }
3121 
3122  $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);
3123  // If element is added dynamically in the flexform of TCEforms, we map the ID-string to the next numerical index we can have in that particular section of elements:
3124  // The fact that the order changes is not important since order is controlled by a separately submitted index.
3125  if (substr($ik, 0, 3) == 'ID-') {
3126  $newIndexCounter++;
3127  // Set mapping index
3128  $this->newIndexMap[$ik] = (is_array($dataValues_current[$key]['el']) && !empty($dataValues_current[$key]['el']) ? max(array_keys($dataValues_current[$key]['el'])) : 0) + $newIndexCounter;
3129  // Transfer values
3130  $dataValues[$key]['el'][$this->newIndexMap[$ik]] = $dataValues[$key]['el'][$ik];
3131  // Unset original
3132  unset($dataValues[$key]['el'][$ik]);
3133  }
3134  }
3135  } else {
3136  if (!isset($dataValues[$key]['el'])) {
3137  $dataValues[$key]['el'] = [];
3138  }
3139  $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $uploadedFiles[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/', $workspaceOptions);
3140  }
3141  } else {
3142  if (!is_array($dsConf['TCEforms']['config']) || !is_array($dataValues[$key])) {
3143  continue;
3144  }
3145 
3146  foreach ($dataValues[$key] as $vKey => $data) {
3147  if ($callBackFunc) {
3148  if (is_object($this->callBackObj)) {
3149  $res = $this->callBackObj->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
3150  } else {
3151  $res = $this->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
3152  }
3153  } else {
3154  // Default
3155  list($CVtable, $CVid, $CVcurValue, $CVstatus, $CVrealPid, $CVrecFID, $CVtscPID) = $pParams;
3156 
3157  $additionalData = [
3158  'flexFormId' => $CVrecFID,
3159  'flexFormPath' => trim(rtrim($structurePath, '/') . '/' . $key . '/' . $vKey, '/'),
3160  ];
3161 
3162  $res = $this->checkValue_SW([], $dataValues[$key][$vKey], $dsConf['TCEforms']['config'], $CVtable, $CVid, $dataValues_current[$key][$vKey], $CVstatus, $CVrealPid, $CVrecFID, '', $uploadedFiles[$key][$vKey], $CVtscPID, $additionalData);
3163  // Look for RTE transformation of field:
3164  if ($dataValues[$key]['_TRANSFORM_' . $vKey] == 'RTE' && !$this->dontProcessTransformations) {
3165  // Unsetting trigger field - we absolutely don't want that into the data storage!
3166  unset($dataValues[$key]['_TRANSFORM_' . $vKey]);
3167  if (isset($res['value'])) {
3168  // Calculating/Retrieving some values here:
3169  list(, , $recFieldName) = explode(':', $CVrecFID);
3170  $theTypeString = BackendUtility::getTCAtypeValue($CVtable, $this->checkValue_currentRecord);
3171  $specConf = BackendUtility::getSpecConfParts($dsConf['TCEforms']['defaultExtras']);
3172  // Find, thisConfig:
3173  $RTEsetup = $this->BE_USER->getTSConfig('RTE', BackendUtility::getPagesTSconfig($CVtscPID));
3174  $thisConfig = BackendUtility::RTEsetup($RTEsetup['properties'], $CVtable, $recFieldName, $theTypeString);
3175  $res['value'] = $this->transformRichtextContentToDatabase(
3176  $res['value'], $CVtable, $recFieldName, $specConf, $thisConfig, $CVrealPid
3177  );
3178  }
3179  }
3180  }
3181  // Adding the value:
3182  if (isset($res['value'])) {
3183  $dataValues[$key][$vKey] = $res['value'];
3184  }
3185  // 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.
3186  // 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).
3187  if (substr($vKey, -9) != '.vDEFbase') {
3188  // @deprecated: flexFormXMLincludeDiffBase is only enabled by ext:compatibility6 since TYPO3 CMS 7, vDEFbase can be unset / ignored with TYPO3 CMS 8
3189  if ($this->clear_flexFormData_vDEFbase) {
3190  $dataValues[$key][$vKey . '.vDEFbase'] = '';
3191  } elseif ($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')) {
3192  // 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:
3193  if (isset($dataValues[$key]['vDEF'])) {
3194  $diffValue = $dataValues[$key]['vDEF'];
3195  } else {
3196  // If not found (for translators with no access to the default language) we use the one from the current-value data set:
3197  $diffValue = $dataValues_current[$key]['vDEF'];
3198  }
3199  // 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.
3200  $dataValues[$key][$vKey . '.vDEFbase'] = $this->updateModeL10NdiffDataClear ? '' : $diffValue;
3201  }
3202  }
3203  }
3204  }
3205  }
3206  }
3207 
3220  protected function checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, array $additionalData = null)
3221  {
3222  $newValue = '';
3223  $foreignTable = $tcaFieldConf['foreign_table'];
3224  $transOrigPointer = 0;
3225  $keepTranslation = false;
3226  $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
3227  // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3229  $dbAnalysis = $this->createRelationHandlerInstance();
3230  $dbAnalysis->start(implode(',', $valueArray), $foreignTable, '', 0, $table, $tcaFieldConf);
3231  // If the localizationMode is set to 'keep', the children for the localized parent are kept as in the original untranslated record:
3232  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $tcaFieldConf);
3233  if ($localizationMode == 'keep' && $status == 'update') {
3234  // Fetch the current record and determine the original record:
3235  $row = BackendUtility::getRecordWSOL($table, $id);
3236  if (is_array($row)) {
3237  $language = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
3238  $transOrigPointer = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
3239  // If language is set (e.g. 1) and also transOrigPointer (e.g. 123), use transOrigPointer as uid:
3240  if ($language > 0 && $transOrigPointer) {
3241  $id = $transOrigPointer;
3242  // If we're in active localizationMode 'keep', prevent from writing data to the field of the parent record:
3243  // (on removing the localized parent, the original (untranslated) children would then also be removed)
3244  $keepTranslation = true;
3245  }
3246  }
3247  }
3248  // IRRE with a pointer field (database normalization):
3249  if ($tcaFieldConf['foreign_field']) {
3250  // if the record was imported, sorting was also imported, so skip this
3251  $skipSorting = (bool)$this->callFromImpExp;
3252  // update record in intermediate table (sorting & pointer uid to parent record)
3253  $dbAnalysis->writeForeignField($tcaFieldConf, $id, 0, $skipSorting);
3254  $newValue = $keepTranslation ? 0 : $dbAnalysis->countItems(false);
3255  } else {
3256  if ($this->getInlineFieldType($tcaFieldConf) == 'mm') {
3257  // In order to fully support all the MM stuff, directly call checkValue_group_select_processDBdata instead of repeating the needed code here
3258  $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
3259  $newValue = $keepTranslation ? 0 : $valueArray[0];
3260  } else {
3261  $valueArray = $dbAnalysis->getValueArray();
3262  // Checking that the number of items is correct:
3263  $valueArray = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
3264  $valueData = $this->castReferenceValue(implode(',', $valueArray), $tcaFieldConf);
3265  // If a valid translation of the 'keep' mode is active, update relations in the original(!) record:
3266  if ($keepTranslation) {
3267  $this->updateDB($table, $transOrigPointer, [$field => $valueData]);
3268  } else {
3269  $newValue = $valueData;
3270  }
3271  }
3272  }
3273  return $newValue;
3274  }
3275 
3276  /*********************************************
3277  *
3278  * PROCESSING COMMANDS
3279  *
3280  ********************************************/
3287  public function process_cmdmap()
3288  {
3289  // Editing frozen:
3290  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
3291  if ($this->enableLogging) {
3292  $this->newlog('All editing in this workspace has been frozen!', 1);
3293  }
3294  return false;
3295  }
3296  // Hook initialization:
3297  $hookObjectsArr = [];
3298  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'])) {
3299  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] as $classRef) {
3300  $hookObj = GeneralUtility::getUserObj($classRef);
3301  if (method_exists($hookObj, 'processCmdmap_beforeStart')) {
3302  $hookObj->processCmdmap_beforeStart($this);
3303  }
3304  $hookObjectsArr[] = $hookObj;
3305  }
3306  }
3307  $pasteDatamap = [];
3308  // Traverse command map:
3309  foreach ($this->cmdmap as $table => $_) {
3310  // Check if the table may be modified!
3311  $modifyAccessList = $this->checkModifyAccessList($table);
3312  if ($this->enableLogging && !$modifyAccessList) {
3313  $this->log($table, 0, 2, 0, 1, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
3314  }
3315  // Check basic permissions and circumstances:
3316  if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->cmdmap[$table]) || !$modifyAccessList) {
3317  continue;
3318  }
3319 
3320  // Traverse the command map:
3321  foreach ($this->cmdmap[$table] as $id => $incomingCmdArray) {
3322  if (!is_array($incomingCmdArray)) {
3323  continue;
3324  }
3325 
3326  if ($table === 'pages') {
3327  // for commands on pages do a pagetree-refresh
3328  $this->pagetreeNeedsRefresh = true;
3329  }
3330 
3331  foreach ($incomingCmdArray as $command => $value) {
3332  $pasteUpdate = false;
3333  if (is_array($value) && isset($value['action']) && $value['action'] === 'paste') {
3334  // Extended paste command: $command is set to "move" or "copy"
3335  // $value['update'] holds field/value pairs which should be updated after copy/move operation
3336  // $value['target'] holds original $value (target of move/copy)
3337  $pasteUpdate = $value['update'];
3338  $value = $value['target'];
3339  }
3340  foreach ($hookObjectsArr as $hookObj) {
3341  if (method_exists($hookObj, 'processCmdmap_preProcess')) {
3342  $hookObj->processCmdmap_preProcess($command, $table, $id, $value, $this, $pasteUpdate);
3343  }
3344  }
3345  // Init copyMapping array:
3346  // Must clear this array before call from here to those functions:
3347  // Contains mapping information between new and old id numbers.
3348  $this->copyMappingArray = [];
3349  // process the command
3350  $commandIsProcessed = false;
3351  foreach ($hookObjectsArr as $hookObj) {
3352  if (method_exists($hookObj, 'processCmdmap')) {
3353  $hookObj->processCmdmap($command, $table, $id, $value, $commandIsProcessed, $this, $pasteUpdate);
3354  }
3355  }
3356  // Only execute default commands if a hook hasn't been processed the command already
3357  if (!$commandIsProcessed) {
3358  $procId = $id;
3359  // Branch, based on command
3360  switch ($command) {
3361  case 'move':
3362  $this->moveRecord($table, $id, $value);
3363  break;
3364  case 'copy':
3365  if ($table === 'pages') {
3366  $this->copyPages($id, $value);
3367  } else {
3368  $this->copyRecord($table, $id, $value, 1);
3369  }
3370  $procId = $this->copyMappingArray[$table][$id];
3371  break;
3372  case 'localize':
3373  $this->useTransOrigPointerField = true;
3374  $this->localize($table, $id, $value);
3375  break;
3376  case 'copyToLanguage':
3377  $this->useTransOrigPointerField = false;
3378  $this->localize($table, $id, $value);
3379  break;
3380  case 'inlineLocalizeSynchronize':
3381  $this->inlineLocalizeSynchronize($table, $id, $value);
3382  break;
3383  case 'delete':
3384  $this->deleteAction($table, $id);
3385  break;
3386  case 'undelete':
3387  $this->undeleteRecord($table, $id);
3388  break;
3389  }
3390  if (is_array($pasteUpdate)) {
3391  $pasteDatamap[$table][$procId] = $pasteUpdate;
3392  }
3393  }
3394  foreach ($hookObjectsArr as $hookObj) {
3395  if (method_exists($hookObj, 'processCmdmap_postProcess')) {
3396  $hookObj->processCmdmap_postProcess($command, $table, $id, $value, $this, $pasteUpdate, $pasteDatamap);
3397  }
3398  }
3399  // Merging the copy-array info together for remapping purposes.
3400  ArrayUtility::mergeRecursiveWithOverrule($this->copyMappingArray_merged, $this->copyMappingArray);
3401  }
3402  }
3403  }
3405  $copyTCE = $this->getLocalTCE();
3406  $copyTCE->start($pasteDatamap, '', $this->BE_USER);
3407  $copyTCE->process_datamap();
3408  $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3409  unset($copyTCE);
3410 
3411  // Finally, before exit, check if there are ID references to remap.
3412  // This might be the case if versioning or copying has taken place!
3413  $this->remapListedDBRecords();
3414  $this->processRemapStack();
3415  foreach ($hookObjectsArr as $hookObj) {
3416  if (method_exists($hookObj, 'processCmdmap_afterFinish')) {
3417  $hookObj->processCmdmap_afterFinish($this);
3418  }
3419  }
3420  if ($this->isOuterMostInstance()) {
3421  $this->processClearCacheQueue();
3422  $this->resetNestedElementCalls();
3423  }
3424  }
3425 
3426  /*********************************************
3427  *
3428  * Cmd: Copying
3429  *
3430  ********************************************/
3444  public function copyRecord($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '', $language = 0, $ignoreLocalization = false)
3445  {
3446  $uid = ($origUid = (int)$uid);
3447  // Only copy if the table is defined in $GLOBALS['TCA'], a uid is given and the record wasn't copied before:
3448  if (empty($GLOBALS['TCA'][$table]) || $uid === 0) {
3449  return null;
3450  }
3451  if ($this->isRecordCopied($table, $uid)) {
3452  if (!empty($overrideValues)) {
3453  $this->log($table, $uid, 1, 0, 1, 'Repeated attempt to copy record "%s:%s" with override values', -1, [$table, $uid]);
3454  }
3455  return null;
3456  }
3457 
3458  // This checks if the record can be selected which is all that a copy action requires.
3459  if (!$this->doesRecordExist($table, $uid, 'show')) {
3460  if ($this->enableLogging) {
3461  $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record "%s:%s" without permission', -1, [$table, $uid]);
3462  }
3463  return null;
3464  }
3465 
3466  // Check if table is allowed on destination page
3467  if ($destPid >= 0 && !$this->isTableAllowedForThisPage($destPid, $table)) {
3468  if ($this->enableLogging) {
3469  $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]);
3470  }
3471  return null;
3472  }
3473 
3474  $fullLanguageCheckNeeded = $table != 'pages';
3475  //Used to check language and general editing rights
3476  if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded)) {
3477  if ($this->enableLogging) {
3478  $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]);
3479  }
3480  return null;
3481  }
3482 
3483  $data = [];
3484  $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));
3485  // So it copies (and localized) content from workspace...
3486  $row = BackendUtility::getRecordWSOL($table, $uid);
3487  if (!is_array($row)) {
3488  if ($this->enableLogging) {
3489  $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record that did not exist!');
3490  }
3491  return null;
3492  }
3493 
3494  // Initializing:
3495  $theNewID = StringUtility::getUniqueId('NEW');
3496  $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3497  $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3498  // Getting default data:
3499  $defaultData = $this->newFieldArray($table);
3500  // Getting "copy-after" fields if applicable:
3501  $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, abs($destPid), 0) : [];
3502  // Page TSconfig related:
3503  // 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...
3504  $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
3505  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
3506  $tE = $this->getTableEntries($table, $TSConfig);
3507  // Traverse ALL fields of the selected record:
3508  $setDefaultOnCopyArray = array_flip(GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['setToDefaultOnCopy']));
3509  foreach ($row as $field => $value) {
3510  if (!in_array($field, $nonFields, true)) {
3511  // Get TCA configuration for the field:
3512  $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3513  // Preparation/Processing of the value:
3514  // "pid" is hardcoded of course:
3515  // isset() won't work here, since values can be NULL in each of the arrays
3516  // except setDefaultOnCopyArray, since we exploded that from a string
3517  if ($field == 'pid') {
3518  $value = $destPid;
3519  } elseif (array_key_exists($field, $overrideValues)) {
3520  // Override value...
3521  $value = $overrideValues[$field];
3522  } elseif (array_key_exists($field, $copyAfterFields)) {
3523  // Copy-after value if available:
3524  $value = $copyAfterFields[$field];
3525  } elseif ($GLOBALS['TCA'][$table]['ctrl']['setToDefaultOnCopy'] && isset($setDefaultOnCopyArray[$field])) {
3526  $value = $defaultData[$field];
3527  } else {
3528  // Hide at copy may override:
3529  if ($first && $field == $enableField && $GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] && !$this->neverHideAtCopy && !$tE['disableHideAtCopy']) {
3530  $value = 1;
3531  }
3532  // Prepend label on copy:
3533  if ($first && $field == $headerField && $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy'] && !$tE['disablePrependAtCopy']) {
3534  $value = $this->getCopyHeader($table, $this->resolvePid($table, $destPid), $field, $this->clearPrefixFromValue($table, $value), 0);
3535  }
3536  // Processing based on the TCA config field type (files, references, flexforms...)
3537  $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $tscPID, $language);
3538  }
3539  // Add value to array.
3540  $data[$table][$theNewID][$field] = $value;
3541  }
3542  }
3543  // Overriding values:
3544  if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
3545  $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3546  }
3547  // Setting original UID:
3548  if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3549  $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3550  }
3551  // Do the copy by simply submitting the array through TCEmain:
3553  $copyTCE = $this->getLocalTCE();
3554  $copyTCE->start($data, '', $this->BE_USER);
3555  $copyTCE->process_datamap();
3556  // Getting the new UID:
3557  $theNewSQLID = $copyTCE->substNEWwithIDs[$theNewID];
3558  if ($theNewSQLID) {
3559  $this->copyRecord_fixRTEmagicImages($table, BackendUtility::wsMapId($table, $theNewSQLID));
3560  $this->copyMappingArray[$table][$origUid] = $theNewSQLID;
3561  // Keep automatically versionized record information:
3562  if (isset($copyTCE->autoVersionIdMap[$table][$theNewSQLID])) {
3563  $this->autoVersionIdMap[$table][$theNewSQLID] = $copyTCE->autoVersionIdMap[$table][$theNewSQLID];
3564  }
3565  }
3566  // Copy back the cached TSconfig
3567  $this->cachedTSconfig = $copyTCE->cachedTSconfig;
3568  $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3569  unset($copyTCE);
3570  if (!$ignoreLocalization && $language == 0) {
3571  //repointing the new translation records to the parent record we just created
3572  $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
3573  $this->copyL10nOverlayRecords($table, $uid, $destPid, $first, $overrideValues, $excludeFields);
3574  }
3575 
3576  return $theNewSQLID;
3577  }
3578 
3587  public function copyPages($uid, $destPid)
3588  {
3589  // Initialize:
3590  $uid = (int)$uid;
3591  $destPid = (int)$destPid;
3592  // Finding list of tables to copy.
3593  // These are the tables, the user may modify
3594  $copyTablesArray = $this->admin ? $this->compileAdminTables() : explode(',', $this->BE_USER->groupData['tables_modify']);
3595  // 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
3596  if (!strstr($this->copyWhichTables, '*')) {
3597  $copyWhichTablesArray = array_flip(GeneralUtility::trimExplode(',', $this->copyWhichTables . ',pages'));
3598  foreach ($copyTablesArray as $k => $table) {
3599  // Pages are always going...
3600  if (!$table || !isset($copyWhichTablesArray[$table])) {
3601  unset($copyTablesArray[$k]);
3602  }
3603  }
3604  }
3605  $copyTablesArray = array_unique($copyTablesArray);
3606  // Begin to copy pages if we're allowed to:
3607  if ($this->admin || in_array('pages', $copyTablesArray, true)) {
3608  // Copy this page we're on. And set first-flag (this will trigger that the record is hidden if that is configured)!
3609  $theNewRootID = $this->copySpecificPage($uid, $destPid, $copyTablesArray, 1);
3610  // If we're going to copy recursively...:
3611  if ($theNewRootID && $this->copyTree) {
3612  // Get ALL subpages to copy (read-permissions are respected!):
3613  $CPtable = $this->int_pageTreeInfo([], $uid, (int)$this->copyTree, $theNewRootID);
3614  // Now copying the subpages:
3615  foreach ($CPtable as $thePageUid => $thePagePid) {
3616  $newPid = $this->copyMappingArray['pages'][$thePagePid];
3617  if (isset($newPid)) {
3618  $this->copySpecificPage($thePageUid, $newPid, $copyTablesArray);
3619  } else {
3620  if ($this->enableLogging) {
3621  $this->log('pages', $uid, 5, 0, 1, 'Something went wrong during copying branch');
3622  }
3623  break;
3624  }
3625  }
3626  }
3627  } elseif ($this->enableLogging) {
3628  $this->log('pages', $uid, 5, 0, 1, 'Attempt to copy page without permission to this table');
3629  }
3630  }
3631 
3641  public function copySpecificPage($uid, $destPid, $copyTablesArray, $first = false)
3642  {
3643  // Copy the page itself:
3644  $theNewRootID = $this->copyRecord('pages', $uid, $destPid, $first);
3645  // If a new page was created upon the copy operation we will proceed with all the tables ON that page:
3646  if ($theNewRootID) {
3647  foreach ($copyTablesArray as $table) {
3648  // All records under the page is copied.
3649  if ($table && is_array($GLOBALS['TCA'][$table]) && $table != 'pages') {
3650  $fields = 'uid';
3651  $languageField = null;
3652  $transOrigPointerField = null;
3653  if (BackendUtility::isTableLocalizable($table)) {
3654  $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
3655  $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3656  $fields .= ',' . $languageField . ',' . $transOrigPointerField;
3657  }
3659  $workspaceStatement = '';
3660  } elseif ((int)$this->BE_USER->workspace === 0) {
3661  $workspaceStatement = ' AND t3ver_wsid=0';
3662  } else {
3663  $workspaceStatement = ' AND t3ver_wsid IN (0,' . (int)$this->BE_USER->workspace . ')';
3664  }
3665  // Fetch records
3666  $rows = $this->databaseConnection->exec_SELECTgetRows(
3667  $fields,
3668  $table,
3669  'pid=' . (int)$uid . $this->deleteClause($table) . $workspaceStatement,
3670  '',
3671  (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby']) ? $GLOBALS['TCA'][$table]['ctrl']['sortby'] . ' DESC' : ''),
3672  '',
3673  'uid'
3674  );
3675  // Resolve placeholders of workspace versions
3676  if (!empty($rows) && (int)$this->BE_USER->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
3677  $rows = array_reverse(
3678  $this->resolveVersionedRecords(
3679  $table,
3680  $fields,
3681  $GLOBALS['TCA'][$table]['ctrl']['sortby'],
3682  array_keys($rows)
3683  ),
3684  true
3685  );
3686  }
3687  if (is_array($rows)) {
3688  foreach ($rows as $row) {
3689  // Skip localized records that will be processed in
3690  // copyL10nOverlayRecords() on copying the default language record
3691  $transOrigPointer = $row[$transOrigPointerField];
3692  if ($row[$languageField] > 0 && $transOrigPointer > 0 && isset($rows[$transOrigPointer])) {
3693  continue;
3694  }
3695  // Copying each of the underlying records...
3696  $this->copyRecord($table, $row['uid'], $theNewRootID);
3697  }
3698  } elseif ($this->enableLogging) {
3699  $this->log($table, $uid, 5, 0, 1, 'An SQL error occurred: ' . $this->databaseConnection->sql_error());
3700  }
3701  }
3702  }
3703  $this->processRemapStack();
3704  return $theNewRootID;
3705  }
3706  return null;
3707  }
3708 
3724  public function copyRecord_raw($table, $uid, $pid, $overrideArray = [], array $workspaceOptions = [])
3725  {
3726  $uid = (int)$uid;
3727  // Stop any actions if the record is marked to be deleted:
3728  // (this can occur if IRRE elements are versionized and child elements are removed)
3729  if ($this->isElementToBeDeleted($table, $uid)) {
3730  return null;
3731  }
3732  // Only copy if the table is defined in TCA, a uid is given and the record wasn't copied before:
3733  if (!$GLOBALS['TCA'][$table] || !$uid || $this->isRecordCopied($table, $uid)) {
3734  return null;
3735  }
3736  if (!$this->doesRecordExist($table, $uid, 'show')) {
3737  if ($this->enableLogging) {
3738  $this->log($table, $uid, 3, 0, 1, 'Attempt to rawcopy/versionize record without copy permission');
3739  }
3740  return null;
3741  }
3742 
3743  // Set up fields which should not be processed. They are still written - just passed through no-questions-asked!
3744  $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'];
3745  // Select main record:
3746  $row = $this->recordInfo($table, $uid, '*');
3747  if (!is_array($row)) {
3748  if ($this->enableLogging) {
3749  $this->log($table, $uid, 3, 0, 1, 'Attempt to rawcopy/versionize record that did not exist!');
3750  }
3751  return null;
3752  }
3753 
3754  // Merge in override array.
3755  $row = array_merge($row, $overrideArray);
3756  // Traverse ALL fields of the selected record:
3757  foreach ($row as $field => $value) {
3758  if (!in_array($field, $nonFields, true)) {
3759  // Get TCA configuration for the field:
3760  $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3761  if (is_array($conf)) {
3762  // Processing based on the TCA config field type (files, references, flexforms...)
3763  $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3764  }
3765  // Add value to array.
3766  $row[$field] = $value;
3767  }
3768  }
3769  // Force versioning related fields:
3770  $row['pid'] = $pid;
3771  // Setting original UID:
3772  if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3773  $row[$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3774  }
3775  // Do the copy by internal function
3776  $theNewSQLID = $this->insertNewCopyVersion($table, $row, $pid);
3777  if ($theNewSQLID) {
3778  $this->dbAnalysisStoreExec();
3779  $this->dbAnalysisStore = [];
3780  $this->copyRecord_fixRTEmagicImages($table, BackendUtility::wsMapId($table, $theNewSQLID));
3781  return $this->copyMappingArray[$table][$uid] = $theNewSQLID;
3782  }
3783  return null;
3784  }
3785 
3795  public function insertNewCopyVersion($table, $fieldArray, $realPid)
3796  {
3797  $id = StringUtility::getUniqueId('NEW');
3798  // $fieldArray is set as current record.
3799  // 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...
3800  $this->checkValue_currentRecord = $fieldArray;
3801  // Makes sure that transformations aren't processed on the copy.
3802  $backupDontProcessTransformations = $this->dontProcessTransformations;
3803  $this->dontProcessTransformations = true;
3804  // Traverse record and input-process each value:
3805  foreach ($fieldArray as $field => $fieldValue) {
3806  if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
3807  // Evaluating the value.
3808  $res = $this->checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0);
3809  if (isset($res['value'])) {
3810  $fieldArray[$field] = $res['value'];
3811  }
3812  }
3813  }
3814  // System fields being set:
3815  if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3816  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
3817  }
3818  if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3819  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
3820  }
3821  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3822  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3823  }
3824  // Finally, insert record:
3825  $this->insertDB($table, $id, $fieldArray, true);
3826  // Resets dontProcessTransformations to the previous state.
3827  $this->dontProcessTransformations = $backupDontProcessTransformations;
3828  // Return new id:
3829  return $this->substNEWwithIDs[$id];
3830  }
3831 
3848  public function copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language = 0, array $workspaceOptions = [])
3849  {
3850  // Process references and files, currently that means only the files, prepending absolute paths (so the TCEmain engine will detect the file as new and one that should be made into a copy)
3851  $value = $this->copyRecord_procFilesRefs($conf, $uid, $value);
3852  $inlineSubType = $this->getInlineFieldType($conf);
3853  // Get the localization mode for the current (parent) record (keep|select):
3854  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $field);
3855  // Register if there are references to take care of or MM is used on an inline field (no change to value):
3856  if ($this->isReferenceField($conf) || $inlineSubType == 'mm') {
3857  $value = $this->copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language, $localizationMode, $inlineSubType);
3858  } elseif ($inlineSubType !== false) {
3859  $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions, $localizationMode, $inlineSubType);
3860  }
3861  // 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())
3862  if ($conf['type'] == 'flex') {
3863  // Get current value array:
3864  $dataStructArray = BackendUtility::getFlexFormDS($conf, $row, $table, $field);
3865  $currentValueArray = GeneralUtility::xml2array($value);
3866  // Traversing the XML structure, processing files:
3867  if (is_array($currentValueArray)) {
3868  $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructArray, [$table, $uid, $field, $realDestPid], 'copyRecord_flexFormCallBack', $workspaceOptions);
3869  // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
3870  $value = $currentValueArray;
3871  }
3872  }
3873  return $value;
3874  }
3875 
3889  protected function copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language, $localizationMode, $inlineSubType)
3890  {
3891  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
3892  $prependName = $conf['type'] == 'group' ? $conf['prepend_tname'] : '';
3893  $mmTable = isset($conf['MM']) && $conf['MM'] ? $conf['MM'] : '';
3894  $localizeForeignTable = isset($conf['foreign_table']) && BackendUtility::isTableLocalizable($conf['foreign_table']);
3895  $localizeReferences = $localizeForeignTable && isset($conf['localizeReferencesAtParentLocalization']) && $conf['localizeReferencesAtParentLocalization'];
3896  $localizeChildren = $localizeForeignTable && isset($conf['behaviour']['localizeChildrenAtParentLocalization']) && $conf['behaviour']['localizeChildrenAtParentLocalization'];
3898  $dbAnalysis = $this->createRelationHandlerInstance();
3899  $dbAnalysis->start($value, $allowedTables, $mmTable, $uid, $table, $conf);
3900  // Localize referenced records of select fields:
3901  $localizingNonManyToManyFieldReferences = $localizeReferences && empty($mmTable);
3902  $isInlineFieldInSelectMode = $localizationMode === 'select' && $inlineSubType === 'mm';
3903  $purgeItems = false;
3904  if ($language > 0 && ($localizingNonManyToManyFieldReferences || $isInlineFieldInSelectMode)) {
3905  foreach ($dbAnalysis->itemArray as $index => $item) {
3906  // Since select fields can reference many records, check whether there's already a localization:
3907  $recordLocalization = BackendUtility::getRecordLocalization($item['table'], $item['id'], $language);
3908  if ($recordLocalization) {
3909  $dbAnalysis->itemArray[$index]['id'] = $recordLocalization[0]['uid'];
3910  } elseif ($this->isNestedElementCallRegistered($item['table'], $item['id'], 'localize') === false) {
3911  if ($localizingNonManyToManyFieldReferences || $localizeChildren) {
3912  $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], $language);
3913  } else {
3914  unset($dbAnalysis->itemArray[$index]);
3915  }
3916  }
3917  }
3918  $purgeItems = true;
3919  }
3920 
3921  if ($purgeItems || $mmTable) {
3922  $dbAnalysis->purgeItemArray();
3923  $value = implode(',', $dbAnalysis->getValueArray($prependName));
3924  }
3925  // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected
3926  if ($value) {
3927  $this->registerDBList[$table][$uid][$field] = $value;
3928  }
3929 
3930  return $value;
3931  }
3932 
3949  protected function copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language,
3950  array $workspaceOptions, $localizationMode, $inlineSubType)
3951  {
3952  // Localization in mode 'keep', isn't a real localization, but keeps the children of the original parent record:
3953  if ($language > 0 && $localizationMode == 'keep') {
3954  $value = $inlineSubType == 'field' ? 0 : '';
3955  } else {
3956  // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3958  $dbAnalysis = $this->createRelationHandlerInstance();
3959  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
3960  // Walk through the items, copy them and remember the new id:
3961  foreach ($dbAnalysis->itemArray as $k => $v) {
3962  $newId = null;
3963  // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
3964  if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
3965  // If children should be localized when the parent gets localized the first time, just do it:
3966  if ($localizationMode != false && isset($conf['behaviour']['localizeChildrenAtParentLocalization']) && $conf['behaviour']['localizeChildrenAtParentLocalization']) {
3967  $newId = $this->localize($v['table'], $v['id'], $language);
3968  }
3969  } else {
3970  if (!MathUtility::canBeInterpretedAsInteger($realDestPid)) {
3971  $newId = $this->copyRecord($v['table'], $v['id'], -$v['id']);
3972  // If the destination page id is a NEW string, keep it on the same page
3973  } elseif ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3974  // A filled $workspaceOptions indicated that this call
3975  // has it's origin in previous versionizeRecord() processing
3976  if (!empty($workspaceOptions)) {
3977  // Versions use live default id, thus the "new"
3978  // id is the original live default child record
3979  $newId = $v['id'];
3980  $this->versionizeRecord(
3981  $v['table'], $v['id'],
3982  (isset($workspaceOptions['label']) ? $workspaceOptions['label'] : 'Auto-created for WS #' . $this->BE_USER->workspace),
3983  (isset($workspaceOptions['delete']) ? $workspaceOptions['delete'] : false)
3984  );
3985  // Otherwise just use plain copyRecord() to create placeholders etc.
3986  } else {
3987  // If a record has been copied already during this request,
3988  // prevent superfluous duplication and use the existing copy
3989  if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3990  $newId = $this->copyMappingArray[$v['table']][$v['id']];
3991  } else {
3992  $newId = $this->copyRecord($v['table'], $v['id'], $realDestPid);
3993  }
3994  }
3995  } else {
3996  // If a record has been copied already during this request,
3997  // prevent superfluous duplication and use the existing copy
3998  if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3999  $newId = $this->copyMappingArray[$v['table']][$v['id']];
4000  } else {
4001  $newId = $this->copyRecord_raw($v['table'], $v['id'], $realDestPid, [], $workspaceOptions);
4002  }
4003  }
4004  }
4005  // If the current field is set on a page record, update the pid of related child records:
4006  if ($table == 'pages') {
4007  $this->registerDBPids[$v['table']][$v['id']] = $uid;
4008  } elseif (isset($this->registerDBPids[$table][$uid])) {
4009  $this->registerDBPids[$v['table']][$v['id']] = $this->registerDBPids[$table][$uid];
4010  }
4011  $dbAnalysis->itemArray[$k]['id'] = $newId;
4012  }
4013  // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4014  $value = implode(',', $dbAnalysis->getValueArray());
4015  $this->registerDBList[$table][$uid][$field] = $value;
4016  }
4017 
4018  return $value;
4019  }
4020 
4034  public function copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $_3, $workspaceOptions)
4035  {
4036  // Extract parameters:
4037  list($table, $uid, $field, $realDestPid) = $pParams;
4038  // Process references and files, currently that means only the files, prepending absolute paths:
4039  $dataValue = $this->copyRecord_procFilesRefs($dsConf, $uid, $dataValue);
4040  // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
4041  if (($this->isReferenceField($dsConf) || $this->getInlineFieldType($dsConf) !== false) && (string)$dataValue !== '') {
4042  $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, [], $dsConf, $realDestPid, 0, $workspaceOptions);
4043  $this->registerDBList[$table][$uid][$field] = 'FlexForm_reference';
4044  }
4045  // Return
4046  return ['value' => $dataValue];
4047  }
4048 
4060  public function copyRecord_procFilesRefs($conf, $uid, $value)
4061  {
4062  // Prepend absolute paths to files:
4063  if ($conf['type'] != 'group' || ($conf['internal_type'] != 'file' && $conf['internal_type'] != 'file_reference')) {
4064  return $value;
4065  }
4066 
4067  // Get an array with files as values:
4068  if ($conf['MM']) {
4069  $theFileValues = [];
4071  $dbAnalysis = $this->createRelationHandlerInstance();
4072  $dbAnalysis->start('', 'files', $conf['MM'], $uid);
4073  foreach ($dbAnalysis->itemArray as $somekey => $someval) {
4074  if ($someval['id']) {
4075  $theFileValues[] = $someval['id'];
4076  }
4077  }
4078  } else {
4079  $theFileValues = GeneralUtility::trimExplode(',', $value, true);
4080  }
4081  // Traverse this array of files:
4082  $uploadFolder = $conf['internal_type'] == 'file' ? $conf['uploadfolder'] : '';
4083  $dest = $this->destPathFromUploadFolder($uploadFolder);
4084  $newValue = [];
4085  foreach ($theFileValues as $file) {
4086  if (trim($file)) {
4087  $realFile = str_replace('//', '/', $dest . '/' . trim($file));
4088  if (@is_file($realFile)) {
4089  $newValue[] = $realFile;
4090  }
4091  }
4092  }
4093  // Implode the new filelist into the new value (all files have absolute paths now which means they will get copied when entering TCEmain as new values...)
4094  $value = implode(',', $newValue);
4095 
4096  // Return the new value:
4097  return $value;
4098  }
4099 
4109  public function copyRecord_fixRTEmagicImages($table, $theNewSQLID)
4110  {
4111  // Creating fileFunc object.
4112  if (!$this->fileFunc) {
4113  $this->fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
4114  $this->include_filefunctions = 1;
4115  }
4116  // Select all RTEmagic files in the reference table from the table/ID
4117  $where = implode(' AND ', [
4118  'ref_table=' . $this->databaseConnection->fullQuoteStr('_FILE', 'sys_refindex'),
4119  'ref_string LIKE ' . $this->databaseConnection->fullQuoteStr('%/RTEmagic%', 'sys_refindex'),
4120  'softref_key=' . $this->databaseConnection->fullQuoteStr('images', 'sys_refindex'),
4121  'tablename=' . $this->databaseConnection->fullQuoteStr($table, 'sys_refindex'),
4122  'recuid=' . (int)$theNewSQLID,
4123  ]);
4124  $rteFileRecords = $this->databaseConnection->exec_SELECTgetRows('*', 'sys_refindex', $where, '', 'sorting DESC');
4125  // Traverse the files found and copy them:
4126  if (!is_array($rteFileRecords)) {
4127  return;
4128  }
4129  foreach ($rteFileRecords as $rteFileRecord) {
4130  $filename = basename($rteFileRecord['ref_string']);
4131  if (!GeneralUtility::isFirstPartOfStr($filename, 'RTEmagicC_')) {
4132  continue;
4133  }
4134  $fileInfo = [];
4135  $fileInfo['exists'] = @is_file((PATH_site . $rteFileRecord['ref_string']));
4136  $fileInfo['original'] = substr($rteFileRecord['ref_string'], 0, -strlen($filename)) . 'RTEmagicP_' . preg_replace('/\\.[[:alnum:]]+$/', '', substr($filename, 10));
4137  $fileInfo['original_exists'] = @is_file((PATH_site . $fileInfo['original']));
4138  // CODE from tx_impexp and class.rte_images.php adapted for use here:
4139  if (!$fileInfo['exists'] || !$fileInfo['original_exists']) {
4140  if ($this->enableLogging) {
4141  $this->newlog('Trying to copy RTEmagic files (' . $rteFileRecord['ref_string'] . ' / ' . $fileInfo['original'] . ') but one or both were missing', 1);
4142  }
4143  continue;
4144  }
4145  // Initialize; Get directory prefix for file and set the original name:
4146  $dirPrefix = dirname($rteFileRecord['ref_string']) . '/';
4147  $rteOrigName = basename($fileInfo['original']);
4148  // If filename looks like an RTE file, and the directory is in "uploads/", then process as a RTE file!
4149  if ($rteOrigName && GeneralUtility::isFirstPartOfStr($dirPrefix, 'uploads/') && @is_dir(PATH_site . $dirPrefix)) {
4150  // RTE:
4151  // From the "original" RTE filename, produce a new "original" destination filename which is unused.
4152  $origDestName = $this->fileFunc->getUniqueName($rteOrigName, PATH_site . $dirPrefix);
4153  // Create copy file name:
4154  $pI = pathinfo($rteFileRecord['ref_string']);
4155  $copyDestName = dirname($origDestName) . '/RTEmagicC_' . substr(basename($origDestName), 10) . '.' . $pI['extension'];
4156  if (!@is_file($copyDestName) && !@is_file($origDestName) && $origDestName === GeneralUtility::getFileAbsFileName($origDestName) && $copyDestName === GeneralUtility::getFileAbsFileName($copyDestName)) {
4157  // Making copies:
4158  GeneralUtility::upload_copy_move(PATH_site . $fileInfo['original'], $origDestName);
4159  GeneralUtility::upload_copy_move(PATH_site . $rteFileRecord['ref_string'], $copyDestName);
4160  clearstatcache();
4161  // Register this:
4162  $this->RTEmagic_copyIndex[$rteFileRecord['tablename']][$rteFileRecord['recuid']][$rteFileRecord['field']][$rteFileRecord['ref_string']] = PathUtility::stripPathSitePrefix($copyDestName);
4163  // Check and update the record using \TYPO3\CMS\Core\Database\ReferenceIndex
4164  if (@is_file($copyDestName)) {
4166  $sysRefObj = GeneralUtility::makeInstance(ReferenceIndex::class);
4167  $error = $sysRefObj->setReferenceValue($rteFileRecord['hash'], PathUtility::stripPathSitePrefix($copyDestName), false, true);
4168  if ($this->enableLogging && $error) {
4169  echo $this->newlog(ReferenceIndex::class . '::setReferenceValue(): ' . $error, 1);
4170  }
4171  } elseif ($this->enableLogging) {
4172  $this->newlog('File "' . $copyDestName . '" was not created!', 1);
4173  }
4174  } elseif ($this->enableLogging) {
4175  $this->newlog('Could not construct new unique names for file!', 1);
4176  }
4177  } elseif ($this->enableLogging) {
4178  $this->newlog('Maybe directory of file was not within "uploads/"?', 1);
4179  }
4180  }
4181  }
4182 
4194  public function copyL10nOverlayRecords($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '')
4195  {
4196  // There's no need to perform this for page-records or for tables that are not localizable
4197  if (!BackendUtility::isTableLocalizable($table) || !empty($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']) || !empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable'])) {
4198  return;
4199  }
4200  $where = '';
4201  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
4202  $where = ' AND t3ver_oid=0';
4203  }
4204  // If $destPid is < 0, get the pid of the record with uid equal to abs($destPid)
4205  $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
4206  // Get the localized records to be copied
4207  $l10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], $uid, $where);
4208  if (is_array($l10nRecords)) {
4209  $localizedDestPids = [];
4210  // If $destPid < 0, then it is the uid of the original language record we are inserting after
4211  if ($destPid < 0) {
4212  // Get the localized records of the record we are inserting after
4213  $destL10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], abs($destPid), $where);
4214  // Index the localized record uids by language
4215  if (is_array($destL10nRecords)) {
4216  foreach ($destL10nRecords as $record) {
4217  $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4218  }
4219  }
4220  }
4221  // Copy the localized records after the corresponding localizations of the destination record
4222  foreach ($l10nRecords as $record) {
4223  $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4224  if ($localizedDestPid < 0) {
4225  $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4226  } else {
4227  $this->copyRecord($table, $record['uid'], $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4228  }
4229  }
4230  }
4231  }
4232 
4233  /*********************************************
4234  *
4235  * Cmd: Moving, Localizing
4236  *
4237  ********************************************/
4246  public function moveRecord($table, $uid, $destPid)
4247  {
4248  if (!$GLOBALS['TCA'][$table]) {
4249  return;
4250  }
4251 
4252  // In case the record to be moved turns out to be an offline version,
4253  // we have to find the live version and work on that one (this case
4254  // happens for pages with "branch" versioning type)
4255  // @deprecated note: as "branch" versioning is deprecated since TYPO3 4.2, this
4256  // functionality will be removed in TYPO3 4.7 (note by benni: a hook could replace this)
4257  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $uid, 'uid')) {
4258  $uid = $lookForLiveVersion['uid'];
4259  }
4260  // Initialize:
4261  $destPid = (int)$destPid;
4262  // Get this before we change the pid (for logging)
4263  $propArr = $this->getRecordProperties($table, $uid);
4264  $moveRec = $this->getRecordProperties($table, $uid, true);
4265  // This is the actual pid of the moving to destination
4266  $resolvedPid = $this->resolvePid($table, $destPid);
4267  // 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.
4268  // If the record is a page, then there are two options: If the page is moved within itself, (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.
4269  if ($table != 'pages' || $resolvedPid == $moveRec['pid']) {
4270  // Edit rights for the record...
4271  $mayMoveAccess = $this->checkRecordUpdateAccess($table, $uid);
4272  } else {
4273  $mayMoveAccess = $this->doesRecordExist($table, $uid, 'delete');
4274  }
4275  // Finding out, if the record may be moved TO another place. Here we check insert-rights (non-pages = edit, pages = new), unless the pages are moved on the same pid, then edit-rights are checked
4276  if ($table != 'pages' || $resolvedPid != $moveRec['pid']) {
4277  // Insert rights for the record...
4278  $mayInsertAccess = $this->checkRecordInsertAccess($table, $resolvedPid, 4);
4279  } else {
4280  $mayInsertAccess = $this->checkRecordUpdateAccess($table, $uid);
4281  }
4282  // Checking if there is anything else disallowing moving the record by checking if editing is allowed
4283  $fullLanguageCheckNeeded = $table != 'pages';
4284  $mayEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded);
4285  // If moving is allowed, begin the processing:
4286  if (!$mayEditAccess) {
4287  if ($this->enableLogging) {
4288  $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']);
4289  }
4290  return;
4291  }
4292 
4293  if (!$mayMoveAccess) {
4294  if ($this->enableLogging) {
4295  $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']);
4296  }
4297  return;
4298  }
4299 
4300  if (!$mayInsertAccess) {
4301  if ($this->enableLogging) {
4302  $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']);
4303  }
4304  return;
4305  }
4306 
4307  $recordWasMoved = false;
4308  // Move the record via a hook, used e.g. for versioning
4309  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'])) {
4310  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] as $classRef) {
4311  $hookObj = GeneralUtility::getUserObj($classRef);
4312  if (method_exists($hookObj, 'moveRecord')) {
4313  $hookObj->moveRecord($table, $uid, $destPid, $propArr, $moveRec, $resolvedPid, $recordWasMoved, $this);
4314  }
4315  }
4316  }
4317  // Move the record if a hook hasn't moved it yet
4318  if (!$recordWasMoved) {
4319  $this->moveRecord_raw($table, $uid, $destPid);
4320  }
4321  }
4322 
4333  public function moveRecord_raw($table, $uid, $destPid)
4334  {
4335  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
4336  $origDestPid = $destPid;
4337  // This is the actual pid of the moving to destination
4338  $resolvedPid = $this->resolvePid($table, $destPid);
4339  // 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...)
4340  // $destPid>=0 because we must correct pid in case of versioning "page" types.
4341  if ($destPid < 0 && !$sortRow || $destPid >= 0) {
4342  $destPid = $resolvedPid;
4343  }
4344  // Get this before we change the pid (for logging)
4345  $propArr = $this->getRecordProperties($table, $uid);
4346  $moveRec = $this->getRecordProperties($table, $uid, true);
4347  // Prepare user defined objects (if any) for hooks which extend this function:
4348  $hookObjectsArr = [];
4349  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'])) {
4350  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] as $classRef) {
4351  $hookObjectsArr[] = GeneralUtility::getUserObj($classRef);
4352  }
4353  }
4354  // Timestamp field:
4355  $updateFields = [];
4356  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4357  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4358  }
4359  // Insert as first element on page (where uid = $destPid)
4360  if ($destPid >= 0) {
4361  if ($table != 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4362  // Clear cache before moving
4363  list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
4364  $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4365  // Setting PID
4366  $updateFields['pid'] = $destPid;
4367  // Table is sorted by 'sortby'
4368  if ($sortRow) {
4369  $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4370  $updateFields[$sortRow] = $sortNumber;
4371  }
4372  // Check for child records that have also to be moved
4373  $this->moveRecord_procFields($table, $uid, $destPid);
4374  // Create query for update:
4375  $this->databaseConnection->exec_UPDATEquery($table, 'uid=' . (int)$uid, $updateFields);
4376  // Check for the localizations of that element
4377  $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4378  // Call post processing hooks:
4379  foreach ($hookObjectsArr as $hookObj) {
4380  if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4381  $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4382  }
4383  }
4384  if ($this->enableLogging) {
4385  // Logging...
4386  $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4387  if ($destPid != $propArr['pid']) {
4388  // Logged to old page
4389  $newPropArr = $this->getRecordProperties($table, $uid);
4390  $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4391  $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']);
4392  // Logged to new page
4393  $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);
4394  } else {
4395  // Logged to new page
4396  $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);
4397  }
4398  }
4399  // Clear cache after moving
4400  $this->registerRecordIdForPageCacheClearing($table, $uid);
4401  $this->fixUniqueInPid($table, $uid);
4402  // fixCopyAfterDuplFields
4403  if ($origDestPid < 0) {
4404  $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
4405  }
4406  } elseif ($this->enableLogging) {
4407  $destPropArr = $this->getRecordProperties('pages', $destPid);
4408  $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']);
4409  }
4410  } else {
4411  // Put after another record
4412  // Table is being sorted
4413  if ($sortRow) {
4414  // Save the position to which the original record is requested to be moved
4415  $originalRecordDestinationPid = $destPid;
4416  $sortInfo = $this->getSortNumber($table, $uid, $destPid);
4417  // Setting the destPid to the new pid of the record.
4418  $destPid = $sortInfo['pid'];
4419  // If not an array, there was an error (which is already logged)
4420  if (is_array($sortInfo)) {
4421  if ($table != 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4422  // clear cache before moving
4424  // We now update the pid and sortnumber
4425  $updateFields['pid'] = $destPid;
4426  $updateFields[$sortRow] = $sortInfo['sortNumber'];
4427  // Check for child records that have also to be moved
4428  $this->moveRecord_procFields($table, $uid, $destPid);
4429  // Create query for update:
4430  $this->databaseConnection->exec_UPDATEquery($table, 'uid=' . (int)$uid, $updateFields);
4431  // Check for the localizations of that element
4432  $this->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
4433  // Call post processing hooks:
4434  foreach ($hookObjectsArr as $hookObj) {
4435  if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4436  $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4437  }
4438  }
4439  if ($this->enableLogging) {
4440  // Logging...
4441  $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4442  if ($destPid != $propArr['pid']) {
4443  // Logged to old page
4444  $newPropArr = $this->getRecordProperties($table, $uid);
4445  $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4446  $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']);
4447  // Logged to old page
4448  $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);
4449  } else {
4450  // Logged to old page
4451  $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);
4452  }
4453  }
4454  // Clear cache after moving
4455  $this->registerRecordIdForPageCacheClearing($table, $uid);
4456  // fixUniqueInPid
4457  $this->fixUniqueInPid($table, $uid);
4458  // fixCopyAfterDuplFields
4459  if ($origDestPid < 0) {
4460  $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
4461  }
4462  } elseif ($this->enableLogging) {
4463  $destPropArr = $this->getRecordProperties('pages', $destPid);
4464  $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']);
4465  }
4466  }
4467  } elseif ($this->enableLogging) {
4468  $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']);
4469  }
4470  }
4471  }
4472 
4482  public function moveRecord_procFields($table, $uid, $destPid)
4483  {
4484  $conf = $GLOBALS['TCA'][$table]['columns'];
4485  $row = BackendUtility::getRecordWSOL($table, $uid);
4486  if (is_array($row)) {
4487  foreach ($row as $field => $value) {
4488  $this->moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf[$field]['config']);
4489  }
4490  }
4491  }
4492 
4504  public function moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf)
4505  {
4506  if ($conf['type'] == 'inline') {
4507  $foreign_table = $conf['foreign_table'];
4508  $moveChildrenWithParent = !isset($conf['behaviour']['disableMovingChildrenWithParent']) || !$conf['behaviour']['disableMovingChildrenWithParent'];
4509  if ($foreign_table && $moveChildrenWithParent) {
4510  $inlineType = $this->getInlineFieldType($conf);
4511  if ($inlineType == 'list' || $inlineType == 'field') {
4512  if ($table == 'pages') {
4513  // If the inline elements are related to a page record,
4514  // make sure they reside at that page and not at its parent
4515  $destPid = $uid;
4516  }
4517  $dbAnalysis = $this->createRelationHandlerInstance();
4518  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
4519  }
4520  }
4521  }
4522  // Move the records
4523  if (isset($dbAnalysis)) {
4524  // Moving records to a positive destination will insert each
4525  // record at the beginning, thus the order is reversed here:
4526  foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4527  $this->moveRecord($v['table'], $v['id'], $destPid);
4528  }
4529  }
4530  }
4531 
4541  public function moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
4542  {
4543  // There's no need to perform this for page-records or not localizable tables
4544  if (!BackendUtility::isTableLocalizable($table) || !empty($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']) || !empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable'])) {
4545  return;
4546  }
4547  $where = '';
4548  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
4549  $where = ' AND t3ver_oid=0';
4550  }
4551  $l10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], $uid, $where);
4552  if (is_array($l10nRecords)) {
4553  $localizedDestPids = [];
4554  // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4555  if ($originalRecordDestinationPid < 0) {
4556  // Get the localized records of the record we are inserting after
4557  $destL10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], abs($originalRecordDestinationPid), $where);
4558  // Index the localized record uids by language
4559  if (is_array($destL10nRecords)) {
4560  foreach ($destL10nRecords as $record) {
4561  $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4562  }
4563  }
4564  }
4565  // Move the localized records after the corresponding localizations of the destination record
4566  foreach ($l10nRecords as $record) {
4567  $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4568  if ($localizedDestPid < 0) {
4569  $this->moveRecord($table, $record['uid'], $localizedDestPid);
4570  } else {
4571  $this->moveRecord($table, $record['uid'], $destPid);
4572  }
4573  }
4574  }
4575  }
4576 
4586  public function localize($table, $uid, $language)
4587  {
4588  $newId = false;
4589  $uid = (int)$uid;
4590  if (!$GLOBALS['TCA'][$table] || !$uid || $this->isNestedElementCallRegistered($table, $uid, 'localize') !== false) {
4591  return false;
4592  }
4593 
4594  $this->registerNestedElementCall($table, $uid, 'localize');
4595  if ((!$GLOBALS['TCA'][$table]['ctrl']['languageField']
4596  || !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
4597  || $table === 'pages_language_overlay')
4598  && $table !== 'pages') {
4599  if ($this->enableLogging) {
4600  $this->newlog('Localization failed; "languageField" and "transOrigPointerField" must be defined for the table!', 1);
4601  }
4602  return false;
4603  }
4604 
4605  $langRec = BackendUtility::getRecord('sys_language', (int)$language, 'uid,title');
4606  if (!$langRec) {
4607  if ($this->enableLogging) {
4608  $this->newlog('Sys language UID "' . $language . '" not found valid!', 1);
4609  }
4610  return false;
4611  }
4612 
4613  if (!$this->doesRecordExist($table, $uid, 'show')) {
4614  if ($this->enableLogging) {
4615  $this->newlog('Attempt to localize record without permission', 1);
4616  }
4617  return false;
4618  }
4619 
4620  // Getting workspace overlay if possible - this will localize versions in workspace if any
4621  $row = BackendUtility::getRecordWSOL($table, $uid);
4622  if (!is_array($row)) {
4623  if ($this->enableLogging) {
4624  $this->newlog('Attempt to localize record that did not exist!', 1);
4625  }
4626  return false;
4627  }
4628 
4629  // Make sure that records which are translated from another language than the default language have a correct
4630  // localization source set themselves, before translating them to another language.
4631  if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4632  && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
4633  && $table !== 'pages') {
4634  $localizationParentRecord = BackendUtility::getRecord(
4635  $table,
4636  $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]);
4637  if ((int)$localizationParentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== 0) {
4638  if ($this->enableLogging) {
4639  $this->newlog('Localization failed; Source record contained a reference to an original record that is not a default record (which is strange)!', 1);
4640  }
4641  return false;
4642  }
4643  }
4644 
4645  // Default language records must never have a localization parent as they are the origin of any translation.
4646  if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4647  && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0
4648  && $table !== 'pages') {
4649  if ($this->enableLogging) {
4650  $this->newlog('Localization failed; Source record contained a reference to an original default record but is a default record itself (which is strange)!', 1);
4651  }
4652  return false;
4653  }
4654 
4655  if ($table === 'pages') {
4656  $pass = $GLOBALS['TCA'][$table]['ctrl']['transForeignTable'] === 'pages_language_overlay' && !BackendUtility::getRecordsByField('pages_language_overlay', 'pid', $uid, (' AND ' . $GLOBALS['TCA']['pages_language_overlay']['ctrl']['languageField'] . '=' . (int)$langRec['uid']));
4657  $Ttable = 'pages_language_overlay';
4658  } else {
4659  $pass = !BackendUtility::getRecordLocalization($table, $uid, $langRec['uid'], ('AND pid=' . (int)$row['pid']));
4660  $Ttable = $table;
4661  }
4662 
4663  if (!$pass) {
4664  if ($this->enableLogging) {
4665  $this->newlog('Localization failed; There already was a localization for this language of the record!', 1);
4666  }
4667  return false;
4668  }
4669 
4670  // Initialize:
4671  $overrideValues = [];
4672  $excludeFields = [];
4673  // Set override values:
4674  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['languageField']] = $langRec['uid'];
4675  // If the translated record is a default language record, set it's uid as localization parent of the new record.
4676  // If translating from any other language, no override is needed; we just can copy the localization parent of
4677  // the original record (which is pointing to the correspondent default language record) to the new record.
4678  // In copy / free mode the TransOrigPointer field is always set to 0, as no connection to the localization parent is wanted in that case.
4679  if (($this->useTransOrigPointerField && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0)
4680  || $table === 'pages') {
4681  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['transOrigPointerField']] = $uid;
4682  } elseif (!$this->useTransOrigPointerField) {
4683  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['transOrigPointerField']] = 0;
4684  }
4685  // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4686  if (isset($GLOBALS['TCA'][$table]['ctrl']['type']) && isset($GLOBALS['TCA'][$Ttable]['ctrl']['type'])) {
4687  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['type']] = $row[$GLOBALS['TCA'][$table]['ctrl']['type']];
4688  }
4689  // Set exclude Fields:
4690  foreach ($GLOBALS['TCA'][$Ttable]['columns'] as $fN => $fCfg) {
4691  $translateToMsg = '';
4692  // Check if we are just prefixing:
4693  if ($fCfg['l10n_mode'] == 'prefixLangTitle') {
4694  if (($fCfg['config']['type'] == 'text' || $fCfg['config']['type'] == 'input') && (string)$row[$fN] !== '') {
4695  list($tscPID) = BackendUtility::getTSCpid($table, $uid, '');
4696  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
4697  if (!empty($TSConfig['translateToMessage'])) {
4698  $translateToMsg = $GLOBALS['LANG'] ? $GLOBALS['LANG']->sL($TSConfig['translateToMessage']) : $TSConfig['translateToMessage'];
4699  $translateToMsg = @sprintf($translateToMsg, $langRec['title']);
4700  }
4701  if (empty($translateToMsg)) {
4702  $translateToMsg = 'Translate to ' . $langRec['title'] . ':';
4703  } else {
4704  $translateToMsg = @sprintf($TSConfig['translateToMessage'], $langRec['title']);
4705  }
4706  $overrideValues[$fN] = '[' . $translateToMsg . '] ' . $row[$fN];
4707  }
4708  } elseif (
4709  ($fCfg['l10n_mode'] === 'exclude' || $fCfg['l10n_mode'] === 'noCopy' || $fCfg['l10n_mode'] === 'mergeIfNotBlank')
4710  && $fN != $GLOBALS['TCA'][$Ttable]['ctrl']['languageField']
4711  && $fN != $GLOBALS['TCA'][$Ttable]['ctrl']['transOrigPointerField']
4712  ) {
4713  // Otherwise, do not copy field (unless it is the language field or
4714  // pointer to the original language)
4715  $excludeFields[] = $fN;
4716  }
4717  }
4718  if ($Ttable === $table) {
4719  // Get the uid of record after which this localized record should be inserted
4720  $previousUid = $this->getPreviousLocalizedRecordUid($table, $uid, $row['pid'], $language);
4721  // Execute the copy:
4722  $newId = $this->copyRecord($table, $uid, -$previousUid, 1, $overrideValues, implode(',', $excludeFields), $language);
4723  $autoVersionNewId = $this->getAutoVersionId($table, $newId);
4724  if (is_null($autoVersionNewId) === false) {
4725  $this->triggerRemapAction($table, $newId, [$this, 'placeholderShadowing'], [$table, $autoVersionNewId], true);
4726  }
4727  } else {
4728  // Create new record:
4730  $copyTCE = $this->getLocalTCE();
4731  $copyTCE->start([$Ttable => ['NEW' => $overrideValues]], '', $this->BE_USER);
4732  $copyTCE->process_datamap();
4733  // Getting the new UID as if it had been copied:
4734  $theNewSQLID = $copyTCE->substNEWwithIDs['NEW'];
4735  if ($theNewSQLID) {
4736  // 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"
4737  $this->copyMappingArray[$Ttable][$uid] = $theNewSQLID;
4738  $newId = $theNewSQLID;
4739  }
4740  }
4741 
4742  return $newId;
4743  }
4744 
4762  protected function inlineLocalizeSynchronize($table, $id, $command)
4763  {
4764  $parentRecord = BackendUtility::getRecordWSOL($table, $id);
4765 
4766  // Backward-compatibility handling
4767  if (!is_array($command)) {
4768  // <field>, (localize | synchronize | <uid>):
4769  $parts = GeneralUtility::trimExplode(',', $command);
4770  $command = [];
4771  $command['field'] = $parts[0];
4772  // The previous process expected $id to point to the localized record already
4773  $command['language'] = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
4774 
4775  if (!MathUtility::canBeInterpretedAsInteger($parts[1])) {
4776  $command['action'] = $parts[1];
4777  } else {
4778  $command['ids'] = [$parts[1]];
4779  }
4780  }
4781 
4782  // In case the parent record is the default language record, fetch the localization
4783  if (empty($parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
4784  // Fetch the live record
4785  $parentRecordLocalization = BackendUtility::getRecordLocalization($table, $id, $command['language'], 'AND pid<>-1');
4786  if (empty($parentRecordLocalization)) {
4787  $this->newlog2('Localization for parent record ' . $table . ':' . $id . '" cannot be fetched', $table, $id, $parentRecord['pid']);
4788  return;
4789  }
4790  $parentRecord = $parentRecordLocalization[0];
4791  $id = $parentRecord['uid'];
4792  // Process overlay for current selected workspace
4793  BackendUtility::workspaceOL($table, $parentRecord);
4794  }
4795 
4796  $field = $command['field'];
4797  $language = $command['language'];
4798  $action = $command['action'];
4799  $ids = $command['ids'];
4800 
4801  if (!$field || !($action === 'localize' || $action === 'synchronize') && empty($ids) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4802  return;
4803  }
4804 
4805  $config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
4806  $foreignTable = $config['foreign_table'];
4807  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $config);
4808  if ($localizationMode !== 'select') {
4809  return;
4810  }
4811 
4812  $transOrigPointer = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
4813  $transOrigTable = BackendUtility::getOriginalTranslationTable($table);
4814  $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
4815 
4816  if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) {
4817  return;
4818  }
4819 
4820  $inlineSubType = $this->getInlineFieldType($config);
4821  $transOrigRecord = BackendUtility::getRecordWSOL($transOrigTable, $transOrigPointer);
4822 
4823  if ($inlineSubType === false) {
4824  return;
4825  }
4826 
4827  $removeArray = [];
4828  $mmTable = $inlineSubType == 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : '';
4829  // Fetch children from original language parent:
4831  $dbAnalysisOriginal = $this->createRelationHandlerInstance();
4832  $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $transOrigTable, $config);
4833  $elementsOriginal = [];
4834  foreach ($dbAnalysisOriginal->itemArray as $item) {
4835  $elementsOriginal[$item['id']] = $item;
4836  }
4837  unset($dbAnalysisOriginal);
4838  // Fetch children from current localized parent:
4840  $dbAnalysisCurrent = $this->createRelationHandlerInstance();
4841  $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config);
4842  // Perform synchronization: Possibly removal of already localized records:
4843  if ($action === 'synchronize') {
4844  foreach ($dbAnalysisCurrent->itemArray as $index => $item) {
4845  $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']);
4846  if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) {
4847  $childTransOrigPointer = $childRecord[$childTransOrigPointerField];
4848  // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it:
4849  if (!isset($elementsOriginal[$childTransOrigPointer])) {
4850  unset($dbAnalysisCurrent->itemArray[$index]);
4851  $removeArray[$item['table']][$item['id']]['delete'] = 1;
4852  }
4853  }
4854  }
4855  }
4856  // Perform synchronization/localization: Possibly add unlocalized records for original language:
4857  if ($action === 'localize' || $action === 'synchronize') {
4858  foreach ($elementsOriginal as $originalId => $item) {
4859  $item['id'] = $this->localize($item['table'], $item['id'], $language);
4860  $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4861  $dbAnalysisCurrent->itemArray[] = $item;
4862  }
4863  } elseif (!empty($ids)) {
4864  foreach ($ids as $childId) {
4865  if (!MathUtility::canBeInterpretedAsInteger($childId) || !isset($elementsOriginal[$childId])) {
4866  continue;
4867  }
4868  $item = $elementsOriginal[$childId];
4869  $item['id'] = $this->localize($item['table'], $item['id'], $language);
4870  $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4871  $dbAnalysisCurrent->itemArray[] = $item;
4872  }
4873  }
4874  // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4875  $value = implode(',', $dbAnalysisCurrent->getValueArray());
4876  $this->registerDBList[$table][$id][$field] = $value;
4877  // Remove child records (if synchronization requested it):
4878  if (is_array($removeArray) && !empty($removeArray)) {
4880  $tce = GeneralUtility::makeInstance(__CLASS__);
4881  $tce->stripslashes_values = false;
4882  $tce->enableLogging = $this->enableLogging;
4883  $tce->start([], $removeArray);
4884  $tce->process_cmdmap();
4885  unset($tce);
4886  }
4887  $updateFields = [];
4888  // Handle, reorder and store relations:
4889  if ($inlineSubType == 'list') {
4890  $updateFields = [$field => $value];
4891  } elseif ($inlineSubType == 'field') {
4892  $dbAnalysisCurrent->writeForeignField($config, $id);
4893  $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
4894  } elseif ($inlineSubType == 'mm') {
4895  $dbAnalysisCurrent->writeMM($config['MM'], $id);
4896  $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
4897  }
4898  // Update field referencing to child records of localized parent record:
4899  if (!empty($updateFields)) {
4900  $this->updateDB($table, $id, $updateFields);
4901  }
4902  }
4903 
4904  /*********************************************
4905  *
4906  * Cmd: Deleting
4907  *
4908  ********************************************/
4916  public function deleteAction($table, $id)
4917  {
4918  $recordToDelete = BackendUtility::getRecord($table, $id);
4919  // Record asked to be deleted was found:
4920  if (is_array($recordToDelete)) {
4921  $recordWasDeleted = false;
4922  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'])) {
4923  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] as $classRef) {
4924  $hookObj = GeneralUtility::getUserObj($classRef);
4925  if (method_exists($hookObj, 'processCmdmap_deleteAction')) {
4926  $hookObj->processCmdmap_deleteAction($table, $id, $recordToDelete, $recordWasDeleted, $this);
4927  }
4928  }
4929  }
4930  // Delete the record if a hook hasn't deleted it yet
4931  if (!$recordWasDeleted) {
4932  $this->deleteEl($table, $id);
4933  }
4934  }
4935  }
4936 
4946  public function deleteEl($table, $uid, $noRecordCheck = false, $forceHardDelete = false)
4947  {
4948  if ($table == 'pages') {
4949  $this->deletePages($uid, $noRecordCheck, $forceHardDelete);
4950  } else {
4951  $this->deleteVersionsForRecord($table, $uid, $forceHardDelete);
4952  $this->deleteRecord($table, $uid, $noRecordCheck, $forceHardDelete);
4953  }
4954  }
4955 
4964  public function deleteVersionsForRecord($table, $uid, $forceHardDelete)
4965  {
4966  $versions = BackendUtility::selectVersionsOfRecord($table, $uid, 'uid,pid,t3ver_wsid,t3ver_state', $this->BE_USER->workspace ?: null);
4967  if (is_array($versions)) {
4968  foreach ($versions as $verRec) {
4969  if (!$verRec['_CURRENT_VERSION']) {
4970  if ($table == 'pages') {
4971  $this->deletePages($verRec['uid'], true, $forceHardDelete);
4972  } else {
4973  $this->deleteRecord($table, $verRec['uid'], true, $forceHardDelete);
4974  }
4975 
4976  // Delete move-placeholder
4977  $versionState = VersionState::cast($verRec['t3ver_state']);
4978  if ($versionState->equals(VersionState::MOVE_POINTER)) {
4979  $versionMovePlaceholder = BackendUtility::getMovePlaceholder($table, $uid, 'uid', $verRec['t3ver_wsid']);
4980  if (!empty($versionMovePlaceholder)) {
4981  $this->deleteEl($table, $versionMovePlaceholder['uid'], true, $forceHardDelete);
4982  }
4983  }
4984  }
4985  }
4986  }
4987  }
4988 
4996  public function undeleteRecord($table, $uid)
4997  {
4998  if ($this->isRecordUndeletable($table, $uid)) {
4999  $this->deleteRecord($table, $uid, true, false, true);
5000  }
5001  }
5002 
5016  public function deleteRecord($table, $uid, $noRecordCheck = false, $forceHardDelete = false, $undeleteRecord = false)
5017  {
5018  $uid = (int)$uid;
5019  if (!$GLOBALS['TCA'][$table] || !$uid) {
5020  if ($this->enableLogging) {
5021  $this->log($table, $uid, 3, 0, 1, 'Attempt to delete record without delete-permissions. [' . $this->BE_USER->errorMsg . ']');
5022  }
5023  return;
5024  }
5025 
5026  // Checking if there is anything else disallowing deleting the record by checking if editing is allowed
5027  $deletedRecord = $forceHardDelete || $undeleteRecord;
5028  $hasEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, $deletedRecord, true);
5029  if (!$hasEditAccess) {
5030  if ($this->enableLogging) {
5031  $this->log($table, $uid, 3, 0, 1, 'Attempt to delete record without delete-permissions');
5032  }
5033  return;
5034  }
5035  if (!$noRecordCheck && !$this->doesRecordExist($table, $uid, 'delete')) {
5036  return;
5037  }
5038 
5039  // Clear cache before deleting the record, else the correct page cannot be identified by clear_cache
5040  list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
5041  $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
5042  $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
5043  if ($deleteField && !$forceHardDelete) {
5044  $updateFields = [
5045  $deleteField => $undeleteRecord ? 0 : 1
5046  ];
5047  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
5048  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
5049  }
5050  // If the table is sorted, then the sorting number is set very high
5051  if ($GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$undeleteRecord) {
5052  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = 1000000000;
5053  }
5054  // before (un-)deleting this record, check for child records or references
5055  $this->deleteRecord_procFields($table, $uid, $undeleteRecord);
5056  $this->databaseConnection->exec_UPDATEquery($table, 'uid=' . (int)$uid, $updateFields);
5057  // Delete all l10n records as well, impossible during undelete because it might bring too many records back to life
5058  if (!$undeleteRecord) {
5059  $this->deletedRecords[$table][] = (int)$uid;
5060  $this->deleteL10nOverlayRecords($table, $uid);
5061  }
5062  } else {
5063  // Fetches all fields with flexforms and look for files to delete:
5064  foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $cfg) {
5065  $conf = $cfg['config'];
5066  switch ($conf['type']) {
5067  case 'flex':
5068  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
5069  $flexObj->traverseFlexFormXMLData($table, $fieldName, BackendUtility::getRecordRaw($table, 'uid=' . (int)$uid), $this, 'deleteRecord_flexFormCallBack');
5070  break;
5071  }
5072  }
5073  // Fetches all fields that holds references to files
5074  $fileFieldArr = $this->extFileFields($table);
5075  if (!empty($fileFieldArr)) {
5076  $mres = $this->databaseConnection->exec_SELECTquery(implode(',', $fileFieldArr), $table, 'uid=' . (int)$uid);
5077  if ($row = $this->databaseConnection->sql_fetch_assoc($mres)) {
5078  $fArray = $fileFieldArr;
5079  // MISSING: Support for MM file relations!
5080  foreach ($fArray as $theField) {
5081  // This deletes files that belonged to this record.
5082  $this->extFileFunctions($table, $theField, $row[$theField], 'deleteAll');
5083  }
5084  } elseif ($this->enableLogging) {
5085  $this->log($table, $uid, 3, 0, 100, 'Delete: Zero rows in result when trying to read filenames from record which should be deleted');
5086  }
5087  $this->databaseConnection->sql_free_result($mres);
5088  }
5089  // Delete the hard way...:
5090  $this->databaseConnection->exec_DELETEquery($table, 'uid=' . (int)$uid);
5091  $this->deletedRecords[$table][] = (int)$uid;
5092  $this->deleteL10nOverlayRecords($table, $uid);
5093  }
5094  if ($this->enableLogging) {
5095  // 1 means insert, 3 means delete
5096  $state = $undeleteRecord ? 1 : 3;
5097  if (!$this->databaseConnection->sql_error()) {
5098  if ($forceHardDelete) {
5099  $message = 'Record \'%s\' (%s) was deleted unrecoverable from page \'%s\' (%s)';
5100  } else {
5101  $message = $state == 1 ? 'Record \'%s\' (%s) was restored on page \'%s\' (%s)' : 'Record \'%s\' (%s) was deleted from page \'%s\' (%s)';
5102  }
5103  $propArr = $this->getRecordProperties($table, $uid);
5104  $pagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
5105 
5106  $this->log($table, $uid, $state, 0, 0, $message, 0, [
5107  $propArr['header'],
5108  $table . ':' . $uid,
5109  $pagePropArr['header'],
5110  $propArr['pid']
5111  ], $propArr['event_pid']);
5112  } else {
5113  $this->log($table, $uid, $state, 0, 100, $this->databaseConnection->sql_error());
5114  }
5115  }
5116  // Update reference index:
5117  $this->updateRefIndex($table, $uid);
5118 
5119  // We track calls to update the reference index as to avoid calling it twice
5120  // with the same arguments. This is done because reference indexing is quite
5121  // costly and the update reference index stack usually contain duplicates.
5122  // NB: also filled and checked in loop below. The initialisation prevents
5123  // running the "root" record twice if it appears in the stack twice.
5124  $updateReferenceIndexCalls = [[$table, $uid]];
5125 
5126  // If there are entries in the updateRefIndexStack
5127  if (is_array($this->updateRefIndexStack[$table]) && is_array($this->updateRefIndexStack[$table][$uid])) {
5128  while ($args = array_pop($this->updateRefIndexStack[$table][$uid])) {
5129  if (!in_array($args, $updateReferenceIndexCalls, true)) {
5130  // $args[0]: table, $args[1]: uid
5131  $this->updateRefIndex($args[0], $args[1]);
5132  $updateReferenceIndexCalls[] = $args;
5133  }
5134  }
5135  unset($this->updateRefIndexStack[$table][$uid]);
5136  }
5137  }
5138 
5149  public function deleteRecord_flexFormCallBack($dsArr, $dataValue, $PA, $structurePath, $pObj)
5150  {
5151  // Use reference index object to find files in fields:
5153  $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
5154  $files = $refIndexObj->getRelations_procFiles($dataValue, $dsArr['TCEforms']['config'], $PA['uid']);
5155  // Traverse files and delete them if the field is a regular file field (and not a file_reference field)
5156  if (is_array($files) && $dsArr['TCEforms']['config']['internal_type'] === 'file') {
5157  foreach ($files as $dat) {
5158  if (@is_file($dat['ID_absFile'])) {
5159  $file = $this->getResourceFactory()->retrieveFileOrFolderObject($dat['ID_absFile']);
5160  $file->delete();
5161  } elseif ($this->enableLogging) {
5162  $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');
5163  }
5164  }
5165  }
5166  }
5167 
5176  public function deletePages($uid, $force = false, $forceHardDelete = false)
5177  {
5178  $uid = (int)$uid;
5179  if ($uid === 0) {
5180  $this->newlog2('Deleting all pages starting from the root-page is disabled.', 'pages', 0, 0, 2);
5181  return;
5182  }
5183  // Getting list of pages to delete:
5184  if ($force) {
5185  // Returns the branch WITHOUT permission checks (0 secures that)
5186  $brExist = $this->doesBranchExist('', $uid, 0, 1);
5187  $res = GeneralUtility::trimExplode(',', $brExist . $uid, true);
5188  } else {
5189  $res = $this->canDeletePage($uid);
5190  }
5191  // Perform deletion if not error:
5192  if (is_array($res)) {
5193  foreach ($res as $deleteId) {
5194  $this->deleteSpecificPage($deleteId, $forceHardDelete);
5195  }
5196  } else {
5198  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, true);
5200  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
5202  $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
5203 
5204  if ($this->enableLogging) {
5205  $this->newlog($res, 1);
5206  }
5207  }
5208  }
5209 
5219  public function deleteSpecificPage($uid, $forceHardDelete = false)
5220  {
5221  $uid = (int)$uid;
5222  if ($uid) {
5223  foreach ($GLOBALS['TCA'] as $table => $_) {
5224  if ($table != 'pages') {
5225  $mres = $this->databaseConnection->exec_SELECTquery('uid', $table, 'pid=' . (int)$uid . $this->deleteClause($table));
5226  while ($row = $this->databaseConnection->sql_fetch_assoc($mres)) {
5227  $this->copyMovedRecordToNewLocation($table, $row['uid']);
5228  $this->deleteVersionsForRecord($table, $row['uid'], $forceHardDelete);
5229  $this->deleteRecord($table, $row['uid'], true, $forceHardDelete);
5230  }
5231  $this->databaseConnection->sql_free_result($mres);
5232  }
5233  }
5234  $this->copyMovedRecordToNewLocation('pages', $uid);
5235  $this->deleteVersionsForRecord('pages', $uid, $forceHardDelete);
5236  $this->deleteRecord('pages', $uid, true, $forceHardDelete);
5237  }
5238  }
5239 
5253  protected function copyMovedRecordToNewLocation($table, $uid)
5254  {
5255  if ($this->BE_USER->workspace > 0) {
5256  $originalRecord = BackendUtility::getRecord($table, $uid);
5257  $movePlaceholder = BackendUtility::getMovePlaceholder($table, $uid);
5258  // Check whether target page to copied to is different to current page
5259  // Cloning on the same page is superfluous and does not help at all
5260  if (!empty($originalRecord) && !empty($movePlaceholder) && (int)$originalRecord['pid'] !== (int)$movePlaceholder['pid']) {
5261  // If move placeholder exists, copy to new location
5262  // This will create a New placeholder on the new location
5263  // and a version for this new placeholder
5264  $command = [
5265  $table => [
5266  $uid => [
5267  'copy' => '-' . $movePlaceholder['uid']
5268  ]
5269  ]
5270  ];
5272  $dataHandler = GeneralUtility::makeInstance(__CLASS__);
5273  $dataHandler->stripslashes_values = false;
5274  $dataHandler->enableLogging = $this->enableLogging;
5275  $dataHandler->neverHideAtCopy = true;
5276  $dataHandler->start([], $command);
5277  $dataHandler->process_cmdmap();
5278  unset($dataHandler);
5279 
5280  // Delete move placeholder
5281  $this->deleteRecord($table, $movePlaceholder['uid'], true, true);
5282  }
5283  }
5284  }
5285 
5292  public function canDeletePage($uid)
5293  {
5294  // If we may at all delete this page
5295  if (!$this->doesRecordExist('pages', $uid, 'delete')) {
5296  return 'Attempt to delete page without permissions';
5297  }
5298 
5299  if ($this->deleteTree) {
5300  // Returns the branch
5301  $brExist = $this->doesBranchExist('', $uid, $this->pMap['delete'], 1);
5302  // Checks if we had permissions
5303  if ($brExist == -1) {
5304  return 'Attempt to delete pages in branch without permissions';
5305  }
5306 
5307  if (!$this->noRecordsFromUnallowedTables($brExist . $uid)) {
5308  return 'Attempt to delete records from disallowed tables';
5309  }
5310 
5311  $pagesInBranch = GeneralUtility::trimExplode(',', $brExist . $uid, true);
5312  foreach ($pagesInBranch as $pageInBranch) {
5313  if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, true)) {
5314  return 'Attempt to delete page which has prohibited localizations.';
5315  }
5316  }
5317  return $pagesInBranch;
5318  } else {
5319  // returns the branch
5320  $brExist = $this->doesBranchExist('', $uid, $this->pMap['delete'], 1);
5321  // Checks if branch exists
5322  if ($brExist != '') {
5323  return 'Attempt to delete page which has subpages';
5324  }
5325 
5326  if (!$this->noRecordsFromUnallowedTables($uid)) {
5327  return 'Attempt to delete records from disallowed tables';
5328  }
5329 
5330  if ($this->BE_USER->recordEditAccessInternals('pages', $uid, false, false, true)) {
5331  return [$uid];
5332  } else {
5333  return 'Attempt to delete page which has prohibited localizations.';
5334  }
5335  }
5336  }
5337 
5345  public function cannotDeleteRecord($table, $id)
5346  {
5347  if ($table === 'pages') {
5348  $res = $this->canDeletePage($id);
5349  return is_array($res) ? false : $res;
5350  } else {
5351  return $this->doesRecordExist($table, $id, 'delete') ? false : 'No permission to delete record';
5352  }
5353  }
5354 
5362  public function isRecordUndeletable($table, $uid)
5363  {
5364  $result = false;
5365  $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5366  if ($record['pid']) {
5367  $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5368  // The page containing the record is not deleted, thus the record can be undeleted:
5369  if (!$page['deleted']) {
5370  $result = true;
5371  } elseif ($this->enableLogging) {
5372  $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');
5373  }
5374  } else {
5375  // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5376  $result = true;
5377  }
5378  return $result;
5379  }
5380 
5391  public function deleteRecord_procFields($table, $uid, $undeleteRecord = false)
5392  {
5393  $conf = $GLOBALS['TCA'][$table]['columns'];
5394  $row = BackendUtility::getRecord($table, $uid, '*', '', false);
5395  if (empty($row)) {
5396  return;
5397  }
5398  foreach ($row as $field => $value) {
5399  $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5400  }
5401  }
5402 
5416  public function deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf, $undeleteRecord = false)
5417  {
5418  if ($conf['type'] == 'inline') {
5419  $foreign_table = $conf['foreign_table'];
5420  if ($foreign_table) {
5421  $inlineType = $this->getInlineFieldType($conf);
5422  if ($inlineType == 'list' || $inlineType == 'field') {
5424  $dbAnalysis = $this->createRelationHandlerInstance();
5425  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
5426  $dbAnalysis->undeleteRecord = true;
5427 
5428  $enableCascadingDelete = true;
5429  // non type save comparison is intended!
5430  if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5431  $enableCascadingDelete = false;
5432  }
5433 
5434  // Walk through the items and remove them
5435  foreach ($dbAnalysis->itemArray as $v) {
5436  if (!$undeleteRecord) {
5437  if ($enableCascadingDelete) {
5438  $this->deleteAction($v['table'], $v['id']);
5439  }
5440  } else {
5441  $this->undeleteRecord($v['table'], $v['id']);
5442  }
5443  }
5444  }
5445  }
5446  } elseif ($this->isReferenceField($conf)) {
5447  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
5448  $dbAnalysis = $this->createRelationHandlerInstance();
5449  $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5450  foreach ($dbAnalysis->itemArray as $v) {
5451  $this->updateRefIndexStack[$table][$uid][] = [$v['table'], $v['id']];
5452  }
5453  }
5454  }
5455 
5463  public function deleteL10nOverlayRecords($table, $uid)
5464  {
5465  // Check whether table can be localized or has a different table defined to store localizations:
5466  if (!BackendUtility::isTableLocalizable($table) || !empty($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']) || !empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable'])) {
5467  return;
5468  }
5469  $where = '';
5470  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
5471  $where = ' AND t3ver_oid=0';
5472  }
5473  $l10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], $uid, $where);
5474  if (is_array($l10nRecords)) {
5475  foreach ($l10nRecords as $record) {
5476  // Ignore workspace delete placeholders. Those records have been marked for
5477  // deletion before - deleting them again in a workspace would revert that state.
5478  if ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5479  BackendUtility::workspaceOL($table, $record);
5480  if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5481  continue;
5482  }
5483  }
5484  $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5485  }
5486  }
5487  }
5488 
5489  /*********************************************
5490  *
5491  * Cmd: Versioning
5492  *
5493  ********************************************/
5505  public function versionizeRecord($table, $id, $label, $delete = false)
5506  {
5507  $id = (int)$id;
5508  // Stop any actions if the record is marked to be deleted:
5509  // (this can occur if IRRE elements are versionized and child elements are removed)
5510  if ($this->isElementToBeDeleted($table, $id)) {
5511  return null;
5512  }
5513  if (!$GLOBALS['TCA'][$table] || !$GLOBALS['TCA'][$table]['ctrl']['versioningWS'] || $id <= 0) {
5514  if ($this->enableLogging) {
5515  $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, 1);
5516  }
5517  return null;
5518  }
5519 
5520  if (!$this->doesRecordExist($table, $id, 'show')) {
5521  if ($this->enableLogging) {
5522  $this->newlog('You didn\'t have correct permissions to make a new version (copy) of this record "' . $table . '" / ' . $id, 1);
5523  }
5524  return null;
5525  }
5526 
5527  // Select main record:
5528  $row = $this->recordInfo($table, $id, 'pid,t3ver_id,t3ver_state');
5529  if (!is_array($row)) {
5530  if ($this->enableLogging) {
5531  $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize did not exist!', 1);
5532  }
5533  return null;
5534  }
5535 
5536  // Record must be online record
5537  if ($row['pid'] < 0) {
5538  if ($this->enableLogging) {
5539  $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (pid=-1)!', 1);
5540  }
5541  return null;
5542  }
5543 
5544  // Record must not be placeholder for moving.
5545  if (VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)) {
5546  if ($this->enableLogging) {
5547  $this->newlog('Record cannot be versioned because it is a placeholder for a moving operation', 1);
5548  }
5549  return null;
5550  }
5551 
5552  if ($delete && $this->cannotDeleteRecord($table, $id)) {
5553  if ($this->enableLogging) {
5554  $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), 1);
5555  }
5556  return null;
5557  }
5558 
5559  // Look for next version number:
5560  $res = $this->databaseConnection->exec_SELECTquery('t3ver_id', $table, '((pid=-1 && t3ver_oid=' . $id . ') OR uid=' . $id . ')' . $this->deleteClause($table), '', 't3ver_id DESC', '1');
5561  list($highestVerNumber) = $this->databaseConnection->sql_fetch_row($res);
5562  $this->databaseConnection->sql_free_result($res);
5563  // Look for version number of the current:
5564  $subVer = $row['t3ver_id'] . '.' . ($highestVerNumber + 1);
5565  // Set up the values to override when making a raw-copy:
5566  $overrideArray = [
5567  't3ver_id' => $highestVerNumber + 1,
5568  't3ver_oid' => $id,
5569  't3ver_label' => $label ?: $subVer . ' / ' . date('d-m-Y H:m:s'),
5570  't3ver_wsid' => $this->BE_USER->workspace,
5571  't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5572  't3ver_count' => 0,
5573  't3ver_stage' => 0,
5574  't3ver_tstamp' => 0
5575  ];
5576  if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5577  $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5578  }
5579  // Checking if the record already has a version in the current workspace of the backend user
5580  if ($this->BE_USER->workspace !== 0) {
5581  // Look for version already in workspace:
5582  $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5583  }
5584  // Create new version of the record and return the new uid
5585  if (empty($versionRecord['uid'])) {
5586  // Create raw-copy and return result:
5587  // The information of the label to be used for the workspace record
5588  // as well as the information whether the record shall be removed
5589  // must be forwarded (creating remove placeholders on a workspace are
5590  // done by copying the record and override several fields).
5591  $workspaceOptions = [
5592  'delete' => $delete,
5593  'label' => $label,
5594  ];
5595  return $this->copyRecord_raw($table, $id, -1, $overrideArray, $workspaceOptions);
5596  // Reuse the existing record and return its uid
5597  // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5598  // did not make much sense since the information is available)
5599  } else {
5600  return $versionRecord['uid'];
5601  }
5602  return null;
5603  }
5604 
5614  public function version_remapMMForVersionSwap($table, $id, $swapWith)
5615  {
5616  // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5617  $currentRec = BackendUtility::getRecord($table, $id);
5618  $swapRec = BackendUtility::getRecord($table, $swapWith);
5619  $this->version_remapMMForVersionSwap_reg = [];
5620  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5621  $conf = $fConf['config'];
5622  if ($this->isReferenceField($conf)) {
5623  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
5624  $prependName = $conf['type'] == 'group' ? $conf['prepend_tname'] : '';
5625  if ($conf['MM']) {
5627  $dbAnalysis = $this->createRelationHandlerInstance();
5628  $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5629  if (!empty($dbAnalysis->getValueArray($prependName))) {
5630  $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5631  }
5633  $dbAnalysis = $this->createRelationHandlerInstance();
5634  $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5635  if (!empty($dbAnalysis->getValueArray($prependName))) {
5636  $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5637  }
5638  }
5639  } elseif ($conf['type'] == 'flex') {
5640  // Current record
5641  $dataStructArray = BackendUtility::getFlexFormDS($conf, $currentRec, $table, $field);
5642  $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5643  if (is_array($currentValueArray)) {
5644  $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5645  }
5646  // Swap record
5647  $dataStructArray = BackendUtility::getFlexFormDS($conf, $swapRec, $table, $field);
5648  $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5649  if (is_array($currentValueArray)) {
5650  $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5651  }
5652  }
5653  }
5654  // Execute:
5655  $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5656  }
5657 
5670  public function version_remapMMForVersionSwap_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
5671  {
5672  // Extract parameters:
5673  list($table, $uid, $field) = $pParams;
5674  if ($this->isReferenceField($dsConf)) {
5675  $allowedTables = $dsConf['type'] == 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5676  $prependName = $dsConf['type'] == 'group' ? $dsConf['prepend_tname'] : '';
5677  if ($dsConf['MM']) {
5679  $dbAnalysis = $this->createRelationHandlerInstance();
5680  $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5681  $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5682  }
5683  }
5684  }
5685 
5696  public function version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
5697  {
5698  if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5699  foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5700  $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5701  }
5702  }
5703  if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5704  foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5705  $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5706  }
5707  }
5708  if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5709  foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5710  $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5711  }
5712  }
5713  }
5714 
5715  /*********************************************
5716  *
5717  * Cmd: Helper functions
5718  *
5719  ********************************************/
5720 
5728  protected function getLocalTCE($stripslashesValues = false, $dontProcessTransformations = true)
5729  {
5730  $copyTCE = GeneralUtility::makeInstance(__CLASS__);
5731  $copyTCE->stripslashes_values = $stripslashesValues;
5732  $copyTCE->copyTree = $this->copyTree;
5733  $copyTCE->enableLogging = $this->enableLogging;
5734  // Copy forth the cached TSconfig
5735  $copyTCE->cachedTSconfig = $this->cachedTSconfig;
5736  // Transformations should NOT be carried out during copy
5737  $copyTCE->dontProcessTransformations = $dontProcessTransformations;
5738  // make sure the isImporting flag is transferred, so all hooks know if
5739  // the current process is an import process
5740  $copyTCE->isImporting = $this->isImporting;
5741  return $copyTCE;
5742  }
5743 
5749  public function remapListedDBRecords()
5750  {
5751  if (!empty($this->registerDBList)) {
5752  foreach ($this->registerDBList as $table => $records) {
5753  foreach ($records as $uid => $fields) {
5754  $newData = [];
5755  $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5756  $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5757  foreach ($fields as $fieldName => $value) {
5758  $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5759  switch ($conf['type']) {
5760  case 'group':
5761 
5762  case 'select':
5763  $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5764  if (is_array($vArray)) {
5765  $newData[$fieldName] = implode(',', $vArray);
5766  }
5767  break;
5768  case 'flex':
5769  if ($value == 'FlexForm_reference') {
5770  // This will fetch the new row for the element
5771  $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5772  if (is_array($origRecordRow)) {
5773  BackendUtility::workspaceOL($table, $origRecordRow);
5774  // Get current data structure and value array:
5775  $dataStructArray = BackendUtility::getFlexFormDS($conf, $origRecordRow, $table, $fieldName);
5776  $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5777  // Do recursive processing of the XML data:
5778  $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
5779  // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5780  if (is_array($currentValueArray['data'])) {
5781  $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
5782  }
5783  }
5784  }
5785  break;
5786  case 'inline':
5787  $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5788  break;
5789  default:
5790  debug('Field type should not appear here: ' . $conf['type']);
5791  }
5792  }
5793  // If any fields were changed, those fields are updated!
5794  if (!empty($newData)) {
5795  $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5796  }
5797  }
5798  }
5799  }
5800  }
5801 
5813  public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
5814  {
5815  // Extract parameters:
5816  list($table, $uid, $field) = $pParams;
5817  // If references are set for this field, set flag so they can be corrected later:
5818  if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
5819  $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
5820  if (is_array($vArray)) {
5821  $dataValue = implode(',', $vArray);
5822  }
5823  }
5824  // Return
5825  return ['value' => $dataValue];
5826  }
5827 
5838  public function remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
5839  {
5840  // Initialize variables
5841  // Will be set TRUE if an upgrade should be done...
5842  $set = false;
5843  // Allowed tables for references.
5844  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
5845  // Table name to prepend the UID
5846  $prependName = $conf['type'] == 'group' ? $conf['prepend_tname'] : '';
5847  // Which tables that should possibly not be remapped
5848  $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
5849  // Convert value to list of references:
5850  $dbAnalysis = $this->createRelationHandlerInstance();
5851  $dbAnalysis->registerNonTableValues = $conf['type'] == 'select' && $conf['allowNonIdValues'];
5852  $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
5853  // Traverse those references and map IDs:
5854  foreach ($dbAnalysis->itemArray as $k => $v) {
5855  $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
5856  if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
5857  $dbAnalysis->itemArray[$k]['id'] = $mapID;
5858  $set = true;
5859  }
5860  }
5861  if (!empty($conf['MM'])) {
5862  // Purge invalid items (live/version)
5863  $dbAnalysis->purgeItemArray();
5864  if ($dbAnalysis->isPurged()) {
5865  $set = true;
5866  }
5867 
5868  // If record has been versioned/copied in this process, handle invalid relations of the live record
5869  $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
5870  if (!empty($this->copyMappingArray_merged[$table])) {
5871  $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
5872  }
5873  if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
5874  $liveRelations = $this->createRelationHandlerInstance();
5875  $liveRelations->setWorkspaceId(0);
5876  $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
5877  // Purge invalid relations in the live workspace ("0")
5878  $liveRelations->purgeItemArray(0);
5879  if ($liveRelations->isPurged()) {
5880  $liveRelations->writeMM($conf['MM'], $liveId, $prependName);
5881  }
5882  }
5883  }
5884  // If a change has been done, set the new value(s)
5885  if ($set) {
5886  if ($conf['MM']) {
5887  $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5888  } else {
5889  return $dbAnalysis->getValueArray($prependName);
5890  }
5891  }
5892  return null;
5893  }
5894 
5904  public function remapListedDBRecords_procInline($conf, $value, $uid, $table)
5905  {
5906  $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5907  if ($conf['foreign_table']) {
5908  $inlineType = $this->getInlineFieldType($conf);
5909  if ($inlineType == 'mm') {
5910  $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5911  } elseif ($inlineType !== false) {
5913  $dbAnalysis = $this->createRelationHandlerInstance();
5914  $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
5915 
5916  // Keep original (live) item array and update values for specific versioned records
5917  $originalItemArray = $dbAnalysis->itemArray;
5918  foreach ($dbAnalysis->itemArray as &$item) {
5919  $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
5920  if (!empty($versionedId)) {
5921  $item['id'] = $versionedId;
5922  }
5923  }
5924 
5925  // Update child records if using pointer fields ('foreign_field'):
5926  if ($inlineType == 'field') {
5927  $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
5928  }
5929  $thePidToUpdate = null;
5930  // If the current field is set on a page record, update the pid of related child records:
5931  if ($table == 'pages') {
5932  $thePidToUpdate = $theUidToUpdate;
5933  } elseif (isset($this->registerDBPids[$table][$uid])) {
5934  $thePidToUpdate = $this->registerDBPids[$table][$uid];
5935  $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
5936  }
5937  // Update child records if change to pid is required (only if the current record is not on a workspace):
5938  if ($thePidToUpdate) {
5939  // ensure, only live page ids are used as 'pid' values
5940  $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
5941  if ($liveId !== null) {
5942  $thePidToUpdate = $liveId;
5943  }
5944  $updateValues = ['pid' => $thePidToUpdate];
5945  foreach ($originalItemArray as $v) {
5946  if ($v['id'] && $v['table'] && is_null(BackendUtility::getLiveVersionIdOfRecord($v['table'], $v['id']))) {
5947  $this->databaseConnection->exec_UPDATEquery($v['table'], 'uid=' . (int)$v['id'], $updateValues);
5948  }
5949  }
5950  }
5951  }
5952  }
5953  }
5954 
5961  public function processRemapStack()
5962  {
5963  // Processes the remap stack:
5964  if (is_array($this->remapStack)) {
5965  $remapFlexForms = [];
5966  $hookPayload = [];
5967 
5968  foreach ($this->remapStack as $remapAction) {
5969  // If no position index for the arguments was set, skip this remap action:
5970  if (!is_array($remapAction['pos'])) {
5971  continue;
5972  }
5973  // Load values from the argument array in remapAction:
5974  $field = $remapAction['field'];
5975  $id = $remapAction['args'][$remapAction['pos']['id']];
5976  $rawId = $id;
5977  $table = $remapAction['args'][$remapAction['pos']['table']];
5978  $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
5979  $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
5980  $additionalData = $remapAction['additionalData'];
5981  // The record is new and has one or more new ids (in case of versioning/workspaces):
5982  if (strpos($id, 'NEW') !== false) {
5983  // Replace NEW...-ID with real uid:
5984  $id = $this->substNEWwithIDs[$id];
5985  // If the new parent record is on a non-live workspace or versionized, it has another new id:
5986  if (isset($this->autoVersionIdMap[$table][$id])) {
5987  $id = $this->autoVersionIdMap[$table][$id];
5988  }
5989  $remapAction['args'][$remapAction['pos']['id']] = $id;
5990  }
5991  // Replace relations to NEW...-IDs in field value (uids of child records):
5992  if (is_array($valueArray)) {
5993  foreach ($valueArray as $key => $value) {
5994  if (strpos($value, 'NEW') !== false) {
5995  if (strpos($value, '_') === false) {
5996  $affectedTable = $tcaFieldConf['foreign_table'];
5997  $prependTable = false;
5998  } else {
5999  $parts = explode('_', $value);
6000  $value = array_pop($parts);
6001  $affectedTable = implode('_', $parts);
6002  $prependTable = true;
6003  }
6004  $value = $this->substNEWwithIDs[$value];
6005  // The record is new, but was also auto-versionized and has another new id:
6006  if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
6007  $value = $this->autoVersionIdMap[$affectedTable][$value];
6008  }
6009  if ($prependTable) {
6010  $value = $affectedTable . '_' . $value;
6011  }
6012  // Set a hint that this was a new child record:
6013  $this->newRelatedIDs[$affectedTable][] = $value;
6014  $valueArray[$key] = $value;
6015  }
6016  }
6017  $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
6018  }
6019  // Process the arguments with the defined function:
6020  $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
6021  // If array is returned, check for maxitems condition, if string is returned this was already done:
6022  if (is_array($newValue)) {
6023  $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
6024  // The reference casting is only required if
6025  // checkValue_group_select_processDBdata() returns an array
6026  $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
6027  }
6028  // Update in database (list of children (csv) or number of relations (foreign_field)):
6029  if (!empty($field)) {
6030  $this->updateDB($table, $id, [$field => $newValue]);
6031  // Collect data to update FlexForms
6032  } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
6033  $flexFormId = $additionalData['flexFormId'];
6034  $flexFormPath = $additionalData['flexFormPath'];
6035 
6036  if (!isset($remapFlexForms[$flexFormId])) {
6037  $remapFlexForms[$flexFormId] = [];
6038  }
6039 
6040  $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
6041  }
6042 
6043  // Collect elements that shall trigger processDatamap_afterDatabaseOperations
6044  if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
6045  $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
6046  if (!isset($hookPayload[$table][$rawId])) {
6047  $hookPayload[$table][$rawId] = [
6048  'status' => $hookArgs['status'],
6049  'fieldArray' => $hookArgs['fieldArray'],
6050  'hookObjects' => $hookArgs['hookObjectsArr'],
6051  ];
6052  }
6053  $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
6054  }
6055  }
6056 
6057  if ($remapFlexForms) {
6058  foreach ($remapFlexForms as $flexFormId => $modifications) {
6059  $this->updateFlexFormData($flexFormId, $modifications);
6060  }
6061  }
6062 
6063  foreach ($hookPayload as $tableName => $rawIdPayload) {
6064  foreach ($rawIdPayload as $rawId => $payload) {
6065  foreach ($payload['hookObjects'] as $hookObject) {
6066  if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
6067  continue;
6068  }
6069  $hookObject->processDatamap_afterDatabaseOperations(
6070  $payload['status'],
6071  $tableName,
6072  $rawId,
6073  $payload['fieldArray'],
6074  $this
6075  );
6076  }
6077  }
6078  }
6079  }
6080  // Processes the remap stack actions:
6081  if ($this->remapStackActions) {
6082  foreach ($this->remapStackActions as $action) {
6083  if (isset($action['callback']) && isset($action['arguments'])) {
6084  call_user_func_array($action['callback'], $action['arguments']);
6085  }
6086  }
6087  }
6088  // Processes the reference index updates of the remap stack:
6089  foreach ($this->remapStackRefIndex as $table => $idArray) {
6090  foreach ($idArray as $id) {
6091  $this->updateRefIndex($table, $id);
6092  unset($this->remapStackRefIndex[$table][$id]);
6093  }
6094  }
6095  // Reset:
6096  $this->remapStack = [];
6097  $this->remapStackRecords = [];
6098  $this->remapStackActions = [];
6099  $this->remapStackRefIndex = [];
6100  }
6101 
6109  protected function updateFlexFormData($flexFormId, array $modifications)
6110  {
6111  list($table, $uid, $field) = explode(':', $flexFormId, 3);
6112 
6113  if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
6114  $uid = $this->substNEWwithIDs[$uid];
6115  }
6116 
6117  $record = $this->recordInfo($table, $uid, '*');
6118 
6119  if (!$table || !$uid || !$field || !is_array($record)) {
6120  return;
6121  }
6122 
6123  BackendUtility::workspaceOL($table, $record);
6124 
6125  // Get current data structure and value array:
6126  $valueStructure = GeneralUtility::xml2array($record[$field]);
6127 
6128  // Do recursive processing of the XML data:
6129  foreach ($modifications as $path => $value) {
6130  $valueStructure['data'] = ArrayUtility::setValueByPath(
6131  $valueStructure['data'], $path, $value
6132  );
6133  }
6134 
6135  if (is_array($valueStructure['data'])) {
6136  // The return value should be compiled back into XML
6137  $values = [
6138  $field => $this->checkValue_flexArray2Xml($valueStructure, true),
6139  ];
6140 
6141  $this->updateDB($table, $uid, $values);
6142  }
6143  }
6144 
6160  protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
6161  {
6162  // Check whether the affected record is marked to be remapped:
6163  if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
6164  call_user_func_array($callback, $arguments);
6165  } else {
6166  $this->addRemapAction($table, $id, $callback, $arguments);
6167  }
6168  }
6169 
6179  public function addRemapAction($table, $id, array $callback, array $arguments)
6180  {
6181  $this->remapStackActions[] = [
6182  'affects' => [
6183  'table' => $table,
6184  'id' => $id
6185  ],
6186  'callback' => $callback,
6187  'arguments' => $arguments
6188  ];
6189  }
6190 
6198  public function addRemapStackRefIndex($table, $id)
6199  {
6200  $this->remapStackRefIndex[$table][$id] = $id;
6201  }
6202 
6215  public function getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
6216  {
6217  if (is_array($registerDBList[$table][$id])) {
6218  foreach ($incomingFieldArray as $field => $value) {
6219  $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
6220  if ($registerDBList[$table][$id][$field] && ($foreignTable = $fieldConf['foreign_table'])) {
6221  $newValueArray = [];
6222  $origValueArray = explode(',', $value);
6223  // Update the uids of the copied records, but also take care about new records:
6224  foreach ($origValueArray as $childId) {
6225  $newValueArray[] = $this->autoVersionIdMap[$foreignTable][$childId] ? $this->autoVersionIdMap[$foreignTable][$childId] : $childId;
6226  }
6227  // Set the changed value to the $incomingFieldArray
6228  $incomingFieldArray[$field] = implode(',', $newValueArray);
6229  }
6230  }
6231  // Clean up the $registerDBList array:
6232  unset($registerDBList[$table][$id]);
6233  if (empty($registerDBList[$table])) {
6234  unset($registerDBList[$table]);
6235  }
6236  }
6237  }
6238 
6239  /*****************************
6240  *
6241  * Access control / Checking functions
6242  *
6243  *****************************/
6250  public function checkModifyAccessList($table)
6251  {
6252  $res = $this->admin || !$this->tableAdminOnly($table) && GeneralUtility::inList($this->BE_USER->groupData['tables_modify'], $table);
6253  // Hook 'checkModifyAccessList': Post-processing of the state of access
6254  foreach ($this->getCheckModifyAccessListHookObjects() as $hookObject) {
6256  $hookObject->checkModifyAccessList($res, $table, $this);
6257  }
6258  return $res;
6259  }
6260 
6268  public function isRecordInWebMount($table, $id)
6269  {
6270  if (!isset($this->isRecordInWebMount_Cache[$table . ':' . $id])) {
6271  $recP = $this->getRecordProperties($table, $id);
6272  $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->isInWebMount($recP['event_pid']);
6273  }
6274  return $this->isRecordInWebMount_Cache[$table . ':' . $id];
6275  }
6276 
6283  public function isInWebMount($pid)
6284  {
6285  if (!isset($this->isInWebMount_Cache[$pid])) {
6286  $this->isInWebMount_Cache[$pid] = $this->BE_USER->isInWebMount($pid);
6287  }
6288  return $this->isInWebMount_Cache[$pid];
6289  }
6290 
6300  public function checkRecordUpdateAccess($table, $id, $data = false, $hookObjectsArr = null)
6301  {
6302  $res = null;
6303  if (is_array($hookObjectsArr)) {
6304  foreach ($hookObjectsArr as $hookObj) {
6305  if (method_exists($hookObj, 'checkRecordUpdateAccess')) {
6306  $res = $hookObj->checkRecordUpdateAccess($table, $id, $data, $res, $this);
6307  }
6308  }
6309  }
6310  if ($res === 1 || $res === 0) {
6311  return $res;
6312  } else {
6313  $res = 0;
6314  }
6315  if ($GLOBALS['TCA'][$table] && (int)$id > 0) {
6316  // If information is cached, return it
6317  if (isset($this->recUpdateAccessCache[$table][$id])) {
6318  return $this->recUpdateAccessCache[$table][$id];
6319  } elseif ($this->doesRecordExist($table, $id, 'edit')) {
6320  $res = 1;
6321  }
6322  // Cache the result
6323  $this->recUpdateAccessCache[$table][$id] = $res;
6324  }
6325  return $res;
6326  }
6327 
6337  public function checkRecordInsertAccess($insertTable, $pid, $action = 1)
6338  {
6339  $pid = (int)$pid;
6340  if ($pid < 0) {
6341  return false;
6342  }
6343  // If information is cached, return it
6344  if (isset($this->recInsertAccessCache[$insertTable][$pid])) {
6345  return $this->recInsertAccessCache[$insertTable][$pid];
6346  }
6347 
6348  $res = false;
6349  if ($insertTable === 'pages') {
6350  $perms = $this->pMap['new'];
6351  // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6352  } elseif (($insertTable === 'sys_file_reference') && array_key_exists('pages', $this->datamap)) {
6353  $perms = $this->pMap['edit'];
6354  } else {
6355  $perms = $this->pMap['editcontent'];
6356  }
6357  $pageExists = (bool)$this->doesRecordExist('pages', $pid, $perms);
6358  // 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
6359  if ($pageExists || $pid === 0 && ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($insertTable))) {
6360  // Check permissions
6361  if ($this->isTableAllowedForThisPage($pid, $insertTable)) {
6362  $res = true;
6363  // Cache the result
6364  $this->recInsertAccessCache[$insertTable][$pid] = $res;
6365  } elseif ($this->enableLogging) {
6366  $propArr = $this->getRecordProperties('pages', $pid);
6367  $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']);
6368  }
6369  } elseif ($this->enableLogging) {
6370  $propArr = $this->getRecordProperties('pages', $pid);
6371  $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']);
6372  }
6373  return $res;
6374  }
6375 
6383  public function isTableAllowedForThisPage($page_uid, $checkTable)
6384  {
6385  $page_uid = (int)$page_uid;
6386  $rootLevelSetting = (int)$GLOBALS['TCA'][$checkTable]['ctrl']['rootLevel'];
6387  // 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.
6388  if ($checkTable !== 'pages' && $rootLevelSetting !== -1 && ($rootLevelSetting xor !$page_uid)) {
6389  return false;
6390  }
6391  $allowed = false;
6392  // Check root-level
6393  if (!$page_uid) {
6394  if ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($checkTable)) {
6395  $allowed = true;
6396  }
6397  } else {
6398  // Check non-root-level
6399  $doktype = $this->pageInfo($page_uid, 'doktype');
6400  $allowedTableList = isset($GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'])
6401  ? $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables']
6402  : $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6403  $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6404  // If all tables or the table is listed as an allowed type, return TRUE
6405  if (strpos($allowedTableList, '*') !== false || in_array($checkTable, $allowedArray, true)) {
6406  $allowed = true;
6407  }
6408  }
6409  return $allowed;
6410  }
6411 
6422  public function doesRecordExist($table, $id, $perms)
6423  {
6424  $id = (int)$id;
6425  if ($this->bypassAccessCheckForRecords) {
6426  return is_array(BackendUtility::getRecordRaw($table, 'uid=' . $id, 'uid'));
6427  }
6428  // Processing the incoming $perms (from possible string to integer that can be AND'ed)
6430  if ($table != 'pages') {
6431  switch ($perms) {
6432  case 'edit':
6433 
6434  case 'delete':
6435 
6436  case 'new':
6437  // This holds it all in case the record is not page!!
6438  if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
6439  $perms = 'edit';
6440  } else {
6441  $perms = 'editcontent';
6442  }
6443  break;
6444  }
6445  }
6446  $perms = (int)$this->pMap[$perms];
6447  } else {
6448  $perms = (int)$perms;
6449  }
6450  if (!$perms) {
6451  throw new \RuntimeException('Internal ERROR: no permissions to check for non-admin user', 1270853920);
6452  }
6453  // For all tables: Check if record exists:
6454  $isWebMountRestrictionIgnored = BackendUtility::isWebMountRestrictionIgnored($table);
6455  if (is_array($GLOBALS['TCA'][$table]) && $id > 0 && ($isWebMountRestrictionIgnored || $this->isRecordInWebMount($table, $id) || $this->admin)) {
6456  if ($table != 'pages') {
6457  // Find record without checking page:
6458  $mres = $this->databaseConnection->exec_SELECTquery('uid,pid', $table, 'uid=' . (int)$id . $this->deleteClause($table));
6459  // THIS SHOULD CHECK FOR editlock I think!
6460  $output = $this->databaseConnection->sql_fetch_assoc($mres);
6461  BackendUtility::fixVersioningPid($table, $output, true);
6462  // If record found, check page as well:
6463  if (is_array($output)) {
6464  // Looking up the page for record:
6465  $mres = $this->doesRecordExist_pageLookUp($output['pid'], $perms);
6466  $pageRec = $this->databaseConnection->sql_fetch_assoc($mres);
6467  // 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):
6468  $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
6469  if (is_array($pageRec) || !$output['pid'] && ($isRootLevelRestrictionIgnored || $this->admin)) {
6470  return true;
6471  }
6472  }
6473  return false;
6474  } else {
6475  $mres = $this->doesRecordExist_pageLookUp($id, $perms);
6476  return $this->databaseConnection->sql_num_rows($mres);
6477  }
6478  }
6479  return false;
6480  }
6481 
6491  public function doesRecordExist_pageLookUp($id, $perms)
6492  {
6493  return $this->databaseConnection->exec_SELECTquery('uid', 'pages', 'uid=' . (int)$id . $this->deleteClause('pages') . ($perms && !$this->admin ? ' AND ' . $this->BE_USER->getPagePermsClause($perms) : '') . (!$this->admin && $GLOBALS['TCA']['pages']['ctrl']['editlock'] && $perms & Permission::PAGE_EDIT + Permission::PAGE_DELETE + Permission::CONTENT_EDIT ? ' AND ' . $GLOBALS['TCA']['pages']['ctrl']['editlock'] . '=0' : ''));
6494  }
6495 
6509  public function doesBranchExist($inList, $pid, $perms, $recurse)
6510  {
6511  $pid = (int)$pid;
6512  $perms = (int)$perms;
6513  if ($pid >= 0) {
6514  $mres = $this->databaseConnection->exec_SELECTquery('uid, perms_userid, perms_groupid, perms_user, perms_group, perms_everybody', 'pages', 'pid=' . (int)$pid . $this->deleteClause('pages'), '', 'sorting');
6515  while ($row = $this->databaseConnection->sql_fetch_assoc($mres)) {
6516  // IF admin, then it's OK
6517  if ($this->admin || $this->BE_USER->doesUserHaveAccess($row, $perms)) {
6518  $inList .= $row['uid'] . ',';
6519  if ($recurse) {
6520  // Follow the subpages recursively...
6521  $inList = $this->doesBranchExist($inList, $row['uid'], $perms, $recurse);
6522  if ($inList == -1) {
6523  return -1;
6524  }
6525  }
6526  } else {
6527  // No permissions
6528  return -1;
6529  }
6530  }
6531  $this->databaseConnection->sql_free_result($mres);
6532  }
6533  return $inList;
6534  }
6535 
6542  public function tableReadOnly($table)
6543  {
6544  // Returns TRUE if table is readonly
6545  return (bool)$GLOBALS['TCA'][$table]['ctrl']['readOnly'];
6546  }
6547 
6554  public function tableAdminOnly($table)
6555  {
6556  // Returns TRUE if table is admin-only
6557  return (bool)$GLOBALS['TCA'][$table]['ctrl']['adminOnly'];
6558  }
6559 
6568  public function destNotInsideSelf($destinationId, $id)
6569  {
6570  $loopCheck = 100;
6571  $destinationId = (int)$destinationId;
6572  $id = (int)$id;
6573  if ($destinationId === $id) {
6574  return false;
6575  }
6576  while ($destinationId !== 0 && $loopCheck > 0) {
6577  $loopCheck--;
6578  $res = $this->databaseConnection->exec_SELECTquery('pid, uid, t3ver_oid,t3ver_wsid', 'pages', 'uid=' . $destinationId . $this->deleteClause('pages'));
6579  if ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
6580  BackendUtility::fixVersioningPid('pages', $row);
6581  if ($row['pid'] == $id) {
6582  return false;
6583  } else {
6584  $destinationId = (int)$row['pid'];
6585  }
6586  } else {
6587  return false;
6588  }
6589  $this->databaseConnection->sql_free_result($res);
6590  }
6591  return true;
6592  }
6593 
6600  public function getExcludeListArray()
6601  {
6602  $list = [];
6603  $nonExcludeFieldsArray = array_flip(GeneralUtility::trimExplode(',', $this->BE_USER->groupData['non_exclude_fields']));
6604  foreach ($GLOBALS['TCA'] as $table => $_) {
6605  if (isset($GLOBALS['TCA'][$table]['columns'])) {
6606  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $config) {
6607  if ($config['exclude'] && !isset($nonExcludeFieldsArray[$table . ':' . $field])) {
6608  $list[] = $table . '-' . $field;
6609  }
6610  }
6611  }
6612  }
6613  return $list;
6614  }
6615 
6623  public function doesPageHaveUnallowedTables($page_uid, $doktype)
6624  {
6625  $page_uid = (int)$page_uid;
6626  if (!$page_uid) {
6627  // Not a number. Probably a new page
6628  return false;
6629  }
6630  $allowedTableList = isset($GLOBALS['PAGES_TYPES'][$doktype]['allowedTables']) ? $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] : $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6631  $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6632  // If all tables is OK the return TRUE
6633  if (strstr($allowedTableList, '*')) {
6634  // OK...
6635  return false;
6636  }
6637  $tableList = [];
6638  foreach ($GLOBALS['TCA'] as $table => $_) {
6639  // If the table is not in the allowed list, check if there are records...
6640  if (!in_array($table, $allowedArray, true)) {
6641  $count = $this->databaseConnection->exec_SELECTcountRows('uid', $table, 'pid=' . (int)$page_uid);
6642  if ($count) {
6643  $tableList[] = $table;
6644  }
6645  }
6646  }
6647  return implode(',', $tableList);
6648  }
6649 
6650  /*****************************
6651  *
6652  * Information lookup
6653  *
6654  *****************************/
6663  public function pageInfo($id, $field)
6664  {
6665  if (!isset($this->pageCache[$id])) {
6666  $res = $this->databaseConnection->exec_SELECTquery('*', 'pages', 'uid=' . (int)$id);
6667  if ($this->databaseConnection->sql_num_rows($res)) {
6668  $this->pageCache[$id] = $this->databaseConnection->sql_fetch_assoc($res);
6669  }
6670  $this->databaseConnection->sql_free_result($res);
6671  }
6672  return $this->pageCache[$id][$field];
6673  }
6674 
6684  public function recordInfo($table, $id, $fieldList)
6685  {
6686  // Skip, if searching for NEW records or there's no TCA table definition
6687  if ((int)$id === 0 || !isset($GLOBALS['TCA'][$table])) {
6688  return null;
6689  }
6690  $result = $this->databaseConnection->exec_SELECTgetSingleRow($fieldList, $table, 'uid=' . (int)$id);
6691  return $result ?: null;
6692  }
6693 
6705  public function getRecordProperties($table, $id, $noWSOL = false)
6706  {
6707  $row = $table == 'pages' && !$id ? ['title' => '[root-level]', 'uid' => 0, 'pid' => 0] : $this->recordInfo($table, $id, '*');
6708  if (!$noWSOL) {
6709  BackendUtility::workspaceOL($table, $row);
6710  }
6711  return $this->getRecordPropertiesFromRow($table, $row);
6712  }
6713 
6721  public function getRecordPropertiesFromRow($table, $row)
6722  {
6723  if ($GLOBALS['TCA'][$table]) {
6724  BackendUtility::fixVersioningPid($table, $row);
6725  $out = [
6726  'header' => BackendUtility::getRecordTitle($table, $row),
6727  'pid' => $row['pid'],
6728  'event_pid' => $this->eventPid($table, isset($row['_ORIG_pid']) ? $row['t3ver_oid'] : $row['uid'], $row['pid']),
6729  't3ver_state' => $GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? $row['t3ver_state'] : '',
6730  '_ORIG_pid' => $row['_ORIG_pid']
6731  ];
6732  return $out;
6733  }
6734  return null;
6735  }
6736 
6743  public function eventPid($table, $uid, $pid)
6744  {
6745  return $table == 'pages' ? $uid : $pid;
6746  }
6747 
6748  /*********************************************
6749  *
6750  * Storing data to Database Layer
6751  *
6752  ********************************************/
6762  public function updateDB($table, $id, $fieldArray)
6763  {
6764  if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && (int)$id) {
6765  // Do NOT update the UID field, ever!
6766  unset($fieldArray['uid']);
6767  if (!empty($fieldArray)) {
6768  $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6769  // Execute the UPDATE query:
6770  $this->databaseConnection->exec_UPDATEquery($table, 'uid=' . (int)$id, $fieldArray);
6771  // If succeeds, do...:
6772  if (!$this->databaseConnection->sql_error()) {
6773  // Update reference index:
6774  $this->updateRefIndex($table, $id);
6775  if ($this->enableLogging) {
6776  $newRow = [];
6777  if ($this->checkStoredRecords) {
6778  $newRow = $this->checkStoredRecord($table, $id, $fieldArray, 2);
6779  }
6780  // Set log entry:
6781  $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6782  $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']);
6783  // Set History data:
6784  $this->setHistory($table, $id, $theLogId);
6785  }
6786  // Clear cache for relevant pages:
6787  $this->registerRecordIdForPageCacheClearing($table, $id);
6788  // Unset the pageCache for the id if table was page.
6789  if ($table == 'pages') {
6790  unset($this->pageCache[$id]);
6791  }
6792  } elseif ($this->enableLogging) {
6793  $this->log($table, $id, 2, 0, 2, 'SQL error: \'%s\' (%s)', 12, [$this->databaseConnection->sql_error(), $table . ':' . $id]);
6794  }
6795  }
6796  }
6797  }
6798 
6811  public function insertDB($table, $id, $fieldArray, $newVersion = false, $suggestedUid = 0, $dontSetNewIdIndex = false)
6812  {
6813  if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && isset($fieldArray['pid'])) {
6814  // Do NOT insert the UID field, ever!
6815  unset($fieldArray['uid']);
6816  if (!empty($fieldArray)) {
6817  // Check for "suggestedUid".
6818  // This feature is used by the import functionality to force a new record to have a certain UID value.
6819  // This is only recommended for use when the destination server is a passive mirror of another server.
6820  // As a security measure this feature is available only for Admin Users (for now)
6821  $suggestedUid = (int)$suggestedUid;
6822  if ($this->BE_USER->isAdmin() && $suggestedUid && $this->suggestedInsertUids[$table . ':' . $suggestedUid]) {
6823  // When the value of ->suggestedInsertUids[...] is "DELETE" it will try to remove the previous record
6824  if ($this->suggestedInsertUids[$table . ':' . $suggestedUid] === 'DELETE') {
6825  // DELETE:
6826  $this->databaseConnection->exec_DELETEquery($table, 'uid=' . (int)$suggestedUid);
6827  }
6828  $fieldArray['uid'] = $suggestedUid;
6829  }
6830  $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6831  // Execute the INSERT query:
6832  $this->databaseConnection->exec_INSERTquery($table, $fieldArray);
6833  // If succees, do...:
6834  if (!$this->databaseConnection->sql_error()) {
6835  // Set mapping for NEW... -> real uid:
6836  // the NEW_id now holds the 'NEW....' -id
6837  $NEW_id = $id;
6838  $id = $this->databaseConnection->sql_insert_id();
6839  if (!$dontSetNewIdIndex) {
6840  $this->substNEWwithIDs[$NEW_id] = $id;
6841  $this->substNEWwithIDs_table[$NEW_id] = $table;
6842  }
6843  $newRow = [];
6844  // Checking the record is properly saved and writing to log
6845  if ($this->enableLogging && $this->checkStoredRecords) {
6846  $newRow = $this->checkStoredRecord($table, $id, $fieldArray, 1);
6847  }
6848  // Update reference index:
6849  $this->updateRefIndex($table, $id);
6850  if ($newVersion) {
6851  if ($this->enableLogging) {
6852  $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6853  $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);
6854  }
6855  } else {
6856  if ($this->enableLogging) {
6857  $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6858  $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
6859  $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);
6860  }
6861  // Clear cache for relevant pages:
6862  $this->registerRecordIdForPageCacheClearing($table, $id);
6863  }
6864  return $id;
6865  } elseif ($this->enableLogging) {
6866  $this->log($table, $id, 1, 0, 2, 'SQL error: \'%s\' (%s)', 12, [$this->databaseConnection->sql_error(), $table . ':' . $id]);
6867  }
6868  }
6869  }
6870  return null;
6871  }
6872 
6883  public function checkStoredRecord($table, $id, $fieldArray, $action)
6884  {
6885  $id = (int)$id;
6886  if (is_array($GLOBALS['TCA'][$table]) && $id) {
6887  $res = $this->databaseConnection->exec_SELECTquery('*', $table, 'uid=' . (int)$id);
6888  if ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
6889  // Traverse array of values that was inserted into the database and compare with the actually stored value:
6890  $errors = [];
6891  foreach ($fieldArray as $key => $value) {
6892  if ($this->checkStoredRecords_loose && !$value && !$row[$key]) {
6893  } elseif ((string)$value !== (string)$row[$key]) {
6894  $errors[] = $key;
6895  }
6896  }
6897  // Set log message if there were fields with unmatching values:
6898  if ($this->enableLogging && !empty($errors)) {
6899  $message = sprintf(
6900  '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.',
6901  $id,
6902  $table,
6903  implode(', ', $errors)
6904  );
6905  $this->log($table, $id, $action, 0, 1, $message);
6906  }
6907  // Return selected rows:
6908  return $row;
6909  }
6910  $this->databaseConnection->sql_free_result($res);
6911  }
6912  return null;
6913  }
6914 
6923  public function setHistory($table, $id, $logId)
6924  {
6925  if (isset($this->historyRecords[$table . ':' . $id]) && (int)$logId > 0) {
6926  $fields_values = [];
6927  $fields_values['history_data'] = serialize($this->historyRecords[$table . ':' . $id]);
6928  $fields_values['fieldlist'] = implode(',', array_keys($this->historyRecords[$table . ':' . $id]['newRecord']));
6929  $fields_values['tstamp'] = $GLOBALS['EXEC_TIME'];
6930  $fields_values['tablename'] = $table;
6931  $fields_values['recuid'] = $id;
6932  $fields_values['sys_log_uid'] = $logId;
6933  $this->databaseConnection->exec_INSERTquery('sys_history', $fields_values);
6934  }
6935  }
6936 
6945  public function updateRefIndex($table, $id)
6946  {
6948  $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
6950  $refIndexObj->setWorkspaceId($this->BE_USER->workspace);
6951  }
6952  $refIndexObj->updateRefIndexTable($table, $id);
6953  }
6954 
6955  /*********************************************
6956  *
6957  * Misc functions
6958  *
6959  ********************************************/
6969  public function getSortNumber($table, $uid, $pid)
6970  {
6971  if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
6972  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
6973  // Sorting number is in the top
6974  if ($pid >= 0) {
6975  // Fetches the first record under this pid
6976  $res = $this->databaseConnection->exec_SELECTquery($sortRow . ',pid,uid', $table, 'pid=' . (int)$pid . $this->deleteClause($table), '', $sortRow . ' ASC', '1');
6977  // There was an element
6978  if ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
6979  // The top record was the record it self, so we return its current sortnumber
6980  if ($row['uid'] == $uid) {
6981  return $row[$sortRow];
6982  }
6983  // If the pages sortingnumber < 1 we must resort the records under this pid
6984  if ($row[$sortRow] < 1) {
6985  $this->resorting($table, $pid, $sortRow, 0);
6986  // First sorting number after resorting
6987  return $this->sortIntervals;
6988  } else {
6989  // Sorting number between current top element and zero
6990  return floor($row[$sortRow] / 2);
6991  }
6992  } else {
6993  // No pages, so we choose the default value as sorting-number
6994  // First sorting number if no elements.
6995  return $this->sortIntervals;
6996  }
6997  } else {
6998  // Sorting number is inside the list
6999  // Fetches the record which is supposed to be the prev record
7000  $res = $this->databaseConnection->exec_SELECTquery($sortRow . ',pid,uid', $table, 'uid=' . abs($pid) . $this->deleteClause($table));
7001  // There was a record
7002  if ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
7003  // 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.
7004  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $row['uid'], $sortRow . ',pid,uid')) {
7005  $row = $lookForLiveVersion;
7006  }
7007  // Fetch move placeholder, since it might point to a new page in the current workspace
7008  if ($movePlaceholder = BackendUtility::getMovePlaceholder($table, $row['uid'], 'uid,pid,' . $sortRow)) {
7009  $row = $movePlaceholder;
7010  }
7011  // If the record should be inserted after itself, keep the current sorting information:
7012  if ($row['uid'] == $uid) {
7013  $sortNumber = $row[$sortRow];
7014  } else {
7015  $subres = $this->databaseConnection->exec_SELECTquery($sortRow . ',pid,uid', $table, 'pid=' . (int)$row['pid'] . ' AND ' . $sortRow . '>=' . (int)$row[$sortRow] . $this->deleteClause($table), '', $sortRow . ' ASC', '2');
7016  // Fetches the next record in order to calculate the in-between sortNumber
7017  // There was a record afterwards
7018  if ($this->databaseConnection->sql_num_rows($subres) == 2) {
7019  // Forward to the second result...
7020  $this->databaseConnection->sql_fetch_assoc($subres);
7021  // There was a record afterwards
7022  $subrow = $this->databaseConnection->sql_fetch_assoc($subres);
7023  // The sortNumber is found in between these values
7024  $sortNumber = $row[$sortRow] + floor(($subrow[$sortRow] - $row[$sortRow]) / 2);
7025  // The sortNumber happened NOT to be between the two surrounding numbers, so we'll have to resort the list
7026  if ($sortNumber <= $row[$sortRow] || $sortNumber >= $subrow[$sortRow]) {
7027  // By this special param, resorting reserves and returns the sortnumber after the uid
7028  $sortNumber = $this->resorting($table, $row['pid'], $sortRow, $row['uid']);
7029  }
7030  } else {
7031  // If after the last record in the list, we just add the sortInterval to the last sortvalue
7032  $sortNumber = $row[$sortRow] + $this->sortIntervals;
7033  }
7034  $this->databaseConnection->sql_free_result($subres);
7035  }
7036  return ['pid' => $row['pid'], 'sortNumber' => $sortNumber];
7037  } else {
7038  if ($this->enableLogging) {
7039  $propArr = $this->getRecordProperties($table, $uid);
7040  // OK, don't insert $propArr['event_pid'] here...
7041  $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']);
7042  }
7043  // There MUST be a page or else this cannot work
7044  return false;
7045  }
7046  }
7047  }
7048  return null;
7049  }
7050 
7063  public function resorting($table, $pid, $sortRow, $return_SortNumber_After_This_Uid)
7064  {
7065  if ($GLOBALS['TCA'][$table] && $sortRow && $GLOBALS['TCA'][$table]['ctrl']['sortby'] == $sortRow) {
7066  $returnVal = 0;
7067  $intervals = $this->sortIntervals;
7068  $i = $intervals * 2;
7069  $res = $this->databaseConnection->exec_SELECTquery('uid', $table, 'pid=' . (int)$pid . $this->deleteClause($table), '', $sortRow . ' ASC');
7070  while ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
7071  $uid = (int)$row['uid'];
7072  if ($uid) {
7073  $this->databaseConnection->exec_UPDATEquery($table, 'uid=' . (int)$uid, [$sortRow => $i]);
7074  // This is used to return a sortingValue if the list is resorted because of inserting records inside the list and not in the top
7075  if ($uid == $return_SortNumber_After_This_Uid) {
7076  $i = $i + $intervals;
7077  $returnVal = $i;
7078  }
7079  } else {
7080  die('Fatal ERROR!! No Uid at resorting.');
7081  }
7082  $i = $i + $intervals;
7083  }
7084  $this->databaseConnection->sql_free_result($res);
7085  return $returnVal;
7086  }
7087  return null;
7088  }
7089 
7100  protected function getPreviousLocalizedRecordUid($table, $uid, $pid, $language)
7101  {
7102  $previousLocalizedRecordUid = $uid;
7103  if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
7104  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
7105  $select = $sortRow . ',pid,uid';
7106  // For content elements, we also need the colPos
7107  if ($table === 'tt_content') {
7108  $select .= ',colPos';
7109  }
7110  // Get the sort value of the default language record
7111  $row = BackendUtility::getRecord($table, $uid, $select);
7112  if (is_array($row)) {
7113  // Find the previous record in default language on the same page
7114  $where = 'pid=' . (int)$pid . ' AND ' . 'sys_language_uid=0' . ' AND ' . $sortRow . '<' . (int)$row[$sortRow];
7115  // Respect the colPos for content elements
7116  if ($table === 'tt_content') {
7117  $where .= ' AND colPos=' . (int)$row['colPos'];
7118  }
7119  $res = $this->databaseConnection->exec_SELECTquery($select, $table, $where . $this->deleteClause($table), '', $sortRow . ' DESC', '1');
7120  // If there is an element, find its localized record in specified localization language
7121  if ($previousRow = $this->databaseConnection->sql_fetch_assoc($res)) {
7122  $previousLocalizedRecord = BackendUtility::getRecordLocalization($table, $previousRow['uid'], $language);
7123  if (is_array($previousLocalizedRecord[0])) {
7124  $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
7125  }
7126  }
7127  $this->databaseConnection->sql_free_result($res);
7128  }
7129  }
7130  return $previousLocalizedRecordUid;
7131  }
7132 
7141  public function setTSconfigPermissions($fieldArray, $TSConfig_p)
7142  {
7143  if ((string)$TSConfig_p['userid'] !== '') {
7144  $fieldArray['perms_userid'] = (int)$TSConfig_p['userid'];
7145  }
7146  if ((string)$TSConfig_p['groupid'] !== '') {
7147  $fieldArray['perms_groupid'] = (int)$TSConfig_p['groupid'];
7148  }
7149  if ((string)$TSConfig_p['user'] !== '') {
7150  $fieldArray['perms_user'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['user']) ? $TSConfig_p['user'] : $this->assemblePermissions($TSConfig_p['user']);
7151  }
7152  if ((string)$TSConfig_p['group'] !== '') {
7153  $fieldArray['perms_group'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['group']) ? $TSConfig_p['group'] : $this->assemblePermissions($TSConfig_p['group']);
7154  }
7155  if ((string)$TSConfig_p['everybody'] !== '') {
7156  $fieldArray['perms_everybody'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['everybody']) ? $TSConfig_p['everybody'] : $this->assemblePermissions($TSConfig_p['everybody']);
7157  }
7158  return $fieldArray;
7159  }
7160 
7168  public function newFieldArray($table)
7169  {
7170  $fieldArray = [];
7171  if (is_array($GLOBALS['TCA'][$table]['columns'])) {
7172  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $content) {
7173  if (isset($this->defaultValues[$table][$field])) {
7174  $fieldArray[$field] = $this->defaultValues[$table][$field];
7175  } elseif (isset($content['config']['default'])) {
7176  $fieldArray[$field] = $content['config']['default'];
7177  }
7178  }
7179  }
7180  // Set default permissions for a page.
7181  if ($table === 'pages') {
7182  $fieldArray['perms_userid'] = $this->userid;
7183  $fieldArray['perms_groupid'] = (int)$this->BE_USER->firstMainGroup;
7184  $fieldArray['perms_user'] = $this->assemblePermissions($this->defaultPermissions['user']);
7185  $fieldArray['perms_group'] = $this->assemblePermissions($this->defaultPermissions['group']);
7186  $fieldArray['perms_everybody'] = $this->assemblePermissions($this->defaultPermissions['everybody']);
7187  }
7188  return $fieldArray;
7189  }
7190 
7198  public function addDefaultPermittedLanguageIfNotSet($table, &$incomingFieldArray)
7199  {
7200  // Checking languages:
7201  if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
7202  if (!isset($incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
7203  // Language field must be found in input row - otherwise it does not make sense.
7204  $rows = array_merge([['uid' => 0]], $this->databaseConnection->exec_SELECTgetRows('uid', 'sys_language', 'pid=0' . BackendUtility::deleteClause('sys_language')), [['uid' => -1]]);
7205  foreach ($rows as $r) {
7206  if ($this->BE_USER->checkLanguageAccess($r['uid'])) {
7207  $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $r['uid'];
7208  break;
7209  }
7210  }
7211  }
7212  }
7213  }
7214 
7222  public function overrideFieldArray($table, $data)
7223  {
7224  if (is_array($this->overrideValues[$table])) {
7225  $data = array_merge($data, $this->overrideValues[$table]);
7226  }
7227  return $data;
7228  }
7229 
7239  public function compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
7240  {
7241  // Fetch the original record:
7242  $res = $this->databaseConnection->exec_SELECTquery('*', $table, 'uid=' . (int)$id);
7243  $currentRecord = $this->databaseConnection->sql_fetch_assoc($res);
7244  // If the current record exists (which it should...), begin comparison:
7245  if (is_array($currentRecord)) {
7246  // Read all field types:
7247  $c = 0;
7248  $cRecTypes = [];
7249  foreach ($currentRecord as $col => $val) {
7250  $cRecTypes[$col] = $this->databaseConnection->sql_field_type($res, $c);
7251  $c++;
7252  }
7253  // Free result:
7254  $this->databaseConnection->sql_free_result($res);
7255  // Unset the fields which are similar:
7256  foreach ($fieldArray as $col => $val) {
7257  $fieldConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
7258  $isNullField = (!empty($fieldConfiguration['eval']) && GeneralUtility::inList($fieldConfiguration['eval'], 'null'));
7259 
7260  // Unset fields if stored and submitted values are equal - except the current field holds MM relations.
7261  // In general this avoids to store superfluous data which also will be visualized in the editing history.
7262  if (!$fieldConfiguration['MM'] && $this->isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $cRecTypes[$col], $isNullField)) {
7263  unset($fieldArray[$col]);
7264  } else {
7265  if (!isset($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col])) {
7266  $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
7267  } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
7268  $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
7269  }
7270  if (!isset($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col])) {
7271  $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
7272  } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
7273  $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
7274  }
7275  }
7276  }
7277  } else {
7278  // If the current record does not exist this is an error anyways and we just return an empty array here.
7279  $fieldArray = [];
7280  }
7281  return $fieldArray;
7282  }
7283 
7296  protected function isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull = false)
7297  {
7298  // No NULL values are allowed, this is the regular behaviour.
7299  // Thus, check whether strings are the same or whether integer values are empty ("0" or "").
7300  if (!$allowNull) {
7301  $result = (string)$submittedValue === (string)$storedValue || $storedType === 'int' && (int)$storedValue === (int)$submittedValue;
7302  // Null values are allowed, but currently there's a real (not NULL) value.
7303  // Thus, ensure no NULL value was submitted and fallback to the regular behaviour.
7304  } elseif ($storedValue !== null) {
7305  $result = (
7306  $submittedValue !== null
7307  && $this->isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, false)
7308  );
7309  // Null values are allowed, and currently there's a NULL value.
7310  // Thus, check whether a NULL value was submitted.
7311  } else {
7312  $result = ($submittedValue === null);
7313  }
7314 
7315  return $result;
7316  }
7317 
7325  public function assemblePermissions($string)
7326  {
7327  $keyArr = GeneralUtility::trimExplode(',', $string, true);
7328  $value = 0;
7329  foreach ($keyArr as $key) {
7330  if ($key && isset($this->pMap[$key])) {
7331  $value |= $this->pMap[$key];
7332  }
7333  }
7334  return $value;
7335  }
7336 
7343  public function rmComma($input)
7344  {
7345  return rtrim($input, ',');
7346  }
7347 
7354  public function convNumEntityToByteValue($input)
7355  {
7356  $token = md5(microtime());
7357  $parts = explode($token, preg_replace('/(&#([0-9]+);)/', $token . '\\2' . $token, $input));
7358  foreach ($parts as $k => $v) {
7359  if ($k % 2) {
7360  $v = (int)$v;
7361  // Just to make sure that control bytes are not converted.
7362  if ($v > 32) {
7363  $parts[$k] = chr((int)$v);
7364  }
7365  }
7366  }
7367  return implode('', $parts);
7368  }
7369 
7376  public function destPathFromUploadFolder($folder)
7377  {
7378  return PATH_site . $folder;
7379  }
7380 
7388  public function disableDeleteClause()
7389  {
7390  $this->disableDeleteClause = true;
7391  }
7392 
7399  public function deleteClause($table)
7400  {
7401  // Returns the proper delete-clause if any for a table from TCA
7402  if (!$this->disableDeleteClause && $GLOBALS['TCA'][$table]['ctrl']['delete']) {
7403  return ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0';
7404  } else {
7405  return '';
7406  }
7407  }
7408 
7417  protected function getOriginalParentOfRecord($table, $uid)
7418  {
7419  if (isset(self::$recordPidsForDeletedRecords[$table][$uid])) {
7420  return self::$recordPidsForDeletedRecords[$table][$uid];
7421  }
7422  list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
7423  return [$parentUid];
7424  }
7425 
7432  public function getTCEMAIN_TSconfig($tscPID)
7433  {
7434  if (!isset($this->cachedTSconfig[$tscPID])) {
7435  $this->cachedTSconfig[$tscPID] = $this->BE_USER->getTSConfig('TCEMAIN', BackendUtility::getPagesTSconfig($tscPID));
7436  }
7437  return $this->cachedTSconfig[$tscPID]['properties'];
7438  }
7439 
7448  public function getTableEntries($table, $TSconfig)
7449  {
7450  $tA = is_array($TSconfig['table.'][$table . '.']) ? $TSconfig['table.'][$table . '.'] : [];
7451  $dA = is_array($TSconfig['default.']) ? $TSconfig['default.'] : [];
7453  return $dA;
7454  }
7455 
7463  public function getPID($table, $uid)
7464  {
7465  $res_tmp = $this->databaseConnection->exec_SELECTquery('pid', $table, 'uid=' . (int)$uid);
7466  if ($row = $this->databaseConnection->sql_fetch_assoc($res_tmp)) {
7467  return $row['pid'];
7468  }
7469  return false;
7470  }
7471 
7478  public function dbAnalysisStoreExec()
7479  {
7480  foreach ($this->dbAnalysisStore as $action) {
7481  $id = BackendUtility::wsMapId($action[4], MathUtility::canBeInterpretedAsInteger($action[2]) ? $action[2] : $this->substNEWwithIDs[$action[2]]);
7482  if ($id) {
7483  $action[0]->writeMM($action[1], $id, $action[3]);
7484  }
7485  }
7486  }
7487 
7493  public function removeRegisteredFiles()
7494  {
7495  foreach ($this->removeFilesStore as $file) {
7496  if (@is_file($file)) {
7497  $file = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
7498  $file->delete();
7499  }
7500  }
7501  }
7502 
7513  public function int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
7514  {
7515  if ($counter) {
7516  if ((int)$this->BE_USER->workspace === 0) {
7517  $workspaceStatement = ' AND t3ver_wsid=0';
7518  } else {
7519  $workspaceStatement = ' AND t3ver_wsid IN (0,' . (int)$this->BE_USER->workspace . ')';
7520  }
7521 
7522  $addW = !$this->admin ? ' AND ' . $this->BE_USER->getPagePermsClause($this->pMap['show']) : '';
7523  $pages = $this->databaseConnection->exec_SELECTgetRows(
7524  'uid',
7525  'pages',
7526  'pid=' . (int)$pid . $this->deleteClause('pages') . $workspaceStatement . $addW,
7527  '',
7528  'sorting DESC',
7529  '',
7530  'uid'
7531  );
7532 
7533  // Resolve placeholders of workspace versions
7534  if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
7535  $pages = array_reverse(
7536  $this->resolveVersionedRecords(
7537  'pages',
7538  'uid',
7539  'sorting',
7540  array_keys($pages)
7541  ),
7542  true
7543  );
7544  }
7545 
7546  foreach ($pages as $page) {
7547  if ($page['uid'] != $rootID) {
7548  $CPtable[$page['uid']] = $pid;
7549  // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
7550  if ($counter - 1) {
7551  $CPtable = $this->int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
7552  }
7553  }
7554  }
7555  }
7556  return $CPtable;
7557  }
7558 
7564  public function compileAdminTables()
7565  {
7566  return array_keys($GLOBALS['TCA']);
7567  }
7568 
7576  public function fixUniqueInPid($table, $uid)
7577  {
7578  if (empty($GLOBALS['TCA'][$table])) {
7579  return;
7580  }
7581 
7582  $curData = $this->recordInfo($table, $uid, '*');
7583  $newData = [];
7584  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7585  if ($conf['config']['type'] === 'input' && (string)$curData[$field] !== '') {
7586  $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7587  if (in_array('uniqueInPid', $evalCodesArray, true)) {
7588  $newV = $this->getUnique($table, $field, $curData[$field], $uid, $curData['pid']);
7589  if ((string)$newV !== (string)$curData[$field]) {
7590  $newData[$field] = $newV;
7591  }
7592  }
7593  }
7594  }
7595  // IF there are changed fields, then update the database
7596  if (!empty($newData)) {
7597  $this->updateDB($table, $uid, $newData);
7598  }
7599  }
7600 
7612  public function fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData = [])
7613  {
7614  if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields']) {
7615  $prevData = $this->recordInfo($table, $prevUid, '*');
7616  $theFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true);
7617  foreach ($theFields as $field) {
7618  if ($GLOBALS['TCA'][$table]['columns'][$field] && ($update || !isset($newData[$field]))) {
7619  $newData[$field] = $prevData[$field];
7620  }
7621  }
7622  if ($update && !empty($newData)) {
7623  $this->updateDB($table, $uid, $newData);
7624  }
7625  }
7626  return $newData;
7627  }
7628 
7635  public function extFileFields($table)
7636  {
7637  $listArr = [];
7638  if (isset($GLOBALS['TCA'][$table]['columns'])) {
7639  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) {
7640  if ($configArr['config']['type'] == 'group' && ($configArr['config']['internal_type'] == 'file' || $configArr['config']['internal_type'] == 'file_reference')) {
7641  $listArr[] = $field;
7642  }
7643  }
7644  }
7645  return $listArr;
7646  }
7647 
7660  protected function castReferenceValue($value, array $configuration)
7661  {
7662  if ((string)$value !== '') {
7663  return $value;
7664  }
7665 
7666  if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) {
7667  return 0;
7668  }
7669 
7670  if (array_key_exists('default', $configuration)) {
7671  return $configuration['default'];
7672  }
7673 
7674  return $value;
7675  }
7676 
7683  public function isReferenceField($conf)
7684  {
7685  return $conf['type'] == 'group' && $conf['internal_type'] == 'db' || $conf['type'] == 'select' && $conf['foreign_table'];
7686  }
7687 
7695  public function getInlineFieldType($conf)
7696  {
7697  if ($conf['type'] !== 'inline' || !$conf['foreign_table']) {
7698  return false;
7699  }
7700  if ($conf['foreign_field']) {
7701  // The reference to the parent is stored in a pointer field in the child record
7702  return 'field';
7703  } elseif ($conf['MM']) {
7704  // Regular MM intermediate table is used to store data
7705  return 'mm';
7706  } else {
7707  // An item list (separated by comma) is stored (like select type is doing)
7708  return 'list';
7709  }
7710  }
7711 
7723  public function getCopyHeader($table, $pid, $field, $value, $count, $prevTitle = '')
7724  {
7725  // Set title value to check for:
7726  if ($count) {
7727  $checkTitle = $value . rtrim(' ' . sprintf($this->prependLabel($table), $count));
7728  } else {
7729  $checkTitle = $value;
7730  }
7731  // Do check:
7732  if ($prevTitle != $checkTitle || $count < 100) {
7733  $rowCount = $this->databaseConnection->exec_SELECTcountRows('uid', $table, 'pid=' . (int)$pid . ' AND ' . $field . '=' . $this->databaseConnection->fullQuoteStr($checkTitle, $table) . $this->deleteClause($table));
7734  if ($rowCount) {
7735  return $this->getCopyHeader($table, $pid, $field, $value, $count + 1, $checkTitle);
7736  }
7737  }
7738  // Default is to just return the current input title if no other was returned before:
7739  return $checkTitle;
7740  }
7741 
7749  public function prependLabel($table)
7750  {
7751  if (is_object($GLOBALS['LANG'])) {
7752  $label = $GLOBALS['LANG']->sL($GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
7753  } else {
7754  list($label) = explode('|', $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
7755  }
7756  return $label;
7757  }
7758 
7766  public function resolvePid($table, $pid)
7767  {
7768  $pid = (int)$pid;
7769  if ($pid < 0) {
7770  $res = $this->databaseConnection->exec_SELECTquery('pid', $table, 'uid=' . abs($pid));
7771  $row = $this->databaseConnection->sql_fetch_assoc($res);
7772  $this->databaseConnection->sql_free_result($res);
7773  // Look, if the record UID happens to be an offline record. If so, find its live version.
7774  // Offline uids will be used when a page is versionized as "branch" so this is when we
7775  // must correct - otherwise a pid of "-1" and a wrong sort-row number
7776  // is returned which we don't want.
7777  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, abs($pid), 'pid')) {
7778  $row = $lookForLiveVersion;
7779  }
7780  $pid = (int)$row['pid'];
7781  }
7782  return $pid;
7783  }
7784 
7792  public function clearPrefixFromValue($table, $value)
7793  {
7794  $regex = '/' . sprintf(quotemeta($this->prependLabel($table)), '[0-9]*') . '$/';
7795  return @preg_replace($regex, '', $value);
7796  }
7797 
7807  public function extFileFunctions($table, $field, $filelist, $func)
7808  {
7809  $uploadFolder = $GLOBALS['TCA'][$table]['columns'][$field]['config']['uploadfolder'];
7810  if ($uploadFolder && trim($filelist) && $GLOBALS['TCA'][$table]['columns'][$field]['config']['internal_type'] == 'file') {
7811  $uploadPath = $this->destPathFromUploadFolder($uploadFolder);
7812  $fileArray = explode(',', $filelist);
7813  foreach ($fileArray as $theFile) {
7814  $theFile = trim($theFile);
7815  if ($theFile) {
7816  switch ($func) {
7817  case 'deleteAll':
7818  $theFileFullPath = $uploadPath . '/' . $theFile;
7819  if (@is_file($theFileFullPath)) {
7820  $file = $this->getResourceFactory()->retrieveFileOrFolderObject($theFileFullPath);
7821  $file->delete();
7822  } elseif ($this->enableLogging) {
7823  $this->log($table, 0, 3, 0, 100, 'Delete: Referenced file that was supposed to be deleted together with it\'s record didn\'t exist');
7824  }
7825  break;
7826  }
7827  }
7828  }
7829  }
7830  }
7831 
7838  public function noRecordsFromUnallowedTables($inList)
7839  {
7840  $inList = trim($this->rmComma(trim($inList)));
7841  if ($inList && !$this->admin) {
7842  foreach ($GLOBALS['TCA'] as $table => $_) {
7843  $count = $this->databaseConnection->exec_SELECTcountRows('uid', $table, 'pid IN (' . $inList . ')' . BackendUtility::deleteClause($table));
7844  if ($count && ($this->tableReadOnly($table) || !$this->checkModifyAccessList($table))) {
7845  return false;
7846  }
7847  }
7848  }
7849  return true;
7850  }
7851 
7859  public function isRecordCopied($table, $uid)
7860  {
7861  // If the record was copied:
7862  if (isset($this->copyMappingArray[$table][$uid])) {
7863  return true;
7864  } elseif (isset($this->copyMappingArray[$table]) && in_array($uid, array_values($this->copyMappingArray[$table]))) {
7865  return true;
7866  }
7867  return false;
7868  }
7869 
7870  /******************************
7871  *
7872  * Clearing cache
7873  *
7874  ******************************/
7875 
7887  public function registerRecordIdForPageCacheClearing($table, $uid, $pid = null)
7888  {
7889  if (!is_array(static::$recordsToClearCacheFor[$table])) {
7890  static::$recordsToClearCacheFor[$table] = [];
7891  }
7892  static::$recordsToClearCacheFor[$table][] = (int)$uid;
7893  if ($pid !== null) {
7894  if (!is_array(static::$recordPidsForDeletedRecords[$table])) {
7895  static::$recordPidsForDeletedRecords[$table] = [];
7896  }
7897  static::$recordPidsForDeletedRecords[$table][$uid][] = (int)$pid;
7898  }
7899  }
7900 
7905  protected function processClearCacheQueue()
7906  {
7907  $tagsToClear = [];
7908  $clearCacheCommands = [];
7909 
7910  foreach (static::$recordsToClearCacheFor as $table => $uids) {
7911  foreach (array_unique($uids) as $uid) {
7912  if (!isset($GLOBALS['TCA'][$table]) || $uid <= 0) {
7913  return;
7914  }
7915  // For move commands we may get more then 1 parent.
7916  $pageUids = $this->getOriginalParentOfRecord($table, $uid);
7917  foreach ($pageUids as $originalParent) {
7918  list($tagsToClearFromPrepare, $clearCacheCommandsFromPrepare)
7919  = $this->prepareCacheFlush($table, $uid, $originalParent);
7920  $tagsToClear = array_merge($tagsToClear, $tagsToClearFromPrepare);
7921  $clearCacheCommands = array_merge($clearCacheCommands, $clearCacheCommandsFromPrepare);
7922  }
7923  }
7924  }
7925 
7927  $cacheManager = $this->getCacheManager();
7928  foreach ($tagsToClear as $tag => $_) {
7929  $cacheManager->flushCachesInGroupByTag('pages', $tag);
7930  }
7931 
7932  // Filter duplicate cache commands from cacheQueue
7933  $clearCacheCommands = array_unique($clearCacheCommands);
7934  // Execute collected clear cache commands from page TSConfig
7935  foreach ($clearCacheCommands as $command) {
7936  $this->clear_cacheCmd($command);
7937  }
7938 
7939  // Reset the cache clearing array
7940  static::$recordsToClearCacheFor = [];
7941 
7942  // Reset the original pid array
7943  static::$recordPidsForDeletedRecords = [];
7944  }
7945 
7955  protected function prepareCacheFlush($table, $uid, $pid)
7956  {
7957  $tagsToClear = [];
7958  $clearCacheCommands = [];
7959  $pageUid = 0;
7960  // Get Page TSconfig relevant:
7961  $TSConfig = $this->getTCEMAIN_TSconfig($pid);
7962  if (empty($TSConfig['clearCache_disable'])) {
7963  // If table is "pages":
7964  $pageIdsThatNeedCacheFlush = [];
7965  if ($table === 'pages' || $table === 'pages_language_overlay') {
7966  if ($table === 'pages_language_overlay') {
7967  $pageUid = $this->getPID($table, $uid);
7968  } else {
7969  $pageUid = $uid;
7970  }
7971  // Builds list of pages on the SAME level as this page (siblings)
7972  $res_tmp = $this->databaseConnection->exec_SELECTquery('A.pid AS pid, B.uid AS uid', 'pages A, pages B', 'A.uid=' . (int)$pageUid . ' AND B.pid=A.pid AND B.deleted=0 AND A.pid >= 0');
7973  $pid_tmp = 0;
7974  while ($row_tmp = $this->databaseConnection->sql_fetch_assoc($res_tmp)) {
7975  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
7976  $pid_tmp = (int)$row_tmp['pid'];
7977  // Add children as well:
7978  if ($TSConfig['clearCache_pageSiblingChildren']) {
7979  $res_tmp2 = $this->databaseConnection->exec_SELECTquery('uid', 'pages', 'pid=' . (int)$row_tmp['uid'] . ' AND deleted=0');
7980  while ($row_tmp2 = $this->databaseConnection->sql_fetch_assoc($res_tmp2)) {
7981  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
7982  }
7983  $this->databaseConnection->sql_free_result($res_tmp2);
7984  }
7985  }
7986  $this->databaseConnection->sql_free_result($res_tmp);
7987  // Finally, add the parent page as well:
7988  if ($pid_tmp > 0) {
7989  $pageIdsThatNeedCacheFlush[] = $pid_tmp;
7990  }
7991  // Add grand-parent as well:
7992  if ($TSConfig['clearCache_pageGrandParent']) {
7993  $res_tmp = $this->databaseConnection->exec_SELECTquery('pid', 'pages', 'uid=' . (int)$pid_tmp);
7994  if ($row_tmp = $this->databaseConnection->sql_fetch_assoc($res_tmp)) {
7995  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
7996  }
7997  $this->databaseConnection->sql_free_result($res_tmp);
7998  }
7999  } else {
8000  // For other tables than "pages", delete cache for the records "parent page".
8001  $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
8002  }
8003  // Call pre-processing function for clearing of cache for page ids:
8004  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'])) {
8005  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] as $funcName) {
8006  $_params = ['pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()'];
8007  // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8008  GeneralUtility::callUserFunction($funcName, $_params, $this);
8009  }
8010  }
8011  // Delete cache for selected pages:
8012  foreach ($pageIdsThatNeedCacheFlush as $pageId) {
8013  // Workspaces always use "-1" as the page id which do not
8014  // point to real pages and caches at all. Flushing caches for
8015  // those records does not make sense and decreases performance
8016  if ($pageId >= 0) {
8017  $tagsToClear['pageId_' . $pageId] = true;
8018  }
8019  }
8020  // Queue delete cache for current table and record
8021  $tagsToClear[$table] = true;
8022  $tagsToClear[$table . '_' . $uid] = true;
8023  }
8024  // Clear cache for pages entered in TSconfig:
8025  if (!empty($TSConfig['clearCacheCmd'])) {
8026  $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
8027  $clearCacheCommands = array_unique($commands);
8028  }
8029  // Call post processing function for clear-cache:
8030  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'])) {
8031  $_params = ['table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig];
8032  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] as $_funcRef) {
8033  GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8034  }
8035  }
8036  return [
8037  $tagsToClear,
8038  $clearCacheCommands
8039  ];
8040  }
8041 
8081  public function clear_cacheCmd($cacheCmd)
8082  {
8083  if (is_object($this->BE_USER)) {
8084  $this->BE_USER->writelog(3, 1, 0, 0, 'User %s has cleared the cache (cacheCmd=%s)', [$this->BE_USER->user['username'], $cacheCmd]);
8085  }
8086  // Clear cache for either ALL pages or ALL tables!
8087  switch (strtolower($cacheCmd)) {
8088  case 'pages':
8089  if ($this->admin || $this->BE_USER->getTSConfigVal('options.clearCache.pages')) {
8090  $this->getCacheManager()->flushCachesInGroup('pages');
8091  }
8092  break;
8093  case 'all':
8094  if ($this->admin || $this->BE_USER->getTSConfigVal('options.clearCache.all')) {
8095  // Clear cache group "all" of caching framework caches
8096  $this->getCacheManager()->flushCachesInGroup('all');
8097  $this->databaseConnection->exec_TRUNCATEquery('cache_treelist');
8098  }
8099 
8100  break;
8101  case 'temp_cached':
8102  case 'system':
8103  if ($this->admin || $this->BE_USER->getTSConfigVal('options.clearCache.system')
8104  || ((bool)$GLOBALS['TYPO3_CONF_VARS']['SYS']['clearCacheSystem'] === true && $this->admin)) {
8105  $this->getCacheManager()->flushCachesInGroup('system');
8106  }
8107  break;
8108  }
8109 
8110  $tagsToFlush = [];
8111  // Clear cache for a page ID!
8112  if (MathUtility::canBeInterpretedAsInteger($cacheCmd)) {
8113  $list_cache = [$cacheCmd];
8114  // Call pre-processing function for clearing of cache for page ids:
8115  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'])) {
8116  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] as $funcName) {
8117  $_params = ['pageIdArray' => &$list_cache, 'cacheCmd' => $cacheCmd, 'functionID' => 'clear_cacheCmd()'];
8118  // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8119  GeneralUtility::callUserFunction($funcName, $_params, $this);
8120  }
8121  }
8122  // Delete cache for selected pages:
8123  if (is_array($list_cache)) {
8124  foreach ($list_cache as $pageId) {
8125  $tagsToFlush[] = 'pageId_' . (int)$pageId;
8126  }
8127  }
8128  }
8129  // flush cache by tag
8130  if (GeneralUtility::isFirstPartOfStr(strtolower($cacheCmd), 'cachetag:')) {
8131  $cacheTag = substr($cacheCmd, 9);
8132  $tagsToFlush[] = $cacheTag;
8133  }
8134  // process caching framwork operations
8135  if (!empty($tagsToFlush)) {
8136  foreach (array_unique($tagsToFlush) as $tag) {
8137  $this->getCacheManager()->flushCachesInGroupByTag('pages', $tag);
8138  }
8139  }
8140 
8141  // Call post processing function for clear-cache:
8142  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'])) {
8143  $_params = ['cacheCmd' => strtolower($cacheCmd)];
8144  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] as $_funcRef) {
8145  GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8146  }
8147  }
8148  }
8149 
8150  /*****************************
8151  *
8152  * Logging
8153  *
8154  *****************************/
8170  public function log($table, $recuid, $action, $recpid, $error, $details, $details_nr = -1, $data = [], $event_pid = -1, $NEWid = '')
8171  {
8172  if (!$this->enableLogging) {
8173  return 0;
8174  }
8175  // Type value for tce_db.php
8176  $type = 1;
8177  if (!$this->storeLogMessages) {
8178  $details = '';
8179  }
8180  if ($error > 0) {
8181  $detailMessage = $details;
8182  if (is_array($data)) {
8183  $detailMessage = vsprintf($details, $data);
8184  }
8185  $this->errorLog[] = '[' . $type . '.' . $action . '.' . $details_nr . ']: ' . $detailMessage;
8186  }
8187  return $this->BE_USER->writelog($type, $action, $error, $details_nr, $details, $data, $table, $recuid, $recpid, $event_pid, $NEWid);
8188  }
8189 
8198  public function newlog($message, $error = 0)
8199  {
8200  return $this->log('', 0, 0, 0, $error, '[newlog()] ' . $message, -1);
8201  }
8202 
8214  public function newlog2($message, $table, $uid, $pid = null, $error = 0)
8215  {
8216  if ($pid === false) {
8217  GeneralUtility::deprecationLog('Setting the $pid parameter of DataHandler::newlog2 to FALSE is deprecated since TYPO3 CMS 7. Either provide an integer or NULL. FALSE will not be supported any more in TYPO3 CMS 8');
8218  $pid = null;
8219  }
8220  if (is_null($pid)) {
8221  $propArr = $this->getRecordProperties($table, $uid);
8222  $pid = $propArr['pid'];
8223  }
8224  return $this->log($table, $uid, 0, 0, $error, $message, -1, [], $this->eventPid($table, $uid, $pid));
8225  }
8226 
8233  public function printLogErrorMessages($redirect)
8234  {
8235  $res_log = $this->databaseConnection->exec_SELECTquery('*', 'sys_log', 'type=1 AND action<256 AND userid=' . (int)$this->BE_USER->user['uid'] . ' AND tstamp=' . (int)$GLOBALS['EXEC_TIME'] . ' AND error<>0');
8236  while ($row = $this->databaseConnection->sql_fetch_assoc($res_log)) {
8237  $log_data = unserialize($row['log_data']);
8238  $msg = $row['error'] . ': ' . sprintf($row['details'], $log_data[0], $log_data[1], $log_data[2], $log_data[3], $log_data[4]);
8240  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', FlashMessage::ERROR, true);
8242  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
8243  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
8244  $defaultFlashMessageQueue->enqueue($flashMessage);
8245  }
8246  $this->databaseConnection->sql_free_result($res_log);
8247  }
8248 
8249  /*****************************
8250  *
8251  * Internal (do not use outside Core!)
8252  *
8253  *****************************/
8254 
8264  public function insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
8265  {
8266  $result = $fieldArray;
8267  foreach ($fieldArray as $field => $value) {
8268  switch ($GLOBALS['TCA'][$table]['columns'][$field]['config']['type']) {
8269  case 'inline':
8270  if ($GLOBALS['TCA'][$table]['columns'][$field]['config']['foreign_field']) {
8272  $result[$field] = count(GeneralUtility::trimExplode(',', $value, true));
8273  }
8274  }
8275  break;
8276  }
8277  }
8278  return $result;
8279  }
8280 
8289  public function hasDeletedRecord($tableName, $uid)
8290  {
8291  return
8292  !empty($this->deletedRecords[$tableName])
8293  && in_array($uid, $this->deletedRecords[$tableName])
8294  ;
8295  }
8296 
8304  public function getAutoVersionId($table, $id)
8305  {
8306  $result = null;
8307  if (isset($this->autoVersionIdMap[$table][$id])) {
8308  $result = $this->autoVersionIdMap[$table][$id];
8309  }
8310  return $result;
8311  }
8312 
8320  protected function overlayAutoVersionId($table, $id)
8321  {
8322  $autoVersionId = $this->getAutoVersionId($table, $id);
8323  if (is_null($autoVersionId) === false) {
8324  $id = $autoVersionId;
8325  }
8326  return $id;
8327  }
8328 
8335  protected function addNewValuesToRemapStackChildIds(array $idValues)
8336  {
8337  foreach ($idValues as $idValue) {
8338  if (strpos($idValue, 'NEW') === 0) {
8339  $this->remapStackChildIds[$idValue] = true;
8340  }
8341  }
8342  }
8343 
8354  protected function resolveVersionedRecords($tableName, $fieldNames, $sortingField, array $liveIds)
8355  {
8357  $resolver = GeneralUtility::makeInstance(
8358  PlainDataResolver::class,
8359  $tableName,
8360  $liveIds,
8361  $sortingField
8362  );
8363 
8364  $resolver->setWorkspaceId($this->BE_USER->workspace);
8365  $resolver->setKeepDeletePlaceholder(false);
8366  $resolver->setKeepMovePlaceholder(false);
8367  $resolver->setKeepLiveIds(true);
8368  $recordIds = $resolver->get();
8369 
8370  $records = [];
8371  foreach ($recordIds as $recordId) {
8372  $records[$recordId] = BackendUtility::getRecord($tableName, $recordId, $fieldNames);
8373  }
8374 
8375  return $records;
8376  }
8377 
8385  protected function getOuterMostInstance()
8386  {
8387  if (!isset($this->outerMostInstance)) {
8388  $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS));
8389  foreach ($stack as $stackItem) {
8390  if (isset($stackItem['object']) && $stackItem['object'] instanceof self) {
8391  $this->outerMostInstance = $stackItem['object'];
8392  break;
8393  }
8394  }
8395  }
8396  return $this->outerMostInstance;
8397  }
8398 
8406  public function isOuterMostInstance()
8407  {
8408  return $this->getOuterMostInstance() === $this;
8409  }
8410 
8416  protected function getRuntimeCache()
8417  {
8418  return $this->getCacheManager()->getCache('cache_runtime');
8419  }
8420 
8429  protected function isNestedElementCallRegistered($table, $id, $identifier)
8430  {
8431  $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8432  return isset($nestedElementCalls[$identifier][$table][$id]);
8433  }
8434 
8444  protected function registerNestedElementCall($table, $id, $identifier)
8445  {
8446  $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8447  $nestedElementCalls[$identifier][$table][$id] = true;
8448  $this->runtimeCache->set($this->cachePrefixNestedElementCalls, $nestedElementCalls);
8449  }
8450 
8456  protected function resetNestedElementCalls()
8457  {
8458  $this->runtimeCache->remove($this->cachePrefixNestedElementCalls);
8459  }
8460 
8472  protected function isElementToBeDeleted($table, $id)
8473  {
8474  $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8475  return isset($elementsToBeDeleted[$table][$id]);
8476  }
8477 
8484  protected function registerElementsToBeDeleted()
8485  {
8486  $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8487  $this->runtimeCache->set('core-datahandler-elementsToBeDeleted', array_merge($elementsToBeDeleted, $this->getCommandMapElements('delete')));
8488  }
8489 
8496  protected function resetElementsToBeDeleted()
8497  {
8498  $this->runtimeCache->remove('core-datahandler-elementsToBeDeleted');
8499  }
8500 
8508  protected function unsetElementsToBeDeleted(array $elements)
8509  {
8510  $elements = ArrayUtility::arrayDiffAssocRecursive($elements, $this->getCommandMapElements('delete'));
8511  foreach ($elements as $key => $value) {
8512  if (empty($value)) {
8513  unset($elements[$key]);
8514  }
8515  }
8516  return $elements;
8517  }
8518 
8525  protected function getCommandMapElements($needle)
8526  {
8527  $elements = [];
8528  foreach ($this->cmdmap as $tableName => $idArray) {
8529  foreach ($idArray as $id => $commandArray) {
8530  foreach ($commandArray as $command => $value) {
8531  if ($value && $command == $needle) {
8532  $elements[$tableName][$id] = true;
8533  }
8534  }
8535  }
8536  }
8537  return $elements;
8538  }
8539 
8546  protected function controlActiveElements()
8547  {
8548  if (!empty($this->control['active'])) {
8549  $this->setNullValues(
8550  $this->control['active'],
8551  $this->datamap
8552  );
8553  }
8554  }
8555 
8565  protected function setNullValues(array $active, array &$haystack)
8566  {
8567  foreach ($active as $key => $value) {
8568  // Nested data is processes recursively
8569  if (is_array($value)) {
8570  $this->setNullValues(
8571  $value,
8572  $haystack[$key]
8573  );
8574  // Field has not been activated in the user interface,
8575  // thus a NULL value shall be stored in the database
8576  } elseif ($value == 0) {
8577  $haystack[$key] = null;
8578  }
8579  }
8580  }
8581 
8588  protected function getFieldEvalCacheIdentifier($additionalIdentifier)
8589  {
8590  return 'core-datahandler-eval-' . md5($additionalIdentifier);
8591  }
8592 
8598  protected function createRelationHandlerInstance()
8599  {
8600  return GeneralUtility::makeInstance(RelationHandler::class);
8601  }
8602 
8608  protected function getCacheManager()
8609  {
8610  return GeneralUtility::makeInstance(CacheManager::class);
8611  }
8612 
8618  protected function getResourceFactory()
8619  {
8621  }
8622 }
version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
copySpecificPage($uid, $destPid, $copyTablesArray, $first=false)
updateFlexFormData($flexFormId, array $modifications)
getPlaceholderTitleForTableLabel($table, $placeholderContent=null)
static stripSlashesOnArray(array &$theArray)
static getTSconfig_pidValue($table, $uid, $pid)
static getPagesTSconfig($id, $rootLine=null, $returnPartArray=false)
int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
transformRichtextContentToDatabase($value, $table, $field, $defaultExtras, $thisConfig, $pid)
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)
checkValue_flex($res, $value, $tcaFieldConf, $PP, $uploadedFiles, $field)
copyL10nOverlayRecords($table, $uid, $destPid, $first=false, $overrideValues=[], $excludeFields='')
insertDB($table, $id, $fieldArray, $newVersion=false, $suggestedUid=0, $dontSetNewIdIndex=false)
doesBranchExist($inList, $pid, $perms, $recurse)
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)
insertNewCopyVersion($table, $fieldArray, $realPid)
static getItemLabel($table, $col, $printAllWrap='')
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)
isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull=false)
static getRecordsByField($theTable, $theField, $theValue, $whereClause='', $groupBy='', $orderBy='', $limit='', $useDeleteClause=true)
setTSconfigPermissions($fieldArray, $TSConfig_p)
moveRecord_procFields($table, $uid, $destPid)
castReferenceValue($value, array $configuration)
checkValue_radio($res, $value, $tcaFieldConf, $PP)
setNullValues(array $active, array &$haystack)
deleteVersionsForRecord($table, $uid, $forceHardDelete)
registerNestedElementCall($table, $id, $identifier)
static getTCAtypes($table, $rec, $useFieldNameAsKey=false)
moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
getLocalTCE($stripslashesValues=false, $dontProcessTransformations=true)
checkValue_check($res, $value, $tcaFieldConf, $PP, $field='')
deleteSpecificPage($uid, $forceHardDelete=false)
getRecordsWithSameValue($tableName, $uid, $fieldName, $value, $pageId=0)
checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData=null)
static getFlexFormDS($conf, $row, $table, $fieldName='', $WSOL=true, $newRecordPidValue=0)
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)
static callUserFunction($funcName, &$params, &$ref, $checkPrefix='', $errorMode=0)
checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructArray, $pParams, $callBackFunc='', array $workspaceOptions=[])
copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $_3, $workspaceOptions)
addRemapAction($table, $id, array $callback, array $arguments)
insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData=[])
updateDB($table, $id, $fieldArray)
static getInlineLocalizationMode($table, $fieldOrConfig)
checkValueForText($value, $tcaFieldConf)
static split_fileref($fileNameWithPath)
static resolveSheetDefInDS($dataStructArray, $sheet='sDEF')
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)
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)
static getRecordRaw($table, $where='', $fields=' *')
triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions=false)
deleteRecord_procFields($table, $uid, $undeleteRecord=false)
doesPageHaveUnallowedTables($page_uid, $doktype)
static RTEsetup($RTEprop, $table, $field, $type='')
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)
$uid
Definition: server.php:38
_ACTION_FLEX_FORMdata(&$valueArray, $actionCMDs)
static getFileAbsFileName($filename, $onlyRelative=true, $relToTYPO3_mainDir=false)
static formatSize($sizeInBytes, $labels='', $base=0)
checkValue_text($res, $value, $tcaFieldConf, $PP, $field='')
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)
static getSpecConfParts($defaultExtrasString, $_='')
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)
static selectVersionsOfRecord($table, $uid, $fields=' *', $workspace=0, $includeDeletedRecords=false, $row=null)
checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field)
static getRecordWSOL($table, $uid, $fields=' *', $where='', $useDeleteClause=true, $unsetMovePointers=false)
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=[])
checkValue_group_select($res, $value, $tcaFieldConf, $PP, $uploadedFiles, $field)
remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
extFileFunctions($table, $field, $filelist, $func)
static deleteClause($table, $tableAlias='')
versionizeRecord($table, $id, $label, $delete=false)
isNestedElementCallRegistered($table, $id, $identifier)
checkValue_input($res, $value, $tcaFieldConf, $PP, $field='')
static upload_copy_move($source, $destination)
static arrayDiffAssocRecursive(array $array1, array $array2)
getFieldEvalCacheIdentifier($additionalIdentifier)