TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
DataHandler.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Core\DataHandling;
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 
17 use Doctrine\DBAL\DBALException;
18 use Doctrine\DBAL\Driver\Statement;
19 use Doctrine\DBAL\Types\IntegerType;
46 
61 {
62  // *********************
63  // Public variables you can configure before using the class:
64  // *********************
71  public $storeLogMessages = true;
72 
78  public $enableLogging = true;
79 
86  public $reverseOrder = false;
87 
94  public $checkSimilar = 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  public $updateModeL10NdiffData = true;
148 
156 
164 
171  public $bypassFileHandling = false;
172 
180 
187  public $copyWhichTables = '*';
188 
196  public $copyTree = 0;
197 
206  public $defaultValues = [];
207 
215  public $overrideValues = [];
216 
224  public $alternativeFileName = [];
225 
231  public $alternativeFilePath = [];
232 
240  public $data_disableFields = [];
241 
251  public $suggestedInsertUids = [];
252 
259  public $callBackObj;
260 
261  // *********************
262  // Internal variables (mapping arrays) which can be used (read-only) from outside
263  // *********************
269  public $autoVersionIdMap = [];
270 
276  public $substNEWwithIDs = [];
277 
284 
290  public $newRelatedIDs = [];
291 
298 
304  protected $deletedRecords = [];
305 
311  public $copiedFileMap = [];
312 
318  public $RTEmagic_copyIndex = [];
319 
325  public $errorLog = [];
326 
332  public $pagetreeRefreshFieldsFromPages = ['pid', 'sorting', 'deleted', 'hidden', 'title', 'doktype', 'is_siteroot', 'fe_group', 'nav_hide', 'nav_title', 'module', 'starttime', 'endtime', 'content_from_pid'];
333 
339  public $pagetreeNeedsRefresh = false;
340 
341  // *********************
342  // Internal Variables, do not touch.
343  // *********************
344 
345  // Variables set in init() function:
346 
352  public $BE_USER;
353 
359  public $userid;
360 
366  public $username;
367 
373  public $admin;
374 
381  'user' => 'show,edit,delete,new,editcontent',
382  'group' => 'show,edit,new,editcontent',
383  'everybody' => ''
384  ];
385 
391  protected $excludedTablesAndFields = [];
392 
399  protected $control = [];
400 
406  public $datamap = [];
407 
413  public $cmdmap = [];
414 
420  protected $mmHistoryRecords = [];
421 
427  protected $historyRecords = [];
428 
429  // Internal static:
435  public $pMap = [
436  'show' => 1,
437  // 1st bit
438  'edit' => 2,
439  // 2nd bit
440  'delete' => 4,
441  // 3rd bit
442  'new' => 8,
443  // 4th bit
444  'editcontent' => 16
445  ];
446 
452  public $sortIntervals = 256;
453 
454  // Internal caching arrays
461 
468 
475 
481  public $isInWebMount_Cache = [];
482 
488  public $cachedTSconfig = [];
489 
495  public $pageCache = [];
496 
502  public $checkWorkspaceCache = [];
503 
504  // Other arrays:
510  public $dbAnalysisStore = [];
511 
517  public $removeFilesStore = [];
518 
524  public $uploadedFileArray = [];
525 
531  public $registerDBList = [];
532 
538  public $registerDBPids = [];
539 
550  public $copyMappingArray = [];
551 
557  public $remapStack = [];
558 
565  public $remapStackRecords = [];
566 
572  protected $remapStackChildIds = [];
573 
579  protected $remapStackActions = [];
580 
586  protected $remapStackRefIndex = [];
587 
593  public $updateRefIndexStack = [];
594 
601  public $callFromImpExp = false;
602 
608  public $newIndexMap = [];
609 
610  // Various
617  public $fileFunc;
618 
625 
631  public $autoVersioningUpdate = false;
632 
638  protected $disableDeleteClause = false;
639 
644 
649 
656  protected $outerMostInstance = null;
657 
663  protected static $recordsToClearCacheFor = [];
664 
671  protected static $recordPidsForDeletedRecords = [];
672 
678  protected $runtimeCache = null;
679 
685  protected $cachePrefixNestedElementCalls = 'core-datahandler-nestedElementCalls-';
686 
690  public function __construct()
691  {
692  $this->runtimeCache = $this->getRuntimeCache();
693  }
694 
698  public function setControl(array $control)
699  {
700  $this->control = $control;
701  }
702 
713  public function start($data, $cmd, $altUserObject = null)
714  {
715  // Initializing BE_USER
716  $this->BE_USER = is_object($altUserObject) ? $altUserObject : $GLOBALS['BE_USER'];
717  $this->userid = $this->BE_USER->user['uid'];
718  $this->username = $this->BE_USER->user['username'];
719  $this->admin = $this->BE_USER->user['admin'];
720  if ($this->BE_USER->uc['recursiveDelete']) {
721  $this->deleteTree = 1;
722  }
723  if ($GLOBALS['TYPO3_CONF_VARS']['BE']['explicitConfirmationOfTranslation'] && $this->updateModeL10NdiffData === true) {
724  $this->updateModeL10NdiffData = false;
725  }
726  // Initializing default permissions for pages
727  $defaultPermissions = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions'];
728  if (isset($defaultPermissions['user'])) {
729  $this->defaultPermissions['user'] = $defaultPermissions['user'];
730  }
731  if (isset($defaultPermissions['group'])) {
732  $this->defaultPermissions['group'] = $defaultPermissions['group'];
733  }
734  if (isset($defaultPermissions['everybody'])) {
735  $this->defaultPermissions['everybody'] = $defaultPermissions['everybody'];
736  }
737  // generates the excludelist, based on TCA/exclude-flag and non_exclude_fields for the user:
738  if (!$this->admin) {
739  $this->excludedTablesAndFields = array_flip($this->getExcludeListArray());
740  }
741  // Setting the data and cmd arrays
742  if (is_array($data)) {
743  reset($data);
744  $this->datamap = $data;
745  }
746  if (is_array($cmd)) {
747  reset($cmd);
748  $this->cmdmap = $cmd;
749  }
750  }
751 
759  public function setMirror($mirror)
760  {
761  if (!is_array($mirror)) {
762  return;
763  }
764 
765  foreach ($mirror as $table => $uid_array) {
766  if (!isset($this->datamap[$table])) {
767  continue;
768  }
769 
770  foreach ($uid_array as $id => $uidList) {
771  if (!isset($this->datamap[$table][$id])) {
772  continue;
773  }
774 
775  $theIdsInArray = GeneralUtility::trimExplode(',', $uidList, true);
776  foreach ($theIdsInArray as $copyToUid) {
777  $this->datamap[$table][$copyToUid] = $this->datamap[$table][$id];
778  }
779  }
780  }
781  }
782 
789  public function setDefaultsFromUserTS($userTS)
790  {
791  if (!is_array($userTS)) {
792  return;
793  }
794 
795  foreach ($userTS as $k => $v) {
796  $k = substr($k, 0, -1);
797  if (!$k || !is_array($v) || !isset($GLOBALS['TCA'][$k])) {
798  continue;
799  }
800 
801  if (is_array($this->defaultValues[$k])) {
802  $this->defaultValues[$k] = array_merge($this->defaultValues[$k], $v);
803  } else {
804  $this->defaultValues[$k] = $v;
805  }
806  }
807  }
808 
816  public function process_uploads($postFiles)
817  {
818  if (!is_array($postFiles)) {
819  return;
820  }
821 
822  // Editing frozen:
823  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
824  if ($this->enableLogging) {
825  $this->newlog('All editing in this workspace has been frozen!', 1);
826  }
827  return;
828  }
829  $subA = reset($postFiles);
830  if (is_array($subA)) {
831  if (is_array($subA['name']) && is_array($subA['type']) && is_array($subA['tmp_name']) && is_array($subA['size'])) {
832  // Initialize the uploadedFilesArray:
833  $this->uploadedFileArray = [];
834  // For each entry:
835  foreach ($subA as $key => $values) {
836  $this->process_uploads_traverseArray($this->uploadedFileArray, $values, $key);
837  }
838  } else {
839  $this->uploadedFileArray = $subA;
840  }
841  }
842  }
843 
854  public function process_uploads_traverseArray(&$outputArr, $inputArr, $keyToSet)
855  {
856  if (is_array($inputArr)) {
857  foreach ($inputArr as $key => $value) {
858  $this->process_uploads_traverseArray($outputArr[$key], $inputArr[$key], $keyToSet);
859  }
860  } else {
861  $outputArr[$keyToSet] = $inputArr;
862  }
863  }
864 
865  /*********************************************
866  *
867  * HOOKS
868  *
869  *********************************************/
884  public function hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray)
885  {
886  // Process hook directly:
887  if (!isset($this->remapStackRecords[$table][$id])) {
888  foreach ($hookObjectsArr as $hookObj) {
889  if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
890  $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);
891  }
892  }
893  } else {
894  $this->remapStackRecords[$table][$id]['processDatamap_afterDatabaseOperations'] = [
895  'status' => $status,
896  'fieldArray' => $fieldArray,
897  'hookObjectsArr' => $hookObjectsArr
898  ];
899  }
900  }
901 
910  {
911  if (!isset($this->checkModifyAccessListHookObjects)) {
912  $this->checkModifyAccessListHookObjects = [];
913  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'])) {
914  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] as $classData) {
915  $hookObject = GeneralUtility::getUserObj($classData);
916  if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
917  throw new \UnexpectedValueException($classData . ' must implement interface ' . DataHandlerCheckModifyAccessListHookInterface::class, 1251892472);
918  }
919  $this->checkModifyAccessListHookObjects[] = $hookObject;
920  }
921  }
922  }
924  }
925 
926  /*********************************************
927  *
928  * PROCESSING DATA
929  *
930  *********************************************/
937  public function process_datamap()
938  {
939  $this->controlActiveElements();
940 
941  // Keep versionized(!) relations here locally:
942  $registerDBList = [];
944  $this->datamap = $this->unsetElementsToBeDeleted($this->datamap);
945  // Editing frozen:
946  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
947  if ($this->enableLogging) {
948  $this->newlog('All editing in this workspace has been frozen!', 1);
949  }
950  return false;
951  }
952  // First prepare user defined objects (if any) for hooks which extend this function:
953  $hookObjectsArr = [];
954  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'])) {
955  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] as $classRef) {
956  $hookObject = GeneralUtility::getUserObj($classRef);
957  if (method_exists($hookObject, 'processDatamap_beforeStart')) {
958  $hookObject->processDatamap_beforeStart($this);
959  }
960  $hookObjectsArr[] = $hookObject;
961  }
962  }
963  // 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.
964  $orderOfTables = [];
965  // Set pages first.
966  if (isset($this->datamap['pages'])) {
967  $orderOfTables[] = 'pages';
968  }
969  $orderOfTables = array_unique(array_merge($orderOfTables, array_keys($this->datamap)));
970  // Process the tables...
971  foreach ($orderOfTables as $table) {
972  // Check if
973  // - table is set in $GLOBALS['TCA'],
974  // - table is NOT readOnly
975  // - the table is set with content in the data-array (if not, there's nothing to process...)
976  // - permissions for tableaccess OK
977  $modifyAccessList = $this->checkModifyAccessList($table);
978  if ($this->enableLogging && !$modifyAccessList) {
979  $this->log($table, 0, 2, 0, 1, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
980  }
981  if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->datamap[$table]) || !$modifyAccessList) {
982  continue;
983  }
984 
985  if ($this->reverseOrder) {
986  $this->datamap[$table] = array_reverse($this->datamap[$table], 1);
987  }
988  // For each record from the table, do:
989  // $id is the record uid, may be a string if new records...
990  // $incomingFieldArray is the array of fields
991  foreach ($this->datamap[$table] as $id => $incomingFieldArray) {
992  if (!is_array($incomingFieldArray)) {
993  continue;
994  }
995  $theRealPid = null;
996 
997  // Hook: processDatamap_preProcessFieldArray
998  foreach ($hookObjectsArr as $hookObj) {
999  if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
1000  $hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
1001  }
1002  }
1003  // ******************************
1004  // Checking access to the record
1005  // ******************************
1006  $createNewVersion = false;
1007  $recordAccess = false;
1008  $old_pid_value = '';
1009  $this->autoVersioningUpdate = false;
1010  // Is it a new record? (Then Id is a string)
1012  // Get a fieldArray with default values
1013  $fieldArray = $this->newFieldArray($table);
1014  // A pid must be set for new records.
1015  if (isset($incomingFieldArray['pid'])) {
1016  // $value = the pid
1017  $pid_value = $incomingFieldArray['pid'];
1018  // Checking and finding numerical pid, it may be a string-reference to another value
1019  $OK = 1;
1020  // If a NEW... id
1021  if (strstr($pid_value, 'NEW')) {
1022  if ($pid_value[0] === '-') {
1023  $negFlag = -1;
1024  $pid_value = substr($pid_value, 1);
1025  } else {
1026  $negFlag = 1;
1027  }
1028  // Trying to find the correct numerical value as it should be mapped by earlier processing of another new record.
1029  if (isset($this->substNEWwithIDs[$pid_value])) {
1030  if ($negFlag === 1) {
1031  $old_pid_value = $this->substNEWwithIDs[$pid_value];
1032  }
1033  $pid_value = (int)($negFlag * $this->substNEWwithIDs[$pid_value]);
1034  } else {
1035  $OK = 0;
1036  }
1037  }
1038  $pid_value = (int)$pid_value;
1039  // The $pid_value is now the numerical pid at this point
1040  if ($OK) {
1041  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
1042  // Points to a page on which to insert the element, possibly in the top of the page
1043  if ($pid_value >= 0) {
1044  // If this table is sorted we better find the top sorting number
1045  if ($sortRow) {
1046  $fieldArray[$sortRow] = $this->getSortNumber($table, 0, $pid_value);
1047  }
1048  // The numerical pid is inserted in the data array
1049  $fieldArray['pid'] = $pid_value;
1050  } else {
1051  // points to another record before ifself
1052  // If this table is sorted we better find the top sorting number
1053  if ($sortRow) {
1054  // Because $pid_value is < 0, getSortNumber returns an array
1055  $tempArray = $this->getSortNumber($table, 0, $pid_value);
1056  $fieldArray['pid'] = $tempArray['pid'];
1057  $fieldArray[$sortRow] = $tempArray['sortNumber'];
1058  } else {
1059  // Here we fetch the PID of the record that we point to...
1060  $tempdata = $this->recordInfo($table, abs($pid_value), 'pid');
1061  $fieldArray['pid'] = $tempdata['pid'];
1062  }
1063  }
1064  }
1065  }
1066  $theRealPid = $fieldArray['pid'];
1067  // Now, check if we may insert records on this pid.
1068  if ($theRealPid >= 0) {
1069  // Checks if records can be inserted on this $pid.
1070  $recordAccess = $this->checkRecordInsertAccess($table, $theRealPid);
1071  if ($recordAccess) {
1072  $this->addDefaultPermittedLanguageIfNotSet($table, $incomingFieldArray);
1073  $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $incomingFieldArray, true);
1074  if (!$recordAccess) {
1075  if ($this->enableLogging) {
1076  $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', 1);
1077  }
1078  } elseif (!$this->bypassWorkspaceRestrictions) {
1079  // Workspace related processing:
1080  // If LIVE records cannot be created in the current PID due to workspace restrictions, prepare creation of placeholder-record
1081  if ($res = $this->BE_USER->workspaceAllowLiveRecordsInPID($theRealPid, $table)) {
1082  if ($res < 0) {
1083  $recordAccess = false;
1084  if ($this->enableLogging) {
1085  $this->newlog('Stage for versioning root point and users access level did not allow for editing', 1);
1086  }
1087  }
1088  } else {
1089  // So, if no live records were allowed, we have to create a new version of this record:
1090  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1091  $createNewVersion = true;
1092  } else {
1093  $recordAccess = false;
1094  if ($this->enableLogging) {
1095  $this->newlog('Record could not be created in this workspace in this branch', 1);
1096  }
1097  }
1098  }
1099  }
1100  }
1101  } else {
1102  debug('Internal ERROR: pid should not be less than zero!');
1103  }
1104  // Yes new record, change $record_status to 'insert'
1105  $status = 'new';
1106  } else {
1107  // Nope... $id is a number
1108  $fieldArray = [];
1109  $recordAccess = $this->checkRecordUpdateAccess($table, $id, $incomingFieldArray, $hookObjectsArr);
1110  if (!$recordAccess) {
1111  if ($this->enableLogging) {
1112  $propArr = $this->getRecordProperties($table, $id);
1113  $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']);
1114  }
1115  continue;
1116  }
1117  // Next check of the record permissions (internals)
1118  $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $id);
1119  if (!$recordAccess) {
1120  if ($this->enableLogging) {
1121  $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', 1);
1122  }
1123  } else {
1124  // Here we fetch the PID of the record that we point to...
1125  $tempdata = $this->recordInfo($table, $id, 'pid' . ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? ',t3ver_wsid,t3ver_stage' : ''));
1126  $theRealPid = $tempdata['pid'];
1127  // Use the new id of the versionized record we're trying to write to:
1128  // (This record is a child record of a parent and has already been versionized.)
1129  if ($this->autoVersionIdMap[$table][$id]) {
1130  // For the reason that creating a new version of this record, automatically
1131  // created related child records (e.g. "IRRE"), update the accordant field:
1132  $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1133  // Use the new id of the copied/versionized record:
1134  $id = $this->autoVersionIdMap[$table][$id];
1135  $recordAccess = true;
1136  $this->autoVersioningUpdate = true;
1137  } elseif (!$this->bypassWorkspaceRestrictions && ($errorCode = $this->BE_USER->workspaceCannotEditRecord($table, $tempdata))) {
1138  $recordAccess = false;
1139  // Versioning is required and it must be offline version!
1140  // Check if there already is a workspace version
1141  $WSversion = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid,t3ver_oid');
1142  if ($WSversion) {
1143  $id = $WSversion['uid'];
1144  $recordAccess = true;
1145  } elseif ($this->BE_USER->workspaceAllowAutoCreation($table, $id, $theRealPid)) {
1146  // new version of a record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1147  $this->pagetreeNeedsRefresh = true;
1148 
1150  $tce = GeneralUtility::makeInstance(__CLASS__);
1151  $tce->enableLogging = $this->enableLogging;
1152  // Setting up command for creating a new version of the record:
1153  $cmd = [];
1154  $cmd[$table][$id]['version'] = [
1155  'action' => 'new',
1156  // Default is to create a version of the individual records... element versioning that is.
1157  'label' => 'Auto-created for WS #' . $this->BE_USER->workspace
1158  ];
1159  $tce->start([], $cmd);
1160  $tce->process_cmdmap();
1161  $this->errorLog = array_merge($this->errorLog, $tce->errorLog);
1162  // If copying was successful, share the new uids (also of related children):
1163  if ($tce->copyMappingArray[$table][$id]) {
1164  foreach ($tce->copyMappingArray as $origTable => $origIdArray) {
1165  foreach ($origIdArray as $origId => $newId) {
1166  $this->uploadedFileArray[$origTable][$newId] = $this->uploadedFileArray[$origTable][$origId];
1167  $this->autoVersionIdMap[$origTable][$origId] = $newId;
1168  }
1169  }
1170  ArrayUtility::mergeRecursiveWithOverrule($this->RTEmagic_copyIndex, $tce->RTEmagic_copyIndex);
1171  // See where RTEmagic_copyIndex is used inside fillInFieldArray() for more information...
1172  // Update registerDBList, that holds the copied relations to child records:
1173  $registerDBList = array_merge($registerDBList, $tce->registerDBList);
1174  // For the reason that creating a new version of this record, automatically
1175  // created related child records (e.g. "IRRE"), update the accordant field:
1176  $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1177  // Use the new id of the copied/versionized record:
1178  $id = $this->autoVersionIdMap[$table][$id];
1179  $recordAccess = true;
1180  $this->autoVersioningUpdate = true;
1181  } elseif ($this->enableLogging) {
1182  $this->newlog('Could not be edited in offline workspace in the branch where found (failure state: \'' . $errorCode . '\'). Auto-creation of version failed!', 1);
1183  }
1184  } elseif ($this->enableLogging) {
1185  $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);
1186  }
1187  }
1188  }
1189  // The default is 'update'
1190  $status = 'update';
1191  }
1192  // If access was granted above, proceed to create or update record:
1193  if (!$recordAccess) {
1194  continue;
1195  }
1196 
1197  // Here the "pid" is set IF NOT the old pid was a string pointing to a place in the subst-id array.
1198  list($tscPID) = BackendUtility::getTSCpid($table, $id, $old_pid_value ? $old_pid_value : $fieldArray['pid']);
1199  if ($status === 'new' && $table === 'pages') {
1200  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
1201  if (isset($TSConfig['permissions.']) && is_array($TSConfig['permissions.'])) {
1202  $fieldArray = $this->setTSconfigPermissions($fieldArray, $TSConfig['permissions.']);
1203  }
1204  }
1205  // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1206  $fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1207  $newVersion_placeholderFieldArray = [];
1208  if ($createNewVersion) {
1209  // create a placeholder array with already processed field content
1210  $newVersion_placeholderFieldArray = $fieldArray;
1211  }
1212  // NOTICE! All manipulation beyond this point bypasses both "excludeFields" AND possible "MM" relations / file uploads to field!
1213  // Forcing some values unto field array:
1214  // NOTICE: This overriding is potentially dangerous; permissions per field is not checked!!!
1215  $fieldArray = $this->overrideFieldArray($table, $fieldArray);
1216  if ($createNewVersion) {
1217  $newVersion_placeholderFieldArray = $this->overrideFieldArray($table, $newVersion_placeholderFieldArray);
1218  }
1219  // Setting system fields
1220  if ($status == 'new') {
1221  if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1222  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1223  if ($createNewVersion) {
1224  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1225  }
1226  }
1227  if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1228  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1229  if ($createNewVersion) {
1230  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1231  }
1232  }
1233  } elseif ($this->checkSimilar) {
1234  // Removing fields which are equal to the current value:
1235  $fieldArray = $this->compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1236  }
1237  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] && !empty($fieldArray)) {
1238  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1239  if ($createNewVersion) {
1240  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1241  }
1242  }
1243  // Set stage to "Editing" to make sure we restart the workflow
1244  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1245  $fieldArray['t3ver_stage'] = 0;
1246  }
1247  // Hook: processDatamap_postProcessFieldArray
1248  foreach ($hookObjectsArr as $hookObj) {
1249  if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1250  $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1251  }
1252  }
1253  // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1254  // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already and files could have been uploaded that are now "lost"
1255  if (is_array($fieldArray)) {
1256  if ($status == 'new') {
1257  if ($table === 'pages') {
1258  // for new pages always a refresh is needed
1259  $this->pagetreeNeedsRefresh = true;
1260  }
1261 
1262  // This creates a new version of the record with online placeholder and offline version
1263  if ($createNewVersion) {
1264  // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1265  $this->pagetreeNeedsRefresh = true;
1266 
1267  $newVersion_placeholderFieldArray['t3ver_label'] = 'INITIAL PLACEHOLDER';
1268  // Setting placeholder state value for temporary record
1269  $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER);
1270  // Setting workspace - only so display of place holders can filter out those from other workspaces.
1271  $newVersion_placeholderFieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1272  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $this->getPlaceholderTitleForTableLabel($table);
1273  // Saving placeholder as 'original'
1274  $this->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1275  // For the actual new offline version, set versioning values to point to placeholder:
1276  $fieldArray['pid'] = -1;
1277  $fieldArray['t3ver_oid'] = $this->substNEWwithIDs[$id];
1278  $fieldArray['t3ver_id'] = 1;
1279  // Setting placeholder state value for version (so it can know it is currently a new version...)
1280  $fieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER_VERSION);
1281  $fieldArray['t3ver_label'] = 'First draft version';
1282  $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1283  // When inserted, $this->substNEWwithIDs[$id] will be changed to the uid of THIS version and so the interface will pick it up just nice!
1284  $phShadowId = $this->insertDB($table, $id, $fieldArray, true, 0, true);
1285  if ($phShadowId) {
1286  // Processes fields of the placeholder record:
1287  $this->triggerRemapAction($table, $id, [$this, 'placeholderShadowing'], [$table, $phShadowId]);
1288  // Hold auto-versionized ids of placeholders:
1289  $this->autoVersionIdMap[$table][$this->substNEWwithIDs[$id]] = $phShadowId;
1290  }
1291  } else {
1292  $this->insertDB($table, $id, $fieldArray, false, $incomingFieldArray['uid']);
1293  }
1294  } else {
1295  if ($table === 'pages') {
1296  // only a certain number of fields needs to be checked for updates
1297  // if $this->checkSimilar is TRUE, fields with unchanged values are already removed here
1298  $fieldsToCheck = array_intersect($this->pagetreeRefreshFieldsFromPages, array_keys($fieldArray));
1299  if (!empty($fieldsToCheck)) {
1300  $this->pagetreeNeedsRefresh = true;
1301  }
1302  }
1303  $this->updateDB($table, $id, $fieldArray);
1304  $this->placeholderShadowing($table, $id);
1305  }
1306  }
1307  // Hook: processDatamap_afterDatabaseOperations
1308  // Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
1309  // but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
1310  $this->hook_processDatamap_afterDatabaseOperations($hookObjectsArr, $status, $table, $id, $fieldArray);
1311  }
1312  }
1313  // Process the stack of relations to remap/correct
1314  $this->processRemapStack();
1315  $this->dbAnalysisStoreExec();
1316  $this->removeRegisteredFiles();
1317  // Hook: processDatamap_afterAllOperations
1318  // Note: When this hook gets called, all operations on the submitted data have been finished.
1319  foreach ($hookObjectsArr as $hookObj) {
1320  if (method_exists($hookObj, 'processDatamap_afterAllOperations')) {
1321  $hookObj->processDatamap_afterAllOperations($this);
1322  }
1323  }
1324  if ($this->isOuterMostInstance()) {
1325  $this->processClearCacheQueue();
1326  $this->resetElementsToBeDeleted();
1327  }
1328  }
1329 
1337  public function placeholderShadowing($table, $id)
1338  {
1339  if ($liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, '*')) {
1340  if (VersionState::cast($liveRec['t3ver_state'])->indicatesPlaceholder()) {
1341  $justStoredRecord = BackendUtility::getRecord($table, $id);
1342  $newRecord = [];
1343  $shadowCols = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'];
1344  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1345  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1346  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['type'];
1347  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label'];
1348  $shadowColumns = array_unique(GeneralUtility::trimExplode(',', $shadowCols, true));
1349  foreach ($shadowColumns as $fieldName) {
1350  if ((string)$justStoredRecord[$fieldName] !== (string)$liveRec[$fieldName] && isset($GLOBALS['TCA'][$table]['columns'][$fieldName]) && $fieldName !== 'uid' && $fieldName !== 'pid') {
1351  $newRecord[$fieldName] = $justStoredRecord[$fieldName];
1352  }
1353  }
1354  if (!empty($newRecord)) {
1355  if ($this->enableLogging) {
1356  $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']);
1357  }
1358  $this->updateDB($table, $liveRec['uid'], $newRecord);
1359  }
1360  }
1361  }
1362  }
1363 
1371  public function getPlaceholderTitleForTableLabel($table, $placeholderContent = null)
1372  {
1373  if ($placeholderContent === null) {
1374  $placeholderContent = 'PLACEHOLDER';
1375  }
1376 
1377  $labelPlaceholder = '[' . $placeholderContent . ', WS#' . $this->BE_USER->workspace . ']';
1378  $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1379  if (!isset($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'])) {
1380  return $labelPlaceholder;
1381  }
1382  $evalCodesArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'], true);
1383  $transformedLabel = $this->checkValue_input_Eval($labelPlaceholder, $evalCodesArray, '');
1384  return isset($transformedLabel['value']) ? $transformedLabel['value'] : $labelPlaceholder;
1385  }
1386 
1400  public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID)
1401  {
1402  // Initialize:
1403  $originalLanguageRecord = null;
1404  $originalLanguage_diffStorage = null;
1405  $diffStorageFlag = false;
1406  // Setting 'currentRecord' and 'checkValueRecord':
1407  if (strstr($id, 'NEW')) {
1408  // Must have the 'current' array - not the values after processing below...
1409  $currentRecord = ($checkValueRecord = $fieldArray);
1410  // IF $incomingFieldArray is an array, overlay it.
1411  // 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...
1412  if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {
1413  ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);
1414  }
1415  } else {
1416  // We must use the current values as basis for this!
1417  $currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*'));
1418  // This is done to make the pid positive for offline versions; Necessary to have diff-view for pages_language_overlay in workspaces.
1419  BackendUtility::fixVersioningPid($table, $currentRecord);
1420  // Get original language record if available:
1421  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) {
1422  $lookUpTable = $table === 'pages_language_overlay' ? 'pages' : $table;
1423  $originalLanguageRecord = $this->recordInfo($lookUpTable, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*');
1424  BackendUtility::workspaceOL($lookUpTable, $originalLanguageRecord);
1425  $originalLanguage_diffStorage = unserialize($currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]);
1426  }
1427  }
1428  $this->checkValue_currentRecord = $checkValueRecord;
1429  // In the following all incoming value-fields are tested:
1430  // - Are the user allowed to change the field?
1431  // - Is the field uid/pid (which are already set)
1432  // - perms-fields for pages-table, then do special things...
1433  // - If the field is nothing of the above and the field is configured in TCA, the fieldvalues are evaluated by ->checkValue
1434  // If everything is OK, the field is entered into $fieldArray[]
1435  foreach ($incomingFieldArray as $field => $fieldValue) {
1436  if (isset($this->excludedTablesAndFields[$table . '-' . $field]) || $this->data_disableFields[$table][$id][$field]) {
1437  continue;
1438  }
1439 
1440  // The field must be editable.
1441  // Checking if a value for language can be changed:
1442  $languageDeny = $GLOBALS['TCA'][$table]['ctrl']['languageField'] && (string)$GLOBALS['TCA'][$table]['ctrl']['languageField'] === (string)$field && !$this->BE_USER->checkLanguageAccess($fieldValue);
1443  if ($languageDeny) {
1444  continue;
1445  }
1446 
1447  switch ($field) {
1448  case 'uid':
1449  case 'pid':
1450  // Nothing happens, already set
1451  break;
1452  case 'perms_userid':
1453  case 'perms_groupid':
1454  case 'perms_user':
1455  case 'perms_group':
1456  case 'perms_everybody':
1457  // Permissions can be edited by the owner or the administrator
1458  if ($table == 'pages' && ($this->admin || $status == 'new' || $this->pageInfo($id, 'perms_userid') == $this->userid)) {
1459  $value = (int)$fieldValue;
1460  switch ($field) {
1461  case 'perms_userid':
1462  $fieldArray[$field] = $value;
1463  break;
1464  case 'perms_groupid':
1465  $fieldArray[$field] = $value;
1466  break;
1467  default:
1468  if ($value >= 0 && $value < pow(2, 5)) {
1469  $fieldArray[$field] = $value;
1470  }
1471  }
1472  }
1473  break;
1474  case 't3ver_oid':
1475  case 't3ver_id':
1476  case 't3ver_wsid':
1477  case 't3ver_state':
1478  case 't3ver_count':
1479  case 't3ver_stage':
1480  case 't3ver_tstamp':
1481  // t3ver_label is not here because it CAN be edited as a regular field!
1482  break;
1483  default:
1484  if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
1485  // Evaluating the value
1486  $res = $this->checkValue($table, $field, $fieldValue, $id, $status, $realPid, $tscPID);
1487  if (array_key_exists('value', $res)) {
1488  $fieldArray[$field] = $res['value'];
1489  }
1490  // Add the value of the original record to the diff-storage content:
1491  if ($this->updateModeL10NdiffData && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
1492  $originalLanguage_diffStorage[$field] = $this->updateModeL10NdiffDataClear ? '' : $originalLanguageRecord[$field];
1493  $diffStorageFlag = true;
1494  }
1495  // If autoversioning is happening we need to perform a nasty hack. The case is parallel to a similar hack inside checkValue_group_select_file().
1496  // 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.
1497  // 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.
1498  // 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.
1499  // 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 !
1500  if ($this->autoVersioningUpdate === true) {
1501  if (is_array($this->RTEmagic_copyIndex[$table][$id][$field])) {
1502  foreach ($this->RTEmagic_copyIndex[$table][$id][$field] as $oldRTEmagicName => $newRTEmagicName) {
1503  $fieldArray[$field] = str_replace(' src="' . $oldRTEmagicName . '"', ' src="' . $newRTEmagicName . '"', $fieldArray[$field]);
1504  }
1505  }
1506  }
1507  } elseif ($GLOBALS['TCA'][$table]['ctrl']['origUid'] === $field) {
1508  // Allow value for original UID to pass by...
1509  $fieldArray[$field] = $fieldValue;
1510  }
1511  }
1512  }
1513  // Add diff-storage information:
1514  if ($diffStorageFlag && !isset($fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']])) {
1515  // 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...
1516  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = serialize($originalLanguage_diffStorage);
1517  }
1518  // Checking for RTE-transformations of fields:
1519  $types_fieldConfig = BackendUtility::getTCAtypes($table, $this->checkValue_currentRecord);
1520  $theTypeString = null;
1521  if (is_array($types_fieldConfig)) {
1522  foreach ($types_fieldConfig as $vconf) {
1523  // RTE transformations:
1524  if ($this->dontProcessTransformations || !isset($fieldArray[$vconf['field']])) {
1525  continue;
1526  }
1527 
1528  // Look for transformation flag:
1529  if ((string)$incomingFieldArray['_TRANSFORM_' . $vconf['field']] === 'RTE') {
1530  if ($theTypeString === null) {
1531  $theTypeString = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
1532  }
1533  $RTEsetup = $this->BE_USER->getTSConfig('RTE', BackendUtility::getPagesTSconfig($tscPID));
1534  $thisConfig = BackendUtility::RTEsetup($RTEsetup['properties'], $table, $vconf['field'], $theTypeString);
1535  $fieldArray[$vconf['field']] = $this->transformRichtextContentToDatabase(
1536  $fieldArray[$vconf['field']], $table, $vconf['field'], $vconf['spec'], $thisConfig, $this->checkValue_currentRecord['pid']
1537  );
1538  }
1539  }
1540  }
1541  // Return fieldArray
1542  return $fieldArray;
1543  }
1544 
1556  protected function transformRichtextContentToDatabase($value, $table, $field, $defaultExtras, $thisConfig, $pid)
1557  {
1558  if ($defaultExtras['rte_transform']) {
1559  // Initialize transformation:
1560  $parseHTML = GeneralUtility::makeInstance(RteHtmlParser::class);
1561  $parseHTML->init($table . ':' . $field, $pid);
1562  // Perform transformation:
1563  $value = $parseHTML->RTE_transform($value, $defaultExtras, 'db', $thisConfig);
1564  }
1565  return $value;
1566  }
1567 
1568  /*********************************************
1569  *
1570  * Evaluation of input values
1571  *
1572  ********************************************/
1587  public function checkValue($table, $field, $value, $id, $status, $realPid, $tscPID)
1588  {
1589  // Result array
1590  $res = [];
1591 
1592  // Processing special case of field pages.doktype
1593  if (($table === 'pages' || $table === 'pages_language_overlay') && $field === 'doktype') {
1594  // If the user may not use this specific doktype, we issue a warning
1595  if (!($this->admin || GeneralUtility::inList($this->BE_USER->groupData['pagetypes_select'], $value))) {
1596  if ($this->enableLogging) {
1597  $propArr = $this->getRecordProperties($table, $id);
1598  $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']);
1599  }
1600  return $res;
1601  }
1602  if ($status == 'update') {
1603  // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1604  $onlyAllowedTables = isset($GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables']) ? $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] : $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1605  if ($onlyAllowedTables) {
1606  $theWrongTables = $this->doesPageHaveUnallowedTables($id, $value);
1607  if ($theWrongTables) {
1608  if ($this->enableLogging) {
1609  $propArr = $this->getRecordProperties($table, $id);
1610  $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']);
1611  }
1612  return $res;
1613  }
1614  }
1615  }
1616  }
1617 
1618  $curValue = null;
1619  if ((int)$id !== 0) {
1620  // Get current value:
1621  $curValueRec = $this->recordInfo($table, $id, $field);
1622  // isset() won't work here, since values can be NULL
1623  if ($curValueRec !== null && array_key_exists($field, $curValueRec)) {
1624  $curValue = $curValueRec[$field];
1625  }
1626  }
1627 
1628  // Getting config for the field
1629  $tcaFieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1630 
1631  // Create $recFID only for those types that need it
1632  if (
1633  $tcaFieldConf['type'] === 'flex'
1634  || $tcaFieldConf['type'] === 'group' && ($tcaFieldConf['internal_type'] === 'file' || $tcaFieldConf['internal_type'] === 'file_reference')
1635  ) {
1636  $recFID = $table . ':' . $id . ':' . $field;
1637  } else {
1638  $recFID = null;
1639  }
1640 
1641  // Perform processing:
1642  $res = $this->checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $this->uploadedFileArray[$table][$id][$field], $tscPID);
1643  return $res;
1644  }
1645 
1665  public function checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $uploadedFiles, $tscPID, array $additionalData = null)
1666  {
1667  // Convert to NULL value if defined in TCA
1668  if ($value === null && !empty($tcaFieldConf['eval']) && GeneralUtility::inList($tcaFieldConf['eval'], 'null')) {
1669  $res = ['value' => null];
1670  return $res;
1671  }
1672 
1673  switch ($tcaFieldConf['type']) {
1674  case 'text':
1675  $res = $this->checkValueForText($value, $tcaFieldConf);
1676  break;
1677  case 'passthrough':
1678  case 'imageManipulation':
1679  case 'user':
1680  $res['value'] = $value;
1681  break;
1682  case 'input':
1683  $res = $this->checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field);
1684  break;
1685  case 'check':
1686  $res = $this->checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1687  break;
1688  case 'radio':
1689  $res = $this->checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1690  break;
1691  case 'group':
1692  case 'select':
1693  $res = $this->checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field);
1694  break;
1695  case 'inline':
1696  $res = $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
1697  break;
1698  case 'flex':
1699  // FlexForms are only allowed for real fields.
1700  if ($field) {
1701  $res = $this->checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field);
1702  }
1703  break;
1704  default:
1705  // Do nothing
1706  }
1707  return $res;
1708  }
1709 
1717  protected function checkValueForText($value, $tcaFieldConf)
1718  {
1719  if (!isset($tcaFieldConf['eval']) || $tcaFieldConf['eval'] === '') {
1720  return ['value' => $value];
1721  }
1722  $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1723  if ($this->runtimeCache->has($cacheId)) {
1724  $evalCodesArray = $this->runtimeCache->get($cacheId);
1725  } else {
1726  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1727  $this->runtimeCache->set($cacheId, $evalCodesArray);
1728  }
1729  return $this->checkValue_text_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1730  }
1731 
1743  protected function checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field)
1744  {
1745  // Handle native date/time fields
1746  $isDateOrDateTimeField = false;
1747  $format = '';
1748  $emptyValue = '';
1749  // normal integer "date" fields (timestamps) are handled in checkValue_input_Eval
1750  if (isset($tcaFieldConf['dbType']) && ($tcaFieldConf['dbType'] === 'date' || $tcaFieldConf['dbType'] === 'datetime')) {
1751  if (empty($value)) {
1752  $value = 0;
1753  } else {
1754  $isDateOrDateTimeField = true;
1755  $dateTimeFormats = QueryHelper::getDateTimeFormats();
1756  $format = $dateTimeFormats[$tcaFieldConf['dbType']]['format'];
1757 
1758  // Convert the date/time into a timestamp for the sake of the checks
1759  $emptyValue = $dateTimeFormats[$tcaFieldConf['dbType']]['empty'];
1760  // We store UTC timestamps in the database, which is what getTimestamp() returns.
1761  $dateTime = new \DateTime($value);
1762  $value = $value === $emptyValue ? 0 : $dateTime->getTimestamp();
1763  }
1764  }
1765  // Secures the string-length to be less than max.
1766  if ((int)$tcaFieldConf['max'] > 0) {
1767  $value = mb_substr((string)$value, 0, (int)$tcaFieldConf['max'], 'utf-8');
1768  }
1769  // Checking range of value:
1770  // @todo: The "checkbox" option was removed for type=input, this check could be probably relaxed?
1771  if ($tcaFieldConf['range'] && $value != $tcaFieldConf['checkbox'] && (int)$value !== (int)$tcaFieldConf['default']) {
1772  if (isset($tcaFieldConf['range']['upper']) && (int)$value > (int)$tcaFieldConf['range']['upper']) {
1773  $value = $tcaFieldConf['range']['upper'];
1774  }
1775  if (isset($tcaFieldConf['range']['lower']) && (int)$value < (int)$tcaFieldConf['range']['lower']) {
1776  $value = $tcaFieldConf['range']['lower'];
1777  }
1778  }
1779 
1780  if (empty($tcaFieldConf['eval'])) {
1781  $res = ['value' => $value];
1782  } else {
1783  // Process evaluation settings:
1784  $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1785  if ($this->runtimeCache->has($cacheId)) {
1786  $evalCodesArray = $this->runtimeCache->get($cacheId);
1787  } else {
1788  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1789  $this->runtimeCache->set($cacheId, $evalCodesArray);
1790  }
1791 
1792  $res = $this->checkValue_input_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1793 
1794  // Process UNIQUE settings:
1795  // 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...
1796  if ($field && $realPid >= 0 && !empty($res['value'])) {
1797  if (in_array('uniqueInPid', $evalCodesArray, true)) {
1798  $res['value'] = $this->getUnique($table, $field, $res['value'], $id, $realPid);
1799  }
1800  if ($res['value'] && in_array('unique', $evalCodesArray, true)) {
1801  $res['value'] = $this->getUnique($table, $field, $res['value'], $id);
1802  }
1803  }
1804  }
1805 
1806  // Handle native date/time fields
1807  if ($isDateOrDateTimeField) {
1808  // Convert the timestamp back to a date/time
1809  $res['value'] = $res['value'] ? date($format, $res['value']) : $emptyValue;
1810  }
1811  return $res;
1812  }
1813 
1826  protected function checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field)
1827  {
1828  $items = $tcaFieldConf['items'];
1829  if ($tcaFieldConf['itemsProcFunc']) {
1831  $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
1832  $items = $processingService->getProcessingItems($table, $realPid, $field,
1833  $this->checkValue_currentRecord,
1834  $tcaFieldConf, $tcaFieldConf['items']);
1835  }
1836 
1837  $itemC = count($items);
1838  if (!$itemC) {
1839  $itemC = 1;
1840  }
1841  $maxV = pow(2, $itemC) - 1;
1842  if ($value < 0) {
1843  // @todo: throw LogicException here? Negative values for checkbox items do not make sense and indicate a coding error.
1844  $value = 0;
1845  }
1846  if ($value > $maxV) {
1847  // @todo: This case is pretty ugly: If there is an itemsProcFunc registered, and if it returns a dynamic,
1848  // @todo: changing list of items, then it may happen that a value is transformed and vanished checkboxes
1849  // @todo: are permanently removed from the value.
1850  // @todo: Suggestion: Throw an exception instead? Maybe a specific, catchable exception that generates a
1851  // @todo: error message to the user - dynamic item sets via itemProcFunc on check would be a bad idea anyway.
1852  $value = $value & $maxV;
1853  }
1854  if ($field && $realPid >= 0 && $value > 0 && !empty($tcaFieldConf['eval'])) {
1855  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1856  $otherRecordsWithSameValue = [];
1857  $maxCheckedRecords = 0;
1858  if (in_array('maximumRecordsCheckedInPid', $evalCodesArray, true)) {
1859  $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value, $realPid);
1860  $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsCheckedInPid'];
1861  }
1862  if (in_array('maximumRecordsChecked', $evalCodesArray, true)) {
1863  $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value);
1864  $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsChecked'];
1865  }
1866 
1867  // there are more than enough records with value "1" in the DB
1868  // if so, set this value to "0" again
1869  if ($maxCheckedRecords && count($otherRecordsWithSameValue) >= $maxCheckedRecords) {
1870  $value = 0;
1871  if ($this->enableLogging) {
1872  $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]);
1873  }
1874  }
1875  }
1876  $res['value'] = $value;
1877  return $res;
1878  }
1879 
1892  protected function checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $pid, $field)
1893  {
1894  if (is_array($tcaFieldConf['items'])) {
1895  foreach ($tcaFieldConf['items'] as $set) {
1896  if ((string)$set[1] === (string)$value) {
1897  $res['value'] = $value;
1898  break;
1899  }
1900  }
1901  }
1902 
1903  // if no value was found and an itemsProcFunc is defined, check that for the value
1904  if ($tcaFieldConf['itemsProcFunc'] && empty($res['value'])) {
1905  $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
1906  $processedItems = $processingService->getProcessingItems($table, $pid, $field, $this->checkValue_currentRecord,
1907  $tcaFieldConf, $tcaFieldConf['items']);
1908 
1909  foreach ($processedItems as $set) {
1910  if ((string)$set[1] === (string)$value) {
1911  $res['value'] = $value;
1912  break;
1913  }
1914  }
1915  }
1916 
1917  return $res;
1918  }
1919 
1935  protected function checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field)
1936  {
1937  // Detecting if value sent is an array and if so, implode it around a comma:
1938  if (is_array($value)) {
1939  $value = implode(',', $value);
1940  }
1941  // 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.
1942  // Anyway, this should NOT disturb anything else:
1943  $value = $this->convNumEntityToByteValue($value);
1944  // When values are sent as group or select they come as comma-separated values which are exploded by this function:
1945  $valueArray = $this->checkValue_group_select_explodeSelectGroupValue($value);
1946  // If multiple is not set, remove duplicates:
1947  if (!$tcaFieldConf['multiple']) {
1948  $valueArray = array_unique($valueArray);
1949  }
1950  // If an exclusive key is found, discard all others:
1951  if ($tcaFieldConf['type'] == 'select' && $tcaFieldConf['exclusiveKeys']) {
1952  $exclusiveKeys = GeneralUtility::trimExplode(',', $tcaFieldConf['exclusiveKeys']);
1953  foreach ($valueArray as $index => $key) {
1954  if (in_array($key, $exclusiveKeys, true)) {
1955  $valueArray = [$index => $key];
1956  break;
1957  }
1958  }
1959  }
1960  // 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?)
1961  // 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!!
1962  $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
1963  // Checking for select / authMode, removing elements from $valueArray if any of them is not allowed!
1964  if ($tcaFieldConf['type'] == 'select' && $tcaFieldConf['authMode']) {
1965  $preCount = count($valueArray);
1966  foreach ($valueArray as $index => $key) {
1967  if (!$this->BE_USER->checkAuthMode($table, $field, $key, $tcaFieldConf['authMode'])) {
1968  unset($valueArray[$index]);
1969  }
1970  }
1971  // 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.
1972  if ($preCount && empty($valueArray)) {
1973  return [];
1974  }
1975  }
1976  // For group types:
1977  if ($tcaFieldConf['type'] == 'group') {
1978  switch ($tcaFieldConf['internal_type']) {
1979  case 'file_reference':
1980  case 'file':
1981  $valueArray = $this->checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFiles, $status, $table, $id, $recFID);
1982  break;
1983  }
1984  }
1985  // For select types which has a foreign table attached:
1986  $unsetResult = false;
1987  if (
1988  $tcaFieldConf['type'] === 'group' && $tcaFieldConf['internal_type'] === 'db'
1989  || $tcaFieldConf['type'] === 'select' && ($tcaFieldConf['foreign_table'] || isset($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages')
1990  ) {
1991  // check, if there is a NEW... id in the value, that should be substituted later
1992  if (strpos($value, 'NEW') !== false) {
1993  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
1994  $this->addNewValuesToRemapStackChildIds($valueArray);
1995  $this->remapStack[] = [
1996  'func' => 'checkValue_group_select_processDBdata',
1997  'args' => [$valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field],
1998  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 5],
1999  'field' => $field
2000  ];
2001  $unsetResult = true;
2002  } else {
2003  $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field);
2004  }
2005  }
2006  if (!$unsetResult) {
2007  $newVal = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
2008  $res['value'] = $this->castReferenceValue(implode(',', $newVal), $tcaFieldConf);
2009  } else {
2010  unset($res['value']);
2011  }
2012  return $res;
2013  }
2014 
2023  protected function applyFiltersToValues(array $tcaFieldConfiguration, array $values)
2024  {
2025  if (empty($tcaFieldConfiguration['filter']) || !is_array($tcaFieldConfiguration['filter'])) {
2026  return $values;
2027  }
2028  foreach ($tcaFieldConfiguration['filter'] as $filter) {
2029  if (empty($filter['userFunc'])) {
2030  continue;
2031  }
2032  $parameters = $filter['parameters'] ?: [];
2033  $parameters['values'] = $values;
2034  $parameters['tcaFieldConfig'] = $tcaFieldConfiguration;
2035  $values = GeneralUtility::callUserFunction($filter['userFunc'], $parameters, $this);
2036  if (!is_array($values)) {
2037  throw new \RuntimeException('Failed calling filter userFunc.', 1336051942);
2038  }
2039  }
2040  return $values;
2041  }
2042 
2058  public function checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFileArray, $status, $table, $id, $recFID)
2059  {
2060  // If file handling should NOT be bypassed, do processing:
2061  if (!$this->bypassFileHandling) {
2062  // If any files are uploaded, add them to value array
2063  // Numeric index means that there are multiple files
2064  if (isset($uploadedFileArray[0])) {
2065  $uploadedFiles = $uploadedFileArray;
2066  } else {
2067  // There is only one file
2068  $uploadedFiles = [$uploadedFileArray];
2069  }
2070  foreach ($uploadedFiles as $uploadedFileArray) {
2071  if (!empty($uploadedFileArray['name']) && $uploadedFileArray['tmp_name'] !== 'none') {
2072  $valueArray[] = $uploadedFileArray['tmp_name'];
2073  $this->alternativeFileName[$uploadedFileArray['tmp_name']] = $uploadedFileArray['name'];
2074  }
2075  }
2076  // Creating fileFunc object.
2077  if (!$this->fileFunc) {
2078  $this->fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
2079  }
2080  // Setting permitted extensions.
2081  $this->fileFunc->setFileExtensionPermissions($tcaFieldConf['allowed'], $tcaFieldConf['disallowed'] ?: '*');
2082  }
2083  // If there is an upload folder defined:
2084  if ($tcaFieldConf['uploadfolder'] && $tcaFieldConf['internal_type'] == 'file') {
2085  $currentFilesForHistory = null;
2086  // If filehandling should NOT be bypassed, do processing:
2087  if (!$this->bypassFileHandling) {
2088  // For logging..
2089  $propArr = $this->getRecordProperties($table, $id);
2090  // Get destrination path:
2091  $dest = $this->destPathFromUploadFolder($tcaFieldConf['uploadfolder']);
2092  // If we are updating:
2093  if ($status == 'update') {
2094  // Traverse the input values and convert to absolute filenames in case the update happens to an autoVersionized record.
2095  // 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!
2096  // 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_.
2097  // 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.
2098  // Illustration of the problem comes here:
2099  // 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.
2100  // 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.
2101  // 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.
2102  // 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.
2103  if ($this->autoVersioningUpdate === true) {
2104  foreach ($valueArray as $key => $theFile) {
2105  // If it is an already attached file...
2106  if ($theFile === basename($theFile)) {
2107  $valueArray[$key] = PATH_site . $tcaFieldConf['uploadfolder'] . '/' . $theFile;
2108  }
2109  }
2110  }
2111  // Finding the CURRENT files listed, either from MM or from the current record.
2112  $theFileValues = [];
2113  // If MM relations for the files also!
2114  if ($tcaFieldConf['MM']) {
2115  $dbAnalysis = $this->createRelationHandlerInstance();
2117  $dbAnalysis->start('', 'files', $tcaFieldConf['MM'], $id);
2118  foreach ($dbAnalysis->itemArray as $item) {
2119  if ($item['id']) {
2120  $theFileValues[] = $item['id'];
2121  }
2122  }
2123  } else {
2124  $theFileValues = GeneralUtility::trimExplode(',', $curValue, true);
2125  }
2126  $currentFilesForHistory = implode(',', $theFileValues);
2127  // DELETE files: If existing files were found, traverse those and register files for deletion which has been removed:
2128  if (!empty($theFileValues)) {
2129  // 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!)
2130  foreach ($valueArray as $key => $theFile) {
2131  if ($theFile && !strstr(GeneralUtility::fixWindowsFilePath($theFile), '/')) {
2132  $theFileValues = ArrayUtility::removeArrayEntryByValue($theFileValues, $theFile);
2133  }
2134  }
2135  // This array contains the filenames in the uploadfolder that should be deleted:
2136  foreach ($theFileValues as $key => $theFile) {
2137  $theFile = trim($theFile);
2138  if (@is_file(($dest . '/' . $theFile))) {
2139  $this->removeFilesStore[] = $dest . '/' . $theFile;
2140  } elseif ($this->enableLogging && $theFile) {
2141  $this->log($table, $id, 5, 0, 1, 'Could not delete file \'%s\' (does not exist). (%s)', 10, [$dest . '/' . $theFile, $recFID], $propArr['event_pid']);
2142  }
2143  }
2144  }
2145  }
2146  // Traverse the submitted values:
2147  foreach ($valueArray as $key => $theFile) {
2148  // Init:
2149  $maxSize = (int)$tcaFieldConf['max_size'];
2150  // Must be cleared. Else a faulty fileref may be inserted if the below code returns an error!
2151  $theDestFile = '';
2152  // a FAL file was added, now resolve the file object and get the absolute path
2153  // @todo in future versions this needs to be modified to handle FAL objects natively
2154  if (!empty($theFile) && MathUtility::canBeInterpretedAsInteger($theFile)) {
2155  $fileObject = ResourceFactory::getInstance()->getFileObject($theFile);
2156  $theFile = $fileObject->getForLocalProcessing(false);
2157  }
2158  // NEW FILES? If the value contains '/' it indicates, that the file
2159  // is new and should be added to the uploadsdir (whether its absolute or relative does not matter here)
2160  if (strstr(GeneralUtility::fixWindowsFilePath($theFile), '/')) {
2161  // Check various things before copying file:
2162  // File and destination must exist
2163  if (@is_dir($dest) && (@is_file($theFile) || @is_uploaded_file($theFile))) {
2164  // Finding size.
2165  if (is_uploaded_file($theFile) && $theFile == $uploadedFileArray['tmp_name']) {
2166  $fileSize = $uploadedFileArray['size'];
2167  } else {
2168  $fileSize = filesize($theFile);
2169  }
2170  // Check file size:
2171  if (!$maxSize || $fileSize <= $maxSize * 1024) {
2172  // Prepare filename:
2173  $theEndFileName = isset($this->alternativeFileName[$theFile]) ? $this->alternativeFileName[$theFile] : $theFile;
2174  $fI = GeneralUtility::split_fileref($theEndFileName);
2175  // Check for allowed extension:
2176  if ($this->fileFunc->checkIfAllowed($fI['fileext'], $dest, $theEndFileName)) {
2177  $theDestFile = $this->fileFunc->getUniqueName($this->fileFunc->cleanFileName($fI['file']), $dest);
2178  // If we have a unique destination filename, then write the file:
2179  if ($theDestFile) {
2180  GeneralUtility::upload_copy_move($theFile, $theDestFile);
2181  // Hook for post-processing the upload action
2182  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processUpload'])) {
2183  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processUpload'] as $classRef) {
2184  $hookObject = GeneralUtility::getUserObj($classRef);
2185  if (!$hookObject instanceof DataHandlerProcessUploadHookInterface) {
2186  throw new \UnexpectedValueException($classRef . ' must implement interface ' . DataHandlerProcessUploadHookInterface::class, 1279962349);
2187  }
2188  $hookObject->processUpload_postProcessAction($theDestFile, $this);
2189  }
2190  }
2191  $this->copiedFileMap[$theFile] = $theDestFile;
2192  clearstatcache();
2193  if ($this->enableLogging && !@is_file($theDestFile)) {
2194  $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']);
2195  }
2196  } elseif ($this->enableLogging) {
2197  $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: No destination file (%s) possible!. (%s)', 11, [$theFile, $theDestFile, $recFID], $propArr['event_pid']);
2198  }
2199  } elseif ($this->enableLogging) {
2200  $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, [$fI['fileext'], $recFID], $propArr['event_pid']);
2201  }
2202  } elseif ($this->enableLogging) {
2203  $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']);
2204  }
2205  } elseif ($this->enableLogging) {
2206  $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']);
2207  }
2208  // If the destination file was created, we will set the new filename in the value array, otherwise unset the entry in the value array!
2209  if (@is_file($theDestFile)) {
2210  $info = GeneralUtility::split_fileref($theDestFile);
2211  // The value is set to the new filename
2212  $valueArray[$key] = $info['file'];
2213  } else {
2214  // The value is set to the new filename
2215  unset($valueArray[$key]);
2216  }
2217  }
2218  }
2219  }
2220  // 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!
2221  if ($tcaFieldConf['MM']) {
2223  $dbAnalysis = $this->createRelationHandlerInstance();
2224  // Dummy
2225  $dbAnalysis->tableArray['files'] = [];
2226  foreach ($valueArray as $key => $theFile) {
2227  // Explode files
2228  $dbAnalysis->itemArray[]['id'] = $theFile;
2229  }
2230  if ($status == 'update') {
2231  $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, 0);
2232  $newFiles = implode(',', $dbAnalysis->getValueArray());
2233  list(, , $recFieldName) = explode(':', $recFID);
2234  if ($currentFilesForHistory != $newFiles) {
2235  $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$recFieldName] = $currentFilesForHistory;
2236  $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$recFieldName] = $newFiles;
2237  } else {
2238  $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$recFieldName] = '';
2239  $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$recFieldName] = '';
2240  }
2241  } else {
2242  $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, 0];
2243  }
2244  $valueArray = $dbAnalysis->countItems();
2245  }
2246  } else {
2247  if (!empty($valueArray)) {
2248  // If filehandling should NOT be bypassed, do processing:
2249  if (!$this->bypassFileHandling) {
2250  // For logging..
2251  $propArr = $this->getRecordProperties($table, $id);
2252  foreach ($valueArray as &$theFile) {
2253  // FAL handling: it's a UID, thus it is resolved to the absolute path
2254  if (!empty($theFile) && MathUtility::canBeInterpretedAsInteger($theFile)) {
2255  $fileObject = ResourceFactory::getInstance()->getFileObject($theFile);
2256  $theFile = $fileObject->getForLocalProcessing(false);
2257  }
2258  if ($this->alternativeFilePath[$theFile]) {
2259  // If alternative File Path is set for the file, then it was an import
2260  // don't import the file if it already exists
2261  if (@is_file((PATH_site . $this->alternativeFilePath[$theFile]))) {
2262  $theFile = PATH_site . $this->alternativeFilePath[$theFile];
2263  } elseif (@is_file($theFile)) {
2264  $dest = dirname(PATH_site . $this->alternativeFilePath[$theFile]);
2265  if (!@is_dir($dest)) {
2266  GeneralUtility::mkdir_deep(PATH_site, dirname($this->alternativeFilePath[$theFile]) . '/');
2267  }
2268  // Init:
2269  $maxSize = (int)$tcaFieldConf['max_size'];
2270  // Must be cleared. Else a faulty fileref may be inserted if the below code returns an error!
2271  $theDestFile = '';
2272  $fileSize = filesize($theFile);
2273  // Check file size:
2274  if (!$maxSize || $fileSize <= $maxSize * 1024) {
2275  // Prepare filename:
2276  $theEndFileName = isset($this->alternativeFileName[$theFile]) ? $this->alternativeFileName[$theFile] : $theFile;
2277  $fI = GeneralUtility::split_fileref($theEndFileName);
2278  // Check for allowed extension:
2279  if ($this->fileFunc->checkIfAllowed($fI['fileext'], $dest, $theEndFileName)) {
2280  $theDestFile = PATH_site . $this->alternativeFilePath[$theFile];
2281  // Write the file:
2282  if ($theDestFile) {
2283  GeneralUtility::upload_copy_move($theFile, $theDestFile);
2284  $this->copiedFileMap[$theFile] = $theDestFile;
2285  clearstatcache();
2286  if ($this->enableLogging && !@is_file($theDestFile)) {
2287  $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']);
2288  }
2289  } elseif ($this->enableLogging) {
2290  $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: No destination file (%s) possible!. (%s)', 11, [$theFile, $theDestFile, $recFID], $propArr['event_pid']);
2291  }
2292  } elseif ($this->enableLogging) {
2293  $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, [$fI['fileext'], $recFID], $propArr['event_pid']);
2294  }
2295  } elseif ($this->enableLogging) {
2296  $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']);
2297  }
2298  // If the destination file was created, we will set the new filename in the value array, otherwise unset the entry in the value array!
2299  if (@is_file($theDestFile)) {
2300  // The value is set to the new filename
2301  $theFile = $theDestFile;
2302  } else {
2303  // The value is set to the new filename
2304  unset($theFile);
2305  }
2306  }
2307  }
2308  if (!empty($theFile)) {
2309  $theFile = GeneralUtility::fixWindowsFilePath($theFile);
2310  if (GeneralUtility::isFirstPartOfStr($theFile, PATH_site)) {
2311  $theFile = PathUtility::stripPathSitePrefix($theFile);
2312  }
2313  }
2314  }
2315  unset($theFile);
2316  }
2317  }
2318  }
2319  return $valueArray;
2320  }
2321 
2339  protected function checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field)
2340  {
2341  if (is_array($value)) {
2342  // This value is necessary for flex form processing to happen on flexform fields in page records when they are copied.
2343  // Problem: when copying a page, flexform XML comes along in the array for the new record - but since $this->checkValue_currentRecord
2344  // does not have a uid or pid for that sake, the FlexFormTools->getDataStructureIdentifier() function returns no good DS. For new
2345  // records we do know the expected PID so therefore we send that with this special parameter. Only active when larger than zero.
2347  if ($status === 'new') {
2348  $row['pid'] = $realPid;
2349  }
2350  // Get current value array:
2351  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
2352  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
2353  [ 'config' => $tcaFieldConf ],
2354  $table,
2355  $field,
2356  $row
2357  );
2358  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
2359  $currentValueArray = (string)$curValue !== '' ? GeneralUtility::xml2array($curValue) : [];
2360  if (!is_array($currentValueArray)) {
2361  $currentValueArray = [];
2362  }
2363  // Remove all old meta for languages...
2364  // Evaluation of input values:
2365  $value['data'] = $this->checkValue_flex_procInData($value['data'], $currentValueArray['data'], $uploadedFiles['data'], $dataStructureArray, [$table, $id, $curValue, $status, $realPid, $recFID, $tscPID]);
2366  // Create XML from input value:
2367  $xmlValue = $this->checkValue_flexArray2Xml($value, true);
2368 
2369  // 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
2370  // (provided that the current value was already stored IN the charset that the new value is converted to).
2371  $arrValue = GeneralUtility::xml2array($xmlValue);
2372 
2373  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'])) {
2374  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'] as $classRef) {
2375  $hookObject = GeneralUtility::getUserObj($classRef);
2376  if (method_exists($hookObject, 'checkFlexFormValue_beforeMerge')) {
2377  $hookObject->checkFlexFormValue_beforeMerge($this, $currentValueArray, $arrValue);
2378  }
2379  }
2380  }
2381 
2382  ArrayUtility::mergeRecursiveWithOverrule($currentValueArray, $arrValue);
2383  $xmlValue = $this->checkValue_flexArray2Xml($currentValueArray, true);
2384 
2385  // Action commands (sorting order and removals of elements) for flexform sections,
2386  // see FormEngine for the use of this GP parameter
2387  $actionCMDs = GeneralUtility::_GP('_ACTION_FLEX_FORMdata');
2388  if (is_array($actionCMDs[$table][$id][$field]['data'])) {
2389  $arrValue = GeneralUtility::xml2array($xmlValue);
2390  $this->_ACTION_FLEX_FORMdata($arrValue['data'], $actionCMDs[$table][$id][$field]['data']);
2391  $xmlValue = $this->checkValue_flexArray2Xml($arrValue, true);
2392  }
2393  // Create the value XML:
2394  $res['value'] = '';
2395  $res['value'] .= $xmlValue;
2396  } else {
2397  // Passthrough...:
2398  $res['value'] = $value;
2399  }
2400 
2401  return $res;
2402  }
2403 
2411  public function checkValue_flexArray2Xml($array, $addPrologue = false)
2412  {
2414  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
2415  return $flexObj->flexArray2Xml($array, $addPrologue);
2416  }
2417 
2425  protected function _ACTION_FLEX_FORMdata(&$valueArray, $actionCMDs)
2426  {
2427  if (!is_array($valueArray) || !is_array($actionCMDs)) {
2428  return;
2429  }
2430 
2431  foreach ($actionCMDs as $key => $value) {
2432  if ($key == '_ACTION') {
2433  // First, check if there are "commands":
2434  if (current($actionCMDs[$key]) === '') {
2435  continue;
2436  }
2437 
2438  asort($actionCMDs[$key]);
2439  $newValueArray = [];
2440  foreach ($actionCMDs[$key] as $idx => $order) {
2441  if (substr($idx, 0, 3) == 'ID-') {
2442  $idx = $this->newIndexMap[$idx];
2443  }
2444  // 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.
2445  if ($order != 'DELETE') {
2446  $newValueArray[$idx] = $valueArray[$idx];
2447  }
2448  unset($valueArray[$idx]);
2449  }
2450  $valueArray = $valueArray + $newValueArray;
2451  } elseif (is_array($actionCMDs[$key]) && isset($valueArray[$key])) {
2452  $this->_ACTION_FLEX_FORMdata($valueArray[$key], $actionCMDs[$key]);
2453  }
2454  }
2455  }
2456 
2469  public function checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData = null)
2470  {
2471  list($table, $id, , $status) = $PP;
2472  $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
2473  }
2474 
2489  public function checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData = null)
2490  {
2491  if (!$tcaFieldConf['foreign_table']) {
2492  // Fatal error, inline fields should always have a foreign_table defined
2493  return false;
2494  }
2495  // When values are sent they come as comma-separated values which are exploded by this function:
2496  $valueArray = GeneralUtility::trimExplode(',', $value);
2497  // Remove duplicates: (should not be needed)
2498  $valueArray = array_unique($valueArray);
2499  // Example for received data:
2500  // $value = 45,NEW4555fdf59d154,12,123
2501  // We need to decide whether we use the stack or can save the relation directly.
2502  if (strpos($value, 'NEW') !== false || !MathUtility::canBeInterpretedAsInteger($id)) {
2503  $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2504  $this->addNewValuesToRemapStackChildIds($valueArray);
2505  $this->remapStack[] = [
2506  'func' => 'checkValue_inline_processDBdata',
2507  'args' => [$valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData],
2508  'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 4],
2509  'additionalData' => $additionalData,
2510  'field' => $field,
2511  ];
2512  unset($res['value']);
2513  } elseif ($value || MathUtility::canBeInterpretedAsInteger($id)) {
2514  $res['value'] = $this->checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData);
2515  }
2516  return $res;
2517  }
2518 
2527  public function checkValue_checkMax($tcaFieldConf, $valueArray)
2528  {
2529  // 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...
2530  $valueArrayC = count($valueArray);
2531  // 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.
2532  $maxI = isset($tcaFieldConf['maxitems']) ? (int)$tcaFieldConf['maxitems'] : 1;
2533  if ($valueArrayC > $maxI) {
2534  $valueArrayC = $maxI;
2535  }
2536  // Checking for not too many elements
2537  // Dumping array to list
2538  $newVal = [];
2539  foreach ($valueArray as $nextVal) {
2540  if ($valueArrayC == 0) {
2541  break;
2542  }
2543  $valueArrayC--;
2544  $newVal[] = $nextVal;
2545  }
2546  return $newVal;
2547  }
2548 
2549  /*********************************************
2550  *
2551  * Helper functions for evaluation functions.
2552  *
2553  ********************************************/
2564  public function getUnique($table, $field, $value, $id, $newPid = 0)
2565  {
2566  // If the field is configured in TCA, proceed:
2567  if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
2568  $newValue = $value;
2569  $statement = $this->getUniqueCountStatement($newValue, $table, $field, (int)$id, (int)$newPid);
2570  // For as long as records with the test-value existing, try again (with incremented numbers appended)
2571  if ($statement->fetchColumn()) {
2572  $statement->bindParam(1, $newValue);
2573  for ($counter = 0; $counter <= 100; $counter++) {
2574  $newValue = $value . $counter;
2575  $statement->execute();
2576  if (!$statement->fetchColumn()) {
2577  break;
2578  }
2579  }
2580  }
2581  $value = $newValue;
2582  }
2583  return $value;
2584  }
2585 
2596  protected function getUniqueCountStatement(
2597  string $value,
2598  string $table,
2599  string $field,
2600  int $uid,
2601  int $pid
2602  ): Statement {
2603  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2604  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
2605  $queryBuilder
2606  ->count('uid')
2607  ->from($table)
2608  ->where(
2609  $queryBuilder->expr()->eq($field, $queryBuilder->createPositionalParameter($value, \PDO::PARAM_STR)),
2610  $queryBuilder->expr()->neq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT))
2611  );
2612  if ($pid !== 0) {
2613  $queryBuilder->andWhere(
2614  $queryBuilder->expr()->eq('pid', $queryBuilder->createPositionalParameter($pid, \PDO::PARAM_INT))
2615  );
2616  } else {
2617  // pid>=0 for versioning
2618  $queryBuilder->andWhere(
2619  $queryBuilder->expr()->gte('pid', $queryBuilder->createPositionalParameter(0, \PDO::PARAM_INT))
2620  );
2621  }
2622 
2623  return $queryBuilder->execute();
2624  }
2625 
2637  public function getRecordsWithSameValue($tableName, $uid, $fieldName, $value, $pageId = 0)
2638  {
2639  $result = [];
2640  if (!empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
2641  $uid = (int)$uid;
2642  $pageId = (int)$pageId;
2643  $whereStatement = ' AND uid <> ' . $uid . ' AND ' . ($pageId ? 'pid = ' . $pageId : 'pid >= 0');
2644  $result = BackendUtility::getRecordsByField($tableName, $fieldName, $value, $whereStatement);
2645  }
2646  return $result;
2647  }
2648 
2655  public function checkValue_text_Eval($value, $evalArray, $is_in)
2656  {
2657  $res = [];
2658  $set = true;
2659  foreach ($evalArray as $func) {
2660  switch ($func) {
2661  case 'trim':
2662  $value = trim($value);
2663  break;
2664  case 'required':
2665  if (!$value) {
2666  $set = false;
2667  }
2668  break;
2669  default:
2670  if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2671  if (class_exists($func)) {
2672  $evalObj = GeneralUtility::makeInstance($func);
2673  if (method_exists($evalObj, 'evaluateFieldValue')) {
2674  $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2675  }
2676  }
2677  }
2678  }
2679  }
2680  if ($set) {
2681  $res['value'] = $value;
2682  }
2683  return $res;
2684  }
2685 
2694  public function checkValue_input_Eval($value, $evalArray, $is_in)
2695  {
2696  $res = [];
2697  $set = true;
2698  foreach ($evalArray as $func) {
2699  switch ($func) {
2700  case 'int':
2701  case 'year':
2702  $value = (int)$value;
2703  break;
2704  case 'time':
2705  case 'timesec':
2706  case 'date':
2707  case 'datetime':
2708  // a hyphen as first character indicates a negative timestamp
2709  if ((strpos($value, '-') === false && strpos($value, ':') === false) || strpos($value, '-') === 0) {
2710  $value = (int)$value;
2711  } else {
2712  // ISO 8601 dates
2713  $dateTime = new \DateTime($value);
2714  // The returned timestamp is always UTC
2715  $value = $dateTime->getTimestamp();
2716  }
2717  // $value is a UTC timestamp here.
2718  // The value will be stored in the server’s local timezone, but treated as UTC, so we brute force
2719  // subtract the offset here. The offset is subtracted instead of added because the value is stored
2720  // in the timezone, but interpreted as UTC, so if we switched the server to UTC, the correct
2721  // value would be returned.
2722  if ($value !== 0 && !$this->dontProcessTransformations) {
2723  $value -= date('Z', $value);
2724  }
2725  break;
2726  case 'double2':
2727  $value = preg_replace('/[^0-9,\\.-]/', '', $value);
2728  $negative = $value[0] === '-';
2729  $value = strtr($value, [',' => '.', '-' => '']);
2730  if (strpos($value, '.') === false) {
2731  $value .= '.0';
2732  }
2733  $valueArray = explode('.', $value);
2734  $dec = array_pop($valueArray);
2735  $value = implode('', $valueArray) . '.' . $dec;
2736  if ($negative) {
2737  $value *= -1;
2738  }
2739  $value = number_format($value, 2, '.', '');
2740  break;
2741  case 'md5':
2742  if (strlen($value) != 32) {
2743  $set = false;
2744  }
2745  break;
2746  case 'trim':
2747  $value = trim($value);
2748  break;
2749  case 'upper':
2750  $value = mb_strtoupper($value, 'utf-8');
2751  break;
2752  case 'lower':
2753  $value = mb_strtolower($value, 'utf-8');
2754  break;
2755  case 'required':
2756  if (!isset($value) || $value === '') {
2757  $set = false;
2758  }
2759  break;
2760  case 'is_in':
2761  $c = strlen($value);
2762  if ($c) {
2763  $newVal = '';
2764  for ($a = 0; $a < $c; $a++) {
2765  $char = substr($value, $a, 1);
2766  if (strpos($is_in, $char) !== false) {
2767  $newVal .= $char;
2768  }
2769  }
2770  $value = $newVal;
2771  }
2772  break;
2773  case 'nospace':
2774  $value = str_replace(' ', '', $value);
2775  break;
2776  case 'alpha':
2777  $value = preg_replace('/[^a-zA-Z]/', '', $value);
2778  break;
2779  case 'num':
2780  $value = preg_replace('/[^0-9]/', '', $value);
2781  break;
2782  case 'alphanum':
2783  $value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
2784  break;
2785  case 'alphanum_x':
2786  $value = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
2787  break;
2788  case 'domainname':
2789  if (!preg_match('/^[a-z0-9.\\-]*$/i', $value)) {
2790  $value = GeneralUtility::idnaEncode($value);
2791  }
2792  break;
2793  case 'email':
2794  if ((string)$value !== '') {
2795  $this->checkValue_input_ValidateEmail($value, $set);
2796  }
2797  break;
2798  default:
2799  if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2800  if (class_exists($func)) {
2801  $evalObj = GeneralUtility::makeInstance($func);
2802  if (method_exists($evalObj, 'evaluateFieldValue')) {
2803  $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2804  }
2805  }
2806  }
2807  }
2808  }
2809  if ($set) {
2810  $res['value'] = $value;
2811  }
2812  return $res;
2813  }
2814 
2826  protected function checkValue_input_ValidateEmail($value, &$set)
2827  {
2828  if (GeneralUtility::validEmail($value)) {
2829  return;
2830  }
2831 
2832  $set = false;
2834  $message = GeneralUtility::makeInstance(FlashMessage::class,
2835  sprintf($GLOBALS['LANG']->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:error.invalidEmail'), $value),
2836  '', // header is optional
2838  true // whether message should be stored in session
2839  );
2841  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
2842  $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
2843  }
2844 
2857  public function checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
2858  {
2859  if ($type === 'group') {
2860  $tables = $tcaFieldConf['allowed'];
2861  } elseif (!empty($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages') {
2862  $tables = 'sys_language';
2863  } else {
2864  $tables = $tcaFieldConf['foreign_table'];
2865  }
2866  $prep = $type == 'group' ? $tcaFieldConf['prepend_tname'] : '';
2867  $newRelations = implode(',', $valueArray);
2869  $dbAnalysis = $this->createRelationHandlerInstance();
2870  $dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
2871  $dbAnalysis->start($newRelations, $tables, '', 0, $currentTable, $tcaFieldConf);
2872  if ($tcaFieldConf['MM']) {
2873  // convert submitted items to use version ids instead of live ids
2874  // (only required for MM relations in a workspace context)
2875  $dbAnalysis->convertItemArray();
2876  if ($status == 'update') {
2878  $oldRelations_dbAnalysis = $this->createRelationHandlerInstance();
2879  $oldRelations_dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
2880  // Db analysis with $id will initialize with the existing relations
2881  $oldRelations_dbAnalysis->start('', $tables, $tcaFieldConf['MM'], $id, $currentTable, $tcaFieldConf);
2882  $oldRelations = implode(',', $oldRelations_dbAnalysis->getValueArray());
2883  $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, $prep);
2884  if ($oldRelations != $newRelations) {
2885  $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
2886  $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
2887  } else {
2888  $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
2889  $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
2890  }
2891  } else {
2892  $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
2893  }
2894  $valueArray = $dbAnalysis->countItems();
2895  } else {
2896  $valueArray = $dbAnalysis->getValueArray($prep);
2897  }
2898  // 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.
2899  return $valueArray;
2900  }
2901 
2909  {
2910  $valueArray = GeneralUtility::trimExplode(',', $value, true);
2911  foreach ($valueArray as &$newVal) {
2912  $temp = explode('|', $newVal, 2);
2913  $newVal = str_replace(',', '', str_replace('|', '', rawurldecode($temp[0])));
2914  }
2915  unset($newVal);
2916  return $valueArray;
2917  }
2918 
2934  public function checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructure, $pParams, $callBackFunc = '', array $workspaceOptions = [])
2935  {
2936  if (is_array($dataPart)) {
2937  foreach ($dataPart as $sKey => $sheetDef) {
2938  if (isset($dataStructure['sheets'][$sKey]) && is_array($dataStructure['sheets'][$sKey]) && is_array($sheetDef)) {
2939  foreach ($sheetDef as $lKey => $lData) {
2941  $dataPart[$sKey][$lKey],
2942  $dataPart_current[$sKey][$lKey],
2943  $uploadedFiles[$sKey][$lKey],
2944  $dataStructure['sheets'][$sKey]['ROOT']['el'],
2945  $pParams,
2946  $callBackFunc,
2947  $sKey . '/' . $lKey . '/', $workspaceOptions
2948  );
2949  }
2950  }
2951  }
2952  }
2953  return $dataPart;
2954  }
2955 
2971  public function checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions = [])
2972  {
2973  if (!is_array($DSelements)) {
2974  return;
2975  }
2976 
2977  // For each DS element:
2978  foreach ($DSelements as $key => $dsConf) {
2979  // Array/Section:
2980  if ($DSelements[$key]['type'] == 'array') {
2981  if (!is_array($dataValues[$key]['el'])) {
2982  continue;
2983  }
2984 
2985  if ($DSelements[$key]['section']) {
2986  $newIndexCounter = 0;
2987  foreach ($dataValues[$key]['el'] as $ik => $el) {
2988  if (!is_array($el)) {
2989  continue;
2990  }
2991 
2992  if (!is_array($dataValues_current[$key]['el'])) {
2993  $dataValues_current[$key]['el'] = [];
2994  }
2995  $theKey = key($el);
2996  if (!is_array($dataValues[$key]['el'][$ik][$theKey]['el'])) {
2997  continue;
2998  }
2999 
3000  $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);
3001  // 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:
3002  // The fact that the order changes is not important since order is controlled by a separately submitted index.
3003  if (substr($ik, 0, 3) == 'ID-') {
3004  $newIndexCounter++;
3005  // Set mapping index
3006  $this->newIndexMap[$ik] = (is_array($dataValues_current[$key]['el']) && !empty($dataValues_current[$key]['el']) ? max(array_keys($dataValues_current[$key]['el'])) : 0) + $newIndexCounter;
3007  // Transfer values
3008  $dataValues[$key]['el'][$this->newIndexMap[$ik]] = $dataValues[$key]['el'][$ik];
3009  // Unset original
3010  unset($dataValues[$key]['el'][$ik]);
3011  }
3012  }
3013  } else {
3014  if (!isset($dataValues[$key]['el'])) {
3015  $dataValues[$key]['el'] = [];
3016  }
3017  $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $uploadedFiles[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/', $workspaceOptions);
3018  }
3019  } else {
3020  if (!is_array($dsConf['TCEforms']['config']) || !is_array($dataValues[$key])) {
3021  continue;
3022  }
3023 
3024  foreach ($dataValues[$key] as $vKey => $data) {
3025  if ($callBackFunc) {
3026  if (is_object($this->callBackObj)) {
3027  $res = $this->callBackObj->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
3028  } else {
3029  $res = $this->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
3030  }
3031  } else {
3032  // Default
3033  list($CVtable, $CVid, $CVcurValue, $CVstatus, $CVrealPid, $CVrecFID, $CVtscPID) = $pParams;
3034 
3035  $additionalData = [
3036  'flexFormId' => $CVrecFID,
3037  'flexFormPath' => trim(rtrim($structurePath, '/') . '/' . $key . '/' . $vKey, '/'),
3038  ];
3039 
3040  $res = $this->checkValue_SW([], $dataValues[$key][$vKey], $dsConf['TCEforms']['config'], $CVtable, $CVid, $dataValues_current[$key][$vKey], $CVstatus, $CVrealPid, $CVrecFID, '', $uploadedFiles[$key][$vKey], $CVtscPID, $additionalData);
3041  // Look for RTE transformation of field:
3042  if ($dataValues[$key]['_TRANSFORM_' . $vKey] == 'RTE' && !$this->dontProcessTransformations) {
3043  // Unsetting trigger field - we absolutely don't want that into the data storage!
3044  unset($dataValues[$key]['_TRANSFORM_' . $vKey]);
3045  if (isset($res['value'])) {
3046  // Calculating/Retrieving some values here:
3047  list(, , $recFieldName) = explode(':', $CVrecFID);
3048  $theTypeString = BackendUtility::getTCAtypeValue($CVtable, $this->checkValue_currentRecord);
3049  $specConf = BackendUtility::getSpecConfParts($dsConf['TCEforms']['defaultExtras']);
3050  // Find, thisConfig:
3051  $RTEsetup = $this->BE_USER->getTSConfig('RTE', BackendUtility::getPagesTSconfig($CVtscPID));
3052  $thisConfig = BackendUtility::RTEsetup($RTEsetup['properties'], $CVtable, $recFieldName, $theTypeString);
3053  $res['value'] = $this->transformRichtextContentToDatabase(
3054  $res['value'], $CVtable, $recFieldName, $specConf, $thisConfig, $CVrealPid
3055  );
3056  }
3057  }
3058  }
3059  // Adding the value:
3060  if (isset($res['value'])) {
3061  $dataValues[$key][$vKey] = $res['value'];
3062  }
3063  // 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.
3064  // 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).
3065  if (substr($vKey, -9) != '.vDEFbase') {
3066  if ($this->updateModeL10NdiffData && $GLOBALS['TYPO3_CONF_VARS']['BE']['flexFormXMLincludeDiffBase'] && $vKey !== 'vDEF' && ((string)$dataValues[$key][$vKey] !== (string)$dataValues_current[$key][$vKey] || !isset($dataValues_current[$key][$vKey . '.vDEFbase']) || $this->updateModeL10NdiffData === 'FORCE_FFUPD')) {
3067  // 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:
3068  if (isset($dataValues[$key]['vDEF'])) {
3069  $diffValue = $dataValues[$key]['vDEF'];
3070  } else {
3071  // If not found (for translators with no access to the default language) we use the one from the current-value data set:
3072  $diffValue = $dataValues_current[$key]['vDEF'];
3073  }
3074  // 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.
3075  $dataValues[$key][$vKey . '.vDEFbase'] = $this->updateModeL10NdiffDataClear ? '' : $diffValue;
3076  }
3077  }
3078  }
3079  }
3080  }
3081  }
3082 
3095  protected function checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, array $additionalData = null)
3096  {
3097  $newValue = '';
3098  $foreignTable = $tcaFieldConf['foreign_table'];
3099  $transOrigPointer = 0;
3100  $keepTranslation = false;
3101  $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
3102  // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3104  $dbAnalysis = $this->createRelationHandlerInstance();
3105  $dbAnalysis->start(implode(',', $valueArray), $foreignTable, '', 0, $table, $tcaFieldConf);
3106  // If the localizationMode is set to 'keep', the children for the localized parent are kept as in the original untranslated record:
3107  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $tcaFieldConf);
3108  if ($localizationMode == 'keep' && $status == 'update') {
3109  // Fetch the current record and determine the original record:
3110  $row = BackendUtility::getRecordWSOL($table, $id);
3111  if (is_array($row)) {
3112  $language = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
3113  $transOrigPointer = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
3114  // If language is set (e.g. 1) and also transOrigPointer (e.g. 123), use transOrigPointer as uid:
3115  if ($language > 0 && $transOrigPointer) {
3116  $id = $transOrigPointer;
3117  // If we're in active localizationMode 'keep', prevent from writing data to the field of the parent record:
3118  // (on removing the localized parent, the original (untranslated) children would then also be removed)
3119  $keepTranslation = true;
3120  }
3121  }
3122  }
3123  // IRRE with a pointer field (database normalization):
3124  if ($tcaFieldConf['foreign_field']) {
3125  // if the record was imported, sorting was also imported, so skip this
3126  $skipSorting = (bool)$this->callFromImpExp;
3127  // update record in intermediate table (sorting & pointer uid to parent record)
3128  $dbAnalysis->writeForeignField($tcaFieldConf, $id, 0, $skipSorting);
3129  $newValue = $keepTranslation ? 0 : $dbAnalysis->countItems(false);
3130  } else {
3131  if ($this->getInlineFieldType($tcaFieldConf) == 'mm') {
3132  // In order to fully support all the MM stuff, directly call checkValue_group_select_processDBdata instead of repeating the needed code here
3133  $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
3134  $newValue = $keepTranslation ? 0 : $valueArray[0];
3135  } else {
3136  $valueArray = $dbAnalysis->getValueArray();
3137  // Checking that the number of items is correct:
3138  $valueArray = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
3139  $valueData = $this->castReferenceValue(implode(',', $valueArray), $tcaFieldConf);
3140  // If a valid translation of the 'keep' mode is active, update relations in the original(!) record:
3141  if ($keepTranslation) {
3142  $this->updateDB($table, $transOrigPointer, [$field => $valueData]);
3143  } else {
3144  $newValue = $valueData;
3145  }
3146  }
3147  }
3148  return $newValue;
3149  }
3150 
3151  /*********************************************
3152  *
3153  * PROCESSING COMMANDS
3154  *
3155  ********************************************/
3162  public function process_cmdmap()
3163  {
3164  // Editing frozen:
3165  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
3166  if ($this->enableLogging) {
3167  $this->newlog('All editing in this workspace has been frozen!', 1);
3168  }
3169  return false;
3170  }
3171  // Hook initialization:
3172  $hookObjectsArr = [];
3173  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'])) {
3174  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] as $classRef) {
3175  $hookObj = GeneralUtility::getUserObj($classRef);
3176  if (method_exists($hookObj, 'processCmdmap_beforeStart')) {
3177  $hookObj->processCmdmap_beforeStart($this);
3178  }
3179  $hookObjectsArr[] = $hookObj;
3180  }
3181  }
3182  $pasteDatamap = [];
3183  // Traverse command map:
3184  foreach ($this->cmdmap as $table => $_) {
3185  // Check if the table may be modified!
3186  $modifyAccessList = $this->checkModifyAccessList($table);
3187  if ($this->enableLogging && !$modifyAccessList) {
3188  $this->log($table, 0, 2, 0, 1, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
3189  }
3190  // Check basic permissions and circumstances:
3191  if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->cmdmap[$table]) || !$modifyAccessList) {
3192  continue;
3193  }
3194 
3195  // Traverse the command map:
3196  foreach ($this->cmdmap[$table] as $id => $incomingCmdArray) {
3197  if (!is_array($incomingCmdArray)) {
3198  continue;
3199  }
3200 
3201  if ($table === 'pages') {
3202  // for commands on pages do a pagetree-refresh
3203  $this->pagetreeNeedsRefresh = true;
3204  }
3205 
3206  foreach ($incomingCmdArray as $command => $value) {
3207  $pasteUpdate = false;
3208  if (is_array($value) && isset($value['action']) && $value['action'] === 'paste') {
3209  // Extended paste command: $command is set to "move" or "copy"
3210  // $value['update'] holds field/value pairs which should be updated after copy/move operation
3211  // $value['target'] holds original $value (target of move/copy)
3212  $pasteUpdate = $value['update'];
3213  $value = $value['target'];
3214  }
3215  foreach ($hookObjectsArr as $hookObj) {
3216  if (method_exists($hookObj, 'processCmdmap_preProcess')) {
3217  $hookObj->processCmdmap_preProcess($command, $table, $id, $value, $this, $pasteUpdate);
3218  }
3219  }
3220  // Init copyMapping array:
3221  // Must clear this array before call from here to those functions:
3222  // Contains mapping information between new and old id numbers.
3223  $this->copyMappingArray = [];
3224  // process the command
3225  $commandIsProcessed = false;
3226  foreach ($hookObjectsArr as $hookObj) {
3227  if (method_exists($hookObj, 'processCmdmap')) {
3228  $hookObj->processCmdmap($command, $table, $id, $value, $commandIsProcessed, $this, $pasteUpdate);
3229  }
3230  }
3231  // Only execute default commands if a hook hasn't been processed the command already
3232  if (!$commandIsProcessed) {
3233  $procId = $id;
3234  // Branch, based on command
3235  switch ($command) {
3236  case 'move':
3237  $this->moveRecord($table, $id, $value);
3238  break;
3239  case 'copy':
3240  if ($table === 'pages') {
3241  $this->copyPages($id, $value);
3242  } else {
3243  $this->copyRecord($table, $id, $value, 1);
3244  }
3245  $procId = $this->copyMappingArray[$table][$id];
3246  break;
3247  case 'localize':
3248  $this->localize($table, $id, $value);
3249  break;
3250  case 'inlineLocalizeSynchronize':
3251  $this->inlineLocalizeSynchronize($table, $id, $value);
3252  break;
3253  case 'delete':
3254  $this->deleteAction($table, $id);
3255  break;
3256  case 'undelete':
3257  $this->undeleteRecord($table, $id);
3258  break;
3259  }
3260  if (is_array($pasteUpdate)) {
3261  $pasteDatamap[$table][$procId] = $pasteUpdate;
3262  }
3263  }
3264  foreach ($hookObjectsArr as $hookObj) {
3265  if (method_exists($hookObj, 'processCmdmap_postProcess')) {
3266  $hookObj->processCmdmap_postProcess($command, $table, $id, $value, $this, $pasteUpdate, $pasteDatamap);
3267  }
3268  }
3269  // Merging the copy-array info together for remapping purposes.
3270  ArrayUtility::mergeRecursiveWithOverrule($this->copyMappingArray_merged, $this->copyMappingArray);
3271  }
3272  }
3273  }
3275  $copyTCE = $this->getLocalTCE();
3276  $copyTCE->start($pasteDatamap, '', $this->BE_USER);
3277  $copyTCE->process_datamap();
3278  $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3279  unset($copyTCE);
3280 
3281  // Finally, before exit, check if there are ID references to remap.
3282  // This might be the case if versioning or copying has taken place!
3283  $this->remapListedDBRecords();
3284  $this->processRemapStack();
3285  foreach ($hookObjectsArr as $hookObj) {
3286  if (method_exists($hookObj, 'processCmdmap_afterFinish')) {
3287  $hookObj->processCmdmap_afterFinish($this);
3288  }
3289  }
3290  if ($this->isOuterMostInstance()) {
3291  $this->processClearCacheQueue();
3292  $this->resetNestedElementCalls();
3293  }
3294  }
3295 
3296  /*********************************************
3297  *
3298  * Cmd: Copying
3299  *
3300  ********************************************/
3314  public function copyRecord($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '', $language = 0, $ignoreLocalization = false)
3315  {
3316  $uid = ($origUid = (int)$uid);
3317  // Only copy if the table is defined in $GLOBALS['TCA'], a uid is given and the record wasn't copied before:
3318  if (empty($GLOBALS['TCA'][$table]) || $uid === 0) {
3319  return null;
3320  }
3321  if ($this->isRecordCopied($table, $uid)) {
3322  if (!empty($overrideValues)) {
3323  $this->log($table, $uid, 1, 0, 1, 'Repeated attempt to copy record "%s:%s" with override values', -1, [$table, $uid]);
3324  }
3325  return null;
3326  }
3327 
3328  // This checks if the record can be selected which is all that a copy action requires.
3329  if (!$this->doesRecordExist($table, $uid, 'show')) {
3330  if ($this->enableLogging) {
3331  $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record "%s:%s" without permission', -1, [$table, $uid]);
3332  }
3333  return null;
3334  }
3335 
3336  // Check if table is allowed on destination page
3337  if ($destPid >= 0 && !$this->isTableAllowedForThisPage($destPid, $table)) {
3338  if ($this->enableLogging) {
3339  $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]);
3340  }
3341  return null;
3342  }
3343 
3344  $fullLanguageCheckNeeded = $table != 'pages';
3345  //Used to check language and general editing rights
3346  if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded)) {
3347  if ($this->enableLogging) {
3348  $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]);
3349  }
3350  return null;
3351  }
3352 
3353  $data = [];
3354  $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));
3355  // So it copies (and localized) content from workspace...
3356  $row = BackendUtility::getRecordWSOL($table, $uid);
3357  if (!is_array($row)) {
3358  if ($this->enableLogging) {
3359  $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record that did not exist!');
3360  }
3361  return null;
3362  }
3363 
3364  // Initializing:
3365  $theNewID = StringUtility::getUniqueId('NEW');
3366  $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3367  $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3368  // Getting default data:
3369  $defaultData = $this->newFieldArray($table);
3370  // Getting "copy-after" fields if applicable:
3371  $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, abs($destPid), 0) : [];
3372  // Page TSconfig related:
3373  // 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...
3374  $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
3375  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
3376  $tE = $this->getTableEntries($table, $TSConfig);
3377  // Traverse ALL fields of the selected record:
3378  $setDefaultOnCopyArray = array_flip(GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['setToDefaultOnCopy']));
3379  foreach ($row as $field => $value) {
3380  if (!in_array($field, $nonFields, true)) {
3381  // Get TCA configuration for the field:
3382  $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3383  // Preparation/Processing of the value:
3384  // "pid" is hardcoded of course:
3385  // isset() won't work here, since values can be NULL in each of the arrays
3386  // except setDefaultOnCopyArray, since we exploded that from a string
3387  if ($field == 'pid') {
3388  $value = $destPid;
3389  } elseif (array_key_exists($field, $overrideValues)) {
3390  // Override value...
3391  $value = $overrideValues[$field];
3392  } elseif (array_key_exists($field, $copyAfterFields)) {
3393  // Copy-after value if available:
3394  $value = $copyAfterFields[$field];
3395  } elseif ($GLOBALS['TCA'][$table]['ctrl']['setToDefaultOnCopy'] && isset($setDefaultOnCopyArray[$field])) {
3396  $value = $defaultData[$field];
3397  } else {
3398  // Hide at copy may override:
3399  if ($first && $field == $enableField && $GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] && !$this->neverHideAtCopy && !$tE['disableHideAtCopy']) {
3400  $value = 1;
3401  }
3402  // Prepend label on copy:
3403  if ($first && $field == $headerField && $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy'] && !$tE['disablePrependAtCopy']) {
3404  $value = $this->getCopyHeader($table, $this->resolvePid($table, $destPid), $field, $this->clearPrefixFromValue($table, $value), 0);
3405  }
3406  // Processing based on the TCA config field type (files, references, flexforms...)
3407  $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $tscPID, $language);
3408  }
3409  // Add value to array.
3410  $data[$table][$theNewID][$field] = $value;
3411  }
3412  }
3413  // Overriding values:
3414  if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
3415  $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3416  }
3417  // Setting original UID:
3418  if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3419  $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3420  }
3421  // Do the copy by simply submitting the array through DataHandler:
3423  $copyTCE = $this->getLocalTCE();
3424  $copyTCE->start($data, '', $this->BE_USER);
3425  $copyTCE->process_datamap();
3426  // Getting the new UID:
3427  $theNewSQLID = $copyTCE->substNEWwithIDs[$theNewID];
3428  if ($theNewSQLID) {
3429  $this->copyRecord_fixRTEmagicImages($table, BackendUtility::wsMapId($table, $theNewSQLID));
3430  $this->copyMappingArray[$table][$origUid] = $theNewSQLID;
3431  // Keep automatically versionized record information:
3432  if (isset($copyTCE->autoVersionIdMap[$table][$theNewSQLID])) {
3433  $this->autoVersionIdMap[$table][$theNewSQLID] = $copyTCE->autoVersionIdMap[$table][$theNewSQLID];
3434  }
3435  }
3436  // Copy back the cached TSconfig
3437  $this->cachedTSconfig = $copyTCE->cachedTSconfig;
3438  $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3439  unset($copyTCE);
3440  if (!$ignoreLocalization && $language == 0) {
3441  //repointing the new translation records to the parent record we just created
3442  $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
3443  $this->copyL10nOverlayRecords($table, $uid, $destPid, $first, $overrideValues, $excludeFields);
3444  }
3445 
3446  return $theNewSQLID;
3447  }
3448 
3457  public function copyPages($uid, $destPid)
3458  {
3459  // Initialize:
3460  $uid = (int)$uid;
3461  $destPid = (int)$destPid;
3462  // Finding list of tables to copy.
3463  // These are the tables, the user may modify
3464  $copyTablesArray = $this->admin ? $this->compileAdminTables() : explode(',', $this->BE_USER->groupData['tables_modify']);
3465  // 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
3466  if (!strstr($this->copyWhichTables, '*')) {
3467  $copyWhichTablesArray = array_flip(GeneralUtility::trimExplode(',', $this->copyWhichTables . ',pages'));
3468  foreach ($copyTablesArray as $k => $table) {
3469  // Pages are always going...
3470  if (!$table || !isset($copyWhichTablesArray[$table])) {
3471  unset($copyTablesArray[$k]);
3472  }
3473  }
3474  }
3475  $copyTablesArray = array_unique($copyTablesArray);
3476  // Begin to copy pages if we're allowed to:
3477  if ($this->admin || in_array('pages', $copyTablesArray, true)) {
3478  // Copy this page we're on. And set first-flag (this will trigger that the record is hidden if that is configured)!
3479  $theNewRootID = $this->copySpecificPage($uid, $destPid, $copyTablesArray, 1);
3480  // If we're going to copy recursively...:
3481  if ($theNewRootID && $this->copyTree) {
3482  // Get ALL subpages to copy (read-permissions are respected!):
3483  $CPtable = $this->int_pageTreeInfo([], $uid, (int)$this->copyTree, $theNewRootID);
3484  // Now copying the subpages:
3485  foreach ($CPtable as $thePageUid => $thePagePid) {
3486  $newPid = $this->copyMappingArray['pages'][$thePagePid];
3487  if (isset($newPid)) {
3488  $this->copySpecificPage($thePageUid, $newPid, $copyTablesArray);
3489  } else {
3490  if ($this->enableLogging) {
3491  $this->log('pages', $uid, 5, 0, 1, 'Something went wrong during copying branch');
3492  }
3493  break;
3494  }
3495  }
3496  }
3497  } elseif ($this->enableLogging) {
3498  $this->log('pages', $uid, 5, 0, 1, 'Attempt to copy page without permission to this table');
3499  }
3500  }
3501 
3511  public function copySpecificPage($uid, $destPid, $copyTablesArray, $first = false)
3512  {
3513  // Copy the page itself:
3514  $theNewRootID = $this->copyRecord('pages', $uid, $destPid, $first);
3515  // If a new page was created upon the copy operation we will proceed with all the tables ON that page:
3516  if ($theNewRootID) {
3517  foreach ($copyTablesArray as $table) {
3518  // All records under the page is copied.
3519  if ($table && is_array($GLOBALS['TCA'][$table]) && $table !== 'pages') {
3520  $fields = ['uid'];
3521  $languageField = null;
3522  $transOrigPointerField = null;
3523  if (BackendUtility::isTableLocalizable($table)) {
3524  $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
3525  $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3526  $fields[] = $languageField;
3527  $fields[] = $transOrigPointerField;
3528  }
3529  $isTableWorkspaceEnabled = BackendUtility::isTableWorkspaceEnabled($table);
3530  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3531  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
3532  $queryBuilder
3533  ->select(...$fields)
3534  ->from($table)
3535  ->where($queryBuilder->expr()->eq(
3536  'pid',
3537  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
3538  );
3539  if ($isTableWorkspaceEnabled && (int)$this->BE_USER->workspace === 0) {
3540  // Table is workspace enabled, user is in default ws -> add t3ver_wsid=0 restriction
3541  $queryBuilder->andWhere(
3542  $queryBuilder->expr()->eq(
3543  't3ver_wsid',
3544  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
3545  )
3546  );
3547  } elseif ($isTableWorkspaceEnabled) {
3548  // Table is workspace enabled, user has a ws selected -> select wsid=0 and selected wsid rows
3549  $queryBuilder->andWhere($queryBuilder->expr()->in(
3550  't3ver_wsid',
3551  $queryBuilder->createNamedParameter(
3552  [0, $this->BE_USER->workspace],
3553  Connection::PARAM_INT_ARRAY
3554  )
3555  ));
3556  }
3557  if (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby'])) {
3558  $queryBuilder->orderBy($GLOBALS['TCA'][$table]['ctrl']['sortby'], 'DESC');
3559  }
3560  try {
3561  $result = $queryBuilder->execute();
3562  $rows = [];
3563  while ($row = $result->fetch()) {
3564  $rows[$row['uid']] = $row;
3565  }
3566  // Resolve placeholders of workspace versions
3567  if (!empty($rows) && (int)$this->BE_USER->workspace !== 0 && $isTableWorkspaceEnabled) {
3568  $rows = array_reverse(
3569  $this->resolveVersionedRecords(
3570  $table,
3571  implode(',', $fields),
3572  $GLOBALS['TCA'][$table]['ctrl']['sortby'],
3573  array_keys($rows)
3574  ),
3575  true
3576  );
3577  }
3578  if (is_array($rows)) {
3579  foreach ($rows as $row) {
3580  // Skip localized records that will be processed in
3581  // copyL10nOverlayRecords() on copying the default language record
3582  $transOrigPointer = $row[$transOrigPointerField];
3583  if ($row[$languageField] > 0 && $transOrigPointer > 0 && isset($rows[$transOrigPointer])) {
3584  continue;
3585  }
3586  // Copying each of the underlying records...
3587  $this->copyRecord($table, $row['uid'], $theNewRootID);
3588  }
3589  }
3590  } catch (DBALException $e) {
3591  if ($this->enableLogging) {
3592  $databaseErrorMessage = $e->getPrevious()->getMessage();
3593  $this->log($table, $uid, 5, 0, 1, 'An SQL error occurred: ' . $databaseErrorMessage);
3594  }
3595  }
3596  }
3597  }
3598  $this->processRemapStack();
3599  return $theNewRootID;
3600  }
3601  return null;
3602  }
3603 
3619  public function copyRecord_raw($table, $uid, $pid, $overrideArray = [], array $workspaceOptions = [])
3620  {
3621  $uid = (int)$uid;
3622  // Stop any actions if the record is marked to be deleted:
3623  // (this can occur if IRRE elements are versionized and child elements are removed)
3624  if ($this->isElementToBeDeleted($table, $uid)) {
3625  return null;
3626  }
3627  // Only copy if the table is defined in TCA, a uid is given and the record wasn't copied before:
3628  if (!$GLOBALS['TCA'][$table] || !$uid || $this->isRecordCopied($table, $uid)) {
3629  return null;
3630  }
3631  if (!$this->doesRecordExist($table, $uid, 'show')) {
3632  if ($this->enableLogging) {
3633  $this->log($table, $uid, 3, 0, 1, 'Attempt to rawcopy/versionize record without copy permission');
3634  }
3635  return null;
3636  }
3637 
3638  // Set up fields which should not be processed. They are still written - just passed through no-questions-asked!
3639  $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'];
3640  // Select main record:
3641  $row = $this->recordInfo($table, $uid, '*');
3642  if (!is_array($row)) {
3643  if ($this->enableLogging) {
3644  $this->log($table, $uid, 3, 0, 1, 'Attempt to rawcopy/versionize record that did not exist!');
3645  }
3646  return null;
3647  }
3648 
3649  // Merge in override array.
3650  $row = array_merge($row, $overrideArray);
3651  // Traverse ALL fields of the selected record:
3652  foreach ($row as $field => $value) {
3653  if (!in_array($field, $nonFields, true)) {
3654  // Get TCA configuration for the field:
3655  $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3656  if (is_array($conf)) {
3657  // Processing based on the TCA config field type (files, references, flexforms...)
3658  $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3659  }
3660  // Add value to array.
3661  $row[$field] = $value;
3662  }
3663  }
3664  // Force versioning related fields:
3665  $row['pid'] = $pid;
3666  // Setting original UID:
3667  if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3668  $row[$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3669  }
3670  // Do the copy by internal function
3671  $theNewSQLID = $this->insertNewCopyVersion($table, $row, $pid);
3672  if ($theNewSQLID) {
3673  $this->dbAnalysisStoreExec();
3674  $this->dbAnalysisStore = [];
3675  $this->copyRecord_fixRTEmagicImages($table, BackendUtility::wsMapId($table, $theNewSQLID));
3676  return $this->copyMappingArray[$table][$uid] = $theNewSQLID;
3677  }
3678  return null;
3679  }
3680 
3690  public function insertNewCopyVersion($table, $fieldArray, $realPid)
3691  {
3692  $id = StringUtility::getUniqueId('NEW');
3693  // $fieldArray is set as current record.
3694  // 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...
3695  $this->checkValue_currentRecord = $fieldArray;
3696  // Makes sure that transformations aren't processed on the copy.
3697  $backupDontProcessTransformations = $this->dontProcessTransformations;
3698  $this->dontProcessTransformations = true;
3699  // Traverse record and input-process each value:
3700  foreach ($fieldArray as $field => $fieldValue) {
3701  if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
3702  // Evaluating the value.
3703  $res = $this->checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0);
3704  if (isset($res['value'])) {
3705  $fieldArray[$field] = $res['value'];
3706  }
3707  }
3708  }
3709  // System fields being set:
3710  if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3711  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
3712  }
3713  if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3714  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
3715  }
3716  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3717  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3718  }
3719  // Finally, insert record:
3720  $this->insertDB($table, $id, $fieldArray, true);
3721  // Resets dontProcessTransformations to the previous state.
3722  $this->dontProcessTransformations = $backupDontProcessTransformations;
3723  // Return new id:
3724  return $this->substNEWwithIDs[$id];
3725  }
3726 
3743  public function copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language = 0, array $workspaceOptions = [])
3744  {
3745  // Process references and files, currently that means only the files, prepending absolute paths (so the DataHandler engine will detect the file as new and one that should be made into a copy)
3746  $value = $this->copyRecord_procFilesRefs($conf, $uid, $value);
3747  $inlineSubType = $this->getInlineFieldType($conf);
3748  // Get the localization mode for the current (parent) record (keep|select):
3749  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $field);
3750  // Register if there are references to take care of or MM is used on an inline field (no change to value):
3751  if ($this->isReferenceField($conf) || $inlineSubType == 'mm') {
3752  $value = $this->copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language, $localizationMode, $inlineSubType);
3753  } elseif ($inlineSubType !== false) {
3754  $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions, $localizationMode, $inlineSubType);
3755  }
3756  // 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())
3757  if ($conf['type'] == 'flex') {
3758  // Get current value array:
3759  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
3760  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
3761  [ 'config' => $conf ],
3762  $table,
3763  $field,
3764  $row
3765  );
3766  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
3767  $currentValueArray = GeneralUtility::xml2array($value);
3768  // Traversing the XML structure, processing files:
3769  if (is_array($currentValueArray)) {
3770  $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $uid, $field, $realDestPid], 'copyRecord_flexFormCallBack', $workspaceOptions);
3771  // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
3772  $value = $currentValueArray;
3773  }
3774  }
3775  return $value;
3776  }
3777 
3791  protected function copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language, $localizationMode, $inlineSubType)
3792  {
3793  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
3794  $prependName = $conf['type'] == 'group' ? $conf['prepend_tname'] : '';
3795  $mmTable = isset($conf['MM']) && $conf['MM'] ? $conf['MM'] : '';
3796  $localizeForeignTable = isset($conf['foreign_table']) && BackendUtility::isTableLocalizable($conf['foreign_table']);
3797  $localizeReferences = $localizeForeignTable && isset($conf['localizeReferencesAtParentLocalization']) && $conf['localizeReferencesAtParentLocalization'];
3798  $localizeChildren = $localizeForeignTable && isset($conf['behaviour']['localizeChildrenAtParentLocalization']) && $conf['behaviour']['localizeChildrenAtParentLocalization'];
3800  $dbAnalysis = $this->createRelationHandlerInstance();
3801  $dbAnalysis->start($value, $allowedTables, $mmTable, $uid, $table, $conf);
3802  // Localize referenced records of select fields:
3803  $localizingNonManyToManyFieldReferences = $localizeReferences && empty($mmTable);
3804  $isInlineFieldInSelectMode = $localizationMode === 'select' && $inlineSubType === 'mm';
3805  $purgeItems = false;
3806  if ($language > 0 && ($localizingNonManyToManyFieldReferences || $isInlineFieldInSelectMode)) {
3807  foreach ($dbAnalysis->itemArray as $index => $item) {
3808  // Since select fields can reference many records, check whether there's already a localization:
3809  $recordLocalization = BackendUtility::getRecordLocalization($item['table'], $item['id'], $language);
3810  if ($recordLocalization) {
3811  $dbAnalysis->itemArray[$index]['id'] = $recordLocalization[0]['uid'];
3812  } elseif ($this->isNestedElementCallRegistered($item['table'], $item['id'], 'localize') === false) {
3813  if ($localizingNonManyToManyFieldReferences || $localizeChildren) {
3814  $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], $language);
3815  } else {
3816  unset($dbAnalysis->itemArray[$index]);
3817  }
3818  }
3819  }
3820  $purgeItems = true;
3821  }
3822 
3823  if ($purgeItems || $mmTable) {
3824  $dbAnalysis->purgeItemArray();
3825  $value = implode(',', $dbAnalysis->getValueArray($prependName));
3826  }
3827  // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected
3828  if ($value) {
3829  $this->registerDBList[$table][$uid][$field] = $value;
3830  }
3831 
3832  return $value;
3833  }
3834 
3851  protected function copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language,
3852  array $workspaceOptions, $localizationMode, $inlineSubType)
3853  {
3854  // Localization in mode 'keep', isn't a real localization, but keeps the children of the original parent record:
3855  if ($language > 0 && $localizationMode == 'keep') {
3856  $value = $inlineSubType == 'field' ? 0 : '';
3857  } else {
3858  // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3860  $dbAnalysis = $this->createRelationHandlerInstance();
3861  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
3862  // Walk through the items, copy them and remember the new id:
3863  foreach ($dbAnalysis->itemArray as $k => $v) {
3864  $newId = null;
3865  // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
3866  if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
3867  // If children should be localized when the parent gets localized the first time, just do it:
3868  if ($localizationMode != false && isset($conf['behaviour']['localizeChildrenAtParentLocalization']) && $conf['behaviour']['localizeChildrenAtParentLocalization']) {
3869  $newId = $this->localize($v['table'], $v['id'], $language);
3870  }
3871  } else {
3872  if (!MathUtility::canBeInterpretedAsInteger($realDestPid)) {
3873  $newId = $this->copyRecord($v['table'], $v['id'], -$v['id']);
3874  // If the destination page id is a NEW string, keep it on the same page
3875  } elseif ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3876  // A filled $workspaceOptions indicated that this call
3877  // has it's origin in previous versionizeRecord() processing
3878  if (!empty($workspaceOptions)) {
3879  // Versions use live default id, thus the "new"
3880  // id is the original live default child record
3881  $newId = $v['id'];
3882  $this->versionizeRecord(
3883  $v['table'], $v['id'],
3884  (isset($workspaceOptions['label']) ? $workspaceOptions['label'] : 'Auto-created for WS #' . $this->BE_USER->workspace),
3885  (isset($workspaceOptions['delete']) ? $workspaceOptions['delete'] : false)
3886  );
3887  // Otherwise just use plain copyRecord() to create placeholders etc.
3888  } else {
3889  // If a record has been copied already during this request,
3890  // prevent superfluous duplication and use the existing copy
3891  if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3892  $newId = $this->copyMappingArray[$v['table']][$v['id']];
3893  } else {
3894  $newId = $this->copyRecord($v['table'], $v['id'], $realDestPid);
3895  }
3896  }
3897  } else {
3898  // If a record has been copied already during this request,
3899  // prevent superfluous duplication and use the existing copy
3900  if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3901  $newId = $this->copyMappingArray[$v['table']][$v['id']];
3902  } else {
3903  $newId = $this->copyRecord_raw($v['table'], $v['id'], $realDestPid, [], $workspaceOptions);
3904  }
3905  }
3906  }
3907  // If the current field is set on a page record, update the pid of related child records:
3908  if ($table == 'pages') {
3909  $this->registerDBPids[$v['table']][$v['id']] = $uid;
3910  } elseif (isset($this->registerDBPids[$table][$uid])) {
3911  $this->registerDBPids[$v['table']][$v['id']] = $this->registerDBPids[$table][$uid];
3912  }
3913  $dbAnalysis->itemArray[$k]['id'] = $newId;
3914  }
3915  // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
3916  $value = implode(',', $dbAnalysis->getValueArray());
3917  $this->registerDBList[$table][$uid][$field] = $value;
3918  }
3919 
3920  return $value;
3921  }
3922 
3936  public function copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $_3, $workspaceOptions)
3937  {
3938  // Extract parameters:
3939  list($table, $uid, $field, $realDestPid) = $pParams;
3940  // Process references and files, currently that means only the files, prepending absolute paths:
3941  $dataValue = $this->copyRecord_procFilesRefs($dsConf, $uid, $dataValue);
3942  // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
3943  if (($this->isReferenceField($dsConf) || $this->getInlineFieldType($dsConf) !== false) && (string)$dataValue !== '') {
3944  $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, [], $dsConf, $realDestPid, 0, $workspaceOptions);
3945  $this->registerDBList[$table][$uid][$field] = 'FlexForm_reference';
3946  }
3947  // Return
3948  return ['value' => $dataValue];
3949  }
3950 
3962  public function copyRecord_procFilesRefs($conf, $uid, $value)
3963  {
3964  // Prepend absolute paths to files:
3965  if ($conf['type'] != 'group' || ($conf['internal_type'] != 'file' && $conf['internal_type'] != 'file_reference')) {
3966  return $value;
3967  }
3968 
3969  // Get an array with files as values:
3970  if ($conf['MM']) {
3971  $theFileValues = [];
3973  $dbAnalysis = $this->createRelationHandlerInstance();
3974  $dbAnalysis->start('', 'files', $conf['MM'], $uid);
3975  foreach ($dbAnalysis->itemArray as $somekey => $someval) {
3976  if ($someval['id']) {
3977  $theFileValues[] = $someval['id'];
3978  }
3979  }
3980  } else {
3981  $theFileValues = GeneralUtility::trimExplode(',', $value, true);
3982  }
3983  // Traverse this array of files:
3984  $uploadFolder = $conf['internal_type'] == 'file' ? $conf['uploadfolder'] : '';
3985  $dest = $this->destPathFromUploadFolder($uploadFolder);
3986  $newValue = [];
3987  foreach ($theFileValues as $file) {
3988  if (trim($file)) {
3989  $realFile = str_replace('//', '/', $dest . '/' . trim($file));
3990  if (@is_file($realFile)) {
3991  $newValue[] = $realFile;
3992  }
3993  }
3994  }
3995  // Implode the new filelist into the new value (all files have absolute paths now which means they will get copied when entering DataHandler as new values...)
3996  $value = implode(',', $newValue);
3997 
3998  // Return the new value:
3999  return $value;
4000  }
4001 
4011  public function copyRecord_fixRTEmagicImages($table, $theNewSQLID)
4012  {
4013  // Creating fileFunc object.
4014  if (!$this->fileFunc) {
4015  $this->fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
4016  }
4017  // Select all RTEmagic files in the reference table from the table/ID
4018  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
4019  $queryBuilder->getRestrictions()->removeAll();
4020  $rteFileRecords = $queryBuilder
4021  ->select('*')
4022  ->from('sys_refindex')
4023  ->where(
4024  $queryBuilder->expr()->eq(
4025  'ref_table',
4026  $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)
4027  ),
4028  $queryBuilder->expr()->like(
4029  'ref_string',
4030  $queryBuilder->createNamedParameter('%/RTEmagic%', \PDO::PARAM_STR)
4031  ),
4032  $queryBuilder->expr()->eq(
4033  'softref_key',
4034  $queryBuilder->createNamedParameter('images', \PDO::PARAM_STR)
4035  ),
4036  $queryBuilder->expr()->eq(
4037  'tablename',
4038  $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
4039  ),
4040  $queryBuilder->expr()->eq(
4041  'recuid',
4042  $queryBuilder->createNamedParameter($theNewSQLID, \PDO::PARAM_INT)
4043  )
4044  )
4045  ->orderBy('sorting', 'DESC')
4046  ->execute()
4047  ->fetchAll();
4048  // Traverse the files found and copy them:
4049  if (!is_array($rteFileRecords)) {
4050  return;
4051  }
4052  foreach ($rteFileRecords as $rteFileRecord) {
4053  $filename = basename($rteFileRecord['ref_string']);
4054  if (!GeneralUtility::isFirstPartOfStr($filename, 'RTEmagicC_')) {
4055  continue;
4056  }
4057  $fileInfo = [];
4058  $fileInfo['exists'] = @is_file((PATH_site . $rteFileRecord['ref_string']));
4059  $fileInfo['original'] = substr($rteFileRecord['ref_string'], 0, -strlen($filename)) . 'RTEmagicP_' . preg_replace('/\\.[[:alnum:]]+$/', '', substr($filename, 10));
4060  $fileInfo['original_exists'] = @is_file((PATH_site . $fileInfo['original']));
4061  // CODE from tx_impexp and class.rte_images.php adapted for use here:
4062  if (!$fileInfo['exists'] || !$fileInfo['original_exists']) {
4063  if ($this->enableLogging) {
4064  $this->newlog('Trying to copy RTEmagic files (' . $rteFileRecord['ref_string'] . ' / ' . $fileInfo['original'] . ') but one or both were missing', 1);
4065  }
4066  continue;
4067  }
4068  // Initialize; Get directory prefix for file and set the original name:
4069  $dirPrefix = dirname($rteFileRecord['ref_string']) . '/';
4070  $rteOrigName = basename($fileInfo['original']);
4071  // If filename looks like an RTE file, and the directory is in "uploads/", then process as a RTE file!
4072  if ($rteOrigName && GeneralUtility::isFirstPartOfStr($dirPrefix, 'uploads/') && @is_dir(PATH_site . $dirPrefix)) {
4073  // RTE:
4074  // From the "original" RTE filename, produce a new "original" destination filename which is unused.
4075  $origDestName = $this->fileFunc->getUniqueName($rteOrigName, PATH_site . $dirPrefix);
4076  // Create copy file name:
4077  $pI = pathinfo($rteFileRecord['ref_string']);
4078  $copyDestName = dirname($origDestName) . '/RTEmagicC_' . substr(basename($origDestName), 10) . '.' . $pI['extension'];
4079  if (!@is_file($copyDestName) && !@is_file($origDestName) && $origDestName === GeneralUtility::getFileAbsFileName($origDestName) && $copyDestName === GeneralUtility::getFileAbsFileName($copyDestName)) {
4080  // Making copies:
4081  GeneralUtility::upload_copy_move(PATH_site . $fileInfo['original'], $origDestName);
4082  GeneralUtility::upload_copy_move(PATH_site . $rteFileRecord['ref_string'], $copyDestName);
4083  clearstatcache();
4084  // Register this:
4085  $this->RTEmagic_copyIndex[$rteFileRecord['tablename']][$rteFileRecord['recuid']][$rteFileRecord['field']][$rteFileRecord['ref_string']] = PathUtility::stripPathSitePrefix($copyDestName);
4086  // Check and update the record using \TYPO3\CMS\Core\Database\ReferenceIndex
4087  if (@is_file($copyDestName)) {
4089  $sysRefObj = GeneralUtility::makeInstance(ReferenceIndex::class);
4090  $error = $sysRefObj->setReferenceValue($rteFileRecord['hash'], PathUtility::stripPathSitePrefix($copyDestName), false, true);
4091  if ($this->enableLogging && $error) {
4092  echo $this->newlog(ReferenceIndex::class . '::setReferenceValue(): ' . $error, 1);
4093  }
4094  } elseif ($this->enableLogging) {
4095  $this->newlog('File "' . $copyDestName . '" was not created!', 1);
4096  }
4097  } elseif ($this->enableLogging) {
4098  $this->newlog('Could not construct new unique names for file!', 1);
4099  }
4100  } elseif ($this->enableLogging) {
4101  $this->newlog('Maybe directory of file was not within "uploads/"?', 1);
4102  }
4103  }
4104  }
4105 
4117  public function copyL10nOverlayRecords($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '')
4118  {
4119  // There's no need to perform this for page-records or for tables that are not localizable
4120  if (!BackendUtility::isTableLocalizable($table) || $table === 'pages' || $table === 'pages_language_overlay') {
4121  return;
4122  }
4123  $where = '';
4124  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
4125  $where = ' AND t3ver_oid=0';
4126  }
4127  // If $destPid is < 0, get the pid of the record with uid equal to abs($destPid)
4128  $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
4129  // Get the localized records to be copied
4130  $l10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], $uid, $where);
4131  if (is_array($l10nRecords)) {
4132  $localizedDestPids = [];
4133  // If $destPid < 0, then it is the uid of the original language record we are inserting after
4134  if ($destPid < 0) {
4135  // Get the localized records of the record we are inserting after
4136  $destL10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], abs($destPid), $where);
4137  // Index the localized record uids by language
4138  if (is_array($destL10nRecords)) {
4139  foreach ($destL10nRecords as $record) {
4140  $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4141  }
4142  }
4143  }
4144  // Copy the localized records after the corresponding localizations of the destination record
4145  foreach ($l10nRecords as $record) {
4146  $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4147  if ($localizedDestPid < 0) {
4148  $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4149  } else {
4150  $this->copyRecord($table, $record['uid'], $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4151  }
4152  }
4153  }
4154  }
4155 
4156  /*********************************************
4157  *
4158  * Cmd: Moving, Localizing
4159  *
4160  ********************************************/
4169  public function moveRecord($table, $uid, $destPid)
4170  {
4171  if (!$GLOBALS['TCA'][$table]) {
4172  return;
4173  }
4174 
4175  // In case the record to be moved turns out to be an offline version,
4176  // we have to find the live version and work on that one (this case
4177  // happens for pages with "branch" versioning type)
4178  // @deprecated note: as "branch" versioning is deprecated since TYPO3 4.2, this
4179  // functionality will be removed in TYPO3 4.7 (note by benni: a hook could replace this)
4180  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $uid, 'uid')) {
4181  $uid = $lookForLiveVersion['uid'];
4182  }
4183  // Initialize:
4184  $destPid = (int)$destPid;
4185  // Get this before we change the pid (for logging)
4186  $propArr = $this->getRecordProperties($table, $uid);
4187  $moveRec = $this->getRecordProperties($table, $uid, true);
4188  // This is the actual pid of the moving to destination
4189  $resolvedPid = $this->resolvePid($table, $destPid);
4190  // 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.
4191  // 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.
4192  if ($table != 'pages' || $resolvedPid == $moveRec['pid']) {
4193  // Edit rights for the record...
4194  $mayMoveAccess = $this->checkRecordUpdateAccess($table, $uid);
4195  } else {
4196  $mayMoveAccess = $this->doesRecordExist($table, $uid, 'delete');
4197  }
4198  // 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
4199  if ($table != 'pages' || $resolvedPid != $moveRec['pid']) {
4200  // Insert rights for the record...
4201  $mayInsertAccess = $this->checkRecordInsertAccess($table, $resolvedPid, 4);
4202  } else {
4203  $mayInsertAccess = $this->checkRecordUpdateAccess($table, $uid);
4204  }
4205  // Checking if there is anything else disallowing moving the record by checking if editing is allowed
4206  $fullLanguageCheckNeeded = $table != 'pages';
4207  $mayEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded);
4208  // If moving is allowed, begin the processing:
4209  if (!$mayEditAccess) {
4210  if ($this->enableLogging) {
4211  $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']);
4212  }
4213  return;
4214  }
4215 
4216  if (!$mayMoveAccess) {
4217  if ($this->enableLogging) {
4218  $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']);
4219  }
4220  return;
4221  }
4222 
4223  if (!$mayInsertAccess) {
4224  if ($this->enableLogging) {
4225  $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']);
4226  }
4227  return;
4228  }
4229 
4230  $recordWasMoved = false;
4231  // Move the record via a hook, used e.g. for versioning
4232  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'])) {
4233  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] as $classRef) {
4234  $hookObj = GeneralUtility::getUserObj($classRef);
4235  if (method_exists($hookObj, 'moveRecord')) {
4236  $hookObj->moveRecord($table, $uid, $destPid, $propArr, $moveRec, $resolvedPid, $recordWasMoved, $this);
4237  }
4238  }
4239  }
4240  // Move the record if a hook hasn't moved it yet
4241  if (!$recordWasMoved) {
4242  $this->moveRecord_raw($table, $uid, $destPid);
4243  }
4244  }
4245 
4256  public function moveRecord_raw($table, $uid, $destPid)
4257  {
4258  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
4259  $origDestPid = $destPid;
4260  // This is the actual pid of the moving to destination
4261  $resolvedPid = $this->resolvePid($table, $destPid);
4262  // 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...)
4263  // $destPid>=0 because we must correct pid in case of versioning "page" types.
4264  if ($destPid < 0 && !$sortRow || $destPid >= 0) {
4265  $destPid = $resolvedPid;
4266  }
4267  // Get this before we change the pid (for logging)
4268  $propArr = $this->getRecordProperties($table, $uid);
4269  $moveRec = $this->getRecordProperties($table, $uid, true);
4270  // Prepare user defined objects (if any) for hooks which extend this function:
4271  $hookObjectsArr = [];
4272  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'])) {
4273  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] as $classRef) {
4274  $hookObjectsArr[] = GeneralUtility::getUserObj($classRef);
4275  }
4276  }
4277  // Timestamp field:
4278  $updateFields = [];
4279  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4280  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4281  }
4282  // Insert as first element on page (where uid = $destPid)
4283  if ($destPid >= 0) {
4284  if ($table != 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4285  // Clear cache before moving
4286  list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
4287  $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4288  // Setting PID
4289  $updateFields['pid'] = $destPid;
4290  // Table is sorted by 'sortby'
4291  if ($sortRow) {
4292  $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4293  $updateFields[$sortRow] = $sortNumber;
4294  }
4295  // Check for child records that have also to be moved
4296  $this->moveRecord_procFields($table, $uid, $destPid);
4297  // Create query for update:
4298  GeneralUtility::makeInstance(ConnectionPool::class)
4299  ->getConnectionForTable($table)
4300  ->update($table, $updateFields, ['uid' => (int)$uid]);
4301  // Check for the localizations of that element
4302  $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4303  // Call post processing hooks:
4304  foreach ($hookObjectsArr as $hookObj) {
4305  if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4306  $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4307  }
4308  }
4309  if ($this->enableLogging) {
4310  // Logging...
4311  $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4312  if ($destPid != $propArr['pid']) {
4313  // Logged to old page
4314  $newPropArr = $this->getRecordProperties($table, $uid);
4315  $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4316  $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']);
4317  // Logged to new page
4318  $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);
4319  } else {
4320  // Logged to new page
4321  $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);
4322  }
4323  }
4324  // Clear cache after moving
4325  $this->registerRecordIdForPageCacheClearing($table, $uid);
4326  $this->fixUniqueInPid($table, $uid);
4327  // fixCopyAfterDuplFields
4328  if ($origDestPid < 0) {
4329  $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
4330  }
4331  } elseif ($this->enableLogging) {
4332  $destPropArr = $this->getRecordProperties('pages', $destPid);
4333  $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']);
4334  }
4335  } else {
4336  // Put after another record
4337  // Table is being sorted
4338  if ($sortRow) {
4339  // Save the position to which the original record is requested to be moved
4340  $originalRecordDestinationPid = $destPid;
4341  $sortInfo = $this->getSortNumber($table, $uid, $destPid);
4342  // Setting the destPid to the new pid of the record.
4343  $destPid = $sortInfo['pid'];
4344  // If not an array, there was an error (which is already logged)
4345  if (is_array($sortInfo)) {
4346  if ($table != 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4347  // clear cache before moving
4348  $this->registerRecordIdForPageCacheClearing($table, $uid);
4349  // We now update the pid and sortnumber
4350  $updateFields['pid'] = $destPid;
4351  $updateFields[$sortRow] = $sortInfo['sortNumber'];
4352  // Check for child records that have also to be moved
4353  $this->moveRecord_procFields($table, $uid, $destPid);
4354  // Create query for update:
4355  GeneralUtility::makeInstance(ConnectionPool::class)
4356  ->getConnectionForTable($table)
4357  ->update($table, $updateFields, ['uid' => (int)$uid]);
4358  // Check for the localizations of that element
4359  $this->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
4360  // Call post processing hooks:
4361  foreach ($hookObjectsArr as $hookObj) {
4362  if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4363  $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4364  }
4365  }
4366  if ($this->enableLogging) {
4367  // Logging...
4368  $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4369  if ($destPid != $propArr['pid']) {
4370  // Logged to old page
4371  $newPropArr = $this->getRecordProperties($table, $uid);
4372  $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4373  $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']);
4374  // Logged to old page
4375  $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);
4376  } else {
4377  // Logged to old page
4378  $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);
4379  }
4380  }
4381  // Clear cache after moving
4382  $this->registerRecordIdForPageCacheClearing($table, $uid);
4383  // fixUniqueInPid
4384  $this->fixUniqueInPid($table, $uid);
4385  // fixCopyAfterDuplFields
4386  if ($origDestPid < 0) {
4387  $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
4388  }
4389  } elseif ($this->enableLogging) {
4390  $destPropArr = $this->getRecordProperties('pages', $destPid);
4391  $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']);
4392  }
4393  }
4394  } elseif ($this->enableLogging) {
4395  $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']);
4396  }
4397  }
4398  }
4399 
4409  public function moveRecord_procFields($table, $uid, $destPid)
4410  {
4411  $conf = $GLOBALS['TCA'][$table]['columns'];
4412  $row = BackendUtility::getRecordWSOL($table, $uid);
4413  if (is_array($row)) {
4414  foreach ($row as $field => $value) {
4415  $this->moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf[$field]['config']);
4416  }
4417  }
4418  }
4419 
4431  public function moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf)
4432  {
4433  if ($conf['type'] == 'inline') {
4434  $foreign_table = $conf['foreign_table'];
4435  $moveChildrenWithParent = !isset($conf['behaviour']['disableMovingChildrenWithParent']) || !$conf['behaviour']['disableMovingChildrenWithParent'];
4436  if ($foreign_table && $moveChildrenWithParent) {
4437  $inlineType = $this->getInlineFieldType($conf);
4438  if ($inlineType == 'list' || $inlineType == 'field') {
4439  if ($table == 'pages') {
4440  // If the inline elements are related to a page record,
4441  // make sure they reside at that page and not at its parent
4442  $destPid = $uid;
4443  }
4444  $dbAnalysis = $this->createRelationHandlerInstance();
4445  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
4446  }
4447  }
4448  }
4449  // Move the records
4450  if (isset($dbAnalysis)) {
4451  // Moving records to a positive destination will insert each
4452  // record at the beginning, thus the order is reversed here:
4453  foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4454  $this->moveRecord($v['table'], $v['id'], $destPid);
4455  }
4456  }
4457  }
4458 
4468  public function moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
4469  {
4470  // There's no need to perform this for page-records or not localizable tables
4471  if (!BackendUtility::isTableLocalizable($table) || $table === 'pages' || $table === 'pages_language_overlay') {
4472  return;
4473  }
4474  $where = '';
4475  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
4476  $where = ' AND t3ver_oid=0';
4477  }
4478  $l10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], $uid, $where);
4479  if (is_array($l10nRecords)) {
4480  $localizedDestPids = [];
4481  // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4482  if ($originalRecordDestinationPid < 0) {
4483  // Get the localized records of the record we are inserting after
4484  $destL10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], abs($originalRecordDestinationPid), $where);
4485  // Index the localized record uids by language
4486  if (is_array($destL10nRecords)) {
4487  foreach ($destL10nRecords as $record) {
4488  $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4489  }
4490  }
4491  }
4492  // Move the localized records after the corresponding localizations of the destination record
4493  foreach ($l10nRecords as $record) {
4494  $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4495  if ($localizedDestPid < 0) {
4496  $this->moveRecord($table, $record['uid'], $localizedDestPid);
4497  } else {
4498  $this->moveRecord($table, $record['uid'], $destPid);
4499  }
4500  }
4501  }
4502  }
4503 
4512  public function localize($table, $uid, $language)
4513  {
4514  $newId = false;
4515  $uid = (int)$uid;
4516  if (!$GLOBALS['TCA'][$table] || !$uid || $this->isNestedElementCallRegistered($table, $uid, 'localize') !== false) {
4517  return false;
4518  }
4519 
4520  $this->registerNestedElementCall($table, $uid, 'localize');
4521  if ((!$GLOBALS['TCA'][$table]['ctrl']['languageField'] || !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] || $table === 'pages_language_overay') && $table !== 'pages') {
4522  if ($this->enableLogging) {
4523  $this->newlog('Localization failed; "languageField" and "transOrigPointerField" must be defined for the table!', 1);
4524  }
4525  return false;
4526  }
4527 
4528  $langRec = BackendUtility::getRecord('sys_language', (int)$language, 'uid,title');
4529  if (!$langRec) {
4530  if ($this->enableLogging) {
4531  $this->newlog('Sys language UID "' . $language . '" not found valid!', 1);
4532  }
4533  return false;
4534  }
4535 
4536  if (!$this->doesRecordExist($table, $uid, 'show')) {
4537  if ($this->enableLogging) {
4538  $this->newlog('Attempt to localize record without permission', 1);
4539  }
4540  return false;
4541  }
4542 
4543  // Getting workspace overlay if possible - this will localize versions in workspace if any
4544  $row = BackendUtility::getRecordWSOL($table, $uid);
4545  if (!is_array($row)) {
4546  if ($this->enableLogging) {
4547  $this->newlog('Attempt to localize record that did not exist!', 1);
4548  }
4549  return false;
4550  }
4551 
4552  // Make sure that records which are translated from another language than the default language have a correct
4553  // localization source set themselves, before translating them to another language.
4554  if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4555  && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
4556  && $table !== 'pages') {
4557  $localizationParentRecord = BackendUtility::getRecord(
4558  $table,
4559  $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]);
4560  if ((int)$localizationParentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== 0) {
4561  if ($this->enableLogging) {
4562  $this->newlog('Localization failed; Source record contained a reference to an original record that is not a default record (which is strange)!', 1);
4563  }
4564  return false;
4565  }
4566  }
4567 
4568  // Default language records must never have a localization parent as they are the origin of any translation.
4569  if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4570  && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0
4571  && $table !== 'pages') {
4572  if ($this->enableLogging) {
4573  $this->newlog('Localization failed; Source record contained a reference to an original default record but is a default record itself (which is strange)!', 1);
4574  }
4575  return false;
4576  }
4577 
4578  if ($table === 'pages') {
4579  $pass = !BackendUtility::getRecordsByField('pages_language_overlay', 'pid', $uid, (' AND ' . $GLOBALS['TCA']['pages_language_overlay']['ctrl']['languageField'] . '=' . (int)$langRec['uid']));
4580  $Ttable = 'pages_language_overlay';
4581  } else {
4582  $pass = !BackendUtility::getRecordLocalization($table, $uid, $langRec['uid'], ('AND pid=' . (int)$row['pid']));
4583  $Ttable = $table;
4584  }
4585 
4586  if (!$pass) {
4587  if ($this->enableLogging) {
4588  $this->newlog('Localization failed; There already was a localization for this language of the record!', 1);
4589  }
4590  return false;
4591  }
4592 
4593  // Initialize:
4594  $overrideValues = [];
4595  $excludeFields = [];
4596  // Set override values:
4597  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['languageField']] = $langRec['uid'];
4598  // If the translated record is a default language record, set it's uid as localization parent of the new record.
4599  // If translating from any other language, no override is needed; we just can copy the localization parent of
4600  // the original record (which is pointing to the correspondent default language record) to the new record.
4601  if ($row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0 || $table === 'pages') {
4602  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['transOrigPointerField']] = $uid;
4603  }
4604  // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4605  if (isset($GLOBALS['TCA'][$table]['ctrl']['type']) && isset($GLOBALS['TCA'][$Ttable]['ctrl']['type'])) {
4606  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['type']] = $row[$GLOBALS['TCA'][$table]['ctrl']['type']];
4607  }
4608  // Set exclude Fields:
4609  foreach ($GLOBALS['TCA'][$Ttable]['columns'] as $fN => $fCfg) {
4610  $translateToMsg = '';
4611  // Check if we are just prefixing:
4612  if ($fCfg['l10n_mode'] == 'prefixLangTitle') {
4613  if (($fCfg['config']['type'] == 'text' || $fCfg['config']['type'] == 'input') && (string)$row[$fN] !== '') {
4614  list($tscPID) = BackendUtility::getTSCpid($table, $uid, '');
4615  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
4616  if (!empty($TSConfig['translateToMessage'])) {
4617  $translateToMsg = $GLOBALS['LANG'] ? $GLOBALS['LANG']->sL($TSConfig['translateToMessage']) : $TSConfig['translateToMessage'];
4618  $translateToMsg = @sprintf($translateToMsg, $langRec['title']);
4619  }
4620  if (empty($translateToMsg)) {
4621  $translateToMsg = 'Translate to ' . $langRec['title'] . ':';
4622  } else {
4623  $translateToMsg = @sprintf($TSConfig['translateToMessage'], $langRec['title']);
4624  }
4625  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processTranslateToClass'])) {
4626  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processTranslateToClass'] as $classRef) {
4627  $hookObj = GeneralUtility::getUserObj($classRef);
4628  if (method_exists($hookObj, 'processTranslateTo_copyAction')) {
4629  $hookObj->processTranslateTo_copyAction($row[$fN], $langRec, $this);
4630  }
4631  }
4632  }
4633  $overrideValues[$fN] = '[' . $translateToMsg . '] ' . $row[$fN];
4634  }
4635  } elseif (
4636  ($fCfg['l10n_mode'] === 'exclude' || $fCfg['l10n_mode'] === 'noCopy' || $fCfg['l10n_mode'] === 'mergeIfNotBlank')
4637  && $fN != $GLOBALS['TCA'][$Ttable]['ctrl']['languageField']
4638  && $fN != $GLOBALS['TCA'][$Ttable]['ctrl']['transOrigPointerField']
4639  ) {
4640  // Otherwise, do not copy field (unless it is the language field or
4641  // pointer to the original language)
4642  $excludeFields[] = $fN;
4643  }
4644  }
4645  if ($Ttable === $table) {
4646  // Get the uid of record after which this localized record should be inserted
4647  $previousUid = $this->getPreviousLocalizedRecordUid($table, $uid, $row['pid'], $language);
4648  // Execute the copy:
4649  $newId = $this->copyRecord($table, $uid, -$previousUid, 1, $overrideValues, implode(',', $excludeFields), $language);
4650  $autoVersionNewId = $this->getAutoVersionId($table, $newId);
4651  if (is_null($autoVersionNewId) === false) {
4652  $this->triggerRemapAction($table, $newId, [$this, 'placeholderShadowing'], [$table, $autoVersionNewId], true);
4653  }
4654  } else {
4655  // Create new record:
4657  $copyTCE = $this->getLocalTCE();
4658  $copyTCE->start([$Ttable => ['NEW' => $overrideValues]], '', $this->BE_USER);
4659  $copyTCE->process_datamap();
4660  // Getting the new UID as if it had been copied:
4661  $theNewSQLID = $copyTCE->substNEWwithIDs['NEW'];
4662  if ($theNewSQLID) {
4663  // 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"
4664  $this->copyMappingArray[$Ttable][$uid] = $theNewSQLID;
4665  $newId = $theNewSQLID;
4666  }
4667  }
4668 
4669  return $newId;
4670  }
4671 
4689  protected function inlineLocalizeSynchronize($table, $id, $command)
4690  {
4691  $parentRecord = BackendUtility::getRecordWSOL($table, $id);
4692 
4693  // Backward-compatibility handling
4694  if (!is_array($command)) {
4695  // <field>, (localize | synchronize | <uid>):
4696  $parts = GeneralUtility::trimExplode(',', $command);
4697  $command = [];
4698  $command['field'] = $parts[0];
4699  // The previous process expected $id to point to the localized record already
4700  $command['language'] = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
4701 
4702  if (!MathUtility::canBeInterpretedAsInteger($parts[1])) {
4703  $command['action'] = $parts[1];
4704  } else {
4705  $command['ids'] = [$parts[1]];
4706  }
4707  }
4708 
4709  // In case the parent record is the default language record, fetch the localization
4710  if (empty($parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
4711  // Fetch the live record
4712  $parentRecordLocalization = BackendUtility::getRecordLocalization($table, $id, $command['language'], 'AND pid<>-1');
4713  if (empty($parentRecordLocalization)) {
4714  $this->newlog2('Localization for parent record ' . $table . ':' . $id . '" cannot be fetched', $table, $id, $parentRecord['pid']);
4715  return;
4716  }
4717  $parentRecord = $parentRecordLocalization[0];
4718  $id = $parentRecord['uid'];
4719  // Process overlay for current selected workspace
4720  BackendUtility::workspaceOL($table, $parentRecord);
4721  }
4722 
4723  $field = $command['field'];
4724  $language = $command['language'];
4725  $action = $command['action'];
4726  $ids = $command['ids'];
4727 
4728  if (!$field || !($action === 'localize' || $action === 'synchronize') && empty($ids) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4729  return;
4730  }
4731 
4732  $config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
4733  $foreignTable = $config['foreign_table'];
4734  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $config);
4735  if ($localizationMode !== 'select') {
4736  return;
4737  }
4738 
4739  $transOrigPointer = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
4740  $transOrigTable = BackendUtility::getOriginalTranslationTable($table);
4741  $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
4742 
4743  if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) {
4744  return;
4745  }
4746 
4747  $inlineSubType = $this->getInlineFieldType($config);
4748  $transOrigRecord = BackendUtility::getRecordWSOL($transOrigTable, $transOrigPointer);
4749 
4750  if ($inlineSubType === false) {
4751  return;
4752  }
4753 
4754  $removeArray = [];
4755  $mmTable = $inlineSubType == 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : '';
4756  // Fetch children from original language parent:
4758  $dbAnalysisOriginal = $this->createRelationHandlerInstance();
4759  $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $transOrigTable, $config);
4760  $elementsOriginal = [];
4761  foreach ($dbAnalysisOriginal->itemArray as $item) {
4762  $elementsOriginal[$item['id']] = $item;
4763  }
4764  unset($dbAnalysisOriginal);
4765  // Fetch children from current localized parent:
4767  $dbAnalysisCurrent = $this->createRelationHandlerInstance();
4768  $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config);
4769  // Perform synchronization: Possibly removal of already localized records:
4770  if ($action === 'synchronize') {
4771  foreach ($dbAnalysisCurrent->itemArray as $index => $item) {
4772  $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']);
4773  if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) {
4774  $childTransOrigPointer = $childRecord[$childTransOrigPointerField];
4775  // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it:
4776  if (!isset($elementsOriginal[$childTransOrigPointer])) {
4777  unset($dbAnalysisCurrent->itemArray[$index]);
4778  $removeArray[$item['table']][$item['id']]['delete'] = 1;
4779  }
4780  }
4781  }
4782  }
4783  // Perform synchronization/localization: Possibly add unlocalized records for original language:
4784  if ($action === 'localize' || $action === 'synchronize') {
4785  foreach ($elementsOriginal as $originalId => $item) {
4786  $item['id'] = $this->localize($item['table'], $item['id'], $language);
4787  $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4788  $dbAnalysisCurrent->itemArray[] = $item;
4789  }
4790  } elseif (!empty($ids)) {
4791  foreach ($ids as $childId) {
4792  if (!MathUtility::canBeInterpretedAsInteger($childId) || !isset($elementsOriginal[$childId])) {
4793  continue;
4794  }
4795  $item = $elementsOriginal[$childId];
4796  $item['id'] = $this->localize($item['table'], $item['id'], $language);
4797  $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4798  $dbAnalysisCurrent->itemArray[] = $item;
4799  }
4800  }
4801  // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4802  $value = implode(',', $dbAnalysisCurrent->getValueArray());
4803  $this->registerDBList[$table][$id][$field] = $value;
4804  // Remove child records (if synchronization requested it):
4805  if (is_array($removeArray) && !empty($removeArray)) {
4807  $tce = GeneralUtility::makeInstance(__CLASS__);
4808  $tce->enableLogging = $this->enableLogging;
4809  $tce->start([], $removeArray);
4810  $tce->process_cmdmap();
4811  unset($tce);
4812  }
4813  $updateFields = [];
4814  // Handle, reorder and store relations:
4815  if ($inlineSubType == 'list') {
4816  $updateFields = [$field => $value];
4817  } elseif ($inlineSubType == 'field') {
4818  $dbAnalysisCurrent->writeForeignField($config, $id);
4819  $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
4820  } elseif ($inlineSubType == 'mm') {
4821  $dbAnalysisCurrent->writeMM($config['MM'], $id);
4822  $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
4823  }
4824  // Update field referencing to child records of localized parent record:
4825  if (!empty($updateFields)) {
4826  $this->updateDB($table, $id, $updateFields);
4827  }
4828  }
4829 
4830  /*********************************************
4831  *
4832  * Cmd: Deleting
4833  *
4834  ********************************************/
4842  public function deleteAction($table, $id)
4843  {
4844  $recordToDelete = BackendUtility::getRecord($table, $id);
4845  // Record asked to be deleted was found:
4846  if (is_array($recordToDelete)) {
4847  $recordWasDeleted = false;
4848  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'])) {
4849  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] as $classRef) {
4850  $hookObj = GeneralUtility::getUserObj($classRef);
4851  if (method_exists($hookObj, 'processCmdmap_deleteAction')) {
4852  $hookObj->processCmdmap_deleteAction($table, $id, $recordToDelete, $recordWasDeleted, $this);
4853  }
4854  }
4855  }
4856  // Delete the record if a hook hasn't deleted it yet
4857  if (!$recordWasDeleted) {
4858  $this->deleteEl($table, $id);
4859  }
4860  }
4861  }
4862 
4872  public function deleteEl($table, $uid, $noRecordCheck = false, $forceHardDelete = false)
4873  {
4874  if ($table == 'pages') {
4875  $this->deletePages($uid, $noRecordCheck, $forceHardDelete);
4876  } else {
4877  $this->deleteVersionsForRecord($table, $uid, $forceHardDelete);
4878  $this->deleteRecord($table, $uid, $noRecordCheck, $forceHardDelete);
4879  }
4880  }
4881 
4890  public function deleteVersionsForRecord($table, $uid, $forceHardDelete)
4891  {
4892  $versions = BackendUtility::selectVersionsOfRecord($table, $uid, 'uid,pid,t3ver_wsid,t3ver_state', $this->BE_USER->workspace ?: null);
4893  if (is_array($versions)) {
4894  foreach ($versions as $verRec) {
4895  if (!$verRec['_CURRENT_VERSION']) {
4896  if ($table == 'pages') {
4897  $this->deletePages($verRec['uid'], true, $forceHardDelete);
4898  } else {
4899  $this->deleteRecord($table, $verRec['uid'], true, $forceHardDelete);
4900  }
4901 
4902  // Delete move-placeholder
4903  $versionState = VersionState::cast($verRec['t3ver_state']);
4904  if ($versionState->equals(VersionState::MOVE_POINTER)) {
4905  $versionMovePlaceholder = BackendUtility::getMovePlaceholder($table, $uid, 'uid', $verRec['t3ver_wsid']);
4906  if (!empty($versionMovePlaceholder)) {
4907  $this->deleteEl($table, $versionMovePlaceholder['uid'], true, $forceHardDelete);
4908  }
4909  }
4910  }
4911  }
4912  }
4913  }
4914 
4922  public function undeleteRecord($table, $uid)
4923  {
4924  if ($this->isRecordUndeletable($table, $uid)) {
4925  $this->deleteRecord($table, $uid, true, false, true);
4926  }
4927  }
4928 
4942  public function deleteRecord($table, $uid, $noRecordCheck = false, $forceHardDelete = false, $undeleteRecord = false)
4943  {
4944  $uid = (int)$uid;
4945  if (!$GLOBALS['TCA'][$table] || !$uid) {
4946  if ($this->enableLogging) {
4947  $this->log($table, $uid, 3, 0, 1, 'Attempt to delete record without delete-permissions. [' . $this->BE_USER->errorMsg . ']');
4948  }
4949  return;
4950  }
4951 
4952  // Checking if there is anything else disallowing deleting the record by checking if editing is allowed
4953  $deletedRecord = $forceHardDelete || $undeleteRecord;
4954  $hasEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, $deletedRecord, true);
4955  if (!$hasEditAccess) {
4956  if ($this->enableLogging) {
4957  $this->log($table, $uid, 3, 0, 1, 'Attempt to delete record without delete-permissions');
4958  }
4959  return;
4960  }
4961  if (!$noRecordCheck && !$this->doesRecordExist($table, $uid, 'delete')) {
4962  return;
4963  }
4964 
4965  // Clear cache before deleting the record, else the correct page cannot be identified by clear_cache
4966  list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
4967  $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4968  $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
4969  $databaseErrorMessage = '';
4970  if ($deleteField && !$forceHardDelete) {
4971  $updateFields = [
4972  $deleteField => $undeleteRecord ? 0 : 1
4973  ];
4974  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4975  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4976  }
4977  // If the table is sorted, then the sorting number is set very high
4978  if ($GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$undeleteRecord) {
4979  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = 1000000000;
4980  }
4981  // before (un-)deleting this record, check for child records or references
4982  $this->deleteRecord_procFields($table, $uid, $undeleteRecord);
4983  try {
4984  GeneralUtility::makeInstance(ConnectionPool::class)
4985  ->getConnectionForTable($table)
4986  ->update($table, $updateFields, ['uid' => (int)$uid]);
4987  // Delete all l10n records as well, impossible during undelete because it might bring too many records back to life
4988  if (!$undeleteRecord) {
4989  $this->deletedRecords[$table][] = (int)$uid;
4990  $this->deleteL10nOverlayRecords($table, $uid);
4991  }
4992  } catch (DBALException $e) {
4993  $databaseErrorMessage = $e->getPrevious()->getMessage();
4994  }
4995  } else {
4996  // Fetches all fields with flexforms and look for files to delete:
4997  foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $cfg) {
4998  $conf = $cfg['config'];
4999  switch ($conf['type']) {
5000  case 'flex':
5001  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
5002  $flexObj->traverseFlexFormXMLData($table, $fieldName, BackendUtility::getRecordRaw($table, 'uid=' . (int)$uid), $this, 'deleteRecord_flexFormCallBack');
5003  break;
5004  }
5005  }
5006  // Fetches all fields that holds references to files
5007  $fileFieldArr = $this->extFileFields($table);
5008  if (!empty($fileFieldArr)) {
5009  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5010  $queryBuilder->getRestrictions()->removeAll();
5011  $result = $queryBuilder
5012  ->select(...$fileFieldArr)
5013  ->from($table)
5014  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
5015  ->execute();
5016  if ($row = $result->fetch()) {
5017  $fArray = $fileFieldArr;
5018  // MISSING: Support for MM file relations!
5019  foreach ($fArray as $theField) {
5020  // This deletes files that belonged to this record.
5021  $this->extFileFunctions($table, $theField, $row[$theField], 'deleteAll');
5022  }
5023  } elseif ($this->enableLogging) {
5024  $this->log($table, $uid, 3, 0, 100, 'Delete: Zero rows in result when trying to read filenames from record which should be deleted');
5025  }
5026  }
5027  // Delete the hard way...:
5028  try {
5029  GeneralUtility::makeInstance(ConnectionPool::class)
5030  ->getConnectionForTable($table)
5031  ->delete($table, ['uid' => (int)$uid]);
5032  $this->deletedRecords[$table][] = (int)$uid;
5033  $this->deleteL10nOverlayRecords($table, $uid);
5034  } catch (DBALException $e) {
5035  $databaseErrorMessage = $e->getPrevious()->getMessage();
5036  }
5037  }
5038  if ($this->enableLogging) {
5039  // 1 means insert, 3 means delete
5040  $state = $undeleteRecord ? 1 : 3;
5041  if ($databaseErrorMessage === '') {
5042  if ($forceHardDelete) {
5043  $message = 'Record \'%s\' (%s) was deleted unrecoverable from page \'%s\' (%s)';
5044  } else {
5045  $message = $state == 1 ? 'Record \'%s\' (%s) was restored on page \'%s\' (%s)' : 'Record \'%s\' (%s) was deleted from page \'%s\' (%s)';
5046  }
5047  $propArr = $this->getRecordProperties($table, $uid);
5048  $pagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
5049 
5050  $this->log($table, $uid, $state, 0, 0, $message, 0, [
5051  $propArr['header'],
5052  $table . ':' . $uid,
5053  $pagePropArr['header'],
5054  $propArr['pid']
5055  ], $propArr['event_pid']);
5056  } else {
5057  $this->log($table, $uid, $state, 0, 100, $databaseErrorMessage);
5058  }
5059  }
5060  // Update reference index:
5061  $this->updateRefIndex($table, $uid);
5062 
5063  // We track calls to update the reference index as to avoid calling it twice
5064  // with the same arguments. This is done because reference indexing is quite
5065  // costly and the update reference index stack usually contain duplicates.
5066  // NB: also filled and checked in loop below. The initialisation prevents
5067  // running the "root" record twice if it appears in the stack twice.
5068  $updateReferenceIndexCalls = [[$table, $uid]];
5069 
5070  // If there are entries in the updateRefIndexStack
5071  if (is_array($this->updateRefIndexStack[$table]) && is_array($this->updateRefIndexStack[$table][$uid])) {
5072  while ($args = array_pop($this->updateRefIndexStack[$table][$uid])) {
5073  if (!in_array($args, $updateReferenceIndexCalls, true)) {
5074  // $args[0]: table, $args[1]: uid
5075  $this->updateRefIndex($args[0], $args[1]);
5076  $updateReferenceIndexCalls[] = $args;
5077  }
5078  }
5079  unset($this->updateRefIndexStack[$table][$uid]);
5080  }
5081  }
5082 
5093  public function deleteRecord_flexFormCallBack($dsArr, $dataValue, $PA, $structurePath, $pObj)
5094  {
5095  // Use reference index object to find files in fields:
5097  $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
5098  $files = $refIndexObj->getRelations_procFiles($dataValue, $dsArr['TCEforms']['config'], $PA['uid']);
5099  // Traverse files and delete them if the field is a regular file field (and not a file_reference field)
5100  if (is_array($files) && $dsArr['TCEforms']['config']['internal_type'] === 'file') {
5101  foreach ($files as $dat) {
5102  if (@is_file($dat['ID_absFile'])) {
5103  $file = $this->getResourceFactory()->retrieveFileOrFolderObject($dat['ID_absFile']);
5104  $file->delete();
5105  } elseif ($this->enableLogging) {
5106  $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');
5107  }
5108  }
5109  }
5110  }
5111 
5120  public function deletePages($uid, $force = false, $forceHardDelete = false)
5121  {
5122  $uid = (int)$uid;
5123  if ($uid === 0) {
5124  $this->newlog2('Deleting all pages starting from the root-page is disabled.', 'pages', 0, 0, 2);
5125  return;
5126  }
5127  // Getting list of pages to delete:
5128  if ($force) {
5129  // Returns the branch WITHOUT permission checks (0 secures that)
5130  $brExist = $this->doesBranchExist('', $uid, 0, 1);
5131  $res = GeneralUtility::trimExplode(',', $brExist . $uid, true);
5132  } else {
5133  $res = $this->canDeletePage($uid);
5134  }
5135  // Perform deletion if not error:
5136  if (is_array($res)) {
5137  foreach ($res as $deleteId) {
5138  $this->deleteSpecificPage($deleteId, $forceHardDelete);
5139  }
5140  } else {
5142  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, true);
5144  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
5146  $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
5147 
5148  if ($this->enableLogging) {
5149  $this->newlog($res, 1);
5150  }
5151  }
5152  }
5153 
5163  public function deleteSpecificPage($uid, $forceHardDelete = false)
5164  {
5165  $uid = (int)$uid;
5166  if ($uid) {
5167  foreach ($GLOBALS['TCA'] as $table => $_) {
5168  if ($table != 'pages') {
5169  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5170  ->getQueryBuilderForTable($table);
5171 
5172  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5173 
5174  $statement = $queryBuilder
5175  ->select('uid')
5176  ->from($table)
5177  ->where($queryBuilder->expr()->eq(
5178  'pid',
5179  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5180  ))
5181  ->execute();
5182 
5183  while ($row = $statement->fetch()) {
5184  $this->copyMovedRecordToNewLocation($table, $row['uid']);
5185  $this->deleteVersionsForRecord($table, $row['uid'], $forceHardDelete);
5186  $this->deleteRecord($table, $row['uid'], true, $forceHardDelete);
5187  }
5188  }
5189  }
5190  $this->copyMovedRecordToNewLocation('pages', $uid);
5191  $this->deleteVersionsForRecord('pages', $uid, $forceHardDelete);
5192  $this->deleteRecord('pages', $uid, true, $forceHardDelete);
5193  }
5194  }
5195 
5209  protected function copyMovedRecordToNewLocation($table, $uid)
5210  {
5211  if ($this->BE_USER->workspace > 0) {
5212  $originalRecord = BackendUtility::getRecord($table, $uid);
5213  $movePlaceholder = BackendUtility::getMovePlaceholder($table, $uid);
5214  // Check whether target page to copied to is different to current page
5215  // Cloning on the same page is superfluous and does not help at all
5216  if (!empty($originalRecord) && !empty($movePlaceholder) && (int)$originalRecord['pid'] !== (int)$movePlaceholder['pid']) {
5217  // If move placeholder exists, copy to new location
5218  // This will create a New placeholder on the new location
5219  // and a version for this new placeholder
5220  $command = [
5221  $table => [
5222  $uid => [
5223  'copy' => '-' . $movePlaceholder['uid']
5224  ]
5225  ]
5226  ];
5228  $dataHandler = GeneralUtility::makeInstance(__CLASS__);
5229  $dataHandler->enableLogging = $this->enableLogging;
5230  $dataHandler->neverHideAtCopy = true;
5231  $dataHandler->start([], $command);
5232  $dataHandler->process_cmdmap();
5233  unset($dataHandler);
5234 
5235  // Delete move placeholder
5236  $this->deleteRecord($table, $movePlaceholder['uid'], true, true);
5237  }
5238  }
5239  }
5240 
5247  public function canDeletePage($uid)
5248  {
5249  // If we may at all delete this page
5250  if (!$this->doesRecordExist('pages', $uid, 'delete')) {
5251  return 'Attempt to delete page without permissions';
5252  }
5253 
5254  if ($this->deleteTree) {
5255  // Returns the branch
5256  $brExist = $this->doesBranchExist('', $uid, $this->pMap['delete'], 1);
5257  // Checks if we had permissions
5258  if ($brExist == -1) {
5259  return 'Attempt to delete pages in branch without permissions';
5260  }
5261 
5262  if (!$this->noRecordsFromUnallowedTables($brExist . $uid)) {
5263  return 'Attempt to delete records from disallowed tables';
5264  }
5265 
5266  $pagesInBranch = GeneralUtility::trimExplode(',', $brExist . $uid, true);
5267  foreach ($pagesInBranch as $pageInBranch) {
5268  if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, true)) {
5269  return 'Attempt to delete page which has prohibited localizations.';
5270  }
5271  }
5272  return $pagesInBranch;
5273  } else {
5274  // returns the branch
5275  $brExist = $this->doesBranchExist('', $uid, $this->pMap['delete'], 1);
5276  // Checks if branch exists
5277  if ($brExist != '') {
5278  return 'Attempt to delete page which has subpages';
5279  }
5280 
5281  if (!$this->noRecordsFromUnallowedTables($uid)) {
5282  return 'Attempt to delete records from disallowed tables';
5283  }
5284 
5285  if ($this->BE_USER->recordEditAccessInternals('pages', $uid, false, false, true)) {
5286  return [$uid];
5287  } else {
5288  return 'Attempt to delete page which has prohibited localizations.';
5289  }
5290  }
5291  }
5292 
5300  public function cannotDeleteRecord($table, $id)
5301  {
5302  if ($table === 'pages') {
5303  $res = $this->canDeletePage($id);
5304  return is_array($res) ? false : $res;
5305  } else {
5306  return $this->doesRecordExist($table, $id, 'delete') ? false : 'No permission to delete record';
5307  }
5308  }
5309 
5317  public function isRecordUndeletable($table, $uid)
5318  {
5319  $result = false;
5320  $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5321  if ($record['pid']) {
5322  $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5323  // The page containing the record is not deleted, thus the record can be undeleted:
5324  if (!$page['deleted']) {
5325  $result = true;
5326  } elseif ($this->enableLogging) {
5327  $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');
5328  }
5329  } else {
5330  // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5331  $result = true;
5332  }
5333  return $result;
5334  }
5335 
5346  public function deleteRecord_procFields($table, $uid, $undeleteRecord = false)
5347  {
5348  $conf = $GLOBALS['TCA'][$table]['columns'];
5349  $row = BackendUtility::getRecord($table, $uid, '*', '', false);
5350  if (empty($row)) {
5351  return;
5352  }
5353  foreach ($row as $field => $value) {
5354  $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5355  }
5356  }
5357 
5371  public function deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf, $undeleteRecord = false)
5372  {
5373  if ($conf['type'] == 'inline') {
5374  $foreign_table = $conf['foreign_table'];
5375  if ($foreign_table) {
5376  $inlineType = $this->getInlineFieldType($conf);
5377  if ($inlineType == 'list' || $inlineType == 'field') {
5379  $dbAnalysis = $this->createRelationHandlerInstance();
5380  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
5381  $dbAnalysis->undeleteRecord = true;
5382 
5383  $enableCascadingDelete = true;
5384  // non type save comparison is intended!
5385  if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5386  $enableCascadingDelete = false;
5387  }
5388 
5389  // Walk through the items and remove them
5390  foreach ($dbAnalysis->itemArray as $v) {
5391  if (!$undeleteRecord) {
5392  if ($enableCascadingDelete) {
5393  $this->deleteAction($v['table'], $v['id']);
5394  }
5395  } else {
5396  $this->undeleteRecord($v['table'], $v['id']);
5397  }
5398  }
5399  }
5400  }
5401  } elseif ($this->isReferenceField($conf)) {
5402  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
5403  $dbAnalysis = $this->createRelationHandlerInstance();
5404  $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5405  foreach ($dbAnalysis->itemArray as $v) {
5406  $this->updateRefIndexStack[$table][$uid][] = [$v['table'], $v['id']];
5407  }
5408  }
5409  }
5410 
5418  public function deleteL10nOverlayRecords($table, $uid)
5419  {
5420  // Check whether table can be localized or has a different table defined to store localizations:
5421  if (!BackendUtility::isTableLocalizable($table) || $table === 'pages' || $table === 'pages_language_overlay') {
5422  return;
5423  }
5424  $where = '';
5425  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
5426  $where = ' AND t3ver_oid=0';
5427  }
5428  $l10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], $uid, $where);
5429  if (is_array($l10nRecords)) {
5430  foreach ($l10nRecords as $record) {
5431  // Ignore workspace delete placeholders. Those records have been marked for
5432  // deletion before - deleting them again in a workspace would revert that state.
5433  if ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5434  BackendUtility::workspaceOL($table, $record);
5435  if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5436  continue;
5437  }
5438  }
5439  $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5440  }
5441  }
5442  }
5443 
5444  /*********************************************
5445  *
5446  * Cmd: Versioning
5447  *
5448  ********************************************/
5460  public function versionizeRecord($table, $id, $label, $delete = false)
5461  {
5462  $id = (int)$id;
5463  // Stop any actions if the record is marked to be deleted:
5464  // (this can occur if IRRE elements are versionized and child elements are removed)
5465  if ($this->isElementToBeDeleted($table, $id)) {
5466  return null;
5467  }
5468  if (!$GLOBALS['TCA'][$table] || !$GLOBALS['TCA'][$table]['ctrl']['versioningWS'] || $id <= 0) {
5469  if ($this->enableLogging) {
5470  $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, 1);
5471  }
5472  return null;
5473  }
5474 
5475  if (!$this->doesRecordExist($table, $id, 'show')) {
5476  if ($this->enableLogging) {
5477  $this->newlog('You didn\'t have correct permissions to make a new version (copy) of this record "' . $table . '" / ' . $id, 1);
5478  }
5479  return null;
5480  }
5481 
5482  // Select main record:
5483  $row = $this->recordInfo($table, $id, 'pid,t3ver_id,t3ver_state');
5484  if (!is_array($row)) {
5485  if ($this->enableLogging) {
5486  $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize did not exist!', 1);
5487  }
5488  return null;
5489  }
5490 
5491  // Record must be online record
5492  if ($row['pid'] < 0) {
5493  if ($this->enableLogging) {
5494  $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (pid=-1)!', 1);
5495  }
5496  return null;
5497  }
5498 
5499  // Record must not be placeholder for moving.
5500  if (VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)) {
5501  if ($this->enableLogging) {
5502  $this->newlog('Record cannot be versioned because it is a placeholder for a moving operation', 1);
5503  }
5504  return null;
5505  }
5506 
5507  if ($delete && $this->cannotDeleteRecord($table, $id)) {
5508  if ($this->enableLogging) {
5509  $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), 1);
5510  }
5511  return null;
5512  }
5513 
5514  // Look for next version number:
5515  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5516  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5517  $highestVerNumber = $queryBuilder
5518  ->select('t3ver_id')
5519  ->from($table)
5520  ->where($queryBuilder->expr()->orX(
5521  $queryBuilder->expr()->andX(
5522  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
5523  $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
5524  ),
5525  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
5526  ))
5527  ->orderBy('t3ver_id', 'DESC')
5528  ->setMaxResults(1)
5529  ->execute()
5530  ->fetchColumn(0);
5531  // Look for version number of the current:
5532  $subVer = $row['t3ver_id'] . '.' . ($highestVerNumber + 1);
5533  // Set up the values to override when making a raw-copy:
5534  $overrideArray = [
5535  't3ver_id' => $highestVerNumber + 1,
5536  't3ver_oid' => $id,
5537  't3ver_label' => $label ?: $subVer . ' / ' . date('d-m-Y H:m:s'),
5538  't3ver_wsid' => $this->BE_USER->workspace,
5539  't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5540  't3ver_count' => 0,
5541  't3ver_stage' => 0,
5542  't3ver_tstamp' => 0
5543  ];
5544  if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5545  $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5546  }
5547  // Checking if the record already has a version in the current workspace of the backend user
5548  if ($this->BE_USER->workspace !== 0) {
5549  // Look for version already in workspace:
5550  $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5551  }
5552  // Create new version of the record and return the new uid
5553  if (empty($versionRecord['uid'])) {
5554  // Create raw-copy and return result:
5555  // The information of the label to be used for the workspace record
5556  // as well as the information whether the record shall be removed
5557  // must be forwarded (creating remove placeholders on a workspace are
5558  // done by copying the record and override several fields).
5559  $workspaceOptions = [
5560  'delete' => $delete,
5561  'label' => $label,
5562  ];
5563  return $this->copyRecord_raw($table, $id, -1, $overrideArray, $workspaceOptions);
5564  // Reuse the existing record and return its uid
5565  // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5566  // did not make much sense since the information is available)
5567  } else {
5568  return $versionRecord['uid'];
5569  }
5570  return null;
5571  }
5572 
5582  public function version_remapMMForVersionSwap($table, $id, $swapWith)
5583  {
5584  // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5585  $currentRec = BackendUtility::getRecord($table, $id);
5586  $swapRec = BackendUtility::getRecord($table, $swapWith);
5587  $this->version_remapMMForVersionSwap_reg = [];
5588  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5589  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5590  $conf = $fConf['config'];
5591  if ($this->isReferenceField($conf)) {
5592  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
5593  $prependName = $conf['type'] == 'group' ? $conf['prepend_tname'] : '';
5594  if ($conf['MM']) {
5596  $dbAnalysis = $this->createRelationHandlerInstance();
5597  $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5598  if (!empty($dbAnalysis->getValueArray($prependName))) {
5599  $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5600  }
5602  $dbAnalysis = $this->createRelationHandlerInstance();
5603  $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5604  if (!empty($dbAnalysis->getValueArray($prependName))) {
5605  $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5606  }
5607  }
5608  } elseif ($conf['type'] == 'flex') {
5609  // Current record
5610  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5611  $fConf,
5612  $table,
5613  $field,
5614  $currentRec
5615  );
5616  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5617  $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5618  if (is_array($currentValueArray)) {
5619  $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5620  }
5621  // Swap record
5622  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5623  $fConf,
5624  $table,
5625  $field,
5626  $swapRec
5627  );
5628  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5629  $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5630  if (is_array($currentValueArray)) {
5631  $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5632  }
5633  }
5634  }
5635  // Execute:
5636  $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5637  }
5638 
5651  public function version_remapMMForVersionSwap_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
5652  {
5653  // Extract parameters:
5654  list($table, $uid, $field) = $pParams;
5655  if ($this->isReferenceField($dsConf)) {
5656  $allowedTables = $dsConf['type'] == 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5657  $prependName = $dsConf['type'] == 'group' ? $dsConf['prepend_tname'] : '';
5658  if ($dsConf['MM']) {
5660  $dbAnalysis = $this->createRelationHandlerInstance();
5661  $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5662  $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5663  }
5664  }
5665  }
5666 
5677  public function version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
5678  {
5679  if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5680  foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5681  $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5682  }
5683  }
5684  if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5685  foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5686  $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5687  }
5688  }
5689  if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5690  foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5691  $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5692  }
5693  }
5694  }
5695 
5696  /*********************************************
5697  *
5698  * Cmd: Helper functions
5699  *
5700  ********************************************/
5701 
5707  protected function getLocalTCE()
5708  {
5709  $copyTCE = GeneralUtility::makeInstance(__CLASS__);
5710  $copyTCE->copyTree = $this->copyTree;
5711  $copyTCE->enableLogging = $this->enableLogging;
5712  // Copy forth the cached TSconfig
5713  $copyTCE->cachedTSconfig = $this->cachedTSconfig;
5714  // Transformations should NOT be carried out during copy
5715  $copyTCE->dontProcessTransformations = true;
5716  // make sure the isImporting flag is transferred, so all hooks know if
5717  // the current process is an import process
5718  $copyTCE->isImporting = $this->isImporting;
5719  return $copyTCE;
5720  }
5721 
5727  public function remapListedDBRecords()
5728  {
5729  if (!empty($this->registerDBList)) {
5730  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5731  foreach ($this->registerDBList as $table => $records) {
5732  foreach ($records as $uid => $fields) {
5733  $newData = [];
5734  $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5735  $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5736  foreach ($fields as $fieldName => $value) {
5737  $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5738  switch ($conf['type']) {
5739  case 'group':
5740 
5741  case 'select':
5742  $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5743  if (is_array($vArray)) {
5744  $newData[$fieldName] = implode(',', $vArray);
5745  }
5746  break;
5747  case 'flex':
5748  if ($value == 'FlexForm_reference') {
5749  // This will fetch the new row for the element
5750  $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5751  if (is_array($origRecordRow)) {
5752  BackendUtility::workspaceOL($table, $origRecordRow);
5753  // Get current data structure and value array:
5754  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5755  [ 'config' => $conf ],
5756  $table,
5757  $fieldName,
5758  $origRecordRow
5759  );
5760  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5761  $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5762  // Do recursive processing of the XML data:
5763  $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
5764  // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5765  if (is_array($currentValueArray['data'])) {
5766  $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
5767  }
5768  }
5769  }
5770  break;
5771  case 'inline':
5772  $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5773  break;
5774  default:
5775  debug('Field type should not appear here: ' . $conf['type']);
5776  }
5777  }
5778  // If any fields were changed, those fields are updated!
5779  if (!empty($newData)) {
5780  $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5781  }
5782  }
5783  }
5784  }
5785  }
5786 
5798  public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
5799  {
5800  // Extract parameters:
5801  list($table, $uid, $field) = $pParams;
5802  // If references are set for this field, set flag so they can be corrected later:
5803  if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
5804  $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
5805  if (is_array($vArray)) {
5806  $dataValue = implode(',', $vArray);
5807  }
5808  }
5809  // Return
5810  return ['value' => $dataValue];
5811  }
5812 
5823  public function remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
5824  {
5825  // Initialize variables
5826  // Will be set TRUE if an upgrade should be done...
5827  $set = false;
5828  // Allowed tables for references.
5829  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
5830  // Table name to prepend the UID
5831  $prependName = $conf['type'] == 'group' ? $conf['prepend_tname'] : '';
5832  // Which tables that should possibly not be remapped
5833  $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
5834  // Convert value to list of references:
5835  $dbAnalysis = $this->createRelationHandlerInstance();
5836  $dbAnalysis->registerNonTableValues = $conf['type'] == 'select' && $conf['allowNonIdValues'];
5837  $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
5838  // Traverse those references and map IDs:
5839  foreach ($dbAnalysis->itemArray as $k => $v) {
5840  $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
5841  if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
5842  $dbAnalysis->itemArray[$k]['id'] = $mapID;
5843  $set = true;
5844  }
5845  }
5846  if (!empty($conf['MM'])) {
5847  // Purge invalid items (live/version)
5848  $dbAnalysis->purgeItemArray();
5849  if ($dbAnalysis->isPurged()) {
5850  $set = true;
5851  }
5852 
5853  // If record has been versioned/copied in this process, handle invalid relations of the live record
5854  $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
5855  if (!empty($this->copyMappingArray_merged[$table])) {
5856  $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
5857  }
5858  if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
5859  $liveRelations = $this->createRelationHandlerInstance();
5860  $liveRelations->setWorkspaceId(0);
5861  $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
5862  // Purge invalid relations in the live workspace ("0")
5863  $liveRelations->purgeItemArray(0);
5864  if ($liveRelations->isPurged()) {
5865  $liveRelations->writeMM($conf['MM'], $liveId, $prependName);
5866  }
5867  }
5868  }
5869  // If a change has been done, set the new value(s)
5870  if ($set) {
5871  if ($conf['MM']) {
5872  $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5873  } else {
5874  return $dbAnalysis->getValueArray($prependName);
5875  }
5876  }
5877  return null;
5878  }
5879 
5889  public function remapListedDBRecords_procInline($conf, $value, $uid, $table)
5890  {
5891  $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5892  if ($conf['foreign_table']) {
5893  $inlineType = $this->getInlineFieldType($conf);
5894  if ($inlineType == 'mm') {
5895  $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5896  } elseif ($inlineType !== false) {
5898  $dbAnalysis = $this->createRelationHandlerInstance();
5899  $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
5900 
5901  // Keep original (live) item array and update values for specific versioned records
5902  $originalItemArray = $dbAnalysis->itemArray;
5903  foreach ($dbAnalysis->itemArray as &$item) {
5904  $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
5905  if (!empty($versionedId)) {
5906  $item['id'] = $versionedId;
5907  }
5908  }
5909 
5910  // Update child records if using pointer fields ('foreign_field'):
5911  if ($inlineType == 'field') {
5912  $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
5913  }
5914  $thePidToUpdate = null;
5915  // If the current field is set on a page record, update the pid of related child records:
5916  if ($table == 'pages') {
5917  $thePidToUpdate = $theUidToUpdate;
5918  } elseif (isset($this->registerDBPids[$table][$uid])) {
5919  $thePidToUpdate = $this->registerDBPids[$table][$uid];
5920  $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
5921  }
5922  // Update child records if change to pid is required (only if the current record is not on a workspace):
5923  if ($thePidToUpdate) {
5924  $updateValues = ['pid' => $thePidToUpdate];
5925  foreach ($originalItemArray as $v) {
5926  if ($v['id'] && $v['table'] && is_null(BackendUtility::getLiveVersionIdOfRecord($v['table'], $v['id']))) {
5927  GeneralUtility::makeInstance(ConnectionPool::class)
5928  ->getConnectionForTable($v['table'])
5929  ->update($v['table'], $updateValues, ['uid' => (int)$v['id']]);
5930  }
5931  }
5932  }
5933  }
5934  }
5935  }
5936 
5943  public function processRemapStack()
5944  {
5945  // Processes the remap stack:
5946  if (is_array($this->remapStack)) {
5947  $remapFlexForms = [];
5948 
5949  foreach ($this->remapStack as $remapAction) {
5950  // If no position index for the arguments was set, skip this remap action:
5951  if (!is_array($remapAction['pos'])) {
5952  continue;
5953  }
5954  // Load values from the argument array in remapAction:
5955  $field = $remapAction['field'];
5956  $id = $remapAction['args'][$remapAction['pos']['id']];
5957  $rawId = $id;
5958  $table = $remapAction['args'][$remapAction['pos']['table']];
5959  $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
5960  $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
5961  $additionalData = $remapAction['additionalData'];
5962  // The record is new and has one or more new ids (in case of versioning/workspaces):
5963  if (strpos($id, 'NEW') !== false) {
5964  // Replace NEW...-ID with real uid:
5965  $id = $this->substNEWwithIDs[$id];
5966  // If the new parent record is on a non-live workspace or versionized, it has another new id:
5967  if (isset($this->autoVersionIdMap[$table][$id])) {
5968  $id = $this->autoVersionIdMap[$table][$id];
5969  }
5970  $remapAction['args'][$remapAction['pos']['id']] = $id;
5971  }
5972  // Replace relations to NEW...-IDs in field value (uids of child records):
5973  if (is_array($valueArray)) {
5974  foreach ($valueArray as $key => $value) {
5975  if (strpos($value, 'NEW') !== false) {
5976  if (strpos($value, '_') === false) {
5977  $affectedTable = $tcaFieldConf['foreign_table'];
5978  $prependTable = false;
5979  } else {
5980  $parts = explode('_', $value);
5981  $value = array_pop($parts);
5982  $affectedTable = implode('_', $parts);
5983  $prependTable = true;
5984  }
5985  $value = $this->substNEWwithIDs[$value];
5986  // The record is new, but was also auto-versionized and has another new id:
5987  if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
5988  $value = $this->autoVersionIdMap[$affectedTable][$value];
5989  }
5990  if ($prependTable) {
5991  $value = $affectedTable . '_' . $value;
5992  }
5993  // Set a hint that this was a new child record:
5994  $this->newRelatedIDs[$affectedTable][] = $value;
5995  $valueArray[$key] = $value;
5996  }
5997  }
5998  $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
5999  }
6000  // Process the arguments with the defined function:
6001  $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
6002  // If array is returned, check for maxitems condition, if string is returned this was already done:
6003  if (is_array($newValue)) {
6004  $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
6005  // The reference casting is only required if
6006  // checkValue_group_select_processDBdata() returns an array
6007  $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
6008  }
6009  // Update in database (list of children (csv) or number of relations (foreign_field)):
6010  if (!empty($field)) {
6011  $this->updateDB($table, $id, [$field => $newValue]);
6012  // Collect data to update FlexForms
6013  } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
6014  $flexFormId = $additionalData['flexFormId'];
6015  $flexFormPath = $additionalData['flexFormPath'];
6016 
6017  if (!isset($remapFlexForms[$flexFormId])) {
6018  $remapFlexForms[$flexFormId] = [];
6019  }
6020 
6021  $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
6022  }
6023  // Process waiting Hook: processDatamap_afterDatabaseOperations:
6024  if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
6025  $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
6026  // Update field with remapped data:
6027  $hookArgs['fieldArray'][$field] = $newValue;
6028  // Process waiting hook objects:
6029  $hookObjectsArr = $hookArgs['hookObjectsArr'];
6030  foreach ($hookObjectsArr as $hookObj) {
6031  if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
6032  $hookObj->processDatamap_afterDatabaseOperations($hookArgs['status'], $table, $rawId, $hookArgs['fieldArray'], $this);
6033  }
6034  }
6035  }
6036  }
6037 
6038  if ($remapFlexForms) {
6039  foreach ($remapFlexForms as $flexFormId => $modifications) {
6040  $this->updateFlexFormData($flexFormId, $modifications);
6041  }
6042  }
6043  }
6044  // Processes the remap stack actions:
6045  if ($this->remapStackActions) {
6046  foreach ($this->remapStackActions as $action) {
6047  if (isset($action['callback']) && isset($action['arguments'])) {
6048  call_user_func_array($action['callback'], $action['arguments']);
6049  }
6050  }
6051  }
6052  // Processes the reference index updates of the remap stack:
6053  foreach ($this->remapStackRefIndex as $table => $idArray) {
6054  foreach ($idArray as $id) {
6055  $this->updateRefIndex($table, $id);
6056  unset($this->remapStackRefIndex[$table][$id]);
6057  }
6058  }
6059  // Reset:
6060  $this->remapStack = [];
6061  $this->remapStackRecords = [];
6062  $this->remapStackActions = [];
6063  $this->remapStackRefIndex = [];
6064  }
6065 
6073  protected function updateFlexFormData($flexFormId, array $modifications)
6074  {
6075  list($table, $uid, $field) = explode(':', $flexFormId, 3);
6076 
6077  if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
6078  $uid = $this->substNEWwithIDs[$uid];
6079  }
6080 
6081  $record = $this->recordInfo($table, $uid, '*');
6082 
6083  if (!$table || !$uid || !$field || !is_array($record)) {
6084  return;
6085  }
6086 
6087  BackendUtility::workspaceOL($table, $record);
6088 
6089  // Get current data structure and value array:
6090  $valueStructure = GeneralUtility::xml2array($record[$field]);
6091 
6092  // Do recursive processing of the XML data:
6093  foreach ($modifications as $path => $value) {
6094  $valueStructure['data'] = ArrayUtility::setValueByPath(
6095  $valueStructure['data'], $path, $value
6096  );
6097  }
6098 
6099  if (is_array($valueStructure['data'])) {
6100  // The return value should be compiled back into XML
6101  $values = [
6102  $field => $this->checkValue_flexArray2Xml($valueStructure, true),
6103  ];
6104 
6105  $this->updateDB($table, $uid, $values);
6106  }
6107  }
6108 
6124  protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
6125  {
6126  // Check whether the affected record is marked to be remapped:
6127  if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
6128  call_user_func_array($callback, $arguments);
6129  } else {
6130  $this->addRemapAction($table, $id, $callback, $arguments);
6131  }
6132  }
6133 
6143  public function addRemapAction($table, $id, array $callback, array $arguments)
6144  {
6145  $this->remapStackActions[] = [
6146  'affects' => [
6147  'table' => $table,
6148  'id' => $id
6149  ],
6150  'callback' => $callback,
6151  'arguments' => $arguments
6152  ];
6153  }
6154 
6162  public function addRemapStackRefIndex($table, $id)
6163  {
6164  $this->remapStackRefIndex[$table][$id] = $id;
6165  }
6166 
6179  public function getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
6180  {
6181  if (is_array($registerDBList[$table][$id])) {
6182  foreach ($incomingFieldArray as $field => $value) {
6183  $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
6184  if ($registerDBList[$table][$id][$field] && ($foreignTable = $fieldConf['foreign_table'])) {
6185  $newValueArray = [];
6186  $origValueArray = explode(',', $value);
6187  // Update the uids of the copied records, but also take care about new records:
6188  foreach ($origValueArray as $childId) {
6189  $newValueArray[] = $this->autoVersionIdMap[$foreignTable][$childId] ? $this->autoVersionIdMap[$foreignTable][$childId] : $childId;
6190  }
6191  // Set the changed value to the $incomingFieldArray
6192  $incomingFieldArray[$field] = implode(',', $newValueArray);
6193  }
6194  }
6195  // Clean up the $registerDBList array:
6196  unset($registerDBList[$table][$id]);
6197  if (empty($registerDBList[$table])) {
6198  unset($registerDBList[$table]);
6199  }
6200  }
6201  }
6202 
6203  /*****************************
6204  *
6205  * Access control / Checking functions
6206  *
6207  *****************************/
6214  public function checkModifyAccessList($table)
6215  {
6216  $res = $this->admin || !$this->tableAdminOnly($table) && GeneralUtility::inList($this->BE_USER->groupData['tables_modify'], $table);
6217  // Hook 'checkModifyAccessList': Post-processing of the state of access
6218  foreach ($this->getCheckModifyAccessListHookObjects() as $hookObject) {
6220  $hookObject->checkModifyAccessList($res, $table, $this);
6221  }
6222  return $res;
6223  }
6224 
6232  public function isRecordInWebMount($table, $id)
6233  {
6234  if (!isset($this->isRecordInWebMount_Cache[$table . ':' . $id])) {
6235  $recP = $this->getRecordProperties($table, $id);
6236  $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->isInWebMount($recP['event_pid']);
6237  }
6238  return $this->isRecordInWebMount_Cache[$table . ':' . $id];
6239  }
6240 
6247  public function isInWebMount($pid)
6248  {
6249  if (!isset($this->isInWebMount_Cache[$pid])) {
6250  $this->isInWebMount_Cache[$pid] = $this->BE_USER->isInWebMount($pid);
6251  }
6252  return $this->isInWebMount_Cache[$pid];
6253  }
6254 
6264  public function checkRecordUpdateAccess($table, $id, $data = false, $hookObjectsArr = null)
6265  {
6266  $res = null;
6267  if (is_array($hookObjectsArr)) {
6268  foreach ($hookObjectsArr as $hookObj) {
6269  if (method_exists($hookObj, 'checkRecordUpdateAccess')) {
6270  $res = $hookObj->checkRecordUpdateAccess($table, $id, $data, $res, $this);
6271  }
6272  }
6273  }
6274  if ($res === 1 || $res === 0) {
6275  return $res;
6276  } else {
6277  $res = 0;
6278  }
6279  if ($GLOBALS['TCA'][$table] && (int)$id > 0) {
6280  // If information is cached, return it
6281  if (isset($this->recUpdateAccessCache[$table][$id])) {
6282  return $this->recUpdateAccessCache[$table][$id];
6283  } elseif ($this->doesRecordExist($table, $id, 'edit')) {
6284  $res = 1;
6285  }
6286  // Cache the result
6287  $this->recUpdateAccessCache[$table][$id] = $res;
6288  }
6289  return $res;
6290  }
6291 
6301  public function checkRecordInsertAccess($insertTable, $pid, $action = 1)
6302  {
6303  $pid = (int)$pid;
6304  if ($pid < 0) {
6305  return false;
6306  }
6307  // If information is cached, return it
6308  if (isset($this->recInsertAccessCache[$insertTable][$pid])) {
6309  return $this->recInsertAccessCache[$insertTable][$pid];
6310  }
6311 
6312  $res = false;
6313  if ($insertTable === 'pages') {
6314  $perms = $this->pMap['new'];
6315  // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6316  } elseif (($insertTable === 'sys_file_reference') && array_key_exists('pages', $this->datamap)) {
6317  $perms = $this->pMap['edit'];
6318  } else {
6319  $perms = $this->pMap['editcontent'];
6320  }
6321  $pageExists = (bool)$this->doesRecordExist('pages', $pid, $perms);
6322  // 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
6323  if ($pageExists || $pid === 0 && ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($insertTable))) {
6324  // Check permissions
6325  if ($this->isTableAllowedForThisPage($pid, $insertTable)) {
6326  $res = true;
6327  // Cache the result
6328  $this->recInsertAccessCache[$insertTable][$pid] = $res;
6329  } elseif ($this->enableLogging) {
6330  $propArr = $this->getRecordProperties('pages', $pid);
6331  $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']);
6332  }
6333  } elseif ($this->enableLogging) {
6334  $propArr = $this->getRecordProperties('pages', $pid);
6335  $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']);
6336  }
6337  return $res;
6338  }
6339 
6347  public function isTableAllowedForThisPage($page_uid, $checkTable)
6348  {
6349  $page_uid = (int)$page_uid;
6350  $rootLevelSetting = (int)$GLOBALS['TCA'][$checkTable]['ctrl']['rootLevel'];
6351  // 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.
6352  if ($checkTable !== 'pages' && $rootLevelSetting !== -1 && ($rootLevelSetting xor !$page_uid)) {
6353  return false;
6354  }
6355  $allowed = false;
6356  // Check root-level
6357  if (!$page_uid) {
6358  if ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($checkTable)) {
6359  $allowed = true;
6360  }
6361  } else {
6362  // Check non-root-level
6363  $doktype = $this->pageInfo($page_uid, 'doktype');
6364  $allowedTableList = isset($GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'])
6365  ? $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables']
6366  : $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6367  $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6368  // If all tables or the table is listed as an allowed type, return TRUE
6369  if (strpos($allowedTableList, '*') !== false || in_array($checkTable, $allowedArray, true)) {
6370  $allowed = true;
6371  }
6372  }
6373  return $allowed;
6374  }
6375 
6386  public function doesRecordExist($table, $id, $perms)
6387  {
6388  $id = (int)$id;
6389  if ($this->bypassAccessCheckForRecords) {
6390  return is_array(BackendUtility::getRecordRaw($table, 'uid=' . $id, 'uid'));
6391  }
6392  // Processing the incoming $perms (from possible string to integer that can be AND'ed)
6394  if ($table != 'pages') {
6395  switch ($perms) {
6396  case 'edit':
6397 
6398  case 'delete':
6399 
6400  case 'new':
6401  // This holds it all in case the record is not page!!
6402  if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
6403  $perms = 'edit';
6404  } else {
6405  $perms = 'editcontent';
6406  }
6407  break;
6408  }
6409  }
6410  $perms = (int)$this->pMap[$perms];
6411  } else {
6412  $perms = (int)$perms;
6413  }
6414  if (!$perms) {
6415  throw new \RuntimeException('Internal ERROR: no permissions to check for non-admin user', 1270853920);
6416  }
6417  // For all tables: Check if record exists:
6418  $isWebMountRestrictionIgnored = BackendUtility::isWebMountRestrictionIgnored($table);
6419  if (is_array($GLOBALS['TCA'][$table]) && $id > 0 && ($isWebMountRestrictionIgnored || $this->isRecordInWebMount($table, $id) || $this->admin)) {
6420  if ($table != 'pages') {
6421  // Find record without checking page
6422  // @todo: Thist should probably check for editlock
6423  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6424  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6425  $output = $queryBuilder
6426  ->select('uid', 'pid')
6427  ->from($table)
6428  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6429  ->execute()
6430  ->fetch();
6431  BackendUtility::fixVersioningPid($table, $output, true);
6432  // If record found, check page as well:
6433  if (is_array($output)) {
6434  // Looking up the page for record:
6435  $queryBuilder = $this->doesRecordExist_pageLookUp($output['pid'], $perms);
6436  $pageRec = $queryBuilder->select('uid')->execute()->fetch();
6437  // 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):
6438  $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
6439  if (is_array($pageRec) || !$output['pid'] && ($isRootLevelRestrictionIgnored || $this->admin)) {
6440  return true;
6441  }
6442  }
6443  return false;
6444  } else {
6445  $queryBuilder = $this->doesRecordExist_pageLookUp($id, $perms);
6446  return $queryBuilder->count('uid')->execute()->fetchColumn(0);
6447  }
6448  }
6449  return false;
6450  }
6451 
6461  protected function doesRecordExist_pageLookUp($id, $perms)
6462  {
6463  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6464  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6465  $queryBuilder
6466  ->select('uid')
6467  ->from('pages')
6468  ->where($queryBuilder->expr()->eq(
6469  'uid',
6470  $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
6471  ));
6472  if ($perms && !$this->admin) {
6473  $queryBuilder->andWhere($this->BE_USER->getPagePermsClause($perms));
6474  }
6475  if (!$this->admin && $GLOBALS['TCA']['pages']['ctrl']['editlock'] &&
6477  ) {
6478  $queryBuilder->andWhere($queryBuilder->expr()->eq(
6479  $GLOBALS['TCA']['pages']['ctrl']['editlock'],
6480  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
6481  ));
6482  }
6483  return $queryBuilder;
6484  }
6485 
6499  public function doesBranchExist($inList, $pid, $perms, $recurse)
6500  {
6501  $pid = (int)$pid;
6502  $perms = (int)$perms;
6503  if ($pid >= 0) {
6504  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6505  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6506  $result = $queryBuilder
6507  ->select('uid', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody')
6508  ->from('pages')
6509  ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
6510  ->orderBy('sorting')
6511  ->execute();
6512  while ($row = $result->fetch()) {
6513  // IF admin, then it's OK
6514  if ($this->admin || $this->BE_USER->doesUserHaveAccess($row, $perms)) {
6515  $inList .= $row['uid'] . ',';
6516  if ($recurse) {
6517  // Follow the subpages recursively...
6518  $inList = $this->doesBranchExist($inList, $row['uid'], $perms, $recurse);
6519  if ($inList == -1) {
6520  return -1;
6521  }
6522  }
6523  } else {
6524  // No permissions
6525  return -1;
6526  }
6527  }
6528  }
6529  return $inList;
6530  }
6531 
6538  public function tableReadOnly($table)
6539  {
6540  // Returns TRUE if table is readonly
6541  return (bool)$GLOBALS['TCA'][$table]['ctrl']['readOnly'];
6542  }
6543 
6550  public function tableAdminOnly($table)
6551  {
6552  // Returns TRUE if table is admin-only
6553  return (bool)$GLOBALS['TCA'][$table]['ctrl']['adminOnly'];
6554  }
6555 
6564  public function destNotInsideSelf($destinationId, $id)
6565  {
6566  $loopCheck = 100;
6567  $destinationId = (int)$destinationId;
6568  $id = (int)$id;
6569  if ($destinationId === $id) {
6570  return false;
6571  }
6572  while ($destinationId !== 0 && $loopCheck > 0) {
6573  $loopCheck--;
6574  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6575  $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6576  $result = $queryBuilder
6577  ->select('pid', 'uid', 't3ver_oid', 't3ver_wsid')
6578  ->from('pages')
6579  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($destinationId, \PDO::PARAM_INT)))
6580  ->execute();
6581  if ($row = $result->fetch()) {
6582  BackendUtility::fixVersioningPid('pages', $row);
6583  if ($row['pid'] == $id) {
6584  return false;
6585  } else {
6586  $destinationId = (int)$row['pid'];
6587  }
6588  } else {
6589  return false;
6590  }
6591  }
6592  return true;
6593  }
6594 
6601  public function getExcludeListArray()
6602  {
6603  $list = [];
6604  $nonExcludeFieldsArray = array_flip(GeneralUtility::trimExplode(',', $this->BE_USER->groupData['non_exclude_fields']));
6605  foreach ($GLOBALS['TCA'] as $table => $_) {
6606  if (isset($GLOBALS['TCA'][$table]['columns'])) {
6607  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $config) {
6608  if ($config['exclude'] && !isset($nonExcludeFieldsArray[$table . ':' . $field])) {
6609  $list[] = $table . '-' . $field;
6610  }
6611  }
6612  }
6613  }
6614  return $list;
6615  }
6616 
6624  public function doesPageHaveUnallowedTables($page_uid, $doktype)
6625  {
6626  $page_uid = (int)$page_uid;
6627  if (!$page_uid) {
6628  // Not a number. Probably a new page
6629  return false;
6630  }
6631  $allowedTableList = isset($GLOBALS['PAGES_TYPES'][$doktype]['allowedTables']) ? $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] : $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6632  $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6633  // If all tables is OK the return TRUE
6634  if (strstr($allowedTableList, '*')) {
6635  // OK...
6636  return false;
6637  }
6638  $tableList = [];
6639  foreach ($GLOBALS['TCA'] as $table => $_) {
6640  // If the table is not in the allowed list, check if there are records...
6641  if (!in_array($table, $allowedArray, true)) {
6642  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6643  $queryBuilder->getRestrictions()->removeAll();
6644  $count = $queryBuilder
6645  ->count('uid')
6646  ->from($table)
6647  ->where($queryBuilder->expr()->eq(
6648  'pid',
6649  $queryBuilder->createNamedParameter($page_uid, \PDO::PARAM_INT)
6650  ))
6651  ->execute()
6652  ->fetchColumn(0);
6653  if ($count) {
6654  $tableList[] = $table;
6655  }
6656  }
6657  }
6658  return implode(',', $tableList);
6659  }
6660 
6661  /*****************************
6662  *
6663  * Information lookup
6664  *
6665  *****************************/
6674  public function pageInfo($id, $field)
6675  {
6676  if (!isset($this->pageCache[$id])) {
6677  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6678  $queryBuilder->getRestrictions()->removeAll();
6679  $row = $queryBuilder
6680  ->select('*')
6681  ->from('pages')
6682  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6683  ->execute()
6684  ->fetch();
6685  if ($row) {
6686  $this->pageCache[$id] = $row;
6687  }
6688  }
6689  return $this->pageCache[$id][$field];
6690  }
6691 
6701  public function recordInfo($table, $id, $fieldList)
6702  {
6703  // Skip, if searching for NEW records or there's no TCA table definition
6704  if ((int)$id === 0 || !isset($GLOBALS['TCA'][$table])) {
6705  return null;
6706  }
6707  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6708  $queryBuilder->getRestrictions()->removeAll();
6709  $result = $queryBuilder
6710  ->select(...GeneralUtility::trimExplode(',', $fieldList))
6711  ->from($table)
6712  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6713  ->execute()
6714  ->fetch();
6715  return $result ?: null;
6716  }
6717 
6729  public function getRecordProperties($table, $id, $noWSOL = false)
6730  {
6731  $row = $table == 'pages' && !$id ? ['title' => '[root-level]', 'uid' => 0, 'pid' => 0] : $this->recordInfo($table, $id, '*');
6732  if (!$noWSOL) {
6733  BackendUtility::workspaceOL($table, $row);
6734  }
6735  return $this->getRecordPropertiesFromRow($table, $row);
6736  }
6737 
6745  public function getRecordPropertiesFromRow($table, $row)
6746  {
6747  if ($GLOBALS['TCA'][$table]) {
6748  BackendUtility::fixVersioningPid($table, $row);
6749  $out = [
6750  'header' => BackendUtility::getRecordTitle($table, $row),
6751  'pid' => $row['pid'],
6752  'event_pid' => $this->eventPid($table, isset($row['_ORIG_pid']) ? $row['t3ver_oid'] : $row['uid'], $row['pid']),
6753  't3ver_state' => $GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? $row['t3ver_state'] : '',
6754  '_ORIG_pid' => $row['_ORIG_pid']
6755  ];
6756  return $out;
6757  }
6758  return null;
6759  }
6760 
6767  public function eventPid($table, $uid, $pid)
6768  {
6769  return $table == 'pages' ? $uid : $pid;
6770  }
6771 
6772  /*********************************************
6773  *
6774  * Storing data to Database Layer
6775  *
6776  ********************************************/
6786  public function updateDB($table, $id, $fieldArray)
6787  {
6788  if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && (int)$id) {
6789  // Do NOT update the UID field, ever!
6790  unset($fieldArray['uid']);
6791  if (!empty($fieldArray)) {
6792  $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6793  // Execute the UPDATE query:
6794  $updateErrorMessage = '';
6795  try {
6796  GeneralUtility::makeInstance(ConnectionPool::class)
6797  ->getConnectionForTable($table)
6798  ->update($table, $fieldArray, ['uid' => (int)$id]);
6799  } catch (DBALException $e) {
6800  $updateErrorMessage = $e->getPrevious()->getMessage();
6801  }
6802  // If succeeds, do...:
6803  if ($updateErrorMessage === '') {
6804  // Update reference index:
6805  $this->updateRefIndex($table, $id);
6806  if ($this->enableLogging) {
6807  $newRow = [];
6808  if ($this->checkStoredRecords) {
6809  $newRow = $this->checkStoredRecord($table, $id, $fieldArray, 2);