TYPO3  7.6
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 
37 
52 {
53  // *********************
54  // Public variables you can configure before using the class:
55  // *********************
62  public $storeLogMessages = true;
63 
69  public $enableLogging = true;
70 
77  public $reverseOrder = false;
78 
85  public $checkSimilar = true;
86 
94  public $stripslashes_values = true;
95 
102  public $checkStoredRecords = true;
103 
110 
117  public $deleteTree = false;
118 
124  public $neverHideAtCopy = false;
125 
131  public $isImporting = false;
132 
139 
147 
155  public $updateModeL10NdiffData = true;
156 
164 
172 
179  public $bypassFileHandling = false;
180 
188 
195  public $copyWhichTables = '*';
196 
204  public $copyTree = 0;
205 
214  public $defaultValues = array();
215 
223  public $overrideValues = array();
224 
232  public $alternativeFileName = array();
233 
239  public $alternativeFilePath = array();
240 
248  public $data_disableFields = array();
249 
259  public $suggestedInsertUids = array();
260 
267  public $callBackObj;
268 
269  // *********************
270  // Internal variables (mapping arrays) which can be used (read-only) from outside
271  // *********************
277  public $autoVersionIdMap = array();
278 
284  public $substNEWwithIDs = array();
285 
291  public $substNEWwithIDs_table = array();
292 
298  public $newRelatedIDs = array();
299 
305  public $copyMappingArray_merged = array();
306 
312  public $copiedFileMap = array();
313 
319  public $RTEmagic_copyIndex = array();
320 
326  public $errorLog = array();
327 
333  public $pagetreeRefreshFieldsFromPages = array('pid', 'sorting', 'deleted', 'hidden', 'title', 'doktype', 'is_siteroot', 'fe_group', 'nav_hide', 'nav_title', 'module', 'starttime', 'endtime', 'content_from_pid');
334 
340  public $pagetreeNeedsRefresh = false;
341 
342 
343  // *********************
344  // Internal Variables, do not touch.
345  // *********************
346 
347  // Variables set in init() function:
348 
354  public $BE_USER;
355 
361  public $userid;
362 
368  public $username;
369 
375  public $admin;
376 
382  public $defaultPermissions = array(
383  'user' => 'show,edit,delete,new,editcontent',
384  'group' => 'show,edit,new,editcontent',
385  'everybody' => ''
386  );
387 
393  protected $excludedTablesAndFields = array();
394 
400 
407  protected $control = array();
408 
414  public $datamap = array();
415 
421  public $cmdmap = array();
422 
428  protected $mmHistoryRecords = array();
429 
435  protected $historyRecords = array();
436 
437  // Internal static:
443  public $pMap = array(
444  'show' => 1,
445  // 1st bit
446  'edit' => 2,
447  // 2nd bit
448  'delete' => 4,
449  // 3rd bit
450  'new' => 8,
451  // 4th bit
452  'editcontent' => 16
453  );
454 
460  public $sortIntervals = 256;
461 
462  // Internal caching arrays
468  public $recUpdateAccessCache = array();
469 
475  public $recInsertAccessCache = array();
476 
482  public $isRecordInWebMount_Cache = array();
483 
489  public $isInWebMount_Cache = array();
490 
496  public $cachedTSconfig = array();
497 
503  public $pageCache = array();
504 
510  public $checkWorkspaceCache = array();
511 
512  // Other arrays:
518  public $dbAnalysisStore = array();
519 
525  public $removeFilesStore = array();
526 
532  public $uploadedFileArray = array();
533 
539  public $registerDBList = array();
540 
546  public $registerDBPids = array();
547 
558  public $copyMappingArray = array();
559 
565  public $remapStack = array();
566 
573  public $remapStackRecords = array();
574 
580  protected $remapStackChildIds = array();
581 
587  protected $remapStackActions = array();
588 
594  protected $remapStackRefIndex = array();
595 
601  public $updateRefIndexStack = array();
602 
609  public $callFromImpExp = false;
610 
616  public $newIndexMap = array();
617 
618  // Various
625  public $fileFunc;
626 
632  public $checkValue_currentRecord = array();
633 
639  public $autoVersioningUpdate = false;
640 
646  protected $disableDeleteClause = false;
647 
652 
657 
664  protected $outerMostInstance = null;
665 
671  protected static $recordsToClearCacheFor = array();
672 
679 
685  protected $runtimeCache = null;
686 
692  protected $cachePrefixNestedElementCalls = 'core-datahandler-nestedElementCalls-';
693 
697  public function __construct()
698  {
699  $this->databaseConnection = $GLOBALS['TYPO3_DB'];
700  $this->runtimeCache = $this->getRuntimeCache();
701  }
702 
706  public function setControl(array $control)
707  {
708  $this->control = $control;
709  }
710 
721  public function start($data, $cmd, $altUserObject = null)
722  {
723  // Initializing BE_USER
724  $this->BE_USER = is_object($altUserObject) ? $altUserObject : $GLOBALS['BE_USER'];
725  $this->userid = $this->BE_USER->user['uid'];
726  $this->username = $this->BE_USER->user['username'];
727  $this->admin = $this->BE_USER->user['admin'];
728  if ($this->BE_USER->uc['recursiveDelete']) {
729  $this->deleteTree = 1;
730  }
731  if ($GLOBALS['TYPO3_CONF_VARS']['BE']['explicitConfirmationOfTranslation'] && $this->updateModeL10NdiffData === true) {
732  $this->updateModeL10NdiffData = false;
733  }
734  // Initializing default permissions for pages
735  $defaultPermissions = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions'];
736  if (isset($defaultPermissions['user'])) {
737  $this->defaultPermissions['user'] = $defaultPermissions['user'];
738  }
739  if (isset($defaultPermissions['group'])) {
740  $this->defaultPermissions['group'] = $defaultPermissions['group'];
741  }
742  if (isset($defaultPermissions['everybody'])) {
743  $this->defaultPermissions['everybody'] = $defaultPermissions['everybody'];
744  }
745  // generates the excludelist, based on TCA/exclude-flag and non_exclude_fields for the user:
746  if (!$this->admin) {
747  $this->excludedTablesAndFields = array_flip($this->getExcludeListArray());
748  }
749  // Setting the data and cmd arrays
750  if (is_array($data)) {
751  reset($data);
752  $this->datamap = $data;
753  }
754  if (is_array($cmd)) {
755  reset($cmd);
756  $this->cmdmap = $cmd;
757  }
758  }
759 
767  public function setMirror($mirror)
768  {
769  if (!is_array($mirror)) {
770  return;
771  }
772 
773  foreach ($mirror as $table => $uid_array) {
774  if (!isset($this->datamap[$table])) {
775  continue;
776  }
777 
778  foreach ($uid_array as $id => $uidList) {
779  if (!isset($this->datamap[$table][$id])) {
780  continue;
781  }
782 
783  $theIdsInArray = GeneralUtility::trimExplode(',', $uidList, true);
784  foreach ($theIdsInArray as $copyToUid) {
785  $this->datamap[$table][$copyToUid] = $this->datamap[$table][$id];
786  }
787  }
788  }
789  }
790 
797  public function setDefaultsFromUserTS($userTS)
798  {
799  if (!is_array($userTS)) {
800  return;
801  }
802 
803  foreach ($userTS as $k => $v) {
804  $k = substr($k, 0, -1);
805  if (!$k || !is_array($v) || !isset($GLOBALS['TCA'][$k])) {
806  continue;
807  }
808 
809  if (is_array($this->defaultValues[$k])) {
810  $this->defaultValues[$k] = array_merge($this->defaultValues[$k], $v);
811  } else {
812  $this->defaultValues[$k] = $v;
813  }
814  }
815  }
816 
824  public function process_uploads($postFiles)
825  {
826  if (!is_array($postFiles)) {
827  return;
828  }
829 
830  // Editing frozen:
831  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
832  if ($this->enableLogging) {
833  $this->newlog('All editing in this workspace has been frozen!', 1);
834  }
835  return;
836  }
837  $subA = reset($postFiles);
838  if (is_array($subA)) {
839  if (is_array($subA['name']) && is_array($subA['type']) && is_array($subA['tmp_name']) && is_array($subA['size'])) {
840  // Initialize the uploadedFilesArray:
841  $this->uploadedFileArray = array();
842  // For each entry:
843  foreach ($subA as $key => $values) {
844  $this->process_uploads_traverseArray($this->uploadedFileArray, $values, $key);
845  }
846  } else {
847  $this->uploadedFileArray = $subA;
848  }
849  }
850  }
851 
862  public function process_uploads_traverseArray(&$outputArr, $inputArr, $keyToSet)
863  {
864  if (is_array($inputArr)) {
865  foreach ($inputArr as $key => $value) {
866  $this->process_uploads_traverseArray($outputArr[$key], $inputArr[$key], $keyToSet);
867  }
868  } else {
869  $outputArr[$keyToSet] = $inputArr;
870  }
871  }
872 
873  /*********************************************
874  *
875  * HOOKS
876  *
877  *********************************************/
892  public function hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray)
893  {
894  // Process hook directly:
895  if (!isset($this->remapStackRecords[$table][$id])) {
896  foreach ($hookObjectsArr as $hookObj) {
897  if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
898  $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);
899  }
900  }
901  } else {
902  $this->remapStackRecords[$table][$id]['processDatamap_afterDatabaseOperations'] = array(
903  'status' => $status,
904  'fieldArray' => $fieldArray,
905  'hookObjectsArr' => $hookObjectsArr
906  );
907  }
908  }
909 
918  {
919  if (!isset($this->checkModifyAccessListHookObjects)) {
920  $this->checkModifyAccessListHookObjects = array();
921  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'])) {
922  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] as $classData) {
923  $hookObject = GeneralUtility::getUserObj($classData);
924  if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
925  throw new \UnexpectedValueException('$hookObject must implement interface \\TYPO3\\CMS\\Core\\DataHandling\\DataHandlerCheckModifyAccessListHookInterface', 1251892472);
926  }
927  $this->checkModifyAccessListHookObjects[] = $hookObject;
928  }
929  }
930  }
932  }
933 
934  /*********************************************
935  *
936  * PROCESSING DATA
937  *
938  *********************************************/
945  public function process_datamap()
946  {
947  $this->controlActiveElements();
948 
949  // Keep versionized(!) relations here locally:
950  $registerDBList = array();
952  $this->datamap = $this->unsetElementsToBeDeleted($this->datamap);
953  // Editing frozen:
954  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
955  if ($this->enableLogging) {
956  $this->newlog('All editing in this workspace has been frozen!', 1);
957  }
958  return false;
959  }
960  // First prepare user defined objects (if any) for hooks which extend this function:
961  $hookObjectsArr = array();
962  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'])) {
963  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] as $classRef) {
964  $hookObject = GeneralUtility::getUserObj($classRef);
965  if (method_exists($hookObject, 'processDatamap_beforeStart')) {
966  $hookObject->processDatamap_beforeStart($this);
967  }
968  $hookObjectsArr[] = $hookObject;
969  }
970  }
971  // 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.
972  $orderOfTables = array();
973  // Set pages first.
974  if (isset($this->datamap['pages'])) {
975  $orderOfTables[] = 'pages';
976  }
977  $orderOfTables = array_unique(array_merge($orderOfTables, array_keys($this->datamap)));
978  // Process the tables...
979  foreach ($orderOfTables as $table) {
980  // Check if
981  // - table is set in $GLOBALS['TCA'],
982  // - table is NOT readOnly
983  // - the table is set with content in the data-array (if not, there's nothing to process...)
984  // - permissions for tableaccess OK
985  $modifyAccessList = $this->checkModifyAccessList($table);
986  if ($this->enableLogging && !$modifyAccessList) {
987  $this->log($table, 0, 2, 0, 1, 'Attempt to modify table \'%s\' without permission', 1, array($table));
988  }
989  if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->datamap[$table]) || !$modifyAccessList) {
990  continue;
991  }
992 
993  if ($this->reverseOrder) {
994  $this->datamap[$table] = array_reverse($this->datamap[$table], 1);
995  }
996  // For each record from the table, do:
997  // $id is the record uid, may be a string if new records...
998  // $incomingFieldArray is the array of fields
999  foreach ($this->datamap[$table] as $id => $incomingFieldArray) {
1000  if (!is_array($incomingFieldArray)) {
1001  continue;
1002  }
1003  $theRealPid = null;
1004 
1005  // Handle native date/time fields
1006  $dateTimeFormats = $this->databaseConnection->getDateTimeFormats($table);
1007  foreach ($GLOBALS['TCA'][$table]['columns'] as $column => $config) {
1008  if (isset($incomingFieldArray[$column])) {
1009  if (isset($config['config']['dbType']) && ($config['config']['dbType'] === 'date' || $config['config']['dbType'] === 'datetime')) {
1010  $emptyValue = $dateTimeFormats[$config['config']['dbType']]['empty'];
1011  $format = $dateTimeFormats[$config['config']['dbType']]['format'];
1012  $incomingFieldArray[$column] = $incomingFieldArray[$column] ? gmdate($format, $incomingFieldArray[$column]) : $emptyValue;
1013  }
1014  }
1015  }
1016  // Hook: processDatamap_preProcessFieldArray
1017  foreach ($hookObjectsArr as $hookObj) {
1018  if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
1019  $hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
1020  }
1021  }
1022  // ******************************
1023  // Checking access to the record
1024  // ******************************
1025  $createNewVersion = false;
1026  $recordAccess = false;
1027  $old_pid_value = '';
1028  $this->autoVersioningUpdate = false;
1029  // Is it a new record? (Then Id is a string)
1031  // Get a fieldArray with default values
1032  $fieldArray = $this->newFieldArray($table);
1033  // A pid must be set for new records.
1034  if (isset($incomingFieldArray['pid'])) {
1035  // $value = the pid
1036  $pid_value = $incomingFieldArray['pid'];
1037  // Checking and finding numerical pid, it may be a string-reference to another value
1038  $OK = 1;
1039  // If a NEW... id
1040  if (strstr($pid_value, 'NEW')) {
1041  if ($pid_value[0] === '-') {
1042  $negFlag = -1;
1043  $pid_value = substr($pid_value, 1);
1044  } else {
1045  $negFlag = 1;
1046  }
1047  // Trying to find the correct numerical value as it should be mapped by earlier processing of another new record.
1048  if (isset($this->substNEWwithIDs[$pid_value])) {
1049  if ($negFlag === 1) {
1050  $old_pid_value = $this->substNEWwithIDs[$pid_value];
1051  }
1052  $pid_value = (int)($negFlag * $this->substNEWwithIDs[$pid_value]);
1053  } else {
1054  $OK = 0;
1055  }
1056  }
1057  $pid_value = (int)$pid_value;
1058  // The $pid_value is now the numerical pid at this point
1059  if ($OK) {
1060  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
1061  // Points to a page on which to insert the element, possibly in the top of the page
1062  if ($pid_value >= 0) {
1063  // If this table is sorted we better find the top sorting number
1064  if ($sortRow) {
1065  $fieldArray[$sortRow] = $this->getSortNumber($table, 0, $pid_value);
1066  }
1067  // The numerical pid is inserted in the data array
1068  $fieldArray['pid'] = $pid_value;
1069  } else {
1070  // points to another record before ifself
1071  // If this table is sorted we better find the top sorting number
1072  if ($sortRow) {
1073  // Because $pid_value is < 0, getSortNumber returns an array
1074  $tempArray = $this->getSortNumber($table, 0, $pid_value);
1075  $fieldArray['pid'] = $tempArray['pid'];
1076  $fieldArray[$sortRow] = $tempArray['sortNumber'];
1077  } else {
1078  // Here we fetch the PID of the record that we point to...
1079  $tempdata = $this->recordInfo($table, abs($pid_value), 'pid');
1080  $fieldArray['pid'] = $tempdata['pid'];
1081  }
1082  }
1083  }
1084  }
1085  $theRealPid = $fieldArray['pid'];
1086  // Now, check if we may insert records on this pid.
1087  if ($theRealPid >= 0) {
1088  // Checks if records can be inserted on this $pid.
1089  $recordAccess = $this->checkRecordInsertAccess($table, $theRealPid);
1090  if ($recordAccess) {
1091  $this->addDefaultPermittedLanguageIfNotSet($table, $incomingFieldArray);
1092  $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $incomingFieldArray, true);
1093  if (!$recordAccess) {
1094  if ($this->enableLogging) {
1095  $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', 1);
1096  }
1097  } elseif (!$this->bypassWorkspaceRestrictions) {
1098  // Workspace related processing:
1099  // If LIVE records cannot be created in the current PID due to workspace restrictions, prepare creation of placeholder-record
1100  if ($res = $this->BE_USER->workspaceAllowLiveRecordsInPID($theRealPid, $table)) {
1101  if ($res < 0) {
1102  $recordAccess = false;
1103  if ($this->enableLogging) {
1104  $this->newlog('Stage for versioning root point and users access level did not allow for editing', 1);
1105  }
1106  }
1107  } else {
1108  // So, if no live records were allowed, we have to create a new version of this record:
1109  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1110  $createNewVersion = true;
1111  } else {
1112  $recordAccess = false;
1113  if ($this->enableLogging) {
1114  $this->newlog('Record could not be created in this workspace in this branch', 1);
1115  }
1116  }
1117  }
1118  }
1119  }
1120  } else {
1121  debug('Internal ERROR: pid should not be less than zero!');
1122  }
1123  // Yes new record, change $record_status to 'insert'
1124  $status = 'new';
1125  } else {
1126  // Nope... $id is a number
1127  $fieldArray = array();
1128  $recordAccess = $this->checkRecordUpdateAccess($table, $id, $incomingFieldArray, $hookObjectsArr);
1129  if (!$recordAccess) {
1130  if ($this->enableLogging) {
1131  $propArr = $this->getRecordProperties($table, $id);
1132  $this->log($table, $id, 2, 0, 1, 'Attempt to modify record \'%s\' (%s) without permission. Or non-existing page.', 2, array($propArr['header'], $table . ':' . $id), $propArr['event_pid']);
1133  }
1134  continue;
1135  }
1136  // Next check of the record permissions (internals)
1137  $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $id);
1138  if (!$recordAccess) {
1139  if ($this->enableLogging) {
1140  $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', 1);
1141  }
1142  } else {
1143  // Here we fetch the PID of the record that we point to...
1144  $tempdata = $this->recordInfo($table, $id, 'pid' . ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? ',t3ver_wsid,t3ver_stage' : ''));
1145  $theRealPid = $tempdata['pid'];
1146  // Use the new id of the versionized record we're trying to write to:
1147  // (This record is a child record of a parent and has already been versionized.)
1148  if ($this->autoVersionIdMap[$table][$id]) {
1149  // For the reason that creating a new version of this record, automatically
1150  // created related child records (e.g. "IRRE"), update the accordant field:
1151  $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1152  // Use the new id of the copied/versionized record:
1153  $id = $this->autoVersionIdMap[$table][$id];
1154  $recordAccess = true;
1155  $this->autoVersioningUpdate = true;
1156  } elseif (!$this->bypassWorkspaceRestrictions && ($errorCode = $this->BE_USER->workspaceCannotEditRecord($table, $tempdata))) {
1157  $recordAccess = false;
1158  // Versioning is required and it must be offline version!
1159  // Check if there already is a workspace version
1160  $WSversion = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid,t3ver_oid');
1161  if ($WSversion) {
1162  $id = $WSversion['uid'];
1163  $recordAccess = true;
1164  } elseif ($this->BE_USER->workspaceAllowAutoCreation($table, $id, $theRealPid)) {
1165  // new version of a record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1166  $this->pagetreeNeedsRefresh = true;
1167 
1169  $tce = GeneralUtility::makeInstance(__CLASS__);
1170  $tce->stripslashes_values = false;
1171  // Setting up command for creating a new version of the record:
1172  $cmd = array();
1173  $cmd[$table][$id]['version'] = array(
1174  'action' => 'new',
1175  'treeLevels' => -1,
1176  // Default is to create a version of the individual records... element versioning that is.
1177  'label' => 'Auto-created for WS #' . $this->BE_USER->workspace
1178  );
1179  $tce->start(array(), $cmd);
1180  $tce->process_cmdmap();
1181  $this->errorLog = array_merge($this->errorLog, $tce->errorLog);
1182  // If copying was successful, share the new uids (also of related children):
1183  if ($tce->copyMappingArray[$table][$id]) {
1184  foreach ($tce->copyMappingArray as $origTable => $origIdArray) {
1185  foreach ($origIdArray as $origId => $newId) {
1186  $this->uploadedFileArray[$origTable][$newId] = $this->uploadedFileArray[$origTable][$origId];
1187  $this->autoVersionIdMap[$origTable][$origId] = $newId;
1188  }
1189  }
1190  ArrayUtility::mergeRecursiveWithOverrule($this->RTEmagic_copyIndex, $tce->RTEmagic_copyIndex);
1191  // See where RTEmagic_copyIndex is used inside fillInFieldArray() for more information...
1192  // Update registerDBList, that holds the copied relations to child records:
1193  $registerDBList = array_merge($registerDBList, $tce->registerDBList);
1194  // For the reason that creating a new version of this record, automatically
1195  // created related child records (e.g. "IRRE"), update the accordant field:
1196  $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1197  // Use the new id of the copied/versionized record:
1198  $id = $this->autoVersionIdMap[$table][$id];
1199  $recordAccess = true;
1200  $this->autoVersioningUpdate = true;
1201  } elseif ($this->enableLogging) {
1202  $this->newlog('Could not be edited in offline workspace in the branch where found (failure state: \'' . $errorCode . '\'). Auto-creation of version failed!', 1);
1203  }
1204  } elseif ($this->enableLogging) {
1205  $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);
1206  }
1207  }
1208  }
1209  // The default is 'update'
1210  $status = 'update';
1211  }
1212  // If access was granted above, proceed to create or update record:
1213  if (!$recordAccess) {
1214  continue;
1215  }
1216 
1217  // Here the "pid" is set IF NOT the old pid was a string pointing to a place in the subst-id array.
1218  list($tscPID) = BackendUtility::getTSCpid($table, $id, $old_pid_value ? $old_pid_value : $fieldArray['pid']);
1219  if ($status === 'new' && $table === 'pages') {
1220  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
1221  if (isset($TSConfig['permissions.']) && is_array($TSConfig['permissions.'])) {
1222  $fieldArray = $this->setTSconfigPermissions($fieldArray, $TSConfig['permissions.']);
1223  }
1224  }
1225  // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1226  $fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1227  $newVersion_placeholderFieldArray = array();
1228  if ($createNewVersion) {
1229  // create a placeholder array with already processed field content
1230  $newVersion_placeholderFieldArray = $fieldArray;
1231  }
1232  // NOTICE! All manipulation beyond this point bypasses both "excludeFields" AND possible "MM" relations / file uploads to field!
1233  // Forcing some values unto field array:
1234  // NOTICE: This overriding is potentially dangerous; permissions per field is not checked!!!
1235  $fieldArray = $this->overrideFieldArray($table, $fieldArray);
1236  if ($createNewVersion) {
1237  $newVersion_placeholderFieldArray = $this->overrideFieldArray($table, $newVersion_placeholderFieldArray);
1238  }
1239  // Setting system fields
1240  if ($status == 'new') {
1241  if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1242  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1243  if ($createNewVersion) {
1244  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1245  }
1246  }
1247  if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1248  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1249  if ($createNewVersion) {
1250  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1251  }
1252  }
1253  } elseif ($this->checkSimilar) {
1254  // Removing fields which are equal to the current value:
1255  $fieldArray = $this->compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1256  }
1257  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] && !empty($fieldArray)) {
1258  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1259  if ($createNewVersion) {
1260  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1261  }
1262  }
1263  // Set stage to "Editing" to make sure we restart the workflow
1264  if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1265  $fieldArray['t3ver_stage'] = 0;
1266  }
1267  // Hook: processDatamap_postProcessFieldArray
1268  foreach ($hookObjectsArr as $hookObj) {
1269  if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1270  $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1271  }
1272  }
1273  // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1274  // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already and files could have been uploaded that are now "lost"
1275  if (is_array($fieldArray)) {
1276  if ($status == 'new') {
1277  if ($table === 'pages') {
1278  // for new pages always a refresh is needed
1279  $this->pagetreeNeedsRefresh = true;
1280  }
1281 
1282  // This creates a new version of the record with online placeholder and offline version
1283  if ($createNewVersion) {
1284  // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1285  $this->pagetreeNeedsRefresh = true;
1286 
1287  $newVersion_placeholderFieldArray['t3ver_label'] = 'INITIAL PLACEHOLDER';
1288  // Setting placeholder state value for temporary record
1289  $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER);
1290  // Setting workspace - only so display of place holders can filter out those from other workspaces.
1291  $newVersion_placeholderFieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1292  $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $this->getPlaceholderTitleForTableLabel($table);
1293  // Saving placeholder as 'original'
1294  $this->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1295  // For the actual new offline version, set versioning values to point to placeholder:
1296  $fieldArray['pid'] = -1;
1297  $fieldArray['t3ver_oid'] = $this->substNEWwithIDs[$id];
1298  $fieldArray['t3ver_id'] = 1;
1299  // Setting placeholder state value for version (so it can know it is currently a new version...)
1300  $fieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER_VERSION);
1301  $fieldArray['t3ver_label'] = 'First draft version';
1302  $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1303  // When inserted, $this->substNEWwithIDs[$id] will be changed to the uid of THIS version and so the interface will pick it up just nice!
1304  $phShadowId = $this->insertDB($table, $id, $fieldArray, true, 0, true);
1305  if ($phShadowId) {
1306  // Processes fields of the placeholder record:
1307  $this->triggerRemapAction($table, $id, array($this, 'placeholderShadowing'), array($table, $phShadowId));
1308  // Hold auto-versionized ids of placeholders:
1309  $this->autoVersionIdMap[$table][$this->substNEWwithIDs[$id]] = $phShadowId;
1310  }
1311  } else {
1312  $this->insertDB($table, $id, $fieldArray, false, $incomingFieldArray['uid']);
1313  }
1314  } else {
1315  if ($table === 'pages') {
1316  // only a certain number of fields needs to be checked for updates
1317  // if $this->checkSimilar is TRUE, fields with unchanged values are already removed here
1318  $fieldsToCheck = array_intersect($this->pagetreeRefreshFieldsFromPages, array_keys($fieldArray));
1319  if (!empty($fieldsToCheck)) {
1320  $this->pagetreeNeedsRefresh = true;
1321  }
1322  }
1323  $this->updateDB($table, $id, $fieldArray);
1324  $this->placeholderShadowing($table, $id);
1325  }
1326  }
1327  // Hook: processDatamap_afterDatabaseOperations
1328  // Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
1329  // but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
1330  $this->hook_processDatamap_afterDatabaseOperations($hookObjectsArr, $status, $table, $id, $fieldArray);
1331  }
1332  }
1333  // Process the stack of relations to remap/correct
1334  $this->processRemapStack();
1335  $this->dbAnalysisStoreExec();
1336  $this->removeRegisteredFiles();
1337  // Hook: processDatamap_afterAllOperations
1338  // Note: When this hook gets called, all operations on the submitted data have been finished.
1339  foreach ($hookObjectsArr as $hookObj) {
1340  if (method_exists($hookObj, 'processDatamap_afterAllOperations')) {
1341  $hookObj->processDatamap_afterAllOperations($this);
1342  }
1343  }
1344  if ($this->isOuterMostInstance()) {
1345  $this->processClearCacheQueue();
1346  $this->resetElementsToBeDeleted();
1347  }
1348  }
1349 
1357  public function placeholderShadowing($table, $id)
1358  {
1359  if ($liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, '*')) {
1360  if (VersionState::cast($liveRec['t3ver_state'])->indicatesPlaceholder()) {
1361  $justStoredRecord = BackendUtility::getRecord($table, $id);
1362  $newRecord = array();
1363  $shadowCols = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'];
1364  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1365  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1366  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['type'];
1367  $shadowCols .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label'];
1368  $shadowColumns = array_unique(GeneralUtility::trimExplode(',', $shadowCols, true));
1369  foreach ($shadowColumns as $fieldName) {
1370  if ((string)$justStoredRecord[$fieldName] !== (string)$liveRec[$fieldName] && isset($GLOBALS['TCA'][$table]['columns'][$fieldName]) && $fieldName !== 'uid' && $fieldName !== 'pid') {
1371  $newRecord[$fieldName] = $justStoredRecord[$fieldName];
1372  }
1373  }
1374  if (!empty($newRecord)) {
1375  if ($this->enableLogging) {
1376  $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']);
1377  }
1378  $this->updateDB($table, $liveRec['uid'], $newRecord);
1379  }
1380  }
1381  }
1382  }
1383 
1391  public function getPlaceholderTitleForTableLabel($table, $placeholderContent = null)
1392  {
1393  if ($placeholderContent === null) {
1394  $placeholderContent = 'PLACEHOLDER';
1395  }
1396 
1397  $labelPlaceholder = '[' . $placeholderContent . ', WS#' . $this->BE_USER->workspace . ']';
1398  $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1399  if (!isset($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'])) {
1400  return $labelPlaceholder;
1401  }
1402  $evalCodesArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'], true);
1403  $transformedLabel = $this->checkValue_input_Eval($labelPlaceholder, $evalCodesArray, '');
1404  return isset($transformedLabel['value']) ? $transformedLabel['value'] : $labelPlaceholder;
1405  }
1406 
1420  public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID)
1421  {
1422  // Initialize:
1423  $originalLanguageRecord = null;
1424  $originalLanguage_diffStorage = null;
1425  $diffStorageFlag = false;
1426  // Setting 'currentRecord' and 'checkValueRecord':
1427  if (strstr($id, 'NEW')) {
1428  // Must have the 'current' array - not the values after processing below...
1429  $currentRecord = ($checkValueRecord = $fieldArray);
1430  // IF $incomingFieldArray is an array, overlay it.
1431  // 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...
1432  if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {
1433  ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);
1434  }
1435  } else {
1436  // We must use the current values as basis for this!
1437  $currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*'));
1438  // This is done to make the pid positive for offline versions; Necessary to have diff-view for pages_language_overlay in workspaces.
1439  BackendUtility::fixVersioningPid($table, $currentRecord);
1440  // Get original language record if available:
1441  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) {
1442  $lookUpTable = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable'] ?: $table;
1443  $originalLanguageRecord = $this->recordInfo($lookUpTable, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*');
1444  BackendUtility::workspaceOL($lookUpTable, $originalLanguageRecord);
1445  $originalLanguage_diffStorage = unserialize($currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]);
1446  }
1447  }
1448  $this->checkValue_currentRecord = $checkValueRecord;
1449  // In the following all incoming value-fields are tested:
1450  // - Are the user allowed to change the field?
1451  // - Is the field uid/pid (which are already set)
1452  // - perms-fields for pages-table, then do special things...
1453  // - If the field is nothing of the above and the field is configured in TCA, the fieldvalues are evaluated by ->checkValue
1454  // If everything is OK, the field is entered into $fieldArray[]
1455  foreach ($incomingFieldArray as $field => $fieldValue) {
1456  if (isset($this->excludedTablesAndFields[$table . '-' . $field]) || $this->data_disableFields[$table][$id][$field]) {
1457  continue;
1458  }
1459 
1460  // The field must be editable.
1461  // Checking if a value for language can be changed:
1462  $languageDeny = $GLOBALS['TCA'][$table]['ctrl']['languageField'] && (string)$GLOBALS['TCA'][$table]['ctrl']['languageField'] === (string)$field && !$this->BE_USER->checkLanguageAccess($fieldValue);
1463  if ($languageDeny) {
1464  continue;
1465  }
1466 
1467  // Stripping slashes - will probably be removed the day $this->stripslashes_values is removed as an option...
1468  // @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
1469  if ($this->stripslashes_values) {
1471  'The option stripslash_values is typically set to FALSE as data should be properly prepared before sending to DataHandler. Do not rely on DataHandler removing extra slashes. The option will be removed in TYPO3 CMS 8.'
1472  );
1473  if (is_array($fieldValue)) {
1475  } else {
1476  $fieldValue = stripslashes($fieldValue);
1477  }
1478  }
1479  switch ($field) {
1480  case 'uid':
1481  case 'pid':
1482  // Nothing happens, already set
1483  break;
1484  case 'perms_userid':
1485  case 'perms_groupid':
1486  case 'perms_user':
1487  case 'perms_group':
1488  case 'perms_everybody':
1489  // Permissions can be edited by the owner or the administrator
1490  if ($table == 'pages' && ($this->admin || $status == 'new' || $this->pageInfo($id, 'perms_userid') == $this->userid)) {
1491  $value = (int)$fieldValue;
1492  switch ($field) {
1493  case 'perms_userid':
1494  $fieldArray[$field] = $value;
1495  break;
1496  case 'perms_groupid':
1497  $fieldArray[$field] = $value;
1498  break;
1499  default:
1500  if ($value >= 0 && $value < pow(2, 5)) {
1501  $fieldArray[$field] = $value;
1502  }
1503  }
1504  }
1505  break;
1506  case 't3ver_oid':
1507  case 't3ver_id':
1508  case 't3ver_wsid':
1509  case 't3ver_state':
1510  case 't3ver_count':
1511  case 't3ver_stage':
1512  case 't3ver_tstamp':
1513  // t3ver_label is not here because it CAN be edited as a regular field!
1514  break;
1515  default:
1516  if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
1517  // Evaluating the value
1518  $res = $this->checkValue($table, $field, $fieldValue, $id, $status, $realPid, $tscPID);
1519  if (array_key_exists('value', $res)) {
1520  $fieldArray[$field] = $res['value'];
1521  }
1522  // Add the value of the original record to the diff-storage content:
1523  if ($this->updateModeL10NdiffData && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
1524  $originalLanguage_diffStorage[$field] = $this->updateModeL10NdiffDataClear ? '' : $originalLanguageRecord[$field];
1525  $diffStorageFlag = true;
1526  }
1527  // If autoversioning is happening we need to perform a nasty hack. The case is parallel to a similar hack inside checkValue_group_select_file().
1528  // When a copy or version is made of a record, a search is made for any RTEmagic* images in fields having the "images" soft reference parser applied.
1529  // That should be TRUE for RTE fields. If any are found they are duplicated to new names and the file reference in the bodytext is updated accordingly.
1530  // However, with auto-versioning the submitted content of the field will just overwrite the corrected values. This leaves a) lost RTEmagic files and b) creates a double reference to the old files.
1531  // The only solution I can come up with is detecting when auto versioning happens, then see if any RTEmagic images was copied and if so make a stupid string-replace of the content !
1532  if ($this->autoVersioningUpdate === true) {
1533  if (is_array($this->RTEmagic_copyIndex[$table][$id][$field])) {
1534  foreach ($this->RTEmagic_copyIndex[$table][$id][$field] as $oldRTEmagicName => $newRTEmagicName) {
1535  $fieldArray[$field] = str_replace(' src="' . $oldRTEmagicName . '"', ' src="' . $newRTEmagicName . '"', $fieldArray[$field]);
1536  }
1537  }
1538  }
1539  } elseif ($GLOBALS['TCA'][$table]['ctrl']['origUid'] === $field) {
1540  // Allow value for original UID to pass by...
1541  $fieldArray[$field] = $fieldValue;
1542  }
1543  }
1544  }
1545  // Add diff-storage information:
1546  if ($diffStorageFlag && !isset($fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']])) {
1547  // If the field is set it would probably be because of an undo-operation - in which case we should not update the field of course...
1548  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = serialize($originalLanguage_diffStorage);
1549  }
1550  // Checking for RTE-transformations of fields:
1551  $types_fieldConfig = BackendUtility::getTCAtypes($table, $currentRecord);
1552  $theTypeString = null;
1553  if (is_array($types_fieldConfig)) {
1554  foreach ($types_fieldConfig as $vconf) {
1555  // RTE transformations:
1556  if ($this->dontProcessTransformations || !isset($fieldArray[$vconf['field']])) {
1557  continue;
1558  }
1559 
1560  // Look for transformation flag:
1561  if ((string)$incomingFieldArray[('_TRANSFORM_' . $vconf['field'])] === 'RTE') {
1562  if ($theTypeString === null) {
1563  $theTypeString = BackendUtility::getTCAtypeValue($table, $currentRecord);
1564  }
1565  $RTEsetup = $this->BE_USER->getTSConfig('RTE', BackendUtility::getPagesTSconfig($tscPID));
1566  $thisConfig = BackendUtility::RTEsetup($RTEsetup['properties'], $table, $vconf['field'], $theTypeString);
1567  $fieldArray[$vconf['field']] = $this->transformRichtextContentToDatabase(
1568  $fieldArray[$vconf['field']], $table, $vconf['field'], $vconf['spec'], $thisConfig, $currentRecord['pid']
1569  );
1570  }
1571  }
1572  }
1573  // Return fieldArray
1574  return $fieldArray;
1575  }
1576 
1588  protected function transformRichtextContentToDatabase($value, $table, $field, $defaultExtras, $thisConfig, $pid)
1589  {
1590  if ($defaultExtras['rte_transform']) {
1591  $parameters = BackendUtility::getSpecConfParametersFromArray($defaultExtras['rte_transform']['parameters']);
1592  // There must be a mode set for transformation, this is typically 'ts_css'
1593  if ($parameters['mode']) {
1594  // Initialize transformation:
1595  $parseHTML = GeneralUtility::makeInstance(RteHtmlParser::class);
1596  $parseHTML->init($table . ':' . $field, $pid);
1597  $parseHTML->setRelPath('');
1598  // Perform transformation:
1599  $value = $parseHTML->RTE_transform($value, $defaultExtras, 'db', $thisConfig);
1600  }
1601  }
1602  return $value;
1603  }
1604 
1605 
1606  /*********************************************
1607  *
1608  * Evaluation of input values
1609  *
1610  ********************************************/
1625  public function checkValue($table, $field, $value, $id, $status, $realPid, $tscPID)
1626  {
1627  // Result array
1628  $res = array();
1629 
1630  // Processing special case of field pages.doktype
1631  if (($table === 'pages' || $table === 'pages_language_overlay') && $field === 'doktype') {
1632  // If the user may not use this specific doktype, we issue a warning
1633  if (!($this->admin || GeneralUtility::inList($this->BE_USER->groupData['pagetypes_select'], $value))) {
1634  if ($this->enableLogging) {
1635  $propArr = $this->getRecordProperties($table, $id);
1636  $this->log($table, $id, 5, 0, 1, 'You cannot change the \'doktype\' of page \'%s\' to the desired value.', 1, array($propArr['header']), $propArr['event_pid']);
1637  }
1638  return $res;
1639  }
1640  if ($status == 'update') {
1641  // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1642  $onlyAllowedTables = isset($GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables']) ? $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] : $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1643  if ($onlyAllowedTables) {
1644  $theWrongTables = $this->doesPageHaveUnallowedTables($id, $value);
1645  if ($theWrongTables) {
1646  if ($this->enableLogging) {
1647  $propArr = $this->getRecordProperties($table, $id);
1648  $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, array($propArr['header'], $theWrongTables), $propArr['event_pid']);
1649  }
1650  return $res;
1651  }
1652  }
1653  }
1654  }
1655 
1656  $curValue = null;
1657  if ((int)$id !== 0) {
1658  // Get current value:
1659  $curValueRec = $this->recordInfo($table, $id, $field);
1660  // isset() won't work here, since values can be NULL
1661  if ($curValueRec !== null && array_key_exists($field, $curValueRec)) {
1662  $curValue = $curValueRec[$field];
1663  }
1664  }
1665 
1666  // Getting config for the field
1667  $tcaFieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1668 
1669  // Create $recFID only for those types that need it
1670  if (
1671  $tcaFieldConf['type'] === 'flex'
1672  || $tcaFieldConf['type'] === 'group' && ($tcaFieldConf['internal_type'] === 'file' || $tcaFieldConf['internal_type'] === 'file_reference')
1673  ) {
1674  $recFID = $table . ':' . $id . ':' . $field;
1675  } else {
1676  $recFID = null;
1677  }
1678 
1679  // Perform processing:
1680  $res = $this->checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $this->uploadedFileArray[$table][$id][$field], $tscPID);
1681  return $res;
1682  }
1683 
1703  public function checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $uploadedFiles, $tscPID, array $additionalData = null)
1704  {
1705  // Convert to NULL value if defined in TCA
1706  if ($value === null && !empty($tcaFieldConf['eval']) && GeneralUtility::inList($tcaFieldConf['eval'], 'null')) {
1707  $res = array('value' => null);
1708  return $res;
1709  }
1710 
1711  switch ($tcaFieldConf['type']) {
1712  case 'text':
1713  $res = $this->checkValueForText($value, $tcaFieldConf);
1714  break;
1715  case 'passthrough':
1716  case 'imageManipulation':
1717  case 'user':
1718  $res['value'] = $value;
1719  break;
1720  case 'input':
1721  $res = $this->checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field);
1722  break;
1723  case 'check':
1724  $res = $this->checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1725  break;
1726  case 'radio':
1727  $res = $this->checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1728  break;
1729  case 'group':
1730  case 'select':
1731  $res = $this->checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field);
1732  break;
1733  case 'inline':
1734  $res = $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
1735  break;
1736  case 'flex':
1737  // FlexForms are only allowed for real fields.
1738  if ($field) {
1739  $res = $this->checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field);
1740  }
1741  break;
1742  default:
1743  // Do nothing
1744  }
1745  return $res;
1746  }
1747 
1759  public function checkValue_text($res, $value, $tcaFieldConf, $PP, $field = '')
1760  {
1762  return $this->checkValueForText($value, $tcaFieldConf);
1763  }
1764 
1772  protected function checkValueForText($value, $tcaFieldConf)
1773  {
1774  if (!isset($tcaFieldConf['eval']) || $tcaFieldConf['eval'] === '') {
1775  return array('value' => $value);
1776  }
1777  $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1778  if ($this->runtimeCache->has($cacheId)) {
1779  $evalCodesArray = $this->runtimeCache->get($cacheId);
1780  } else {
1781  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1782  $this->runtimeCache->set($cacheId, $evalCodesArray);
1783  }
1784  return $this->checkValue_text_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1785  }
1786 
1798  public function checkValue_input($res, $value, $tcaFieldConf, $PP, $field = '')
1799  {
1801  list($table, $id, , , $realPid) = $PP;
1802  return $this->checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field);
1803  }
1804 
1816  protected function checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field)
1817  {
1818  // Handle native date/time fields
1819  $isDateOrDateTimeField = false;
1820  $format = '';
1821  $emptyValue = '';
1822  if (isset($tcaFieldConf['dbType']) && ($tcaFieldConf['dbType'] === 'date' || $tcaFieldConf['dbType'] === 'datetime')) {
1823  $isDateOrDateTimeField = true;
1824  $dateTimeFormats = $this->databaseConnection->getDateTimeFormats($table);
1825  // Convert the date/time into a timestamp for the sake of the checks
1826  $emptyValue = $dateTimeFormats[$tcaFieldConf['dbType']]['empty'];
1827  $format = $dateTimeFormats[$tcaFieldConf['dbType']]['format'];
1828  // At this point in the processing, the timestamps are still based on UTC
1829  $timeZone = new \DateTimeZone('UTC');
1830  $dateTime = \DateTime::createFromFormat('!' . $format, $value, $timeZone);
1831  $value = $value === $emptyValue ? 0 : $dateTime->getTimestamp();
1832  }
1833  // Secures the string-length to be less than max.
1834  if ((int)$tcaFieldConf['max'] > 0) {
1835  $value = $GLOBALS['LANG']->csConvObj->substr($GLOBALS['LANG']->charSet, (string)$value, 0, (int)$tcaFieldConf['max']);
1836  }
1837  // Checking range of value:
1838  // @todo: The "checkbox" option was removed for type=input, this check could be probably relaxed?
1839  if ($tcaFieldConf['range'] && $value != $tcaFieldConf['checkbox'] && (int)$value !== (int)$tcaFieldConf['default']) {
1840  if (isset($tcaFieldConf['range']['upper']) && (int)$value > (int)$tcaFieldConf['range']['upper']) {
1841  $value = $tcaFieldConf['range']['upper'];
1842  }
1843  if (isset($tcaFieldConf['range']['lower']) && (int)$value < (int)$tcaFieldConf['range']['lower']) {
1844  $value = $tcaFieldConf['range']['lower'];
1845  }
1846  }
1847 
1848  if (empty($tcaFieldConf['eval'])) {
1849  $res = array('value' => $value);
1850  } else {
1851  // Process evaluation settings:
1852  $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1853  if ($this->runtimeCache->has($cacheId)) {
1854  $evalCodesArray = $this->runtimeCache->get($cacheId);
1855  } else {
1856  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1857  $this->runtimeCache->set($cacheId, $evalCodesArray);
1858  }
1859 
1860  $res = $this->checkValue_input_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1861 
1862  // Process UNIQUE settings:
1863  // 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...
1864  if ($field && $realPid >= 0 && !empty($res['value'])) {
1865  if (in_array('uniqueInPid', $evalCodesArray, true)) {
1866  $res['value'] = $this->getUnique($table, $field, $res['value'], $id, $realPid);
1867  }
1868  if ($res['value'] && in_array('unique', $evalCodesArray, true)) {
1869  $res['value'] = $this->getUnique($table, $field, $res['value'], $id);
1870  }
1871  }
1872  }
1873 
1874  // Handle native date/time fields
1875  if ($isDateOrDateTimeField) {
1876  // Convert the timestamp back to a date/time
1877  $res['value'] = $res['value'] ? date($format, $res['value']) : $emptyValue;
1878  }
1879  return $res;
1880  }
1881 
1893  public function checkValue_check($res, $value, $tcaFieldConf, $PP, $field = '')
1894  {
1896  list($table, $id, , , $realPid) = $PP;
1897  return $this->checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1898  }
1899 
1912  protected function checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field)
1913  {
1914  $itemC = count($tcaFieldConf['items']);
1915  if (!$itemC) {
1916  $itemC = 1;
1917  }
1918  $maxV = pow(2, $itemC) - 1;
1919  if ($value < 0) {
1920  $value = 0;
1921  }
1922  if ($value > $maxV) {
1923  $value = $maxV;
1924  }
1925  if ($field && $realPid >= 0 && $value > 0 && !empty($tcaFieldConf['eval'])) {
1926  $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1927  $otherRecordsWithSameValue = array();
1928  $maxCheckedRecords = 0;
1929  if (in_array('maximumRecordsCheckedInPid', $evalCodesArray, true)) {
1930  $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value, $realPid);
1931  $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsCheckedInPid'];
1932  }
1933  if (in_array('maximumRecordsChecked', $evalCodesArray, true)) {
1934  $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value);
1935  $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsChecked'];
1936  }
1937 
1938  // there are more than enough records with value "1" in the DB
1939  // if so, set this value to "0" again
1940  if ($maxCheckedRecords && count($otherRecordsWithSameValue) >= $maxCheckedRecords) {
1941  $value = 0;
1942  if ($this->enableLogging) {
1943  $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, array($GLOBALS['LANG']->sL(BackendUtility::getItemLabel($table, $field)), $maxCheckedRecords));
1944  }
1945  }
1946  }
1947  $res['value'] = $value;
1948  return $res;
1949  }
1950 
1961  public function checkValue_radio($res, $value, $tcaFieldConf, $PP)
1962  {
1964  // TODO find a way to get the field name; it should not be set in $recFID as this is only created for some record types, see checkValue()
1965  return $this->checkValueForRadio($res, $value, $tcaFieldConf, $PP[0], $PP[1], $PP[4], '');
1966  }
1967 
1980  protected function checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $pid, $field)
1981  {
1982  if (is_array($tcaFieldConf['items'])) {
1983  foreach ($tcaFieldConf['items'] as $set) {
1984  if ((string)$set[1] === (string)$value) {
1985  $res['value'] = $value;
1986  break;
1987  }
1988  }
1989  }
1990 
1991  // if no value was found and an itemsProcFunc is defined, check that for the value
1992  if ($tcaFieldConf['itemsProcFunc'] && empty($res['value'])) {
1993  $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
1994  $processedItems = $processingService->getProcessingItems($table, $pid, $field, $this->checkValue_currentRecord,
1995  $tcaFieldConf, $tcaFieldConf['items']);
1996 
1997  foreach ($processedItems as $set) {
1998  if ((string)$set[1] === (string)$value) {
1999  $res['value'] = $value;
2000  break;
2001  }
2002  }
2003  }
2004 
2005  return $res;
2006  }
2007 
2020  public function checkValue_group_select($res, $value, $tcaFieldConf, $PP, $uploadedFiles, $field)
2021  {
2023  list($table, $id, $curValue, $status, , $recFID) = $PP;
2024  return $this->checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field);
2025  }
2026 
2042  protected function checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field)
2043  {
2044  // Detecting if value sent is an array and if so, implode it around a comma:
2045  if (is_array($value)) {
2046  $value = implode(',', $value);
2047  }
2048  // 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.
2049  // Anyway, this should NOT disturb anything else:
2050  $value = $this->convNumEntityToByteValue($value);
2051  // When values are sent as group or select they come as comma-separated values which are exploded by this function:
2052  $valueArray = $this->checkValue_group_select_explodeSelectGroupValue($value);
2053  // If multiple is not set, remove duplicates:
2054  if (!$tcaFieldConf['multiple']) {
2055  $valueArray = array_unique($valueArray);
2056  }
2057  // If an exclusive key is found, discard all others:
2058  if ($tcaFieldConf['type'] == 'select' && $tcaFieldConf['exclusiveKeys']) {
2059  $exclusiveKeys = GeneralUtility::trimExplode(',', $tcaFieldConf['exclusiveKeys']);
2060  foreach ($valueArray as $index => $key) {
2061  if (in_array($key, $exclusiveKeys, true)) {
2062  $valueArray = array($index => $key);
2063  break;
2064  }
2065  }
2066  }
2067  // 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?)
2068  // 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!!
2069  $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
2070  // Checking for select / authMode, removing elements from $valueArray if any of them is not allowed!
2071  if ($tcaFieldConf['type'] == 'select' && $tcaFieldConf['authMode']) {
2072  $preCount = count($valueArray);
2073  foreach ($valueArray as $index => $key) {
2074  if (!$this->BE_USER->checkAuthMode($table, $field, $key, $tcaFieldConf['authMode'])) {
2075  unset($valueArray[$index]);
2076  }
2077  }
2078  // 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.
2079  if ($preCount && empty($valueArray)) {
2080  return array();
2081  }
2082  }
2083  // For group types:
2084  if ($tcaFieldConf['type'] == 'group') {
2085  switch ($tcaFieldConf['internal_type']) {
2086  case 'file_reference':
2087 
2088  case 'file':
2089  $valueArray = $this->checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFiles, $status, $table, $id, $recFID);
2090  break;
2091  case 'db':
2092  $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'group', $table, $field);
2093  break;
2094  }
2095  }
2096  // For select types which has a foreign table attached:
2097  $unsetResult = false;
2098  if ($tcaFieldConf['type'] == 'select' && $tcaFieldConf['foreign_table']) {
2099  // check, if there is a NEW... id in the value, that should be substituted later
2100  if (strpos($value, 'NEW') !== false) {
2101  $this->remapStackRecords[$table][$id] = array('remapStackIndex' => count($this->remapStack));
2102  $this->addNewValuesToRemapStackChildIds($valueArray);
2103  $this->remapStack[] = array(
2104  'func' => 'checkValue_group_select_processDBdata',
2105  'args' => array($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field),
2106  'pos' => array('valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 5),
2107  'field' => $field
2108  );
2109  $unsetResult = true;
2110  } else {
2111  $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
2112  }
2113  }
2114  if (!$unsetResult) {
2115  $newVal = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
2116  $res['value'] = $this->castReferenceValue(implode(',', $newVal), $tcaFieldConf);
2117  } else {
2118  unset($res['value']);
2119  }
2120  return $res;
2121  }
2122 
2131  protected function applyFiltersToValues(array $tcaFieldConfiguration, array $values)
2132  {
2133  if (empty($tcaFieldConfiguration['filter']) || !is_array($tcaFieldConfiguration['filter'])) {
2134  return $values;
2135  }
2136  foreach ($tcaFieldConfiguration['filter'] as $filter) {
2137  if (empty($filter['userFunc'])) {
2138  continue;
2139  }
2140  $parameters = $filter['parameters'] ?: array();
2141  $parameters['values'] = $values;
2142  $parameters['tcaFieldConfig'] = $tcaFieldConfiguration;
2143  $values = GeneralUtility::callUserFunction($filter['userFunc'], $parameters, $this);
2144  if (!is_array($values)) {
2145  throw new \RuntimeException('Failed calling filter userFunc.', 1336051942);
2146  }
2147  }
2148  return $values;
2149  }
2150 
2167  public function checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFileArray, $status, $table, $id, $recFID)
2168  {
2169  // If file handling should NOT be bypassed, do processing:
2170  if (!$this->bypassFileHandling) {
2171  // If any files are uploaded, add them to value array
2172  // Numeric index means that there are multiple files
2173  if (isset($uploadedFileArray[0])) {
2174  $uploadedFiles = $uploadedFileArray;
2175  } else {
2176  // There is only one file
2177  $uploadedFiles = array($uploadedFileArray);
2178  }
2179  foreach ($uploadedFiles as $uploadedFileArray) {
2180  if (!empty($uploadedFileArray['name']) && $uploadedFileArray['tmp_name'] !== 'none') {
2181  $valueArray[] = $uploadedFileArray['tmp_name'];
2182  $this->alternativeFileName[$uploadedFileArray['tmp_name']] = $uploadedFileArray['name'];
2183  }
2184  }
2185  // Creating fileFunc object.
2186  if (!$this->fileFunc) {
2187  $this->fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
2188  $this->include_filefunctions = 1;
2189  }
2190  // Setting permitted extensions.
2191  $all_files = array();
2192  $all_files['webspace']['allow'] = $tcaFieldConf['allowed'];
2193  $all_files['webspace']['deny'] = $tcaFieldConf['disallowed'] ?: '*';
2194  $all_files['ftpspace'] = $all_files['webspace'];
2195  $this->fileFunc->init('', $all_files);
2196  }
2197  // If there is an upload folder defined:
2198  if ($tcaFieldConf['uploadfolder'] && $tcaFieldConf['internal_type'] == 'file') {
2199  $currentFilesForHistory = null;
2200  // If filehandling should NOT be bypassed, do processing:
2201  if (!$this->bypassFileHandling) {
2202  // For logging..
2203  $propArr = $this->getRecordProperties($table, $id);
2204  // Get destrination path:
2205  $dest = $this->destPathFromUploadFolder($tcaFieldConf['uploadfolder']);
2206  // If we are updating:
2207  if ($status == 'update') {
2208  // Traverse the input values and convert to absolute filenames in case the update happens to an autoVersionized record.
2209  // 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!
2210  // 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_.
2211  // 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.
2212  // Illustration of the problem comes here:
2213  // 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.
2214  // 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.
2215  // 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.
2216  // 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.
2217  if ($this->autoVersioningUpdate === true) {
2218  foreach ($valueArray as $key => $theFile) {
2219  // If it is an already attached file...
2220  if ($theFile === basename($theFile)) {
2221  $valueArray[$key] = PATH_site . $tcaFieldConf['uploadfolder'] . '/' . $theFile;
2222  }
2223  }
2224  }
2225  // Finding the CURRENT files listed, either from MM or from the current record.
2226  $theFileValues = array();
2227  // If MM relations for the files also!
2228  if ($tcaFieldConf['MM']) {
2229  $dbAnalysis = $this->createRelationHandlerInstance();
2231  $dbAnalysis->start('', 'files', $tcaFieldConf['MM'], $id);
2232  foreach ($dbAnalysis->itemArray as $item) {
2233  if ($item['id']) {
2234  $theFileValues[] = $item['id'];
2235  }
2236  }
2237  } else {
2238  $theFileValues = GeneralUtility::trimExplode(',', $curValue, true);
2239  }
2240  $currentFilesForHistory = implode(',', $theFileValues);
2241  // DELETE files: If existing files were found, traverse those and register files for deletion which has been removed:
2242  if (!empty($theFileValues)) {
2243  // 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!)
2244  foreach ($valueArray as $key => $theFile) {
2245  if ($theFile && !strstr(GeneralUtility::fixWindowsFilePath($theFile), '/')) {
2246  $theFileValues = ArrayUtility::removeArrayEntryByValue($theFileValues, $theFile);
2247  }
2248  }
2249  // This array contains the filenames in the uploadfolder that should be deleted:
2250  foreach ($theFileValues as $key => $theFile) {
2251  $theFile = trim($theFile);
2252  if (@is_file(($dest . '/' . $theFile))) {
2253  $this->removeFilesStore[] = $dest . '/' . $theFile;
2254  } elseif ($this->enableLogging && $theFile) {
2255  $this->log($table, $id, 5, 0, 1, 'Could not delete file \'%s\' (does not exist). (%s)', 10, array($dest . '/' . $theFile, $recFID), $propArr['event_pid']);
2256  }
2257  }
2258  }
2259  }
2260  // Traverse the submitted values:
2261  foreach ($valueArray as $key => $theFile) {
2262  // Init:
2263  $maxSize = (int)$tcaFieldConf['max_size'];
2264  // Must be cleared. Else a faulty fileref may be inserted if the below code returns an error!
2265  $theDestFile = '';
2266  // a FAL file was added, now resolve the file object and get the absolute path
2267  // @todo in future versions this needs to be modified to handle FAL objects natively
2268  if (!empty($theFile) && MathUtility::canBeInterpretedAsInteger($theFile)) {
2269  $fileObject = ResourceFactory::getInstance()->getFileObject($theFile);
2270  $theFile = $fileObject->getForLocalProcessing(false);
2271  }
2272  // NEW FILES? If the value contains '/' it indicates, that the file
2273  // is new and should be added to the uploadsdir (whether its absolute or relative does not matter here)
2274  if (strstr(GeneralUtility::fixWindowsFilePath($theFile), '/')) {
2275  // Check various things before copying file:
2276  // File and destination must exist
2277  if (@is_dir($dest) && (@is_file($theFile) || @is_uploaded_file($theFile))) {
2278  // Finding size.
2279  if (is_uploaded_file($theFile) && $theFile == $uploadedFileArray['tmp_name']) {
2280  $fileSize = $uploadedFileArray['size'];
2281  } else {
2282  $fileSize = filesize($theFile);
2283  }
2284  // Check file size:
2285  if (!$maxSize || $fileSize <= $maxSize * 1024) {
2286  // Prepare filename:
2287  $theEndFileName = isset($this->alternativeFileName[$theFile]) ? $this->alternativeFileName[$theFile] : $theFile;
2288  $fI = GeneralUtility::split_fileref($theEndFileName);
2289  // Check for allowed extension:
2290  if ($this->fileFunc->checkIfAllowed($fI['fileext'], $dest, $theEndFileName)) {
2291  $theDestFile = $this->fileFunc->getUniqueName($this->fileFunc->cleanFileName($fI['file']), $dest);
2292  // If we have a unique destination filename, then write the file:
2293  if ($theDestFile) {
2294  GeneralUtility::upload_copy_move($theFile, $theDestFile);
2295  // Hook for post-processing the upload action
2296  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processUpload'])) {
2297  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processUpload'] as $classRef) {
2298  $hookObject = GeneralUtility::getUserObj($classRef);
2299  if (!$hookObject instanceof DataHandlerProcessUploadHookInterface) {
2300  throw new \UnexpectedValueException('$hookObject must implement interface TYPO3\\CMS\\Core\\DataHandling\\DataHandlerProcessUploadHookInterface', 1279962349);
2301  }
2302  $hookObject->processUpload_postProcessAction($theDestFile, $this);
2303  }
2304  }
2305  $this->copiedFileMap[$theFile] = $theDestFile;
2306  clearstatcache();
2307  if ($this->enableLogging && !@is_file($theDestFile)) {
2308  $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, array($theFile, dirname($theDestFile), $recFID), $propArr['event_pid']);
2309  }
2310  } elseif ($this->enableLogging) {
2311  $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: No destination file (%s) possible!. (%s)', 11, array($theFile, $theDestFile, $recFID), $propArr['event_pid']);
2312  }
2313  } elseif ($this->enableLogging) {
2314  $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, array($fI['fileext'], $recFID), $propArr['event_pid']);
2315  }
2316  } elseif ($this->enableLogging) {
2317  $this->log($table, $id, 5, 0, 1, 'Filesize (%s) of file \'%s\' exceeds limit (%s). (%s)', 13, array(GeneralUtility::formatSize($fileSize), $theFile, GeneralUtility::formatSize($maxSize * 1024), $recFID), $propArr['event_pid']);
2318  }
2319  } elseif ($this->enableLogging) {
2320  $this->log($table, $id, 5, 0, 1, 'The destination (%s) or the source file (%s) does not exist. (%s)', 14, array($dest, $theFile, $recFID), $propArr['event_pid']);
2321  }
2322  // If the destination file was created, we will set the new filename in the value array, otherwise unset the entry in the value array!
2323  if (@is_file($theDestFile)) {
2324  $info = GeneralUtility::split_fileref($theDestFile);
2325  // The value is set to the new filename
2326  $valueArray[$key] = $info['file'];
2327  } else {
2328  // The value is set to the new filename
2329  unset($valueArray[$key]);
2330  }
2331  }
2332  }
2333  }
2334  // 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!
2335  if ($tcaFieldConf['MM']) {
2337  $dbAnalysis = $this->createRelationHandlerInstance();
2338  // Dummy
2339  $dbAnalysis->tableArray['files'] = array();
2340  foreach ($valueArray as $key => $theFile) {
2341  // Explode files
2342  $dbAnalysis->itemArray[]['id'] = $theFile;
2343  }
2344  if ($status == 'update') {
2345  $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, 0);
2346  $newFiles = implode(',', $dbAnalysis->getValueArray());
2347  list(, , $recFieldName) = explode(':', $recFID);
2348  if ($currentFilesForHistory != $newFiles) {
2349  $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$recFieldName] = $currentFilesForHistory;
2350  $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$recFieldName] = $newFiles;
2351  } else {
2352  $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$recFieldName] = '';
2353  $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$recFieldName] = '';
2354  }
2355  } else {
2356  $this->dbAnalysisStore[] = array($dbAnalysis, $tcaFieldConf['MM'], $id, 0);
2357  }
2358  $valueArray = $dbAnalysis->countItems();
2359  }
2360  } else {
2361  if (!empty($valueArray)) {
2362  // If filehandling should NOT be bypassed, do processing:
2363  if (!$this->bypassFileHandling) {
2364  // For logging..
2365  $propArr = $this->getRecordProperties($table, $id);
2366  foreach ($valueArray as &$theFile) {
2367  // FAL handling: it's a UID, thus it is resolved to the absolute path
2368  if (!empty($theFile) && MathUtility::canBeInterpretedAsInteger($theFile)) {
2369  $fileObject = ResourceFactory::getInstance()->getFileObject($theFile);
2370  $theFile = $fileObject->getForLocalProcessing(false);
2371  }
2372  if ($this->alternativeFilePath[$theFile]) {
2373  // If alternative File Path is set for the file, then it was an import
2374  // don't import the file if it already exists
2375  if (@is_file((PATH_site . $this->alternativeFilePath[$theFile]))) {
2376  $theFile = PATH_site . $this->alternativeFilePath[$theFile];
2377  } elseif (@is_file($theFile)) {
2378  $dest = dirname(PATH_site . $this->alternativeFilePath[$theFile]);
2379  if (!@is_dir($dest)) {
2380  GeneralUtility::mkdir_deep(PATH_site, dirname($this->alternativeFilePath[$theFile]) . '/');
2381  }
2382  // Init:
2383  $maxSize = (int)$tcaFieldConf['max_size'];
2384  // Must be cleared. Else a faulty fileref may be inserted if the below code returns an error!
2385  $theDestFile = '';
2386  $fileSize = filesize($theFile);
2387  // Check file size:
2388  if (!$maxSize || $fileSize <= $maxSize * 1024) {
2389  // Prepare filename:
2390  $theEndFileName = isset($this->alternativeFileName[$theFile]) ? $this->alternativeFileName[$theFile] : $theFile;
2391  $fI = GeneralUtility::split_fileref($theEndFileName);
2392  // Check for allowed extension:
2393  if ($this->fileFunc->checkIfAllowed($fI['fileext'], $dest, $theEndFileName)) {
2394  $theDestFile = PATH_site . $this->alternativeFilePath[$theFile];
2395  // Write the file:
2396  if ($theDestFile) {
2397  GeneralUtility::upload_copy_move($theFile, $theDestFile);
2398  $this->copiedFileMap[$theFile] = $theDestFile;
2399  clearstatcache();
2400  if ($this->enableLogging && !@is_file($theDestFile)) {
2401  $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, array($theFile, dirname($theDestFile), $recFID), $propArr['event_pid']);
2402  }
2403  } elseif ($this->enableLogging) {
2404  $this->log($table, $id, 5, 0, 1, 'Copying file \'%s\' failed!: No destination file (%s) possible!. (%s)', 11, array($theFile, $theDestFile, $recFID), $propArr['event_pid']);
2405  }
2406  } elseif ($this->enableLogging) {
2407  $this->log($table, $id, 5, 0, 1, 'File extension \'%s\' not allowed. (%s)', 12, array($fI['fileext'], $recFID), $propArr['event_pid']);
2408  }
2409  } elseif ($this->enableLogging) {
2410  $this->log($table, $id, 5, 0, 1, 'Filesize (%s) of file \'%s\' exceeds limit (%s). (%s)', 13, array(GeneralUtility::formatSize($fileSize), $theFile, GeneralUtility::formatSize($maxSize * 1024), $recFID), $propArr['event_pid']);
2411  }
2412  // If the destination file was created, we will set the new filename in the value array, otherwise unset the entry in the value array!
2413  if (@is_file($theDestFile)) {
2414  // The value is set to the new filename
2415  $theFile = $theDestFile;
2416  } else {
2417  // The value is set to the new filename
2418  unset($theFile);
2419  }
2420  }
2421  }
2422  if (!empty($theFile)) {
2423  $theFile = GeneralUtility::fixWindowsFilePath($theFile);
2424  if (GeneralUtility::isFirstPartOfStr($theFile, PATH_site)) {
2425  $theFile = PathUtility::stripPathSitePrefix($theFile);
2426  }
2427  }
2428  }
2429  unset($theFile);
2430  }
2431  }
2432  }
2433  return $valueArray;
2434  }
2435 
2448  public function checkValue_flex($res, $value, $tcaFieldConf, $PP, $uploadedFiles, $field)
2449  {
2451  list($table, $id, $curValue, $status, $realPid, $recFID, $tscPID) = $PP;
2452  $this->checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field);
2453  }
2454 
2472  protected function checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field)
2473  {
2474  if (is_array($value)) {
2475  // This value is necessary for flex form processing to happen on flexform fields in page records when they are copied.
2476  // Problem: when copying a page, flexform XML comes along in the array for the new record - but since $this->checkValue_currentRecord does not have a uid or pid for that
2477  // sake, the BackendUtility::getFlexFormDS() function returns no good DS. For new records we do know the expected PID so therefore we send that with this special parameter.
2478  // Only active when larger than zero.
2479  $newRecordPidValue = $status == 'new' ? $realPid : 0;
2480  // Get current value array:
2481  $dataStructArray = BackendUtility::getFlexFormDS($tcaFieldConf, $this->checkValue_currentRecord, $table, $field, true, $newRecordPidValue);
2482  $currentValueArray = (string)$curValue !== '' ? GeneralUtility::xml2array($curValue) : array();
2483  if (!is_array($currentValueArray)) {
2484  $currentValueArray = array();
2485  }
2486  if (isset($currentValueArray['meta']['currentLangId'])) {
2487  // @deprecated call since TYPO3 7, will be removed with TYPO3 8
2488  unset($currentValueArray['meta']['currentLangId']);
2489  }
2490  // Remove all old meta for languages...
2491  // Evaluation of input values:
2492  $value['data'] = $this->checkValue_flex_procInData($value['data'], $currentValueArray['data'], $uploadedFiles['data'], $dataStructArray, array($table, $id, $curValue, $status, $realPid, $recFID, $tscPID));
2493  // Create XML from input value:
2494  $xmlValue = $this->checkValue_flexArray2Xml($value, true);
2495 
2496  // 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
2497  // (provided that the current value was already stored IN the charset that the new value is converted to).
2498  $arrValue = GeneralUtility::xml2array($xmlValue);
2499 
2500  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'])) {
2501  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'] as $classRef) {
2502  $hookObject = GeneralUtility::getUserObj($classRef);
2503  if (method_exists($hookObject, 'checkFlexFormValue_beforeMerge')) {
2504  $hookObject->checkFlexFormValue_beforeMerge($this, $currentValueArray, $arrValue);
2505  }
2506  }
2507  }
2508 
2509  ArrayUtility::mergeRecursiveWithOverrule($currentValueArray, $arrValue);
2510  $xmlValue = $this->checkValue_flexArray2Xml($currentValueArray, true);
2511 
2512  // Action commands (sorting order and removals of elements) for flexform sections,
2513  // see FormEngine for the use of this GP parameter
2514  $actionCMDs = GeneralUtility::_GP('_ACTION_FLEX_FORMdata');
2515  if (is_array($actionCMDs[$table][$id][$field]['data'])) {
2516  $arrValue = GeneralUtility::xml2array($xmlValue);
2517  $this->_ACTION_FLEX_FORMdata($arrValue['data'], $actionCMDs[$table][$id][$field]['data']);
2518  $xmlValue = $this->checkValue_flexArray2Xml($arrValue, true);
2519  }
2520  // Create the value XML:
2521  $res['value'] = '';
2522  $res['value'] .= $xmlValue;
2523  } else {
2524  // Passthrough...:
2525  $res['value'] = $value;
2526  }
2527 
2528  return $res;
2529  }
2530 
2538  public function checkValue_flexArray2Xml($array, $addPrologue = false)
2539  {
2541  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
2542  return $flexObj->flexArray2Xml($array, $addPrologue);
2543  }
2544 
2552  protected function _ACTION_FLEX_FORMdata(&$valueArray, $actionCMDs)
2553  {
2554  if (!is_array($valueArray) || !is_array($actionCMDs)) {
2555  return;
2556  }
2557 
2558  foreach ($actionCMDs as $key => $value) {
2559  if ($key == '_ACTION') {
2560  // First, check if there are "commands":
2561  if (current($actionCMDs[$key]) === '') {
2562  continue;
2563  }
2564 
2565  asort($actionCMDs[$key]);
2566  $newValueArray = array();
2567  foreach ($actionCMDs[$key] as $idx => $order) {
2568  if (substr($idx, 0, 3) == 'ID-') {
2569  $idx = $this->newIndexMap[$idx];
2570  }
2571  // 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.
2572  if ($order != 'DELETE') {
2573  $newValueArray[$idx] = $valueArray[$idx];
2574  }
2575  unset($valueArray[$idx]);
2576  }
2577  $valueArray = $valueArray + $newValueArray;
2578  } elseif (is_array($actionCMDs[$key]) && isset($valueArray[$key])) {
2579  $this->_ACTION_FLEX_FORMdata($valueArray[$key], $actionCMDs[$key]);
2580  }
2581  }
2582  }
2583 
2596  public function checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData = null)
2597  {
2598  list($table, $id, , $status) = $PP;
2599  $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
2600  }
2601 
2616  public function checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData = null)
2617  {
2618  if (!$tcaFieldConf['foreign_table']) {
2619  // Fatal error, inline fields should always have a foreign_table defined
2620  return false;
2621  }
2622  // When values are sent they come as comma-separated values which are exploded by this function:
2623  $valueArray = GeneralUtility::trimExplode(',', $value);
2624  // Remove duplicates: (should not be needed)
2625  $valueArray = array_unique($valueArray);
2626  // Example for received data:
2627  // $value = 45,NEW4555fdf59d154,12,123
2628  // We need to decide whether we use the stack or can save the relation directly.
2629  if (strpos($value, 'NEW') !== false || !MathUtility::canBeInterpretedAsInteger($id)) {
2630  $this->remapStackRecords[$table][$id] = array('remapStackIndex' => count($this->remapStack));
2631  $this->addNewValuesToRemapStackChildIds($valueArray);
2632  $this->remapStack[] = array(
2633  'func' => 'checkValue_inline_processDBdata',
2634  'args' => array($valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData),
2635  'pos' => array('valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 4),
2636  'additionalData' => $additionalData,
2637  'field' => $field,
2638  );
2639  unset($res['value']);
2640  } elseif ($value || MathUtility::canBeInterpretedAsInteger($id)) {
2641  $res['value'] = $this->checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData);
2642  }
2643  return $res;
2644  }
2645 
2654  public function checkValue_checkMax($tcaFieldConf, $valueArray)
2655  {
2656  // 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...
2657  $valueArrayC = count($valueArray);
2658  // 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.
2659  $maxI = isset($tcaFieldConf['maxitems']) ? (int)$tcaFieldConf['maxitems'] : 1;
2660  if ($valueArrayC > $maxI) {
2661  $valueArrayC = $maxI;
2662  }
2663  // Checking for not too many elements
2664  // Dumping array to list
2665  $newVal = array();
2666  foreach ($valueArray as $nextVal) {
2667  if ($valueArrayC == 0) {
2668  break;
2669  }
2670  $valueArrayC--;
2671  $newVal[] = $nextVal;
2672  }
2673  return $newVal;
2674  }
2675 
2676  /*********************************************
2677  *
2678  * Helper functions for evaluation functions.
2679  *
2680  ********************************************/
2691  public function getUnique($table, $field, $value, $id, $newPid = 0)
2692  {
2693  // Initialize:
2694  $whereAdd = '';
2695  $newValue = '';
2696  if ((int)$newPid) {
2697  $whereAdd .= ' AND pid=' . (int)$newPid;
2698  } else {
2699  $whereAdd .= ' AND pid>=0';
2700  }
2701  // "AND pid>=0" for versioning
2702  $whereAdd .= $this->deleteClause($table);
2703  // If the field is configured in TCA, proceed:
2704  if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
2705  // Look for a record which might already have the value:
2706  $res = $this->databaseConnection->exec_SELECTquery('uid', $table, $field . '=' . $this->databaseConnection->fullQuoteStr($value, $table) . ' AND uid<>' . (int)$id . $whereAdd);
2707  $counter = 0;
2708  // For as long as records with the test-value existing, try again (with incremented numbers appended).
2709  while ($this->databaseConnection->sql_num_rows($res)) {
2710  $newValue = $value . $counter;
2711  $res = $this->databaseConnection->exec_SELECTquery('uid', $table, $field . '=' . $this->databaseConnection->fullQuoteStr($newValue, $table) . ' AND uid<>' . (int)$id . $whereAdd);
2712  $counter++;
2713  if ($counter > 100) {
2714  break;
2715  }
2716  }
2717  $this->databaseConnection->sql_free_result($res);
2718  // If the new value is there:
2719  $value = $newValue !== '' ? $newValue : $value;
2720  }
2721  return $value;
2722  }
2723 
2735  public function getRecordsWithSameValue($tableName, $uid, $fieldName, $value, $pageId = 0)
2736  {
2737  $result = array();
2738  if (!empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
2739  $uid = (int)$uid;
2740  $pageId = (int)$pageId;
2741  $whereStatement = ' AND uid <> ' . $uid . ' AND ' . ($pageId ? 'pid = ' . $pageId : 'pid >= 0');
2742  $result = BackendUtility::getRecordsByField($tableName, $fieldName, $value, $whereStatement);
2743  }
2744  return $result;
2745  }
2746 
2753  public function checkValue_text_Eval($value, $evalArray, $is_in)
2754  {
2755  $res = array();
2756  $set = true;
2757  foreach ($evalArray as $func) {
2758  switch ($func) {
2759  case 'trim':
2760  $value = trim($value);
2761  break;
2762  case 'required':
2763  if (!$value) {
2764  $set = false;
2765  }
2766  break;
2767  default:
2768  if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2769  if (class_exists($func)) {
2770  $evalObj = GeneralUtility::makeInstance($func);
2771  if (method_exists($evalObj, 'evaluateFieldValue')) {
2772  $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2773  }
2774  }
2775  }
2776  }
2777  }
2778  if ($set) {
2779  $res['value'] = $value;
2780  }
2781  return $res;
2782  }
2783 
2792  public function checkValue_input_Eval($value, $evalArray, $is_in)
2793  {
2794  $res = array();
2795  $set = true;
2796  foreach ($evalArray as $func) {
2797  switch ($func) {
2798  case 'int':
2799  case 'year':
2800  case 'time':
2801  case 'timesec':
2802  $value = (int)$value;
2803  break;
2804  case 'date':
2805  case 'datetime':
2806  $value = (int)$value;
2807  if ($value > 0 && !$this->dontProcessTransformations) {
2808  $value -= date('Z', $value);
2809  }
2810  break;
2811  case 'double2':
2812  $value = preg_replace('/[^0-9,\\.-]/', '', $value);
2813  $negative = $value[0] === '-';
2814  $value = strtr($value, array(',' => '.', '-' => ''));
2815  if (strpos($value, '.') === false) {
2816  $value .= '.0';
2817  }
2818  $valueArray = explode('.', $value);
2819  $dec = array_pop($valueArray);
2820  $value = join('', $valueArray) . '.' . $dec;
2821  if ($negative) {
2822  $value *= -1;
2823  }
2824  $value = number_format($value, 2, '.', '');
2825  break;
2826  case 'md5':
2827  if (strlen($value) != 32) {
2828  $set = false;
2829  }
2830  break;
2831  case 'trim':
2832  $value = trim($value);
2833  break;
2834  case 'upper':
2835  $value = $GLOBALS['LANG']->csConvObj->conv_case($GLOBALS['LANG']->charSet, $value, 'toUpper');
2836  break;
2837  case 'lower':
2838  $value = $GLOBALS['LANG']->csConvObj->conv_case($GLOBALS['LANG']->charSet, $value, 'toLower');
2839  break;
2840  case 'required':
2841  if (!isset($value) || $value === '') {
2842  $set = false;
2843  }
2844  break;
2845  case 'is_in':
2846  $c = strlen($value);
2847  if ($c) {
2848  $newVal = '';
2849  for ($a = 0; $a < $c; $a++) {
2850  $char = substr($value, $a, 1);
2851  if (strpos($is_in, $char) !== false) {
2852  $newVal .= $char;
2853  }
2854  }
2855  $value = $newVal;
2856  }
2857  break;
2858  case 'nospace':
2859  $value = str_replace(' ', '', $value);
2860  break;
2861  case 'alpha':
2862  $value = preg_replace('/[^a-zA-Z]/', '', $value);
2863  break;
2864  case 'num':
2865  $value = preg_replace('/[^0-9]/', '', $value);
2866  break;
2867  case 'alphanum':
2868  $value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
2869  break;
2870  case 'alphanum_x':
2871  $value = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
2872  break;
2873  case 'domainname':
2874  if (!preg_match('/^[a-z0-9.\\-]*$/i', $value)) {
2875  $value = GeneralUtility::idnaEncode($value);
2876  }
2877  break;
2878  case 'email':
2879  $this->checkValue_input_ValidateEmail($value, $set);
2880  break;
2881  default:
2882  if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2883  if (class_exists($func)) {
2884  $evalObj = GeneralUtility::makeInstance($func);
2885  if (method_exists($evalObj, 'evaluateFieldValue')) {
2886  $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2887  }
2888  }
2889  }
2890  }
2891  }
2892  if ($set) {
2893  $res['value'] = $value;
2894  }
2895  return $res;
2896  }
2897 
2909  protected function checkValue_input_ValidateEmail($value, &$set)
2910  {
2911  if (GeneralUtility::validEmail($value)) {
2912  return;
2913  }
2914 
2915  $set = false;
2917  $message = GeneralUtility::makeInstance(FlashMessage::class,
2918  sprintf($GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:error.invalidEmail'), $value),
2919  '', // header is optional
2921  true // whether message should be stored in session
2922  );
2924  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
2925  $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
2926  }
2927 
2940  public function checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
2941  {
2942  $tables = $type == 'group' ? $tcaFieldConf['allowed'] : $tcaFieldConf['foreign_table'];
2943  $prep = $type == 'group' ? $tcaFieldConf['prepend_tname'] : '';
2944  $newRelations = implode(',', $valueArray);
2946  $dbAnalysis = $this->createRelationHandlerInstance();
2947  $dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
2948  $dbAnalysis->start($newRelations, $tables, '', 0, $currentTable, $tcaFieldConf);
2949  if ($tcaFieldConf['MM']) {
2950  if ($status == 'update') {
2952  $oldRelations_dbAnalysis = $this->createRelationHandlerInstance();
2953  $oldRelations_dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
2954  // Db analysis with $id will initialize with the existing relations
2955  $oldRelations_dbAnalysis->start('', $tables, $tcaFieldConf['MM'], $id, $currentTable, $tcaFieldConf);
2956  $oldRelations = implode(',', $oldRelations_dbAnalysis->getValueArray());
2957  $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, $prep);
2958  if ($oldRelations != $newRelations) {
2959  $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
2960  $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
2961  } else {
2962  $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
2963  $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
2964  }
2965  } else {
2966  $this->dbAnalysisStore[] = array($dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable);
2967  }
2968  $valueArray = $dbAnalysis->countItems();
2969  } else {
2970  $valueArray = $dbAnalysis->getValueArray($prep);
2971  }
2972  // 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.
2973  return $valueArray;
2974  }
2975 
2983  {
2984  $valueArray = GeneralUtility::trimExplode(',', $value, true);
2985  foreach ($valueArray as &$newVal) {
2986  $temp = explode('|', $newVal, 2);
2987  $newVal = str_replace(',', '', str_replace('|', '', rawurldecode($temp[0])));
2988  }
2989  unset($newVal);
2990  return $valueArray;
2991  }
2992 
3007  public function checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructArray, $pParams, $callBackFunc = '')
3008  {
3009  if (is_array($dataPart)) {
3010  foreach ($dataPart as $sKey => $sheetDef) {
3011  list($dataStruct, $actualSheet) = GeneralUtility::resolveSheetDefInDS($dataStructArray, $sKey);
3012  if (is_array($dataStruct) && $actualSheet == $sKey && is_array($sheetDef)) {
3013  foreach ($sheetDef as $lKey => $lData) {
3014  $this->checkValue_flex_procInData_travDS($dataPart[$sKey][$lKey], $dataPart_current[$sKey][$lKey], $uploadedFiles[$sKey][$lKey], $dataStruct['ROOT']['el'], $pParams, $callBackFunc, $sKey . '/' . $lKey . '/');
3015  }
3016  }
3017  }
3018  }
3019  return $dataPart;
3020  }
3021 
3036  public function checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath)
3037  {
3038  if (!is_array($DSelements)) {
3039  return;
3040  }
3041 
3042  // For each DS element:
3043  foreach ($DSelements as $key => $dsConf) {
3044  // Array/Section:
3045  if ($DSelements[$key]['type'] == 'array') {
3046  if (!is_array($dataValues[$key]['el'])) {
3047  continue;
3048  }
3049 
3050  if ($DSelements[$key]['section']) {
3051  $newIndexCounter = 0;
3052  foreach ($dataValues[$key]['el'] as $ik => $el) {
3053  if (!is_array($el)) {
3054  continue;
3055  }
3056 
3057  if (!is_array($dataValues_current[$key]['el'])) {
3058  $dataValues_current[$key]['el'] = array();
3059  }
3060  $theKey = key($el);
3061  if (!is_array($dataValues[$key]['el'][$ik][$theKey]['el'])) {
3062  continue;
3063  }
3064 
3065  $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'] : array(), $uploadedFiles[$key]['el'][$ik][$theKey]['el'], $DSelements[$key]['el'][$theKey]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/' . $ik . '/' . $theKey . '/el/');
3066  // 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:
3067  // The fact that the order changes is not important since order is controlled by a separately submitted index.
3068  if (substr($ik, 0, 3) == 'ID-') {
3069  $newIndexCounter++;
3070  // Set mapping index
3071  $this->newIndexMap[$ik] = (is_array($dataValues_current[$key]['el']) && !empty($dataValues_current[$key]['el']) ? max(array_keys($dataValues_current[$key]['el'])) : 0) + $newIndexCounter;
3072  // Transfer values
3073  $dataValues[$key]['el'][$this->newIndexMap[$ik]] = $dataValues[$key]['el'][$ik];
3074  // Unset original
3075  unset($dataValues[$key]['el'][$ik]);
3076  }
3077  }
3078  } else {
3079  if (!isset($dataValues[$key]['el'])) {
3080  $dataValues[$key]['el'] = array();
3081  }
3082  $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $uploadedFiles[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/');
3083  }
3084  } else {
3085  if (!is_array($dsConf['TCEforms']['config']) || !is_array($dataValues[$key])) {
3086  continue;
3087  }
3088 
3089  foreach ($dataValues[$key] as $vKey => $data) {
3090  if ($callBackFunc) {
3091  if (is_object($this->callBackObj)) {
3092  $res = $this->callBackObj->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/');
3093  } else {
3094  $res = $this->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/');
3095  }
3096  } else {
3097  // Default
3098  list($CVtable, $CVid, $CVcurValue, $CVstatus, $CVrealPid, $CVrecFID, $CVtscPID) = $pParams;
3099 
3100  $additionalData = array(
3101  'flexFormId' => $CVrecFID,
3102  'flexFormPath' => trim(rtrim($structurePath, '/') . '/' . $key . '/' . $vKey, '/'),
3103  );
3104 
3105  $res = $this->checkValue_SW(array(), $dataValues[$key][$vKey], $dsConf['TCEforms']['config'], $CVtable, $CVid, $dataValues_current[$key][$vKey], $CVstatus, $CVrealPid, $CVrecFID, '', $uploadedFiles[$key][$vKey], $CVtscPID, $additionalData);
3106  // Look for RTE transformation of field:
3107  if ($dataValues[$key]['_TRANSFORM_' . $vKey] == 'RTE' && !$this->dontProcessTransformations) {
3108  // Unsetting trigger field - we absolutely don't want that into the data storage!
3109  unset($dataValues[$key]['_TRANSFORM_' . $vKey]);
3110  if (isset($res['value'])) {
3111  // Calculating/Retrieving some values here:
3112  list(, , $recFieldName) = explode(':', $CVrecFID);
3113  $theTypeString = BackendUtility::getTCAtypeValue($CVtable, $this->checkValue_currentRecord);
3114  $specConf = BackendUtility::getSpecConfParts($dsConf['TCEforms']['defaultExtras']);
3115  // Find, thisConfig:
3116  $RTEsetup = $this->BE_USER->getTSConfig('RTE', BackendUtility::getPagesTSconfig($CVtscPID));
3117  $thisConfig = BackendUtility::RTEsetup($RTEsetup['properties'], $CVtable, $recFieldName, $theTypeString);
3118  $res['value'] = $this->transformRichtextContentToDatabase(
3119  $res['value'], $CVtable, $recFieldName, $specConf, $thisConfig, $CVrealPid
3120  );
3121  }
3122  }
3123  }
3124  // Adding the value:
3125  if (isset($res['value'])) {
3126  $dataValues[$key][$vKey] = $res['value'];
3127  }
3128  // 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.
3129  // 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).
3130  if (substr($vKey, -9) != '.vDEFbase') {
3131  // @deprecated: flexFormXMLincludeDiffBase is only enabled by ext:compatibility6 since TYPO3 CMS 7, vDEFbase can be unset / ignored with TYPO3 CMS 8
3132  if ($this->clear_flexFormData_vDEFbase) {
3133  $dataValues[$key][$vKey . '.vDEFbase'] = '';
3134  } elseif ($this->updateModeL10NdiffData && $GLOBALS['TYPO3_CONF_VARS']['BE']['flexFormXMLincludeDiffBase'] && $vKey !== 'vDEF' && ((string)$dataValues[$key][$vKey] !== (string)$dataValues_current[$key][$vKey] || !isset($dataValues_current[$key][($vKey . '.vDEFbase')]) || $this->updateModeL10NdiffData === 'FORCE_FFUPD')) {
3135  // 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:
3136  if (isset($dataValues[$key]['vDEF'])) {
3137  $diffValue = $dataValues[$key]['vDEF'];
3138  } else {
3139  // If not found (for translators with no access to the default language) we use the one from the current-value data set:
3140  $diffValue = $dataValues_current[$key]['vDEF'];
3141  }
3142  // 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.
3143  $dataValues[$key][$vKey . '.vDEFbase'] = $this->updateModeL10NdiffDataClear ? '' : $diffValue;
3144  }
3145  }
3146  }
3147  }
3148  }
3149  }
3150 
3163  protected function checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, array $additionalData = null)
3164  {
3165  $newValue = '';
3166  $foreignTable = $tcaFieldConf['foreign_table'];
3167  $transOrigPointer = 0;
3168  $keepTranslation = false;
3169  $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
3170  // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3172  $dbAnalysis = $this->createRelationHandlerInstance();
3173  $dbAnalysis->start(implode(',', $valueArray), $foreignTable, '', 0, $table, $tcaFieldConf);
3174  // If the localizationMode is set to 'keep', the children for the localized parent are kept as in the original untranslated record:
3175  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $tcaFieldConf);
3176  if ($localizationMode == 'keep' && $status == 'update') {
3177  // Fetch the current record and determine the original record:
3178  $row = BackendUtility::getRecordWSOL($table, $id);
3179  if (is_array($row)) {
3180  $language = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
3181  $transOrigPointer = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
3182  // If language is set (e.g. 1) and also transOrigPointer (e.g. 123), use transOrigPointer as uid:
3183  if ($language > 0 && $transOrigPointer) {
3184  $id = $transOrigPointer;
3185  // If we're in active localizationMode 'keep', prevent from writing data to the field of the parent record:
3186  // (on removing the localized parent, the original (untranslated) children would then also be removed)
3187  $keepTranslation = true;
3188  }
3189  }
3190  }
3191  // IRRE with a pointer field (database normalization):
3192  if ($tcaFieldConf['foreign_field']) {
3193  // if the record was imported, sorting was also imported, so skip this
3194  $skipSorting = (bool)$this->callFromImpExp;
3195  // update record in intermediate table (sorting & pointer uid to parent record)
3196  $dbAnalysis->writeForeignField($tcaFieldConf, $id, 0, $skipSorting);
3197  $newValue = $keepTranslation ? 0 : $dbAnalysis->countItems(false);
3198  } else {
3199  if ($this->getInlineFieldType($tcaFieldConf) == 'mm') {
3200  // In order to fully support all the MM stuff, directly call checkValue_group_select_processDBdata instead of repeating the needed code here
3201  $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
3202  $newValue = $keepTranslation ? 0 : $valueArray[0];
3203  } else {
3204  $valueArray = $dbAnalysis->getValueArray();
3205  // Checking that the number of items is correct:
3206  $valueArray = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
3207  $valueData = $this->castReferenceValue(implode(',', $valueArray), $tcaFieldConf);
3208  // If a valid translation of the 'keep' mode is active, update relations in the original(!) record:
3209  if ($keepTranslation) {
3210  $this->updateDB($table, $transOrigPointer, array($field => $valueData));
3211  } else {
3212  $newValue = $valueData;
3213  }
3214  }
3215  }
3216  return $newValue;
3217  }
3218 
3219  /*********************************************
3220  *
3221  * PROCESSING COMMANDS
3222  *
3223  ********************************************/
3230  public function process_cmdmap()
3231  {
3232  // Editing frozen:
3233  if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
3234  if ($this->enableLogging) {
3235  $this->newlog('All editing in this workspace has been frozen!', 1);
3236  }
3237  return false;
3238  }
3239  // Hook initialization:
3240  $hookObjectsArr = array();
3241  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'])) {
3242  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] as $classRef) {
3243  $hookObj = GeneralUtility::getUserObj($classRef);
3244  if (method_exists($hookObj, 'processCmdmap_beforeStart')) {
3245  $hookObj->processCmdmap_beforeStart($this);
3246  }
3247  $hookObjectsArr[] = $hookObj;
3248  }
3249  }
3250  $pasteDatamap = array();
3251  // Traverse command map:
3252  foreach ($this->cmdmap as $table => $_) {
3253  // Check if the table may be modified!
3254  $modifyAccessList = $this->checkModifyAccessList($table);
3255  if ($this->enableLogging && !$modifyAccessList) {
3256  $this->log($table, 0, 2, 0, 1, 'Attempt to modify table \'%s\' without permission', 1, array($table));
3257  }
3258  // Check basic permissions and circumstances:
3259  if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->cmdmap[$table]) || !$modifyAccessList) {
3260  continue;
3261  }
3262 
3263  // Traverse the command map:
3264  foreach ($this->cmdmap[$table] as $id => $incomingCmdArray) {
3265  if (!is_array($incomingCmdArray)) {
3266  continue;
3267  }
3268 
3269  if ($table === 'pages') {
3270  // for commands on pages do a pagetree-refresh
3271  $this->pagetreeNeedsRefresh = true;
3272  }
3273 
3274  foreach ($incomingCmdArray as $command => $value) {
3275  $pasteUpdate = false;
3276  if (is_array($value) && isset($value['action']) && $value['action'] === 'paste') {
3277  // Extended paste command: $command is set to "move" or "copy"
3278  // $value['update'] holds field/value pairs which should be updated after copy/move operation
3279  // $value['target'] holds original $value (target of move/copy)
3280  $pasteUpdate = $value['update'];
3281  $value = $value['target'];
3282  }
3283  foreach ($hookObjectsArr as $hookObj) {
3284  if (method_exists($hookObj, 'processCmdmap_preProcess')) {
3285  $hookObj->processCmdmap_preProcess($command, $table, $id, $value, $this, $pasteUpdate);
3286  }
3287  }
3288  // Init copyMapping array:
3289  // Must clear this array before call from here to those functions:
3290  // Contains mapping information between new and old id numbers.
3291  $this->copyMappingArray = array();
3292  // process the command
3293  $commandIsProcessed = false;
3294  foreach ($hookObjectsArr as $hookObj) {
3295  if (method_exists($hookObj, 'processCmdmap')) {
3296  $hookObj->processCmdmap($command, $table, $id, $value, $commandIsProcessed, $this, $pasteUpdate);
3297  }
3298  }
3299  // Only execute default commands if a hook hasn't been processed the command already
3300  if (!$commandIsProcessed) {
3301  $procId = $id;
3302  // Branch, based on command
3303  switch ($command) {
3304  case 'move':
3305  $this->moveRecord($table, $id, $value);
3306  break;
3307  case 'copy':
3308  if ($table === 'pages') {
3309  $this->copyPages($id, $value);
3310  } else {
3311  $this->copyRecord($table, $id, $value, 1);
3312  }
3313  $procId = $this->copyMappingArray[$table][$id];
3314  break;
3315  case 'localize':
3316  $this->localize($table, $id, $value);
3317  break;
3318  case 'inlineLocalizeSynchronize':
3319  $this->inlineLocalizeSynchronize($table, $id, $value);
3320  break;
3321  case 'delete':
3322  $this->deleteAction($table, $id);
3323  break;
3324  case 'undelete':
3325  $this->undeleteRecord($table, $id);
3326  break;
3327  }
3328  if (is_array($pasteUpdate)) {
3329  $pasteDatamap[$table][$procId] = $pasteUpdate;
3330  }
3331  }
3332  foreach ($hookObjectsArr as $hookObj) {
3333  if (method_exists($hookObj, 'processCmdmap_postProcess')) {
3334  $hookObj->processCmdmap_postProcess($command, $table, $id, $value, $this, $pasteUpdate, $pasteDatamap);
3335  }
3336  }
3337  // Merging the copy-array info together for remapping purposes.
3338  ArrayUtility::mergeRecursiveWithOverrule($this->copyMappingArray_merged, $this->copyMappingArray);
3339  }
3340  }
3341  }
3343  $copyTCE = $this->getLocalTCE();
3344  $copyTCE->start($pasteDatamap, '', $this->BE_USER);
3345  $copyTCE->process_datamap();
3346  $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3347  unset($copyTCE);
3348 
3349  // Finally, before exit, check if there are ID references to remap.
3350  // This might be the case if versioning or copying has taken place!
3351  $this->remapListedDBRecords();
3352  $this->processRemapStack();
3353  foreach ($hookObjectsArr as $hookObj) {
3354  if (method_exists($hookObj, 'processCmdmap_afterFinish')) {
3355  $hookObj->processCmdmap_afterFinish($this);
3356  }
3357  }
3358  if ($this->isOuterMostInstance()) {
3359  $this->processClearCacheQueue();
3360  $this->resetNestedElementCalls();
3361  }
3362  }
3363 
3364  /*********************************************
3365  *
3366  * Cmd: Copying
3367  *
3368  ********************************************/
3382  public function copyRecord($table, $uid, $destPid, $first = false, $overrideValues = array(), $excludeFields = '', $language = 0, $ignoreLocalization = false)
3383  {
3384  $uid = ($origUid = (int)$uid);
3385  // Only copy if the table is defined in $GLOBALS['TCA'], a uid is given and the record wasn't copied before:
3386  if (empty($GLOBALS['TCA'][$table]) || $uid === 0) {
3387  return null;
3388  }
3389  if ($this->isRecordCopied($table, $uid)) {
3390  if (!empty($overrideValues)) {
3391  $this->log($table, $uid, 5, 0, 1, 'Repeated attempt to copy record "' . $table . ':' . $uid . '" with override values');
3392  }
3393  return null;
3394  }
3395 
3396  // This checks if the record can be selected which is all that a copy action requires.
3397  if (!$this->doesRecordExist($table, $uid, 'show')) {
3398  if ($this->enableLogging) {
3399  $this->log($table, $uid, 3, 0, 1, 'Attempt to copy record without permission');
3400  }
3401  return null;
3402  }
3403 
3404  // Check if table is allowed on destination page
3405  if ($destPid >= 0 && !$this->isTableAllowedForThisPage($destPid, $table)) {
3406  if ($this->enableLogging) {
3407  $this->log($table, $uid, 3, 0, 1, 'Attempt to insert record on a page that can\'t store record type.');
3408  }
3409  return null;
3410  }
3411 
3412  $fullLanguageCheckNeeded = $table != 'pages';
3413  //Used to check language and general editing rights
3414  if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded)) {
3415  if ($this->enableLogging) {
3416  $this->log($table, $uid, 3, 0, 1, 'Attempt to copy record without having permissions to do so. [' . $this->BE_USER->errorMsg . '].');
3417  }
3418  return null;
3419  }
3420 
3421  $data = array();
3422  $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));
3423  // So it copies (and localized) content from workspace...
3424  $row = BackendUtility::getRecordWSOL($table, $uid);
3425  if (!is_array($row)) {
3426  if ($this->enableLogging) {
3427  $this->log($table, $uid, 3, 0, 1, 'Attempt to copy record that did not exist!');
3428  }
3429  return null;
3430  }
3431 
3432  // Initializing:
3433  $theNewID = StringUtility::getUniqueId('NEW');
3434  $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3435  $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3436  // Getting default data:
3437  $defaultData = $this->newFieldArray($table);
3438  // Getting "copy-after" fields if applicable:
3439  $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, abs($destPid), 0) : array();
3440  // Page TSconfig related:
3441  // 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...
3442  $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
3443  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
3444  $tE = $this->getTableEntries($table, $TSConfig);
3445  // Traverse ALL fields of the selected record:
3446  $setDefaultOnCopyArray = array_flip(GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['setToDefaultOnCopy']));
3447  foreach ($row as $field => $value) {
3448  if (!in_array($field, $nonFields, true)) {
3449  // Get TCA configuration for the field:
3450  $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3451  // Preparation/Processing of the value:
3452  // "pid" is hardcoded of course:
3453  // isset() won't work here, since values can be NULL in each of the arrays
3454  // except setDefaultOnCopyArray, since we exploded that from a string
3455  if ($field == 'pid') {
3456  $value = $destPid;
3457  } elseif (array_key_exists($field, $overrideValues)) {
3458  // Override value...
3459  $value = $overrideValues[$field];
3460  } elseif (array_key_exists($field, $copyAfterFields)) {
3461  // Copy-after value if available:
3462  $value = $copyAfterFields[$field];
3463  } elseif ($GLOBALS['TCA'][$table]['ctrl']['setToDefaultOnCopy'] && isset($setDefaultOnCopyArray[$field])) {
3464  $value = $defaultData[$field];
3465  } else {
3466  // Hide at copy may override:
3467  if ($first && $field == $enableField && $GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] && !$this->neverHideAtCopy && !$tE['disableHideAtCopy']) {
3468  $value = 1;
3469  }
3470  // Prepend label on copy:
3471  if ($first && $field == $headerField && $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy'] && !$tE['disablePrependAtCopy']) {
3472  $value = $this->getCopyHeader($table, $this->resolvePid($table, $destPid), $field, $this->clearPrefixFromValue($table, $value), 0);
3473  }
3474  // Processing based on the TCA config field type (files, references, flexforms...)
3475  $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $tscPID, $language);
3476  }
3477  // Add value to array.
3478  $data[$table][$theNewID][$field] = $value;
3479  }
3480  }
3481  // Overriding values:
3482  if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
3483  $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3484  }
3485  // Setting original UID:
3486  if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3487  $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3488  }
3489  // Do the copy by simply submitting the array through TCEmain:
3491  $copyTCE = $this->getLocalTCE();
3492  $copyTCE->start($data, '', $this->BE_USER);
3493  $copyTCE->process_datamap();
3494  // Getting the new UID:
3495  $theNewSQLID = $copyTCE->substNEWwithIDs[$theNewID];
3496  if ($theNewSQLID) {
3497  $this->copyRecord_fixRTEmagicImages($table, BackendUtility::wsMapId($table, $theNewSQLID));
3498  $this->copyMappingArray[$table][$origUid] = $theNewSQLID;
3499  // Keep automatically versionized record information:
3500  if (isset($copyTCE->autoVersionIdMap[$table][$theNewSQLID])) {
3501  $this->autoVersionIdMap[$table][$theNewSQLID] = $copyTCE->autoVersionIdMap[$table][$theNewSQLID];
3502  }
3503  }
3504  // Copy back the cached TSconfig
3505  $this->cachedTSconfig = $copyTCE->cachedTSconfig;
3506  $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3507  unset($copyTCE);
3508  if (!$ignoreLocalization && $language == 0) {
3509  //repointing the new translation records to the parent record we just created
3510  $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
3511  $this->copyL10nOverlayRecords($table, $uid, $destPid, $first, $overrideValues, $excludeFields);
3512  }
3513 
3514  return $theNewSQLID;
3515  }
3516 
3525  public function copyPages($uid, $destPid)
3526  {
3527  // Initialize:
3528  $uid = (int)$uid;
3529  $destPid = (int)$destPid;
3530  // Finding list of tables to copy.
3531  // These are the tables, the user may modify
3532  $copyTablesArray = $this->admin ? $this->compileAdminTables() : explode(',', $this->BE_USER->groupData['tables_modify']);
3533  // 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
3534  if (!strstr($this->copyWhichTables, '*')) {
3535  $copyWhichTablesArray = array_flip(GeneralUtility::trimExplode(',', $this->copyWhichTables . ',pages'));
3536  foreach ($copyTablesArray as $k => $table) {
3537  // Pages are always going...
3538  if (!$table || !isset($copyWhichTablesArray[$table])) {
3539  unset($copyTablesArray[$k]);
3540  }
3541  }
3542  }
3543  $copyTablesArray = array_unique($copyTablesArray);
3544  // Begin to copy pages if we're allowed to:
3545  if ($this->admin || in_array('pages', $copyTablesArray, true)) {
3546  // Copy this page we're on. And set first-flag (this will trigger that the record is hidden if that is configured)!
3547  $theNewRootID = $this->copySpecificPage($uid, $destPid, $copyTablesArray, 1);
3548  // If we're going to copy recursively...:
3549  if ($theNewRootID && $this->copyTree) {
3550  // Get ALL subpages to copy (read-permissions are respected!):
3551  $CPtable = $this->int_pageTreeInfo(array(), $uid, (int)$this->copyTree, $theNewRootID);
3552  // Now copying the subpages:
3553  foreach ($CPtable as $thePageUid => $thePagePid) {
3554  $newPid = $this->copyMappingArray['pages'][$thePagePid];
3555  if (isset($newPid)) {
3556  $this->copySpecificPage($thePageUid, $newPid, $copyTablesArray);
3557  } else {
3558  if ($this->enableLogging) {
3559  $this->log('pages', $uid, 5, 0, 1, 'Something went wrong during copying branch');
3560  }
3561  break;
3562  }
3563  }
3564  }
3565  } elseif ($this->enableLogging) {
3566  $this->log('pages', $uid, 5, 0, 1, 'Attempt to copy page without permission to this table');
3567  }
3568  }
3569 
3579  public function copySpecificPage($uid, $destPid, $copyTablesArray, $first = false)
3580  {
3581  // Copy the page itself:
3582  $theNewRootID = $this->copyRecord('pages', $uid, $destPid, $first);
3583  // If a new page was created upon the copy operation we will proceed with all the tables ON that page:
3584  if ($theNewRootID) {
3585  foreach ($copyTablesArray as $table) {
3586  // All records under the page is copied.
3587  if ($table && is_array($GLOBALS['TCA'][$table]) && $table != 'pages') {
3588  $fields = 'uid';
3589  $languageField = null;
3590  $transOrigPointerField = null;
3591  if (BackendUtility::isTableLocalizable($table)) {
3592  $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
3593  $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3594  $fields .= ',' . $languageField . ',' . $transOrigPointerField;
3595  }
3596  $rows = $this->databaseConnection->exec_SELECTgetRows(
3597  $fields,
3598  $table,
3599  'pid=' . (int)$uid . $this->deleteClause($table),
3600  '',
3601  (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby']) ? $GLOBALS['TCA'][$table]['ctrl']['sortby'] . ' DESC' : ''),
3602  '',
3603  'uid'
3604  );
3605  foreach ($rows as $row) {
3606  // Skip localized records that will be processed in
3607  // copyL10nOverlayRecords() on copying the default language record
3608  $transOrigPointer = $row[$transOrigPointerField];
3609  if ($row[$languageField] > 0 && $transOrigPointer > 0 && isset($rows[$transOrigPointer])) {
3610  continue;
3611  }
3612  // Copying each of the underlying records...
3613  $this->copyRecord($table, $row['uid'], $theNewRootID);
3614  }
3615  }
3616  }
3617  $this->processRemapStack();
3618  return $theNewRootID;
3619  }
3620  return null;
3621  }
3622 
3638  public function copyRecord_raw($table, $uid, $pid, $overrideArray = array(), array $workspaceOptions = array())
3639  {
3640  $uid = (int)$uid;
3641  // Stop any actions if the record is marked to be deleted:
3642  // (this can occur if IRRE elements are versionized and child elements are removed)
3643  if ($this->isElementToBeDeleted($table, $uid)) {
3644  return null;
3645  }
3646  // Only copy if the table is defined in TCA, a uid is given and the record wasn't copied before:
3647  if (!$GLOBALS['TCA'][$table] || !$uid || $this->isRecordCopied($table, $uid)) {
3648  return null;
3649  }
3650  if (!$this->doesRecordExist($table, $uid, 'show')) {
3651  if ($this->enableLogging) {
3652  $this->log($table, $uid, 3, 0, 1, 'Attempt to rawcopy/versionize record without copy permission');
3653  }
3654  return null;
3655  }
3656 
3657  // Set up fields which should not be processed. They are still written - just passed through no-questions-asked!
3658  $nonFields = array('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');
3659  // Select main record:
3660  $row = $this->recordInfo($table, $uid, '*');
3661  if (!is_array($row)) {
3662  if ($this->enableLogging) {
3663  $this->log($table, $uid, 3, 0, 1, 'Attempt to rawcopy/versionize record that did not exist!');
3664  }
3665  return null;
3666  }
3667 
3668  // Merge in override array.
3669  $row = array_merge($row, $overrideArray);
3670  // Traverse ALL fields of the selected record:
3671  foreach ($row as $field => $value) {
3672  if (!in_array($field, $nonFields, true)) {
3673  // Get TCA configuration for the field:
3674  $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3675  if (is_array($conf)) {
3676  // Processing based on the TCA config field type (files, references, flexforms...)
3677  $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3678  }
3679  // Add value to array.
3680  $row[$field] = $value;
3681  }
3682  }
3683  // Force versioning related fields:
3684  $row['pid'] = $pid;
3685  // Setting original UID:
3686  if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3687  $row[$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3688  }
3689  // Do the copy by internal function
3690  $theNewSQLID = $this->insertNewCopyVersion($table, $row, $pid);
3691  if ($theNewSQLID) {
3692  $this->dbAnalysisStoreExec();
3693  $this->dbAnalysisStore = array();
3694  $this->copyRecord_fixRTEmagicImages($table, BackendUtility::wsMapId($table, $theNewSQLID));
3695  return $this->copyMappingArray[$table][$uid] = $theNewSQLID;
3696  }
3697  return null;
3698  }
3699 
3709  public function insertNewCopyVersion($table, $fieldArray, $realPid)
3710  {
3711  $id = StringUtility::getUniqueId('NEW');
3712  // $fieldArray is set as current record.
3713  // 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...
3714  $this->checkValue_currentRecord = $fieldArray;
3715  // Makes sure that transformations aren't processed on the copy.
3716  $backupDontProcessTransformations = $this->dontProcessTransformations;
3717  $this->dontProcessTransformations = true;
3718  // Traverse record and input-process each value:
3719  foreach ($fieldArray as $field => $fieldValue) {
3720  if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
3721  // Evaluating the value.
3722  $res = $this->checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0);
3723  if (isset($res['value'])) {
3724  $fieldArray[$field] = $res['value'];
3725  }
3726  }
3727  }
3728  // System fields being set:
3729  if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3730  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
3731  }
3732  if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3733  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
3734  }
3735  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3736  $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3737  }
3738  // Finally, insert record:
3739  $this->insertDB($table, $id, $fieldArray, true);
3740  // Resets dontProcessTransformations to the previous state.
3741  $this->dontProcessTransformations = $backupDontProcessTransformations;
3742  // Return new id:
3743  return $this->substNEWwithIDs[$id];
3744  }
3745 
3762  public function copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language = 0, array $workspaceOptions = array())
3763  {
3764  // Process references and files, currently that means only the files, prepending absolute paths (so the TCEmain engine will detect the file as new and one that should be made into a copy)
3765  $value = $this->copyRecord_procFilesRefs($conf, $uid, $value);
3766  $inlineSubType = $this->getInlineFieldType($conf);
3767  // Get the localization mode for the current (parent) record (keep|select):
3768  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $field);
3769  // Register if there are references to take care of or MM is used on an inline field (no change to value):
3770  if ($this->isReferenceField($conf) || $inlineSubType == 'mm') {
3771  $value = $this->copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language, $localizationMode, $inlineSubType);
3772  } elseif ($inlineSubType !== false) {
3773  $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions, $localizationMode, $inlineSubType);
3774  }
3775  // 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())
3776  if ($conf['type'] == 'flex') {
3777  // Get current value array:
3778  $dataStructArray = BackendUtility::getFlexFormDS($conf, $row, $table, $field);
3779  $currentValueArray = GeneralUtility::xml2array($value);
3780  // Traversing the XML structure, processing files:
3781  if (is_array($currentValueArray)) {
3782  $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], array(), array(), $dataStructArray, array($table, $uid, $field, $realDestPid), 'copyRecord_flexFormCallBack');
3783  // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
3784  $value = $currentValueArray;
3785  }
3786  }
3787  return $value;
3788  }
3789 
3803  protected function copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language, $localizationMode, $inlineSubType)
3804  {
3805  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
3806  $prependName = $conf['type'] == 'group' ? $conf['prepend_tname'] : '';
3807  $mmTable = isset($conf['MM']) && $conf['MM'] ? $conf['MM'] : '';
3808  $localizeForeignTable = isset($conf['foreign_table']) && BackendUtility::isTableLocalizable($conf['foreign_table']);
3809  $localizeReferences = $localizeForeignTable && isset($conf['localizeReferencesAtParentLocalization']) && $conf['localizeReferencesAtParentLocalization'];
3810  $localizeChildren = $localizeForeignTable && isset($conf['behaviour']['localizeChildrenAtParentLocalization']) && $conf['behaviour']['localizeChildrenAtParentLocalization'];
3812  $dbAnalysis = $this->createRelationHandlerInstance();
3813  $dbAnalysis->start($value, $allowedTables, $mmTable, $uid, $table, $conf);
3814  // Localize referenced records of select fields:
3815  $localizingNonManyToManyFieldReferences = $localizeReferences && empty($mmTable);
3816  $isInlineFieldInSelectMode = $localizationMode === 'select' && $inlineSubType === 'mm';
3817  $purgeItems = false;
3818  if ($language > 0 && ($localizingNonManyToManyFieldReferences || $isInlineFieldInSelectMode)) {
3819  foreach ($dbAnalysis->itemArray as $index => $item) {
3820  // Since select fields can reference many records, check whether there's already a localization:
3821  $recordLocalization = BackendUtility::getRecordLocalization($item['table'], $item['id'], $language);
3822  if ($recordLocalization) {
3823  $dbAnalysis->itemArray[$index]['id'] = $recordLocalization[0]['uid'];
3824  } elseif ($this->isNestedElementCallRegistered($item['table'], $item['id'], 'localize') === false) {
3825  if ($localizingNonManyToManyFieldReferences || $localizeChildren) {
3826  $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], $language);
3827  } else {
3828  unset($dbAnalysis->itemArray[$index]);
3829  }
3830  }
3831  }
3832  $purgeItems = true;
3833  }
3834 
3835  if ($purgeItems || $mmTable) {
3836  $dbAnalysis->purgeItemArray();
3837  $value = implode(',', $dbAnalysis->getValueArray($prependName));
3838  }
3839  // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected
3840  if ($value) {
3841  $this->registerDBList[$table][$uid][$field] = $value;
3842  }
3843 
3844  return $value;
3845  }
3846 
3863  protected function copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language,
3864  array $workspaceOptions, $localizationMode, $inlineSubType)
3865  {
3866  // Localization in mode 'keep', isn't a real localization, but keeps the children of the original parent record:
3867  if ($language > 0 && $localizationMode == 'keep') {
3868  $value = $inlineSubType == 'field' ? 0 : '';
3869  } else {
3870  // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3872  $dbAnalysis = $this->createRelationHandlerInstance();
3873  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
3874  // Walk through the items, copy them and remember the new id:
3875  foreach ($dbAnalysis->itemArray as $k => $v) {
3876  $newId = null;
3877  // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
3878  if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
3879  // If children should be localized when the parent gets localized the first time, just do it:
3880  if ($localizationMode != false && isset($conf['behaviour']['localizeChildrenAtParentLocalization']) && $conf['behaviour']['localizeChildrenAtParentLocalization']) {
3881  $newId = $this->localize($v['table'], $v['id'], $language);
3882  }
3883  } else {
3884  if (!MathUtility::canBeInterpretedAsInteger($realDestPid)) {
3885  $newId = $this->copyRecord($v['table'], $v['id'], -$v['id']);
3886  // If the destination page id is a NEW string, keep it on the same page
3887  } elseif ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3888  // A filled $workspaceOptions indicated that this call
3889  // has it's origin in previous versionizeRecord() processing
3890  if (!empty($workspaceOptions)) {
3891  // Versions use live default id, thus the "new"
3892  // id is the original live default child record
3893  $newId = $v['id'];
3894  $this->versionizeRecord(
3895  $v['table'], $v['id'],
3896  (isset($workspaceOptions['label']) ? $workspaceOptions['label'] : 'Auto-created for WS #' . $this->BE_USER->workspace),
3897  (isset($workspaceOptions['delete']) ? $workspaceOptions['delete'] : false)
3898  );
3899  // Otherwise just use plain copyRecord() to create placeholders etc.
3900  } else {
3901  // If a record has been copied already during this request,
3902  // prevent superfluous duplication and use the existing copy
3903  if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3904  $newId = $this->copyMappingArray[$v['table']][$v['id']];
3905  } else {
3906  $newId = $this->copyRecord($v['table'], $v['id'], $realDestPid);
3907  }
3908  }
3909  } else {
3910  // If a record has been copied already during this request,
3911  // prevent superfluous duplication and use the existing copy
3912  if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3913  $newId = $this->copyMappingArray[$v['table']][$v['id']];
3914  } else {
3915  $newId = $this->copyRecord_raw($v['table'], $v['id'], $realDestPid, array(), $workspaceOptions);
3916  }
3917  }
3918  }
3919  // If the current field is set on a page record, update the pid of related child records:
3920  if ($table == 'pages') {
3921  $this->registerDBPids[$v['table']][$v['id']] = $uid;
3922  } elseif (isset($this->registerDBPids[$table][$uid])) {
3923  $this->registerDBPids[$v['table']][$v['id']] = $this->registerDBPids[$table][$uid];
3924  }
3925  $dbAnalysis->itemArray[$k]['id'] = $newId;
3926  }
3927  // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
3928  $value = implode(',', $dbAnalysis->getValueArray());
3929  $this->registerDBList[$table][$uid][$field] = $value;
3930  }
3931 
3932  return $value;
3933  }
3934 
3946  public function copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
3947  {
3948  // Extract parameters:
3949  list($table, $uid, $field, $realDestPid) = $pParams;
3950  // Process references and files, currently that means only the files, prepending absolute paths:
3951  $dataValue = $this->copyRecord_procFilesRefs($dsConf, $uid, $dataValue);
3952  // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
3953  if (($this->isReferenceField($dsConf) || $this->getInlineFieldType($dsConf) !== false) && (string)$dataValue !== '') {
3954  $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, array(), $dsConf, $realDestPid);
3955  $this->registerDBList[$table][$uid][$field] = 'FlexForm_reference';
3956  }
3957  // Return
3958  return array('value' => $dataValue);
3959  }
3960 
3972  public function copyRecord_procFilesRefs($conf, $uid, $value)
3973  {
3974  // Prepend absolute paths to files:
3975  if ($conf['type'] != 'group' || ($conf['internal_type'] != 'file' && $conf['internal_type'] != 'file_reference')) {
3976  return $value;
3977  }
3978 
3979  // Get an array with files as values:
3980  if ($conf['MM']) {
3981  $theFileValues = array();
3983  $dbAnalysis = $this->createRelationHandlerInstance();
3984  $dbAnalysis->start('', 'files', $conf['MM'], $uid);
3985  foreach ($dbAnalysis->itemArray as $somekey => $someval) {
3986  if ($someval['id']) {
3987  $theFileValues[] = $someval['id'];
3988  }
3989  }
3990  } else {
3991  $theFileValues = GeneralUtility::trimExplode(',', $value, true);
3992  }
3993  // Traverse this array of files:
3994  $uploadFolder = $conf['internal_type'] == 'file' ? $conf['uploadfolder'] : '';
3995  $dest = $this->destPathFromUploadFolder($uploadFolder);
3996  $newValue = array();
3997  foreach ($theFileValues as $file) {
3998  if (trim($file)) {
3999  $realFile = str_replace('//', '/', $dest . '/' . trim($file));
4000  if (@is_file($realFile)) {
4001  $newValue[] = $realFile;
4002  }
4003  }
4004  }
4005  // Implode the new filelist into the new value (all files have absolute paths now which means they will get copied when entering TCEmain as new values...)
4006  $value = implode(',', $newValue);
4007 
4008  // Return the new value:
4009  return $value;
4010  }
4011 
4021  public function copyRecord_fixRTEmagicImages($table, $theNewSQLID)
4022  {
4023  // Creating fileFunc object.
4024  if (!$this->fileFunc) {
4025  $this->fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
4026  $this->include_filefunctions = 1;
4027  }
4028  // Select all RTEmagic files in the reference table from the table/ID
4029  $where = join(' AND ', array(
4030  'ref_table=' . $this->databaseConnection->fullQuoteStr('_FILE', 'sys_refindex'),
4031  'ref_string LIKE ' . $this->databaseConnection->fullQuoteStr('%/RTEmagic%', 'sys_refindex'),
4032  'softref_key=' . $this->databaseConnection->fullQuoteStr('images', 'sys_refindex'),
4033  'tablename=' . $this->databaseConnection->fullQuoteStr($table, 'sys_refindex'),
4034  'recuid=' . (int)$theNewSQLID,
4035  ));
4036  $rteFileRecords = $this->databaseConnection->exec_SELECTgetRows('*', 'sys_refindex', $where, '', 'sorting DESC');
4037  // Traverse the files found and copy them:
4038  if (!is_array($rteFileRecords)) {
4039  return;
4040  }
4041  foreach ($rteFileRecords as $rteFileRecord) {
4042  $filename = basename($rteFileRecord['ref_string']);
4043  if (!GeneralUtility::isFirstPartOfStr($filename, 'RTEmagicC_')) {
4044  continue;
4045  }
4046  $fileInfo = array();
4047  $fileInfo['exists'] = @is_file((PATH_site . $rteFileRecord['ref_string']));
4048  $fileInfo['original'] = substr($rteFileRecord['ref_string'], 0, -strlen($filename)) . 'RTEmagicP_' . preg_replace('/\\.[[:alnum:]]+$/', '', substr($filename, 10));
4049  $fileInfo['original_exists'] = @is_file((PATH_site . $fileInfo['original']));
4050  // CODE from tx_impexp and class.rte_images.php adapted for use here:
4051  if (!$fileInfo['exists'] || !$fileInfo['original_exists']) {
4052  if ($this->enableLogging) {
4053  $this->newlog('Trying to copy RTEmagic files (' . $rteFileRecord['ref_string'] . ' / ' . $fileInfo['original'] . ') but one or both were missing', 1);
4054  }
4055  continue;
4056  }
4057  // Initialize; Get directory prefix for file and set the original name:
4058  $dirPrefix = dirname($rteFileRecord['ref_string']) . '/';
4059  $rteOrigName = basename($fileInfo['original']);
4060  // If filename looks like an RTE file, and the directory is in "uploads/", then process as a RTE file!
4061  if ($rteOrigName && GeneralUtility::isFirstPartOfStr($dirPrefix, 'uploads/') && @is_dir(PATH_site . $dirPrefix)) {
4062  // RTE:
4063  // From the "original" RTE filename, produce a new "original" destination filename which is unused.
4064  $origDestName = $this->fileFunc->getUniqueName($rteOrigName, PATH_site . $dirPrefix);
4065  // Create copy file name:
4066  $pI = pathinfo($rteFileRecord['ref_string']);
4067  $copyDestName = dirname($origDestName) . '/RTEmagicC_' . substr(basename($origDestName), 10) . '.' . $pI['extension'];
4068  if (!@is_file($copyDestName) && !@is_file($origDestName) && $origDestName === GeneralUtility::getFileAbsFileName($origDestName) && $copyDestName === GeneralUtility::getFileAbsFileName($copyDestName)) {
4069  // Making copies:
4070  GeneralUtility::upload_copy_move(PATH_site . $fileInfo['original'], $origDestName);
4071  GeneralUtility::upload_copy_move(PATH_site . $rteFileRecord['ref_string'], $copyDestName);
4072  clearstatcache();
4073  // Register this:
4074  $this->RTEmagic_copyIndex[$rteFileRecord['tablename']][$rteFileRecord['recuid']][$rteFileRecord['field']][$rteFileRecord['ref_string']] = PathUtility::stripPathSitePrefix($copyDestName);
4075  // Check and update the record using \TYPO3\CMS\Core\Database\ReferenceIndex
4076  if (@is_file($copyDestName)) {
4078  $sysRefObj = GeneralUtility::makeInstance(ReferenceIndex::class);
4079  $error = $sysRefObj->setReferenceValue($rteFileRecord['hash'], PathUtility::stripPathSitePrefix($copyDestName), false, true);
4080  if ($this->enableLogging && $error) {
4081  echo $this->newlog(ReferenceIndex::class . '::setReferenceValue(): ' . $error, 1);
4082  }
4083  } elseif ($this->enableLogging) {
4084  $this->newlog('File "' . $copyDestName . '" was not created!', 1);
4085  }
4086  } elseif ($this->enableLogging) {
4087  $this->newlog('Could not construct new unique names for file!', 1);
4088  }
4089  } elseif ($this->enableLogging) {
4090  $this->newlog('Maybe directory of file was not within "uploads/"?', 1);
4091  }
4092  }
4093  }
4094 
4106  public function copyL10nOverlayRecords($table, $uid, $destPid, $first = false, $overrideValues = array(), $excludeFields = '')
4107  {
4108  // There's no need to perform this for page-records or for tables that are not localizable
4109  if (!BackendUtility::isTableLocalizable($table) || !empty($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']) || !empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable'])) {
4110  return;
4111  }
4112  $where = '';
4113  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
4114  $where = ' AND t3ver_oid=0';
4115  }
4116  // If $destPid is < 0, get the pid of the record with uid equal to abs($destPid)
4117  $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
4118  // Get the localized records to be copied
4119  $l10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], $uid, $where);
4120  if (is_array($l10nRecords)) {
4121  $localizedDestPids = array();
4122  // If $destPid < 0, then it is the uid of the original language record we are inserting after
4123  if ($destPid < 0) {
4124  // Get the localized records of the record we are inserting after
4125  $destL10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], abs($destPid), $where);
4126  // Index the localized record uids by language
4127  if (is_array($destL10nRecords)) {
4128  foreach ($destL10nRecords as $record) {
4129  $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4130  }
4131  }
4132  }
4133  // Copy the localized records after the corresponding localizations of the destination record
4134  foreach ($l10nRecords as $record) {
4135  $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4136  if ($localizedDestPid < 0) {
4137  $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4138  } else {
4139  $this->copyRecord($table, $record['uid'], $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
4140  }
4141  }
4142  }
4143  }
4144 
4145  /*********************************************
4146  *
4147  * Cmd: Moving, Localizing
4148  *
4149  ********************************************/
4158  public function moveRecord($table, $uid, $destPid)
4159  {
4160  if (!$GLOBALS['TCA'][$table]) {
4161  return;
4162  }
4163 
4164  // In case the record to be moved turns out to be an offline version,
4165  // we have to find the live version and work on that one (this case
4166  // happens for pages with "branch" versioning type)
4167  // @deprecated note: as "branch" versioning is deprecated since TYPO3 4.2, this
4168  // functionality will be removed in TYPO3 4.7 (note by benni: a hook could replace this)
4169  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $uid, 'uid')) {
4170  $uid = $lookForLiveVersion['uid'];
4171  }
4172  // Initialize:
4173  $destPid = (int)$destPid;
4174  // Get this before we change the pid (for logging)
4175  $propArr = $this->getRecordProperties($table, $uid);
4176  $moveRec = $this->getRecordProperties($table, $uid, true);
4177  // This is the actual pid of the moving to destination
4178  $resolvedPid = $this->resolvePid($table, $destPid);
4179  // 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.
4180  // 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.
4181  if ($table != 'pages' || $resolvedPid == $moveRec['pid']) {
4182  // Edit rights for the record...
4183  $mayMoveAccess = $this->checkRecordUpdateAccess($table, $uid);
4184  } else {
4185  $mayMoveAccess = $this->doesRecordExist($table, $uid, 'delete');
4186  }
4187  // 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
4188  if ($table != 'pages' || $resolvedPid != $moveRec['pid']) {
4189  // Insert rights for the record...
4190  $mayInsertAccess = $this->checkRecordInsertAccess($table, $resolvedPid, 4);
4191  } else {
4192  $mayInsertAccess = $this->checkRecordUpdateAccess($table, $uid);
4193  }
4194  // Checking if there is anything else disallowing moving the record by checking if editing is allowed
4195  $fullLanguageCheckNeeded = $table != 'pages';
4196  $mayEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded);
4197  // If moving is allowed, begin the processing:
4198  if (!$mayEditAccess) {
4199  if ($this->enableLogging) {
4200  $this->log($table, $uid, 4, 0, 1, 'Attempt to move record "%s" (%s) without having permissions to do so. [' . $this->BE_USER->errorMsg . ']', 14, array($propArr['header'], $table . ':' . $uid), $propArr['event_pid']);
4201  }
4202  return;
4203  }
4204 
4205  if (!$mayMoveAccess) {
4206  if ($this->enableLogging) {
4207  $this->log($table, $uid, 4, 0, 1, 'Attempt to move record \'%s\' (%s) without having permissions to do so.', 14, array($propArr['header'], $table . ':' . $uid), $propArr['event_pid']);
4208  }
4209  return;
4210  }
4211 
4212  if (!$mayInsertAccess) {
4213  if ($this->enableLogging) {
4214  $this->log($table, $uid, 4, 0, 1, 'Attempt to move record \'%s\' (%s) without having permissions to insert.', 14, array($propArr['header'], $table . ':' . $uid), $propArr['event_pid']);
4215  }
4216  return;
4217  }
4218 
4219  $recordWasMoved = false;
4220  // Move the record via a hook, used e.g. for versioning
4221  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'])) {
4222  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] as $classRef) {
4223  $hookObj = GeneralUtility::getUserObj($classRef);
4224  if (method_exists($hookObj, 'moveRecord')) {
4225  $hookObj->moveRecord($table, $uid, $destPid, $propArr, $moveRec, $resolvedPid, $recordWasMoved, $this);
4226  }
4227  }
4228  }
4229  // Move the record if a hook hasn't moved it yet
4230  if (!$recordWasMoved) {
4231  $this->moveRecord_raw($table, $uid, $destPid);
4232  }
4233  }
4234 
4245  public function moveRecord_raw($table, $uid, $destPid)
4246  {
4247  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
4248  $origDestPid = $destPid;
4249  // This is the actual pid of the moving to destination
4250  $resolvedPid = $this->resolvePid($table, $destPid);
4251  // 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...)
4252  // $destPid>=0 because we must correct pid in case of versioning "page" types.
4253  if ($destPid < 0 && !$sortRow || $destPid >= 0) {
4254  $destPid = $resolvedPid;
4255  }
4256  // Get this before we change the pid (for logging)
4257  $propArr = $this->getRecordProperties($table, $uid);
4258  $moveRec = $this->getRecordProperties($table, $uid, true);
4259  // Prepare user defined objects (if any) for hooks which extend this function:
4260  $hookObjectsArr = array();
4261  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'])) {
4262  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] as $classRef) {
4263  $hookObjectsArr[] = GeneralUtility::getUserObj($classRef);
4264  }
4265  }
4266  // Timestamp field:
4267  $updateFields = array();
4268  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4269  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4270  }
4271  // Insert as first element on page (where uid = $destPid)
4272  if ($destPid >= 0) {
4273  if ($table != 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4274  // Clear cache before moving
4275  $this->registerRecordIdForPageCacheClearing($table, $uid);
4276  // Setting PID
4277  $updateFields['pid'] = $destPid;
4278  // Table is sorted by 'sortby'
4279  if ($sortRow) {
4280  $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4281  $updateFields[$sortRow] = $sortNumber;
4282  }
4283  // Check for child records that have also to be moved
4284  $this->moveRecord_procFields($table, $uid, $destPid);
4285  // Create query for update:
4286  $this->databaseConnection->exec_UPDATEquery($table, 'uid=' . (int)$uid, $updateFields);
4287  // Check for the localizations of that element
4288  $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4289  // Call post processing hooks:
4290  foreach ($hookObjectsArr as $hookObj) {
4291  if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4292  $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4293  }
4294  }
4295  if ($this->enableLogging) {
4296  // Logging...
4297  $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4298  if ($destPid != $propArr['pid']) {
4299  // Logged to old page
4300  $newPropArr = $this->getRecordProperties($table, $uid);
4301  $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4302  $this->log($table, $uid, 4, $destPid, 0, 'Moved record \'%s\' (%s) to page \'%s\' (%s)', 2, array($propArr['header'], $table . ':' . $uid, $newpagePropArr['header'], $newPropArr['pid']), $propArr['pid']);
4303  // Logged to new page
4304  $this->log($table, $uid, 4, $destPid, 0, 'Moved record \'%s\' (%s) from page \'%s\' (%s)', 3, array($propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']), $destPid);
4305  } else {
4306  // Logged to new page
4307  $this->log($table, $uid, 4, $destPid, 0, 'Moved record \'%s\' (%s) on page \'%s\' (%s)', 4, array($propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']), $destPid);
4308  }
4309  }
4310  // Clear cache after moving
4311  $this->registerRecordIdForPageCacheClearing($table, $uid);
4312  $this->fixUniqueInPid($table, $uid);
4313  // fixCopyAfterDuplFields
4314  if ($origDestPid < 0) {
4315  $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
4316  }
4317  } elseif ($this->enableLogging) {
4318  $destPropArr = $this->getRecordProperties('pages', $destPid);
4319  $this->log($table, $uid, 4, 0, 1, 'Attempt to move page \'%s\' (%s) to inside of its own rootline (at page \'%s\' (%s))', 10, array($propArr['header'], $uid, $destPropArr['header'], $destPid), $propArr['pid']);
4320  }
4321  } else {
4322  // Put after another record
4323  // Table is being sorted
4324  if ($sortRow) {
4325  // Save the position to which the original record is requested to be moved
4326  $originalRecordDestinationPid = $destPid;
4327  $sortInfo = $this->getSortNumber($table, $uid, $destPid);
4328  // Setting the destPid to the new pid of the record.
4329  $destPid = $sortInfo['pid'];
4330  // If not an array, there was an error (which is already logged)
4331  if (is_array($sortInfo)) {
4332  if ($table != 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4333  // clear cache before moving
4334  $this->registerRecordIdForPageCacheClearing($table, $uid);
4335  // We now update the pid and sortnumber
4336  $updateFields['pid'] = $destPid;
4337  $updateFields[$sortRow] = $sortInfo['sortNumber'];
4338  // Check for child records that have also to be moved
4339  $this->moveRecord_procFields($table, $uid, $destPid);
4340  // Create query for update:
4341  $this->databaseConnection->exec_UPDATEquery($table, 'uid=' . (int)$uid, $updateFields);
4342  // Check for the localizations of that element
4343  $this->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
4344  // Call post processing hooks:
4345  foreach ($hookObjectsArr as $hookObj) {
4346  if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4347  $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4348  }
4349  }
4350  if ($this->enableLogging) {
4351  // Logging...
4352  $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4353  if ($destPid != $propArr['pid']) {
4354  // Logged to old page
4355  $newPropArr = $this->getRecordProperties($table, $uid);
4356  $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4357  $this->log($table, $uid, 4, 0, 0, 'Moved record \'%s\' (%s) to page \'%s\' (%s)', 2, array($propArr['header'], $table . ':' . $uid, $newpagePropArr['header'], $newPropArr['pid']), $propArr['pid']);
4358  // Logged to old page
4359  $this->log($table, $uid, 4, 0, 0, 'Moved record \'%s\' (%s) from page \'%s\' (%s)', 3, array($propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']), $destPid);
4360  } else {
4361  // Logged to old page
4362  $this->log($table, $uid, 4, 0, 0, 'Moved record \'%s\' (%s) on page \'%s\' (%s)', 4, array($propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']), $destPid);
4363  }
4364  }
4365  // Clear cache after moving
4366  $this->registerRecordIdForPageCacheClearing($table, $uid);
4367  // fixUniqueInPid
4368  $this->fixUniqueInPid($table, $uid);
4369  // fixCopyAfterDuplFields
4370  if ($origDestPid < 0) {
4371  $this->fixCopyAfterDuplFields($table, $uid, abs($origDestPid), 1);
4372  }
4373  } elseif ($this->enableLogging) {
4374  $destPropArr = $this->getRecordProperties('pages', $destPid);
4375  $this->log($table, $uid, 4, 0, 1, 'Attempt to move page \'%s\' (%s) to inside of its own rootline (at page \'%s\' (%s))', 10, array($propArr['header'], $uid, $destPropArr['header'], $destPid), $propArr['pid']);
4376  }
4377  }
4378  } elseif ($this->enableLogging) {
4379  $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, array($propArr['header'], $table . ':' . $uid), $propArr['event_pid']);
4380  }
4381  }
4382  }
4383 
4393  public function moveRecord_procFields($table, $uid, $destPid)
4394  {
4395  $conf = $GLOBALS['TCA'][$table]['columns'];
4396  $row = BackendUtility::getRecordWSOL($table, $uid);
4397  if (is_array($row)) {
4398  foreach ($row as $field => $value) {
4399  $this->moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf[$field]['config']);
4400  }
4401  }
4402  }
4403 
4415  public function moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf)
4416  {
4417  if ($conf['type'] == 'inline') {
4418  $foreign_table = $conf['foreign_table'];
4419  $moveChildrenWithParent = !isset($conf['behaviour']['disableMovingChildrenWithParent']) || !$conf['behaviour']['disableMovingChildrenWithParent'];
4420  if ($foreign_table && $moveChildrenWithParent) {
4421  $inlineType = $this->getInlineFieldType($conf);
4422  if ($inlineType == 'list' || $inlineType == 'field') {
4423  if ($table == 'pages') {
4424  // If the inline elements are related to a page record,
4425  // make sure they reside at that page and not at its parent
4426  $destPid = $uid;
4427  }
4428  $dbAnalysis = $this->createRelationHandlerInstance();
4429  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
4430  }
4431  }
4432  }
4433  // Move the records
4434  if (isset($dbAnalysis)) {
4435  // Moving records to a positive destination will insert each
4436  // record at the beginning, thus the order is reversed here:
4437  foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4438  $this->moveRecord($v['table'], $v['id'], $destPid);
4439  }
4440  }
4441  }
4442 
4452  public function moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
4453  {
4454  // There's no need to perform this for page-records or not localizable tables
4455  if (!BackendUtility::isTableLocalizable($table) || !empty($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']) || !empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable'])) {
4456  return;
4457  }
4458  $where = '';
4459  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
4460  $where = ' AND t3ver_oid=0';
4461  }
4462  $l10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], $uid, $where);
4463  if (is_array($l10nRecords)) {
4464  $localizedDestPids = array();
4465  // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4466  if ($originalRecordDestinationPid < 0) {
4467  // Get the localized records of the record we are inserting after
4468  $destL10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], abs($originalRecordDestinationPid), $where);
4469  // Index the localized record uids by language
4470  if (is_array($destL10nRecords)) {
4471  foreach ($destL10nRecords as $record) {
4472  $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4473  }
4474  }
4475  }
4476  // Move the localized records after the corresponding localizations of the destination record
4477  foreach ($l10nRecords as $record) {
4478  $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4479  if ($localizedDestPid < 0) {
4480  $this->moveRecord($table, $record['uid'], $localizedDestPid);
4481  } else {
4482  $this->moveRecord($table, $record['uid'], $destPid);
4483  }
4484  }
4485  }
4486  }
4487 
4497  public function localize($table, $uid, $language)
4498  {
4499  $newId = false;
4500  $uid = (int)$uid;
4501  if (!$GLOBALS['TCA'][$table] || !$uid || $this->isNestedElementCallRegistered($table, $uid, 'localize') !== false) {
4502  return false;
4503  }
4504 
4505  $this->registerNestedElementCall($table, $uid, 'localize');
4506  if ((!$GLOBALS['TCA'][$table]['ctrl']['languageField'] || !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] || $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']) && $table !== 'pages') {
4507  if ($this->enableLogging) {
4508  $this->newlog('Localization failed; "languageField" and "transOrigPointerField" must be defined for the table!', 1);
4509  }
4510  return false;
4511  }
4512 
4513  $langRec = BackendUtility::getRecord('sys_language', (int)$language, 'uid,title');
4514  if (!$langRec) {
4515  if ($this->enableLogging) {
4516  $this->newlog('Sys language UID "' . $language . '" not found valid!', 1);
4517  }
4518  return false;
4519  }
4520 
4521  if (!$this->doesRecordExist($table, $uid, 'show')) {
4522  if ($this->enableLogging) {
4523  $this->newlog('Attempt to localize record without permission', 1);
4524  }
4525  return false;
4526  }
4527 
4528  // Getting workspace overlay if possible - this will localize versions in workspace if any
4529  $row = BackendUtility::getRecordWSOL($table, $uid);
4530  if (!is_array($row)) {
4531  if ($this->enableLogging) {
4532  $this->newlog('Attempt to localize record that did not exist!', 1);
4533  }
4534  return false;
4535  }
4536 
4537  if ($row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0 && $table !== 'pages') {
4538  if ($this->enableLogging) {
4539  $this->newlog('Localization failed; Source record had another language than "Default" or "All" defined!', 1);
4540  }
4541  return false;
4542  }
4543 
4544  if ($row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0 && $table !== 'pages') {
4545  if ($this->enableLogging) {
4546  $this->newlog('Localization failed; Source record contained a reference to an original default record (which is strange)!', 1);
4547  }
4548  return false;
4549  }
4550 
4551  if ($table === 'pages') {
4552  $pass = $GLOBALS['TCA'][$table]['ctrl']['transForeignTable'] === 'pages_language_overlay' && !BackendUtility::getRecordsByField('pages_language_overlay', 'pid', $uid, (' AND ' . $GLOBALS['TCA']['pages_language_overlay']['ctrl']['languageField'] . '=' . (int)$langRec['uid']));
4553  $Ttable = 'pages_language_overlay';
4554  } else {
4555  $pass = !BackendUtility::getRecordLocalization($table, $uid, $langRec['uid'], ('AND pid=' . (int)$row['pid']));
4556  $Ttable = $table;
4557  }
4558 
4559  if (!$pass) {
4560  if ($this->enableLogging) {
4561  $this->newlog('Localization failed; There already was a localization for this language of the record!', 1);
4562  }
4563  return false;
4564  }
4565 
4566  // Initialize:
4567  $overrideValues = array();
4568  $excludeFields = array();
4569  // Set override values:
4570  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['languageField']] = $langRec['uid'];
4571  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['transOrigPointerField']] = $uid;
4572  // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4573  if (isset($GLOBALS['TCA'][$table]['ctrl']['type']) && isset($GLOBALS['TCA'][$Ttable]['ctrl']['type'])) {
4574  $overrideValues[$GLOBALS['TCA'][$Ttable]['ctrl']['type']] = $row[$GLOBALS['TCA'][$table]['ctrl']['type']];
4575  }
4576  // Set exclude Fields:
4577  foreach ($GLOBALS['TCA'][$Ttable]['columns'] as $fN => $fCfg) {
4578  // Check if we are just prefixing:
4579  if ($fCfg['l10n_mode'] == 'prefixLangTitle') {
4580  if (($fCfg['config']['type'] == 'text' || $fCfg['config']['type'] == 'input') && (string)$row[$fN] !== '') {
4581  list($tscPID) = BackendUtility::getTSCpid($table, $uid, '');
4582  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
4583  if (!empty($TSConfig['translateToMessage'])) {
4584  $translateToMsg = $GLOBALS['LANG'] ? $GLOBALS['LANG']->sL($TSConfig['translateToMessage']) : $TSConfig['translateToMessage'];
4585  $translateToMsg = @sprintf($translateToMsg, $langRec['title']);
4586  }
4587  if (empty($translateToMsg)) {
4588  $translateToMsg = 'Translate to ' . $langRec['title'] . ':';
4589  } else {
4590  $translateToMsg = @sprintf($TSConfig['translateToMessage'], $langRec['title']);
4591  }
4592  $overrideValues[$fN] = '[' . $translateToMsg . '] ' . $row[$fN];
4593  }
4594  } elseif (
4595  ($fCfg['l10n_mode'] === 'exclude' || $fCfg['l10n_mode'] === 'noCopy' || $fCfg['l10n_mode'] === 'mergeIfNotBlank')
4596  && $fN != $GLOBALS['TCA'][$Ttable]['ctrl']['languageField']
4597  && $fN != $GLOBALS['TCA'][$Ttable]['ctrl']['transOrigPointerField']
4598  ) {
4599  // Otherwise, do not copy field (unless it is the language field or
4600  // pointer to the original language)
4601  $excludeFields[] = $fN;
4602  }
4603  }
4604  if ($Ttable === $table) {
4605  // Get the uid of record after which this localized record should be inserted
4606  $previousUid = $this->getPreviousLocalizedRecordUid($table, $uid, $row['pid'], $language);
4607  // Execute the copy:
4608  $newId = $this->copyRecord($table, $uid, -$previousUid, 1, $overrideValues, implode(',', $excludeFields), $language);
4609  $autoVersionNewId = $this->getAutoVersionId($table, $newId);
4610  if (is_null($autoVersionNewId) === false) {
4611  $this->triggerRemapAction($table, $newId, array($this, 'placeholderShadowing'), array($table, $autoVersionNewId), true);
4612  }
4613  } else {
4614  // Create new record:
4616  $copyTCE = $this->getLocalTCE();
4617  $copyTCE->start(array($Ttable => array('NEW' => $overrideValues)), '', $this->BE_USER);
4618  $copyTCE->process_datamap();
4619  // Getting the new UID as if it had been copied:
4620  $theNewSQLID = $copyTCE->substNEWwithIDs['NEW'];
4621  if ($theNewSQLID) {
4622  // 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"
4623  $this->copyMappingArray[$Ttable][$uid] = $theNewSQLID;
4624  $newId = $theNewSQLID;
4625  }
4626  }
4627 
4628 
4629  return $newId;
4630  }
4631 
4640  protected function inlineLocalizeSynchronize($table, $id, $command)
4641  {
4642  // <field>, (localize | synchronize | <uid>):
4643  $parts = GeneralUtility::trimExplode(',', $command);
4644  $field = $parts[0];
4645  $type = $parts[1];
4646  if (!$field || (($type !== 'localize' && $type !== 'synchronize') && !MathUtility::canBeInterpretedAsInteger($type)) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4647  return;
4648  }
4649 
4650  $config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
4651  $foreignTable = $config['foreign_table'];
4652  $localizationMode = BackendUtility::getInlineLocalizationMode($table, $config);
4653  if ($localizationMode != 'select') {
4654  return;
4655  }
4656 
4657  $parentRecord = BackendUtility::getRecordWSOL($table, $id);
4658  $language = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
4659  $transOrigPointer = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
4660  $transOrigTable = BackendUtility::getOriginalTranslationTable($table);
4661  $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
4662 
4663  if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) {
4664  return;
4665  }
4666 
4667  $inlineSubType = $this->getInlineFieldType($config);
4668  $transOrigRecord = BackendUtility::getRecordWSOL($transOrigTable, $transOrigPointer);
4669 
4670  if ($inlineSubType === false) {
4671  return;
4672  }
4673 
4674  $removeArray = array();
4675  $mmTable = $inlineSubType == 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : '';
4676  // Fetch children from original language parent:
4678  $dbAnalysisOriginal = $this->createRelationHandlerInstance();
4679  $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $transOrigTable, $config);
4680  $elementsOriginal = array();
4681  foreach ($dbAnalysisOriginal->itemArray as $item) {
4682  $elementsOriginal[$item['id']] = $item;
4683  }
4684  unset($dbAnalysisOriginal);
4685  // Fetch children from current localized parent:
4687  $dbAnalysisCurrent = $this->createRelationHandlerInstance();
4688  $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config);
4689  // Perform synchronization: Possibly removal of already localized records:
4690  if ($type == 'synchronize') {
4691  foreach ($dbAnalysisCurrent->itemArray as $index => $item) {
4692  $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']);
4693  if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) {
4694  $childTransOrigPointer = $childRecord[$childTransOrigPointerField];
4695  // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it:
4696  if (!isset($elementsOriginal[$childTransOrigPointer])) {
4697  unset($dbAnalysisCurrent->itemArray[$index]);
4698  $removeArray[$item['table']][$item['id']]['delete'] = 1;
4699  }
4700  }
4701  }
4702  }
4703  // Perform synchronization/localization: Possibly add unlocalized records for original language:
4704  if (MathUtility::canBeInterpretedAsInteger($type) && isset($elementsOriginal[$type])) {
4705  $item = $elementsOriginal[$type];
4706  $item['id'] = $this->localize($item['table'], $item['id'], $language);
4707  $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4708  $dbAnalysisCurrent->itemArray[] = $item;
4709  } elseif ($type === 'localize' || $type === 'synchronize') {
4710  foreach ($elementsOriginal as $originalId => $item) {
4711  $item['id'] = $this->localize($item['table'], $item['id'], $language);
4712  $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4713  $dbAnalysisCurrent->itemArray[] = $item;
4714  }
4715  }
4716  // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4717  $value = implode(',', $dbAnalysisCurrent->getValueArray());
4718  $this->registerDBList[$table][$id][$field] = $value;
4719  // Remove child records (if synchronization requested it):
4720  if (is_array($removeArray) && !empty($removeArray)) {
4722  $tce = GeneralUtility::makeInstance(__CLASS__);
4723  $tce->stripslashes_values = false;
4724  $tce->start(array(), $removeArray);
4725  $tce->process_cmdmap();
4726  unset($tce);
4727  }
4728  $updateFields = array();
4729  // Handle, reorder and store relations:
4730  if ($inlineSubType == 'list') {
4731  $updateFields = array($field => $value);
4732  } elseif ($inlineSubType == 'field') {
4733  $dbAnalysisCurrent->writeForeignField($config, $id);
4734  $updateFields = array($field => $dbAnalysisCurrent->countItems(false));
4735  } elseif ($inlineSubType == 'mm') {
4736  $dbAnalysisCurrent->writeMM($config['MM'], $id);
4737  $updateFields = array($field => $dbAnalysisCurrent->countItems(false));
4738  }
4739  // Update field referencing to child records of localized parent record:
4740  if (!empty($updateFields)) {
4741  $this->updateDB($table, $id, $updateFields);
4742  }
4743  }
4744 
4745  /*********************************************
4746  *
4747  * Cmd: Deleting
4748  *
4749  ********************************************/
4757  public function deleteAction($table, $id)
4758  {
4759  $recordToDelete = BackendUtility::getRecord($table, $id);
4760  // Record asked to be deleted was found:
4761  if (is_array($recordToDelete)) {
4762  $recordWasDeleted = false;
4763  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'])) {
4764  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] as $classRef) {
4765  $hookObj = GeneralUtility::getUserObj($classRef);
4766  if (method_exists($hookObj, 'processCmdmap_deleteAction')) {
4767  $hookObj->processCmdmap_deleteAction($table, $id, $recordToDelete, $recordWasDeleted, $this);
4768  }
4769  }
4770  }
4771  // Delete the record if a hook hasn't deleted it yet
4772  if (!$recordWasDeleted) {
4773  $this->deleteEl($table, $id);
4774  }
4775  }
4776  }
4777 
4787  public function deleteEl($table, $uid, $noRecordCheck = false, $forceHardDelete = false)
4788  {
4789  if ($table == 'pages') {
4790  $this->deletePages($uid, $noRecordCheck, $forceHardDelete);
4791  } else {
4792  $this->deleteVersionsForRecord($table, $uid, $forceHardDelete);
4793  $this->deleteRecord($table, $uid, $noRecordCheck, $forceHardDelete);
4794  }
4795  }
4796 
4805  public function deleteVersionsForRecord($table, $uid, $forceHardDelete)
4806  {
4807  $versions = BackendUtility::selectVersionsOfRecord($table, $uid, 'uid,pid,t3ver_wsid,t3ver_state', $this->BE_USER->workspace ?: null);
4808  if (is_array($versions)) {
4809  foreach ($versions as $verRec) {
4810  if (!$verRec['_CURRENT_VERSION']) {
4811  if ($table == 'pages') {
4812  $this->deletePages($verRec['uid'], true, $forceHardDelete);
4813  } else {
4814  $this->deleteRecord($table, $verRec['uid'], true, $forceHardDelete);
4815  }
4816 
4817  // Delete move-placeholder
4818  $versionState = VersionState::cast($verRec['t3ver_state']);
4819  if ($versionState->equals(VersionState::MOVE_POINTER)) {
4820  $versionMovePlaceholder = BackendUtility::getMovePlaceholder($table, $uid, 'uid', $verRec['t3ver_wsid']);
4821  $this->deleteEl($table, $versionMovePlaceholder['uid'], true, $forceHardDelete);
4822  }
4823  }
4824  }
4825  }
4826  }
4827 
4835  public function undeleteRecord($table, $uid)
4836  {
4837  if ($this->isRecordUndeletable($table, $uid)) {
4838  $this->deleteRecord($table, $uid, true, false, true);
4839  }
4840  }
4841 
4855  public function deleteRecord($table, $uid, $noRecordCheck = false, $forceHardDelete = false, $undeleteRecord = false)
4856  {
4857  $uid = (int)$uid;
4858  if (!$GLOBALS['TCA'][$table] || !$uid) {
4859  if ($this->enableLogging) {
4860  $this->log($table, $uid, 3, 0, 1, 'Attempt to delete record without delete-permissions. [' . $this->BE_USER->errorMsg . ']');
4861  }
4862  return;
4863  }
4864 
4865  // Checking if there is anything else disallowing deleting the record by checking if editing is allowed
4866  $deletedRecord = $forceHardDelete || $undeleteRecord;
4867  $hasEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, $deletedRecord, true);
4868  if (!$hasEditAccess) {
4869  if ($this->enableLogging) {
4870  $this->log($table, $uid, 3, 0, 1, 'Attempt to delete record without delete-permissions');
4871  }
4872  return;
4873  }
4874  if (!$noRecordCheck && !$this->doesRecordExist($table, $uid, 'delete')) {
4875  return;
4876  }
4877 
4878  // Clear cache before deleting the record, else the correct page cannot be identified by clear_cache
4879  $this->registerRecordIdForPageCacheClearing($table, $uid);
4880  $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
4881  if ($deleteField && !$forceHardDelete) {
4882  $updateFields = array(
4883  $deleteField => $undeleteRecord ? 0 : 1
4884  );
4885  if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4886  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4887  }
4888  // If the table is sorted, then the sorting number is set very high
4889  if ($GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$undeleteRecord) {
4890  $updateFields[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = 1000000000;
4891  }
4892  // before (un-)deleting this record, check for child records or references
4893  $this->deleteRecord_procFields($table, $uid, $undeleteRecord);
4894  $this->databaseConnection->exec_UPDATEquery($table, 'uid=' . (int)$uid, $updateFields);
4895  // Delete all l10n records as well, impossible during undelete because it might bring too many records back to life
4896  if (!$undeleteRecord) {
4897  $this->deleteL10nOverlayRecords($table, $uid);
4898  }
4899  } else {
4900  // Fetches all fields with flexforms and look for files to delete:
4901  foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $cfg) {
4902  $conf = $cfg['config'];
4903  switch ($conf['type']) {
4904  case 'flex':
4905  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
4906  $flexObj->traverseFlexFormXMLData($table, $fieldName, BackendUtility::getRecordRaw($table, 'uid=' . (int)$uid), $this, 'deleteRecord_flexFormCallBack');
4907  break;
4908  }
4909  }
4910  // Fetches all fields that holds references to files
4911  $fileFieldArr = $this->extFileFields($table);
4912  if (!empty($fileFieldArr)) {
4913  $mres = $this->databaseConnection->exec_SELECTquery(implode(',', $fileFieldArr), $table, 'uid=' . (int)$uid);
4914  if ($row = $this->databaseConnection->sql_fetch_assoc($mres)) {
4915  $fArray = $fileFieldArr;
4916  // MISSING: Support for MM file relations!
4917  foreach ($fArray as $theField) {
4918  // This deletes files that belonged to this record.
4919  $this->extFileFunctions($table, $theField, $row[$theField], 'deleteAll');
4920  }
4921  } elseif ($this->enableLogging) {
4922  $this->log($table, $uid, 3, 0, 100, 'Delete: Zero rows in result when trying to read filenames from record which should be deleted');
4923  }
4924  $this->databaseConnection->sql_free_result($mres);
4925  }
4926  // Delete the hard way...:
4927  $this->databaseConnection->exec_DELETEquery($table, 'uid=' . (int)$uid);
4928  $this->deleteL10nOverlayRecords($table, $uid);
4929  }
4930  if ($this->enableLogging) {
4931  // 1 means insert, 3 means delete
4932  $state = $undeleteRecord ? 1 : 3;
4933  if (!$this->databaseConnection->sql_error()) {
4934  if ($forceHardDelete) {
4935  $message = 'Record \'%s\' (%s) was deleted unrecoverable from page \'%s\' (%s)';
4936  } else {
4937  $message = $state == 1 ? 'Record \'%s\' (%s) was restored on page \'%s\' (%s)' : 'Record \'%s\' (%s) was deleted from page \'%s\' (%s)';
4938  }
4939  $propArr = $this->getRecordProperties($table, $uid);
4940  $pagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4941 
4942  $this->log($table, $uid, $state, 0, 0, $message, 0, array(
4943  $propArr['header'],
4944  $table . ':' . $uid,
4945  $pagePropArr['header'],
4946  $propArr['pid']
4947  ), $propArr['event_pid']);
4948  } else {
4949  $this->log($table, $uid, $state, 0, 100, $this->databaseConnection->sql_error());
4950  }
4951  }
4952  // Update reference index:
4953  $this->updateRefIndex($table, $uid);
4954  // If there are entries in the updateRefIndexStack
4955  if (is_array($this->updateRefIndexStack[$table]) && is_array($this->updateRefIndexStack[$table][$uid])) {
4956  while ($args = array_pop($this->updateRefIndexStack[$table][$uid])) {
4957  // $args[0]: table, $args[1]: uid
4958  $this->updateRefIndex($args[0], $args[1]);
4959  }
4960  unset($this->updateRefIndexStack[$table][$uid]);
4961  }
4962  }
4963 
4974  public function deleteRecord_flexFormCallBack($dsArr, $dataValue, $PA, $structurePath, $pObj)
4975  {
4976  // Use reference index object to find files in fields:
4978  $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
4979  $files = $refIndexObj->getRelations_procFiles($dataValue, $dsArr['TCEforms']['config'], $PA['uid']);
4980  // Traverse files and delete them if the field is a regular file field (and not a file_reference field)
4981  if (is_array($files) && $dsArr['TCEforms']['config']['internal_type'] === 'file') {
4982  foreach ($files as $dat) {
4983  if (@is_file($dat['ID_absFile'])) {
4984  $file = $this->getResourceFactory()->retrieveFileOrFolderObject($dat['ID_absFile']);
4985  $file->delete();
4986  } elseif ($this->enableLogging) {
4987  $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');
4988  }
4989  }
4990  }
4991  }
4992 
5001  public function deletePages($uid, $force = false, $forceHardDelete = false)
5002  {
5003  // Getting list of pages to delete:
5004  if ($force) {
5005  // Returns the branch WITHOUT permission checks (0 secures that)
5006  $brExist = $this->doesBranchExist('', $uid, 0, 1);
5007  $res = GeneralUtility::trimExplode(',', $brExist . $uid, true);
5008  } else {
5009  $res = $this->canDeletePage($uid);
5010  }
5011  // Perform deletion if not error:
5012  if (is_array($res)) {
5013  foreach ($res as $deleteId) {
5014  $this->deleteSpecificPage($deleteId, $forceHardDelete);
5015  }
5016  } else {
5018  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, htmlspecialchars($res), '', FlashMessage::ERROR, true);
5020  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
5022  $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
5023 
5024  if ($this->enableLogging) {
5025  $this->newlog($res, 1);
5026  }
5027  }
5028  }
5029 
5039  public function deleteSpecificPage($uid, $forceHardDelete = false)
5040  {
5041  $uid = (int)$uid;
5042  if ($uid) {
5043  foreach ($GLOBALS['TCA'] as $table => $_) {
5044  if ($table != 'pages') {
5045  $mres = $this->databaseConnection->exec_SELECTquery('uid', $table, 'pid=' . (int)$uid . $this->deleteClause($table));
5046  while ($row = $this->databaseConnection->sql_fetch_assoc($mres)) {
5047  $this->copyMovedRecordToNewLocation($table, $row['uid']);
5048  $this->deleteVersionsForRecord($table, $row['uid'], $forceHardDelete);
5049  $this->deleteRecord($table, $row['uid'], true, $forceHardDelete);
5050  }
5051  $this->databaseConnection->sql_free_result($mres);
5052  }
5053  }
5054  $this->copyMovedRecordToNewLocation('pages', $uid);
5055  $this->deleteVersionsForRecord('pages', $uid, $forceHardDelete);
5056  $this->deleteRecord('pages', $uid, true, $forceHardDelete);
5057  }
5058  }
5059 
5073  protected function copyMovedRecordToNewLocation($table, $uid)
5074  {
5075  if ($this->BE_USER->workspace > 0) {
5076  // Check move placeholder at workspace
5077  $movePlaceholder = BackendUtility::getMovePlaceholder($table, $uid);
5078  if ($movePlaceholder !== false) {
5079  // If move placeholder exists, copy to new location
5080  // This will create a New placeholder on the new location
5081  // and a version for this new placeholder
5082  $command = array(
5083  $table => array(
5084  $uid => array(
5085  'copy' => '-' . $movePlaceholder['uid']
5086  )
5087  )
5088  );
5090  $dataHandler = GeneralUtility::makeInstance(__CLASS__);
5091  $dataHandler->stripslashes_values = false;
5092  $dataHandler->neverHideAtCopy = true;
5093  $dataHandler->start(array(), $command);
5094  $dataHandler->process_cmdmap();
5095  unset($dataHandler);
5096 
5097  // Delete move placeholder
5098  $this->deleteRecord($table, $movePlaceholder['uid'], true, true);
5099  }
5100  }
5101  }
5102 
5109  public function canDeletePage($uid)
5110  {
5111  // If we may at all delete this page
5112  if (!$this->doesRecordExist('pages', $uid, 'delete')) {
5113  return 'Attempt to delete page without permissions';
5114  }
5115 
5116  if ($this->deleteTree) {
5117  // Returns the branch
5118  $brExist = $this->doesBranchExist('', $uid, $this->pMap['delete'], 1);
5119  // Checks if we had permissions
5120  if ($brExist == -1) {
5121  return 'Attempt to delete pages in branch without permissions';
5122  }
5123 
5124  if (!$this->noRecordsFromUnallowedTables($brExist . $uid)) {
5125  return 'Attempt to delete records from disallowed tables';
5126  }
5127 
5128  $pagesInBranch = GeneralUtility::trimExplode(',', $brExist . $uid, true);
5129  foreach ($pagesInBranch as $pageInBranch) {
5130  if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, true)) {
5131  return 'Attempt to delete page which has prohibited localizations.';
5132  }
5133  }
5134  return $pagesInBranch;
5135  } else {
5136  // returns the branch
5137  $brExist = $this->doesBranchExist('', $uid, $this->pMap['delete'], 1);
5138  // Checks if branch exists
5139  if ($brExist != '') {
5140  return 'Attempt to delete page which has subpages';
5141  }
5142 
5143  if (!$this->noRecordsFromUnallowedTables($uid)) {
5144  return 'Attempt to delete records from disallowed tables';
5145  }
5146 
5147  if ($this->BE_USER->recordEditAccessInternals('pages', $uid, false, false, true)) {
5148  return array($uid);
5149  } else {
5150  return 'Attempt to delete page which has prohibited localizations.';
5151  }
5152  }
5153  }
5154 
5162  public function cannotDeleteRecord($table, $id)
5163  {
5164  if ($table === 'pages') {
5165  $res = $this->canDeletePage($id);
5166  return is_array($res) ? false : $res;
5167  } else {
5168  return $this->doesRecordExist($table, $id, 'delete') ? false : 'No permission to delete record';
5169  }
5170  }
5171 
5179  public function isRecordUndeletable($table, $uid)
5180  {
5181  $result = false;
5182  $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5183  if ($record['pid']) {
5184  $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5185  // The page containing the record is not deleted, thus the record can be undeleted:
5186  if (!$page['deleted']) {
5187  $result = true;
5188  } elseif ($this->enableLogging) {
5189  $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');
5190  }
5191  } else {
5192  // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5193  $result = true;
5194  }
5195  return $result;
5196  }
5197 
5208  public function deleteRecord_procFields($table, $uid, $undeleteRecord = false)
5209  {
5210  $conf = $GLOBALS['TCA'][$table]['columns'];
5211  $row = BackendUtility::getRecord($table, $uid, '*', '', false);
5212  foreach ($row as $field => $value) {
5213  $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5214  }
5215  }
5216 
5230  public function deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf, $undeleteRecord = false)
5231  {
5232  if ($conf['type'] == 'inline') {
5233  $foreign_table = $conf['foreign_table'];
5234  if ($foreign_table) {
5235  $inlineType = $this->getInlineFieldType($conf);
5236  if ($inlineType == 'list' || $inlineType == 'field') {
5238  $dbAnalysis = $this->createRelationHandlerInstance();
5239  $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
5240  $dbAnalysis->undeleteRecord = true;
5241 
5242  $enableCascadingDelete = true;
5243  // non type save comparison is intended!
5244  if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5245  $enableCascadingDelete = false;
5246  }
5247 
5248  // Walk through the items and remove them
5249  foreach ($dbAnalysis->itemArray as $v) {
5250  if (!$undeleteRecord) {
5251  if ($enableCascadingDelete) {
5252  $this->deleteAction($v['table'], $v['id']);
5253  }
5254  } else {
5255  $this->undeleteRecord($v['table'], $v['id']);
5256  }
5257  }
5258  }
5259  }
5260  } elseif ($this->isReferenceField($conf)) {
5261  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
5262  $dbAnalysis = $this->createRelationHandlerInstance();
5263  $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5264  foreach ($dbAnalysis->itemArray as $v) {
5265  $this->updateRefIndexStack[$table][$uid][] = array($v['table'], $v['id']);
5266  }
5267  }
5268  }
5269 
5277  public function deleteL10nOverlayRecords($table, $uid)
5278  {
5279  // Check whether table can be localized or has a different table defined to store localizations:
5280  if (!BackendUtility::isTableLocalizable($table) || !empty($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']) || !empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable'])) {
5281  return;
5282  }
5283  $where = '';
5284  if (isset($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
5285  $where = ' AND t3ver_oid=0';
5286  }
5287  $l10nRecords = BackendUtility::getRecordsByField($table, $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], $uid, $where);
5288  if (is_array($l10nRecords)) {
5289  foreach ($l10nRecords as $record) {
5290  // Ignore workspace delete placeholders. Those records have been marked for
5291  // deletion before - deleting them again in a workspace would revert that state.
5292  if ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5293  BackendUtility::workspaceOL($table, $record);
5294  if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5295  continue;
5296  }
5297  }
5298  $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5299  }
5300  }
5301  }
5302 
5303  /*********************************************
5304  *
5305  * Cmd: Versioning
5306  *
5307  ********************************************/
5319  public function versionizeRecord($table, $id, $label, $delete = false)
5320  {
5321  $id = (int)$id;
5322  // Stop any actions if the record is marked to be deleted:
5323  // (this can occur if IRRE elements are versionized and child elements are removed)
5324  if ($this->isElementToBeDeleted($table, $id)) {
5325  return null;
5326  }
5327  if (!$GLOBALS['TCA'][$table] || !$GLOBALS['TCA'][$table]['ctrl']['versioningWS'] || $id <= 0) {
5328  if ($this->enableLogging) {
5329  $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, 1);
5330  }
5331  return null;
5332  }
5333 
5334  if (!$this->doesRecordExist($table, $id, 'show')) {
5335  if ($this->enableLogging) {
5336  $this->newlog('You didn\'t have correct permissions to make a new version (copy) of this record "' . $table . '" / ' . $id, 1);
5337  }
5338  return null;
5339  }
5340 
5341  // Select main record:
5342  $row = $this->recordInfo($table, $id, 'pid,t3ver_id,t3ver_state');
5343  if (!is_array($row)) {
5344  if ($this->enableLogging) {
5345  $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize did not exist!', 1);
5346  }
5347  return null;
5348  }
5349 
5350  // Record must be online record
5351  if ($row['pid'] < 0) {
5352  if ($this->enableLogging) {
5353  $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (pid=-1)!', 1);
5354  }
5355  return null;
5356  }
5357 
5358  // Record must not be placeholder for moving.
5359  if (VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)) {
5360  if ($this->enableLogging) {
5361  $this->newlog('Record cannot be versioned because it is a placeholder for a moving operation', 1);
5362  }
5363  return null;
5364  }
5365 
5366  if ($delete && $this->cannotDeleteRecord($table, $id)) {
5367  if ($this->enableLogging) {
5368  $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), 1);
5369  }
5370  return null;
5371  }
5372 
5373  // Look for next version number:
5374  $res = $this->databaseConnection->exec_SELECTquery('t3ver_id', $table, '((pid=-1 && t3ver_oid=' . $id . ') OR uid=' . $id . ')' . $this->deleteClause($table), '', 't3ver_id DESC', '1');
5375  list($highestVerNumber) = $this->databaseConnection->sql_fetch_row($res);
5376  $this->databaseConnection->sql_free_result($res);
5377  // Look for version number of the current:
5378  $subVer = $row['t3ver_id'] . '.' . ($highestVerNumber + 1);
5379  // Set up the values to override when making a raw-copy:
5380  $overrideArray = array(
5381  't3ver_id' => $highestVerNumber + 1,
5382  't3ver_oid' => $id,
5383  't3ver_label' => $label ?: $subVer . ' / ' . date('d-m-Y H:m:s'),
5384  't3ver_wsid' => $this->BE_USER->workspace,
5385  't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5386  't3ver_count' => 0,
5387  't3ver_stage' => 0,
5388  't3ver_tstamp' => 0
5389  );
5390  if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5391  $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5392  }
5393  // Checking if the record already has a version in the current workspace of the backend user
5394  if ($this->BE_USER->workspace !== 0) {
5395  // Look for version already in workspace:
5396  $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5397  }
5398  // Create new version of the record and return the new uid
5399  if (empty($versionRecord['uid'])) {
5400  // Create raw-copy and return result:
5401  // The information of the label to be used for the workspace record
5402  // as well as the information whether the record shall be removed
5403  // must be forwarded (creating remove placeholders on a workspace are
5404  // done by copying the record and override several fields).
5405  $workspaceOptions = array(
5406  'delete' => $delete,
5407  'label' => $label,
5408  );
5409  return $this->copyRecord_raw($table, $id, -1, $overrideArray, $workspaceOptions);
5410  // Reuse the existing record and return its uid
5411  // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5412  // did not make much sense since the information is available)
5413  } else {
5414  return $versionRecord['uid'];
5415  }
5416  return null;
5417  }
5418 
5428  public function version_remapMMForVersionSwap($table, $id, $swapWith)
5429  {
5430  // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5431  $currentRec = BackendUtility::getRecord($table, $id);
5432  $swapRec = BackendUtility::getRecord($table, $swapWith);
5433  $this->version_remapMMForVersionSwap_reg = array();
5434  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5435  $conf = $fConf['config'];
5436  if ($this->isReferenceField($conf)) {
5437  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
5438  $prependName = $conf['type'] == 'group' ? $conf['prepend_tname'] : '';
5439  if ($conf['MM']) {
5441  $dbAnalysis = $this->createRelationHandlerInstance();
5442  $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5443  if (!empty($dbAnalysis->getValueArray($prependName))) {
5444  $this->version_remapMMForVersionSwap_reg[$id][$field] = array($dbAnalysis, $conf['MM'], $prependName);
5445  }
5447  $dbAnalysis = $this->createRelationHandlerInstance();
5448  $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5449  if (!empty($dbAnalysis->getValueArray($prependName))) {
5450  $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = array($dbAnalysis, $conf['MM'], $prependName);
5451  }
5452  }
5453  } elseif ($conf['type'] == 'flex') {
5454  // Current record
5455  $dataStructArray = BackendUtility::getFlexFormDS($conf, $currentRec, $table, $field);
5456  $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5457  if (is_array($currentValueArray)) {
5458  $this->checkValue_flex_procInData($currentValueArray['data'], array(), array(), $dataStructArray, array($table, $id, $field), 'version_remapMMForVersionSwap_flexFormCallBack');
5459  }
5460  // Swap record
5461  $dataStructArray = BackendUtility::getFlexFormDS($conf, $swapRec, $table, $field);
5462  $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5463  if (is_array($currentValueArray)) {
5464  $this->checkValue_flex_procInData($currentValueArray['data'], array(), array(), $dataStructArray, array($table, $swapWith, $field), 'version_remapMMForVersionSwap_flexFormCallBack');
5465  }
5466  }
5467  }
5468  // Execute:
5469  $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5470  }
5471 
5484  public function version_remapMMForVersionSwap_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
5485  {
5486  // Extract parameters:
5487  list($table, $uid, $field) = $pParams;
5488  if ($this->isReferenceField($dsConf)) {
5489  $allowedTables = $dsConf['type'] == 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5490  $prependName = $dsConf['type'] == 'group' ? $dsConf['prepend_tname'] : '';
5491  if ($dsConf['MM']) {
5493  $dbAnalysis = $this->createRelationHandlerInstance();
5494  $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5495  $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = array($dbAnalysis, $dsConf['MM'], $prependName);
5496  }
5497  }
5498  }
5499 
5510  public function version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
5511  {
5512  if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5513  foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5514  $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5515  }
5516  }
5517  if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5518  foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5519  $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5520  }
5521  }
5522  if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5523  foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5524  $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5525  }
5526  }
5527  }
5528 
5529  /*********************************************
5530  *
5531  * Cmd: Helper functions
5532  *
5533  ********************************************/
5534 
5542  protected function getLocalTCE($stripslashesValues = false, $dontProcessTransformations = true)
5543  {
5544  $copyTCE = GeneralUtility::makeInstance(__CLASS__);
5545  $copyTCE->stripslashes_values = $stripslashesValues;
5546  $copyTCE->copyTree = $this->copyTree;
5547  // Copy forth the cached TSconfig
5548  $copyTCE->cachedTSconfig = $this->cachedTSconfig;
5549  // Transformations should NOT be carried out during copy
5550  $copyTCE->dontProcessTransformations = $dontProcessTransformations;
5551  return $copyTCE;
5552  }
5553 
5559  public function remapListedDBRecords()
5560  {
5561  if (!empty($this->registerDBList)) {
5562  foreach ($this->registerDBList as $table => $records) {
5563  foreach ($records as $uid => $fields) {
5564  $newData = array();
5565  $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5566  $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5567  foreach ($fields as $fieldName => $value) {
5568  $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5569  switch ($conf['type']) {
5570  case 'group':
5571 
5572  case 'select':
5573  $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5574  if (is_array($vArray)) {
5575  $newData[$fieldName] = implode(',', $vArray);
5576  }
5577  break;
5578  case 'flex':
5579  if ($value == 'FlexForm_reference') {
5580  // This will fetch the new row for the element
5581  $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5582  if (is_array($origRecordRow)) {
5583  BackendUtility::workspaceOL($table, $origRecordRow);
5584  // Get current data structure and value array:
5585  $dataStructArray = BackendUtility::getFlexFormDS($conf, $origRecordRow, $table, $fieldName);
5586  $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5587  // Do recursive processing of the XML data:
5588  $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], array(), array(), $dataStructArray, array($table, $theUidToUpdate, $fieldName), 'remapListedDBRecords_flexFormCallBack');
5589  // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5590  if (is_array($currentValueArray['data'])) {
5591  $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
5592  }
5593  }
5594  }
5595  break;
5596  case 'inline':
5597  $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5598  break;
5599  default:
5600  debug('Field type should not appear here: ' . $conf['type']);
5601  }
5602  }
5603  // If any fields were changed, those fields are updated!
5604  if (!empty($newData)) {
5605  $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5606  }
5607  }
5608  }
5609  }
5610  }
5611 
5623  public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
5624  {
5625  // Extract parameters:
5626  list($table, $uid, $field) = $pParams;
5627  // If references are set for this field, set flag so they can be corrected later:
5628  if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
5629  $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
5630  if (is_array($vArray)) {
5631  $dataValue = implode(',', $vArray);
5632  }
5633  }
5634  // Return
5635  return array('value' => $dataValue);
5636  }
5637 
5648  public function remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
5649  {
5650  // Initialize variables
5651  // Will be set TRUE if an upgrade should be done...
5652  $set = false;
5653  // Allowed tables for references.
5654  $allowedTables = $conf['type'] == 'group' ? $conf['allowed'] : $conf['foreign_table'];
5655  // Table name to prepend the UID
5656  $prependName = $conf['type'] == 'group' ? $conf['prepend_tname'] : '';
5657  // Which tables that should possibly not be remapped
5658  $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
5659  // Convert value to list of references:
5660  $dbAnalysis = $this->createRelationHandlerInstance();
5661  $dbAnalysis->registerNonTableValues = $conf['type'] == 'select' && $conf['allowNonIdValues'];
5662  $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
5663  // Traverse those references and map IDs:
5664  foreach ($dbAnalysis->itemArray as $k => $v) {
5665  $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
5666  if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
5667  $dbAnalysis->itemArray[$k]['id'] = $mapID;
5668  $set = true;
5669  }
5670  }
5671  if (!empty($conf['MM'])) {
5672  // Purge invalid items (live/version)
5673  $dbAnalysis->purgeItemArray();
5674  if ($dbAnalysis->isPurged()) {
5675  $set = true;
5676  }
5677 
5678  // If record has been versioned/copied in this process, handle invalid relations of the live record
5679  $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
5680  if (!empty($this->copyMappingArray_merged[$table])) {
5681  $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
5682  }
5683  if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
5684  $liveRelations = $this->createRelationHandlerInstance();
5685  $liveRelations->setWorkspaceId(0);
5686  $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
5687  // Purge invalid relations in the live workspace ("0")
5688  $liveRelations->purgeItemArray(0);
5689  if ($liveRelations->isPurged()) {
5690  $liveRelations->writeMM($conf['MM'], $liveId, $prependName);
5691  }
5692  }
5693  }
5694  // If a change has been done, set the new value(s)
5695  if ($set) {
5696  if ($conf['MM']) {
5697  $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5698  } else {
5699  return $dbAnalysis->getValueArray($prependName);
5700  }
5701  }
5702  return null;
5703  }
5704 
5714  public function remapListedDBRecords_procInline($conf, $value, $uid, $table)
5715  {
5716  $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5717  if ($conf['foreign_table']) {
5718  $inlineType = $this->getInlineFieldType($conf);
5719  if ($inlineType == 'mm') {
5720  $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5721  } elseif ($inlineType !== false) {
5723  $dbAnalysis = $this->createRelationHandlerInstance();
5724  $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
5725 
5726  // Keep original (live) item array and update values for specific versioned records
5727  $originalItemArray = $dbAnalysis->itemArray;
5728  foreach ($dbAnalysis->itemArray as &$item) {
5729  $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
5730  if (!empty($versionedId)) {
5731  $item['id'] = $versionedId;
5732  }
5733  }
5734 
5735  // Update child records if using pointer fields ('foreign_field'):
5736  if ($inlineType == 'field') {
5737  $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
5738  }
5739  $thePidToUpdate = null;
5740  // If the current field is set on a page record, update the pid of related child records:
5741  if ($table == 'pages') {
5742  $thePidToUpdate = $theUidToUpdate;
5743  } elseif (isset($this->registerDBPids[$table][$uid])) {
5744  $thePidToUpdate = $this->registerDBPids[$table][$uid];
5745  $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
5746  }
5747  // Update child records if change to pid is required (only if the current record is not on a workspace):
5748  if ($thePidToUpdate) {
5749  $updateValues = array('pid' => $thePidToUpdate);
5750  foreach ($originalItemArray as $v) {
5751  if ($v['id'] && $v['table'] && is_null(BackendUtility::getLiveVersionIdOfRecord($v['table'], $v['id']))) {
5752  $this->databaseConnection->exec_UPDATEquery($v['table'], 'uid=' . (int)$v['id'], $updateValues);
5753  }
5754  }
5755  }
5756  }
5757  }
5758  }
5759 
5766  public function processRemapStack()
5767  {
5768  // Processes the remap stack:
5769  if (is_array($this->remapStack)) {
5770  $remapFlexForms = array();
5771 
5772  foreach ($this->remapStack as $remapAction) {
5773  // If no position index for the arguments was set, skip this remap action:
5774  if (!is_array($remapAction['pos'])) {
5775  continue;
5776  }
5777  // Load values from the argument array in remapAction:
5778  $field = $remapAction['field'];
5779  $id = $remapAction['args'][$remapAction['pos']['id']];
5780  $rawId = $id;
5781  $table = $remapAction['args'][$remapAction['pos']['table']];
5782  $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
5783  $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
5784  $additionalData = $remapAction['additionalData'];
5785  // The record is new and has one or more new ids (in case of versioning/workspaces):
5786  if (strpos($id, 'NEW') !== false) {
5787  // Replace NEW...-ID with real uid:
5788  $id = $this->substNEWwithIDs[$id];
5789  // If the new parent record is on a non-live workspace or versionized, it has another new id:
5790  if (isset($this->autoVersionIdMap[$table][$id])) {
5791  $id = $this->autoVersionIdMap[$table][$id];
5792  }
5793  $remapAction['args'][$remapAction['pos']['id']] = $id;
5794  }
5795  // Replace relations to NEW...-IDs in field value (uids of child records):
5796  if (is_array($valueArray)) {
5797  $foreign_table = $tcaFieldConf['foreign_table'];
5798  foreach ($valueArray as $key => $value) {
5799  if (strpos($value, 'NEW') !== false) {
5800  $value = $this->substNEWwithIDs[$value];
5801  // The record is new, but was also auto-versionized and has another new id:
5802  if (isset($this->autoVersionIdMap[$foreign_table][$value])) {
5803  $value = $this->autoVersionIdMap[$foreign_table][$value];
5804  }
5805  // Set a hint that this was a new child record:
5806  $this->newRelatedIDs[$foreign_table][] = $value;
5807  $valueArray[$key] = $value;
5808  }
5809  }
5810  $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
5811  }
5812  // Process the arguments with the defined function:
5813  $newValue = call_user_func_array(array($this, $remapAction['func']), $remapAction['args']);
5814  // If array is returned, check for maxitems condition, if string is returned this was already done:
5815  if (is_array($newValue)) {
5816  $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
5817  // The reference casting is only required if
5818  // checkValue_group_select_processDBdata() returns an array
5819  $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
5820  }
5821  // Update in database (list of children (csv) or number of relations (foreign_field)):
5822  if (!empty($field)) {
5823  $this->updateDB($table, $id, array($field => $newValue));
5824  // Collect data to update FlexForms
5825  } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
5826  $flexFormId = $additionalData['flexFormId'];
5827  $flexFormPath = $additionalData['flexFormPath'];
5828 
5829  if (!isset($remapFlexForms[$flexFormId])) {
5830  $remapFlexForms[$flexFormId] = array();
5831  }
5832 
5833  $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
5834  }
5835  // Process waiting Hook: processDatamap_afterDatabaseOperations:
5836  if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
5837  $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
5838  // Update field with remapped data:
5839  $hookArgs['fieldArray'][$field] = $newValue;
5840  // Process waiting hook objects:
5841  $hookObjectsArr = $hookArgs['hookObjectsArr'];
5842  foreach ($hookObjectsArr as $hookObj) {
5843  if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
5844  $hookObj->processDatamap_afterDatabaseOperations($hookArgs['status'], $table, $rawId, $hookArgs['fieldArray'], $this);
5845  }
5846  }
5847  }
5848  }
5849 
5850  if ($remapFlexForms) {
5851  foreach ($remapFlexForms as $flexFormId => $modifications) {
5852  $this->updateFlexFormData($flexFormId, $modifications);
5853  }
5854  }
5855  }
5856  // Processes the remap stack actions:
5857  if ($this->remapStackActions) {
5858  foreach ($this->remapStackActions as $action) {
5859  if (isset($action['callback']) && isset($action['arguments'])) {
5860  call_user_func_array($action['callback'], $action['arguments']);
5861  }
5862  }
5863  }
5864  // Processes the reference index updates of the remap stack:
5865  foreach ($this->remapStackRefIndex as $table => $idArray) {
5866  foreach ($idArray as $id) {
5867  $this->updateRefIndex($table, $id);
5868  unset($this->remapStackRefIndex[$table][$id]);
5869  }
5870  }
5871  // Reset:
5872  $this->remapStack = array();
5873  $this->remapStackRecords = array();
5874  $this->remapStackActions = array();
5875  $this->remapStackRefIndex = array();
5876  }
5877 
5885  protected function updateFlexFormData($flexFormId, array $modifications)
5886  {
5887  list($table, $uid, $field) = explode(':', $flexFormId, 3);
5888 
5889  if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
5890  $uid = $this->substNEWwithIDs[$uid];
5891  }
5892 
5893  $record = $this->recordInfo($table, $uid, '*');
5894 
5895  if (!$table || !$uid || !$field || !is_array($record)) {
5896  return;
5897  }
5898 
5899  BackendUtility::workspaceOL($table, $record);
5900 
5901  // Get current data structure and value array:
5902  $valueStructure = GeneralUtility::xml2array($record[$field]);
5903 
5904  // Do recursive processing of the XML data:
5905  foreach ($modifications as $path => $value) {
5906  $valueStructure['data'] = ArrayUtility::setValueByPath(
5907  $valueStructure['data'], $path, $value
5908  );
5909  }
5910 
5911  if (is_array($valueStructure['data'])) {
5912  // The return value should be compiled back into XML
5913  $values = array(
5914  $field => $this->checkValue_flexArray2Xml($valueStructure, true),
5915  );
5916 
5917  $this->updateDB($table, $uid, $values);
5918  }
5919  }
5920 
5936  protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
5937  {
5938  // Check whether the affected record is marked to be remapped:
5939  if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
5940  call_user_func_array($callback, $arguments);
5941  } else {
5942  $this->addRemapAction($table, $id, $callback, $arguments);
5943  }
5944  }
5945 
5955  public function addRemapAction($table, $id, array $callback, array $arguments)
5956  {
5957  $this->remapStackActions[] = array(
5958  'affects' => array(
5959  'table' => $table,
5960  'id' => $id
5961  ),
5962  'callback' => $callback,
5963  'arguments' => $arguments
5964  );
5965  }
5966 
5974  public function addRemapStackRefIndex($table, $id)
5975  {
5976  $this->remapStackRefIndex[$table][$id] = $id;
5977  }
5978 
5991  public function getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
5992  {
5993  if (is_array($registerDBList[$table][$id])) {
5994  foreach ($incomingFieldArray as $field => $value) {
5995  $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
5996  if ($registerDBList[$table][$id][$field] && ($foreignTable = $fieldConf['foreign_table'])) {
5997  $newValueArray = array();
5998  $origValueArray = explode(',', $value);
5999  // Update the uids of the copied records, but also take care about new records:
6000  foreach ($origValueArray as $childId) {
6001  $newValueArray[] = $this->autoVersionIdMap[$foreignTable][$childId] ? $this->autoVersionIdMap[$foreignTable][$childId] : $childId;
6002  }
6003  // Set the changed value to the $incomingFieldArray
6004  $incomingFieldArray[$field] = implode(',', $newValueArray);
6005  }
6006  }
6007  // Clean up the $registerDBList array:
6008  unset($registerDBList[$table][$id]);
6009  if (empty($registerDBList[$table])) {
6010  unset($registerDBList[$table]);
6011  }
6012  }
6013  }
6014 
6015  /*****************************
6016  *
6017  * Access control / Checking functions
6018  *
6019  *****************************/
6026  public function checkModifyAccessList($table)
6027  {
6028  $res = $this->admin || !$this->tableAdminOnly($table) && GeneralUtility::inList($this->BE_USER->groupData['tables_modify'], $table);
6029  // Hook 'checkModifyAccessList': Post-processing of the state of access
6030  foreach ($this->getCheckModifyAccessListHookObjects() as $hookObject) {
6032  $hookObject->checkModifyAccessList($res, $table, $this);
6033  }
6034  return $res;
6035  }
6036 
6044  public function isRecordInWebMount($table, $id)
6045  {
6046  if (!isset($this->isRecordInWebMount_Cache[($table . ':' . $id)])) {
6047  $recP = $this->getRecordProperties($table, $id);
6048  $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->isInWebMount($recP['event_pid']);
6049  }
6050  return $this->isRecordInWebMount_Cache[$table . ':' . $id];
6051  }
6052 
6059  public function isInWebMount($pid)
6060  {
6061  if (!isset($this->isInWebMount_Cache[$pid])) {
6062  $this->isInWebMount_Cache[$pid] = $this->BE_USER->isInWebMount($pid);
6063  }
6064  return $this->isInWebMount_Cache[$pid];
6065  }
6066 
6076  public function checkRecordUpdateAccess($table, $id, $data = false, $hookObjectsArr = null)
6077  {
6078  $res = null;
6079  if (is_array($hookObjectsArr)) {
6080  foreach ($hookObjectsArr as $hookObj) {
6081  if (method_exists($hookObj, 'checkRecordUpdateAccess')) {
6082  $res = $hookObj->checkRecordUpdateAccess($table, $id, $data, $res, $this);
6083  }
6084  }
6085  }
6086  if ($res === 1 || $res === 0) {
6087  return $res;
6088  } else {
6089  $res = 0;
6090  }
6091  if ($GLOBALS['TCA'][$table] && (int)$id > 0) {
6092  // If information is cached, return it
6093  if (isset($this->recUpdateAccessCache[$table][$id])) {
6094  return $this->recUpdateAccessCache[$table][$id];
6095  } elseif ($this->doesRecordExist($table, $id, 'edit')) {
6096  $res = 1;
6097  }
6098  // Cache the result
6099  $this->recUpdateAccessCache[$table][$id] = $res;
6100  }
6101  return $res;
6102  }
6103 
6113  public function checkRecordInsertAccess($insertTable, $pid, $action = 1)
6114  {
6115  $pid = (int)$pid;
6116  if ($pid < 0) {
6117  return false;
6118  }
6119  // If information is cached, return it
6120  if (isset($this->recInsertAccessCache[$insertTable][$pid])) {
6121  return $this->recInsertAccessCache[$insertTable][$pid];
6122  }
6123 
6124  $res = false;
6125  if ($insertTable === 'pages') {
6126  $perms = $this->pMap['new'];
6127  // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6128  } elseif (($insertTable === 'sys_file_reference') && array_key_exists('pages', $this->datamap)) {
6129  $perms = $this->pMap['edit'];
6130  } else {
6131  $perms = $this->pMap['editcontent'];
6132  }
6133  $pageExists = (bool)$this->doesRecordExist('pages', $pid, $perms);
6134  // 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
6135  if ($pageExists || $pid === 0 && ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($insertTable))) {
6136  // Check permissions
6137  if ($this->isTableAllowedForThisPage($pid, $insertTable)) {
6138  $res = true;
6139  // Cache the result
6140  $this->recInsertAccessCache[$insertTable][$pid] = $res;
6141  } elseif ($this->enableLogging) {
6142  $propArr = $this->getRecordProperties('pages', $pid);
6143  $this->log($insertTable, $pid, $action, 0, 1, 'Attempt to insert record on page \'%s\' (%s) where this table, %s, is not allowed', 11, array($propArr['header'], $pid, $insertTable), $propArr['event_pid']);
6144  }
6145  } elseif ($this->enableLogging) {
6146  $propArr = $this->getRecordProperties('pages', $pid);
6147  $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, array($propArr['header'], $pid, $insertTable), $propArr['event_pid']);
6148  }
6149  return $res;
6150  }
6151 
6159  public function isTableAllowedForThisPage($page_uid, $checkTable)
6160  {
6161  $page_uid = (int)$page_uid;
6162  $rootLevelSetting = (int)$GLOBALS['TCA'][$checkTable]['ctrl']['rootLevel'];
6163  // 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.
6164  if ($checkTable !== 'pages' && $rootLevelSetting !== -1 && ($rootLevelSetting xor !$page_uid)) {
6165  return false;
6166  }
6167  $allowed = false;
6168  // Check root-level
6169  if (!$page_uid) {
6170  if ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($checkTable)) {
6171  $allowed = true;
6172  }
6173  } else {
6174  // Check non-root-level
6175  $doktype = $this->pageInfo($page_uid, 'doktype');
6176  $allowedTableList = isset($GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'])
6177  ? $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables']
6178  : $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6179  $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6180  // If all tables or the table is listed as an allowed type, return TRUE
6181  if (strpos($allowedTableList, '*') !== false || in_array($checkTable, $allowedArray, true)) {
6182  $allowed = true;
6183  }
6184  }
6185  return $allowed;
6186  }
6187 
6198  public function doesRecordExist($table, $id, $perms)
6199  {
6200  $id = (int)$id;
6201  if ($this->bypassAccessCheckForRecords) {
6202  return is_array(BackendUtility::getRecordRaw($table, 'uid=' . $id, 'uid'));
6203  }
6204  // Processing the incoming $perms (from possible string to integer that can be AND'ed)
6206  if ($table != 'pages') {
6207  switch ($perms) {
6208  case 'edit':
6209 
6210  case 'delete':
6211 
6212  case 'new':
6213  // This holds it all in case the record is not page!!
6214  if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
6215  $perms = 'edit';
6216  } else {
6217  $perms = 'editcontent';
6218  }
6219  break;
6220  }
6221  }
6222  $perms = (int)$this->pMap[$perms];
6223  } else {
6224  $perms = (int)$perms;
6225  }
6226  if (!$perms) {
6227  throw new \RuntimeException('Internal ERROR: no permissions to check for non-admin user', 1270853920);
6228  }
6229  // For all tables: Check if record exists:
6230  $isWebMountRestrictionIgnored = BackendUtility::isWebMountRestrictionIgnored($table);
6231  if (is_array($GLOBALS['TCA'][$table]) && $id > 0 && ($isWebMountRestrictionIgnored || $this->isRecordInWebMount($table, $id) || $this->admin)) {
6232  if ($table != 'pages') {
6233  // Find record without checking page:
6234  $mres = $this->databaseConnection->exec_SELECTquery('uid,pid', $table, 'uid=' . (int)$id . $this->deleteClause($table));
6235  // THIS SHOULD CHECK FOR editlock I think!
6236  $output = $this->databaseConnection->sql_fetch_assoc($mres);
6237  BackendUtility::fixVersioningPid($table, $output, true);
6238  // If record found, check page as well:
6239  if (is_array($output)) {
6240  // Looking up the page for record:
6241  $mres = $this->doesRecordExist_pageLookUp($output['pid'], $perms);
6242  $pageRec = $this->databaseConnection->sql_fetch_assoc($mres);
6243  // 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):
6244  $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
6245  if (is_array($pageRec) || !$output['pid'] && ($isRootLevelRestrictionIgnored || $this->admin)) {
6246  return true;
6247  }
6248  }
6249  return false;
6250  } else {
6251  $mres = $this->doesRecordExist_pageLookUp($id, $perms);
6252  return $this->databaseConnection->sql_num_rows($mres);
6253  }
6254  }
6255  return false;
6256  }
6257 
6267  public function doesRecordExist_pageLookUp($id, $perms)
6268  {
6269  return $this->databaseConnection->exec_SELECTquery('uid', 'pages', 'uid=' . (int)$id . $this->deleteClause('pages') . ($perms && !$this->admin ? ' AND ' . $this->BE_USER->getPagePermsClause($perms) : '') . (!$this->admin && $GLOBALS['TCA']['pages']['ctrl']['editlock'] && $perms & Permission::PAGE_EDIT + Permission::PAGE_DELETE + Permission::CONTENT_EDIT ? ' AND ' . $GLOBALS['TCA']['pages']['ctrl']['editlock'] . '=0' : ''));
6270  }
6271 
6285  public function doesBranchExist($inList, $pid, $perms, $recurse)
6286  {
6287  $pid = (int)$pid;
6288  $perms = (int)$perms;
6289  if ($pid >= 0) {
6290  $mres = $this->databaseConnection->exec_SELECTquery('uid, perms_userid, perms_groupid, perms_user, perms_group, perms_everybody', 'pages', 'pid=' . (int)$pid . $this->deleteClause('pages'), '', 'sorting');
6291  while ($row = $this->databaseConnection->sql_fetch_assoc($mres)) {
6292  // IF admin, then it's OK
6293  if ($this->admin || $this->BE_USER->doesUserHaveAccess($row, $perms)) {
6294  $inList .= $row['uid'] . ',';
6295  if ($recurse) {
6296  // Follow the subpages recursively...
6297  $inList = $this->doesBranchExist($inList, $row['uid'], $perms, $recurse);
6298  if ($inList == -1) {
6299  return -1;
6300  }
6301  }
6302  } else {
6303  // No permissions
6304  return -1;
6305  }
6306  }
6307  $this->databaseConnection->sql_free_result($mres);
6308  }
6309  return $inList;
6310  }
6311 
6318  public function tableReadOnly($table)
6319  {
6320  // Returns TRUE if table is readonly
6321  return (bool)$GLOBALS['TCA'][$table]['ctrl']['readOnly'];
6322  }
6323 
6330  public function tableAdminOnly($table)
6331  {
6332  // Returns TRUE if table is admin-only
6333  return (bool)$GLOBALS['TCA'][$table]['ctrl']['adminOnly'];
6334  }
6335 
6344  public function destNotInsideSelf($destinationId, $id)
6345  {
6346  $loopCheck = 100;
6347  $destinationId = (int)$destinationId;
6348  $id = (int)$id;
6349  if ($destinationId === $id) {
6350  return false;
6351  }
6352  while ($destinationId !== 0 && $loopCheck > 0) {
6353  $loopCheck--;
6354  $res = $this->databaseConnection->exec_SELECTquery('pid, uid, t3ver_oid,t3ver_wsid', 'pages', 'uid=' . $destinationId . $this->deleteClause('pages'));
6355  if ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
6356  BackendUtility::fixVersioningPid('pages', $row);
6357  if ($row['pid'] == $id) {
6358  return false;
6359  } else {
6360  $destinationId = (int)$row['pid'];
6361  }
6362  } else {
6363  return false;
6364  }
6365  $this->databaseConnection->sql_free_result($res);
6366  }
6367  return true;
6368  }
6369 
6376  public function getExcludeListArray()
6377  {
6378  $list = array();
6379  $nonExcludeFieldsArray = array_flip(GeneralUtility::trimExplode(',', $this->BE_USER->groupData['non_exclude_fields']));
6380  foreach ($GLOBALS['TCA'] as $table => $_) {
6381  if (isset($GLOBALS['TCA'][$table]['columns'])) {
6382  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $config) {
6383  if ($config['exclude'] && !isset($nonExcludeFieldsArray[$table . ':' . $field])) {
6384  $list[] = $table . '-' . $field;
6385  }
6386  }
6387  }
6388  }
6389  return $list;
6390  }
6391 
6399  public function doesPageHaveUnallowedTables($page_uid, $doktype)
6400  {
6401  $page_uid = (int)$page_uid;
6402  if (!$page_uid) {
6403  // Not a number. Probably a new page
6404  return false;
6405  }
6406  $allowedTableList = isset($GLOBALS['PAGES_TYPES'][$doktype]['allowedTables']) ? $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] : $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6407  $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6408  // If all tables is OK the return TRUE
6409  if (strstr($allowedTableList, '*')) {
6410  // OK...
6411  return false;
6412  }
6413  $tableList = array();
6414  foreach ($GLOBALS['TCA'] as $table => $_) {
6415  // If the table is not in the allowed list, check if there are records...
6416  if (!in_array($table, $allowedArray, true)) {
6417  $count = $this->databaseConnection->exec_SELECTcountRows('uid', $table, 'pid=' . (int)$page_uid);
6418  if ($count) {
6419  $tableList[] = $table;
6420  }
6421  }
6422  }
6423  return implode(',', $tableList);
6424  }
6425 
6426  /*****************************
6427  *
6428  * Information lookup
6429  *
6430  *****************************/
6439  public function pageInfo($id, $field)
6440  {
6441  if (!isset($this->pageCache[$id])) {
6442  $res = $this->databaseConnection->exec_SELECTquery('*', 'pages', 'uid=' . (int)$id);
6443  if ($this->databaseConnection->sql_num_rows($res)) {
6444  $this->pageCache[$id] = $this->databaseConnection->sql_fetch_assoc($res);
6445  }
6446  $this->databaseConnection->sql_free_result($res);
6447  }
6448  return $this->pageCache[$id][$field];
6449  }
6450 
6460  public function recordInfo($table, $id, $fieldList)
6461  {
6462  // Skip, if searching for NEW records or there's no TCA table definition
6463  if ((int)$id === 0 || !isset($GLOBALS['TCA'][$table])) {
6464  return null;
6465  }
6466  $result = $this->databaseConnection->exec_SELECTgetSingleRow($fieldList, $table, 'uid=' . (int)$id);
6467  return $result ?: null;
6468  }
6469 
6481  public function getRecordProperties($table, $id, $noWSOL = false)
6482  {
6483  $row = $table == 'pages' && !$id ? array('title' => '[root-level]', 'uid' => 0, 'pid' => 0) : $this->recordInfo($table, $id, '*');
6484  if (!$noWSOL) {
6485  BackendUtility::workspaceOL($table, $row);
6486  }
6487  return $this->getRecordPropertiesFromRow($table, $row);
6488  }
6489 
6497  public function getRecordPropertiesFromRow($table, $row)
6498  {
6499  if ($GLOBALS['TCA'][$table]) {
6500  BackendUtility::fixVersioningPid($table, $row);
6501  $out = array(
6502  'header' => BackendUtility::getRecordTitle($table, $row),
6503  'pid' => $row['pid'],
6504  'event_pid' => $this->eventPid($table, isset($row['_ORIG_pid']) ? $row['t3ver_oid'] : $row['uid'], $row['pid']),
6505  't3ver_state' => $GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? $row['t3ver_state'] : '',
6506  '_ORIG_pid' => $row['_ORIG_pid']
6507  );
6508  return $out;
6509  }
6510  return null;
6511  }
6512 
6519  public function eventPid($table, $uid, $pid)
6520  {
6521  return $table == 'pages' ? $uid : $pid;
6522  }
6523 
6524  /*********************************************
6525  *
6526  * Storing data to Database Layer
6527  *
6528  ********************************************/
6538  public function updateDB($table, $id, $fieldArray)
6539  {
6540  if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && (int)$id) {
6541  // Do NOT update the UID field, ever!
6542  unset($fieldArray['uid']);
6543  if (!empty($fieldArray)) {
6544  $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6545  // Execute the UPDATE query:
6546  $this->databaseConnection->exec_UPDATEquery($table, 'uid=' . (int)$id, $fieldArray);
6547  // If succeeds, do...:
6548  if (!$this->databaseConnection->sql_error()) {
6549  // Update reference index:
6550  $this->updateRefIndex($table, $id);
6551  if ($this->enableLogging) {
6552  $newRow = array();
6553  if ($this->checkStoredRecords) {
6554  $newRow = $this->checkStoredRecord($table, $id, $fieldArray, 2);
6555  }
6556  // Set log entry:
6557  $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6558  $theLogId = $this->log($table, $id, 2, $propArr['pid'], 0, 'Record \'%s\' (%s) was updated.' . ($propArr['_ORIG_pid'] == -1 ? ' (Offline version).' : ' (Online).'), 10, array($propArr['header'], $table . ':' . $id), $propArr['event_pid']);
6559  // Set History data:
6560  $this->setHistory($table, $id, $theLogId);
6561  }
6562  // Clear cache for relevant pages:
6563  $this->registerRecordIdForPageCacheClearing($table, $id);
6564  // Unset the pageCache for the id if table was page.
6565  if ($table == 'pages') {
6566  unset($this->pageCache[$id]);
6567  }
6568  } elseif ($this->enableLogging) {
6569  $this->log($table, $id, 2, 0, 2, 'SQL error: \'%s\' (%s)', 12, array($this->databaseConnection->sql_error(), $table . ':' . $id));
6570  }
6571  }
6572  }
6573  }
6574 
6587  public function insertDB($table, $id, $fieldArray, $newVersion = false, $suggestedUid = 0, $dontSetNewIdIndex = false)
6588  {
6589  if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && isset($fieldArray['pid'])) {
6590  // Do NOT insert the UID field, ever!
6591  unset($fieldArray['uid']);
6592  if (!empty($fieldArray)) {
6593  // Check for "suggestedUid".
6594  // This feature is used by the import functionality to force a new record to have a certain UID value.
6595  // This is only recommended for use when the destination server is a passive mirror of another server.
6596  // As a security measure this feature is available only for Admin Users (for now)
6597  $suggestedUid = (int)$suggestedUid;
6598  if ($this->BE_USER->isAdmin() && $suggestedUid && $this->suggestedInsertUids[$table . ':' . $suggestedUid]) {
6599  // When the value of ->suggestedInsertUids[...] is "DELETE" it will try to remove the previous record
6600  if ($this->suggestedInsertUids[$table . ':' . $suggestedUid] === 'DELETE') {
6601  // DELETE:
6602  $this->databaseConnection->exec_DELETEquery($table, 'uid=' . (int)$suggestedUid);
6603  }
6604  $fieldArray['uid'] = $suggestedUid;
6605  }
6606  $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6607  // Execute the INSERT query:
6608  $this->databaseConnection->exec_INSERTquery($table, $fieldArray);
6609  // If succees, do...:
6610  if (!$this->databaseConnection->sql_error()) {
6611  // Set mapping for NEW... -> real uid:
6612  // the NEW_id now holds the 'NEW....' -id
6613  $NEW_id = $id;
6614  $id = $this->databaseConnection->sql_insert_id();
6615  if (!$dontSetNewIdIndex) {
6616  $this->substNEWwithIDs[$NEW_id] = $id;
6617  $this->substNEWwithIDs_table[$NEW_id] = $table;
6618  }
6619  $newRow = array();
6620  // Checking the record is properly saved and writing to log
6621  if ($this->enableLogging && $this->checkStoredRecords) {
6622  $newRow = $this->checkStoredRecord($table, $id, $fieldArray, 1);
6623  }
6624  // Update reference index:
6625  $this->updateRefIndex($table, $id);
6626  if ($newVersion) {
6627  if ($this->enableLogging) {
6628  $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6629  $this->log($table, $id, 1, 0, 0, 'New version created of table \'%s\', uid \'%s\'. UID of new version is \'%s\'', 10, array($table, $fieldArray['t3ver_oid'], $id), $propArr['event_pid'], $NEW_id);
6630  }
6631  } else {
6632  if ($this->enableLogging) {
6633  $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6634  $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
6635  $this->log($table, $id, 1, 0, 0, 'Record \'%s\' (%s) was inserted on page \'%s\' (%s)', 10, array($propArr['header'], $table . ':' . $id, $page_propArr['header'], $newRow['pid']), $newRow['pid'], $NEW_id);
6636  }
6637  // Clear cache for relevant pages:
6638  $this->registerRecordIdForPageCacheClearing($table, $id);
6639  }
6640  return $id;
6641  } elseif ($this->enableLogging) {
6642  $this->log($table, $id, 1, 0, 2, 'SQL error: \'%s\' (%s)', 12, array($this->databaseConnection->sql_error(), $table . ':' . $id));
6643  }
6644  }
6645  }
6646  return null;
6647  }
6648 
6659  public function checkStoredRecord($table, $id, $fieldArray, $action)
6660  {
6661  $id = (int)$id;
6662  if (is_array($GLOBALS['TCA'][$table]) && $id) {
6663  $res = $this->databaseConnection->exec_SELECTquery('*', $table, 'uid=' . (int)$id);
6664  if ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
6665  // Traverse array of values that was inserted into the database and compare with the actually stored value:
6666  $errors = array();
6667  foreach ($fieldArray as $key => $value) {
6668  if ($this->checkStoredRecords_loose && !$value && !$row[$key]) {
6669  } elseif ((string)$value !== (string)$row[$key]) {
6670  $errors[] = $key;
6671  }
6672  }
6673  // Set log message if there were fields with unmatching values:
6674  if ($this->enableLogging && !empty($errors)) {
6675  $message = sprintf(
6676  'These fields of record %d in table "%s" have not been saved correctly: %s! The values might have changed due to type casting of the database.',
6677  $id,
6678  $table,
6679  implode(', ', $errors)
6680  );
6681  $this->log($table, $id, $action, 0, 1, $message);
6682  }
6683  // Return selected rows:
6684  return $row;
6685  }
6686  $this->databaseConnection->sql_free_result($res);
6687  }
6688  return null;
6689  }
6690 
6699  public function setHistory($table, $id, $logId)
6700  {
6701  if (isset($this->historyRecords[$table . ':' . $id]) && (int)$logId > 0) {
6702  $fields_values = array();
6703  $fields_values['history_data'] = serialize($this->historyRecords[$table . ':' . $id]);
6704  $fields_values['fieldlist'] = implode(',', array_keys($this->historyRecords[$table . ':' . $id]['newRecord']));
6705  $fields_values['tstamp'] = $GLOBALS['EXEC_TIME'];
6706  $fields_values['tablename'] = $table;
6707  $fields_values['recuid'] = $id;
6708  $fields_values['sys_log_uid'] = $logId;
6709  $this->databaseConnection->exec_INSERTquery('sys_history', $fields_values);
6710  }
6711  }
6712 
6721  public function updateRefIndex($table, $id)
6722  {
6724  $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
6726  $refIndexObj->setWorkspaceId($this->BE_USER->workspace);
6727  }
6728  $refIndexObj->updateRefIndexTable($table, $id);
6729  }
6730 
6731  /*********************************************
6732  *
6733  * Misc functions
6734  *
6735  ********************************************/
6745  public function getSortNumber($table, $uid, $pid)
6746  {
6747  if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
6748  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
6749  // Sorting number is in the top
6750  if ($pid >= 0) {
6751  // Fetches the first record under this pid
6752  $res = $this->databaseConnection->exec_SELECTquery($sortRow . ',pid,uid', $table, 'pid=' . (int)$pid . $this->deleteClause($table), '', $sortRow . ' ASC', '1');
6753  // There was an element
6754  if ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
6755  // The top record was the record it self, so we return its current sortnumber
6756  if ($row['uid'] == $uid) {
6757  return $row[$sortRow];
6758  }
6759  // If the pages sortingnumber < 1 we must resort the records under this pid
6760  if ($row[$sortRow] < 1) {
6761  $this->resorting($table, $pid, $sortRow, 0);
6762  // First sorting number after resorting
6763  return $this->sortIntervals;
6764  } else {
6765  // Sorting number between current top element and zero
6766  return floor($row[$sortRow] / 2);
6767  }
6768  } else {
6769  // No pages, so we choose the default value as sorting-number
6770  // First sorting number if no elements.
6771  return $this->sortIntervals;
6772  }
6773  } else {
6774  // Sorting number is inside the list
6775  // Fetches the record which is supposed to be the prev record
6776  $res = $this->databaseConnection->exec_SELECTquery($sortRow . ',pid,uid', $table, 'uid=' . abs($pid) . $this->deleteClause($table));
6777  // There was a record
6778  if ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
6779  // Look, if the record UID happens to be an offline record. If so, find its live version. Offline uids will be used when a page is versionized as "branch" so this is when we must correct - otherwise a pid of "-1" and a wrong sort-row number is returned which we don't want.
6780  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $row['uid'], $sortRow . ',pid,uid')) {
6781  $row = $lookForLiveVersion;
6782  }
6783  // Fetch move placeholder, since it might point to a new page in the current workspace
6784  if ($movePlaceholder = BackendUtility::getMovePlaceholder($table, $row['uid'], 'uid,pid,' . $sortRow)) {
6785  $row = $movePlaceholder;
6786  }
6787  // If the record should be inserted after itself, keep the current sorting information:
6788  if ($row['uid'] == $uid) {
6789  $sortNumber = $row[$sortRow];
6790  } else {
6791  $subres = $this->databaseConnection->exec_SELECTquery($sortRow . ',pid,uid', $table, 'pid=' . (int)$row['pid'] . ' AND ' . $sortRow . '>=' . (int)$row[$sortRow] . $this->deleteClause($table), '', $sortRow . ' ASC', '2');
6792  // Fetches the next record in order to calculate the in-between sortNumber
6793  // There was a record afterwards
6794  if ($this->databaseConnection->sql_num_rows($subres) == 2) {
6795  // Forward to the second result...
6796  $this->databaseConnection->sql_fetch_assoc($subres);
6797  // There was a record afterwards
6798  $subrow = $this->databaseConnection->sql_fetch_assoc($subres);
6799  // The sortNumber is found in between these values
6800  $sortNumber = $row[$sortRow] + floor(($subrow[$sortRow] - $row[$sortRow]) / 2);
6801  // The sortNumber happened NOT to be between the two surrounding numbers, so we'll have to resort the list
6802  if ($sortNumber <= $row[$sortRow] || $sortNumber >= $subrow[$sortRow]) {
6803  // By this special param, resorting reserves and returns the sortnumber after the uid
6804  $sortNumber = $this->resorting($table, $row['pid'], $sortRow, $row['uid']);
6805  }
6806  } else {
6807  // If after the last record in the list, we just add the sortInterval to the last sortvalue
6808  $sortNumber = $row[$sortRow] + $this->sortIntervals;
6809  }
6810  $this->databaseConnection->sql_free_result($subres);
6811  }
6812  return array('pid' => $row['pid'], 'sortNumber' => $sortNumber);
6813  } else {
6814  if ($this->enableLogging) {
6815  $propArr = $this->getRecordProperties($table, $uid);
6816  // OK, don't insert $propArr['event_pid'] here...
6817  $this->log($table, $uid, 4, 0, 1, 'Attempt to move record \'%s\' (%s) to after a non-existing record (uid=%s)', 1, array($propArr['header'], $table . ':' . $uid, abs($pid)), $propArr['pid']);
6818  }
6819  // There MUST be a page or else this cannot work
6820  return false;
6821  }
6822  }
6823  }
6824  return null;
6825  }
6826 
6839  public function resorting($table, $pid, $sortRow, $return_SortNumber_After_This_Uid)
6840  {
6841  if ($GLOBALS['TCA'][$table] && $sortRow && $GLOBALS['TCA'][$table]['ctrl']['sortby'] == $sortRow) {
6842  $returnVal = 0;
6843  $intervals = $this->sortIntervals;
6844  $i = $intervals * 2;
6845  $res = $this->databaseConnection->exec_SELECTquery('uid', $table, 'pid=' . (int)$pid . $this->deleteClause($table), '', $sortRow . ' ASC');
6846  while ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
6847  $uid = (int)$row['uid'];
6848  if ($uid) {
6849  $this->databaseConnection->exec_UPDATEquery($table, 'uid=' . (int)$uid, array($sortRow => $i));
6850  // This is used to return a sortingValue if the list is resorted because of inserting records inside the list and not in the top
6851  if ($uid == $return_SortNumber_After_This_Uid) {
6852  $i = $i + $intervals;
6853  $returnVal = $i;
6854  }
6855  } else {
6856  die('Fatal ERROR!! No Uid at resorting.');
6857  }
6858  $i = $i + $intervals;
6859  }
6860  $this->databaseConnection->sql_free_result($res);
6861  return $returnVal;
6862  }
6863  return null;
6864  }
6865 
6876  protected function getPreviousLocalizedRecordUid($table, $uid, $pid, $language)
6877  {
6878  $previousLocalizedRecordUid = $uid;
6879  if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
6880  $sortRow = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
6881  $select = $sortRow . ',pid,uid';
6882  // For content elements, we also need the colPos
6883  if ($table === 'tt_content') {
6884  $select .= ',colPos';
6885  }
6886  // Get the sort value of the default language record
6887  $row = BackendUtility::getRecord($table, $uid, $select);
6888  if (is_array($row)) {
6889  // Find the previous record in default language on the same page
6890  $where = 'pid=' . (int)$pid . ' AND ' . 'sys_language_uid=0' . ' AND ' . $sortRow . '<' . (int)$row[$sortRow];
6891  // Respect the colPos for content elements
6892  if ($table === 'tt_content') {
6893  $where .= ' AND colPos=' . (int)$row['colPos'];
6894  }
6895  $res = $this->databaseConnection->exec_SELECTquery($select, $table, $where . $this->deleteClause($table), '', $sortRow . ' DESC', '1');
6896  // If there is an element, find its localized record in specified localization language
6897  if ($previousRow = $this->databaseConnection->sql_fetch_assoc($res)) {
6898  $previousLocalizedRecord = BackendUtility::getRecordLocalization($table, $previousRow['uid'], $language);
6899  if (is_array($previousLocalizedRecord[0])) {
6900  $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
6901  }
6902  }
6903  $this->databaseConnection->sql_free_result($res);
6904  }
6905  }
6906  return $previousLocalizedRecordUid;
6907  }
6908 
6917  public function setTSconfigPermissions($fieldArray, $TSConfig_p)
6918  {
6919  if ((string)$TSConfig_p['userid'] !== '') {
6920  $fieldArray['perms_userid'] = (int)$TSConfig_p['userid'];
6921  }
6922  if ((string)$TSConfig_p['groupid'] !== '') {
6923  $fieldArray['perms_groupid'] = (int)$TSConfig_p['groupid'];
6924  }
6925  if ((string)$TSConfig_p['user'] !== '') {
6926  $fieldArray['perms_user'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['user']) ? $TSConfig_p['user'] : $this->assemblePermissions($TSConfig_p['user']);
6927  }
6928  if ((string)$TSConfig_p['group'] !== '') {
6929  $fieldArray['perms_group'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['group']) ? $TSConfig_p['group'] : $this->assemblePermissions($TSConfig_p['group']);
6930  }
6931  if ((string)$TSConfig_p['everybody'] !== '') {
6932  $fieldArray['perms_everybody'] = MathUtility::canBeInterpretedAsInteger($TSConfig_p['everybody']) ? $TSConfig_p['everybody'] : $this->assemblePermissions($TSConfig_p['everybody']);
6933  }
6934  return $fieldArray;
6935  }
6936 
6944  public function newFieldArray($table)
6945  {
6946  $fieldArray = array();
6947  if (is_array($GLOBALS['TCA'][$table]['columns'])) {
6948  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $content) {
6949  if (isset($this->defaultValues[$table][$field])) {
6950  $fieldArray[$field] = $this->defaultValues[$table][$field];
6951  } elseif (isset($content['config']['default'])) {
6952  $fieldArray[$field] = $content['config']['default'];
6953  }
6954  }
6955  }
6956  // Set default permissions for a page.
6957  if ($table === 'pages') {
6958  $fieldArray['perms_userid'] = $this->userid;
6959  $fieldArray['perms_groupid'] = (int)$this->BE_USER->firstMainGroup;
6960  $fieldArray['perms_user'] = $this->assemblePermissions($this->defaultPermissions['user']);
6961  $fieldArray['perms_group'] = $this->assemblePermissions($this->defaultPermissions['group']);
6962  $fieldArray['perms_everybody'] = $this->assemblePermissions($this->defaultPermissions['everybody']);
6963  }
6964  return $fieldArray;
6965  }
6966 
6974  public function addDefaultPermittedLanguageIfNotSet($table, &$incomingFieldArray)
6975  {
6976  // Checking languages:
6977  if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
6978  if (!isset($incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
6979  // Language field must be found in input row - otherwise it does not make sense.
6980  $rows = array_merge(array(array('uid' => 0)), $this->databaseConnection->exec_SELECTgetRows('uid', 'sys_language', 'pid=0' . BackendUtility::deleteClause('sys_language')), array(array('uid' => -1)));
6981  foreach ($rows as $r) {
6982  if ($this->BE_USER->checkLanguageAccess($r['uid'])) {
6983  $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $r['uid'];
6984  break;
6985  }
6986  }
6987  }
6988  }
6989  }
6990 
6998  public function overrideFieldArray($table, $data)
6999  {
7000  if (is_array($this->overrideValues[$table])) {
7001  $data = array_merge($data, $this->overrideValues[$table]);
7002  }
7003  return $data;
7004  }
7005 
7015  public function compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
7016  {
7017  // Fetch the original record:
7018  $res = $this->databaseConnection->exec_SELECTquery('*', $table, 'uid=' . (int)$id);
7019  $currentRecord = $this->databaseConnection->sql_fetch_assoc($res);
7020  // If the current record exists (which it should...), begin comparison:
7021  if (is_array($currentRecord)) {
7022  // Read all field types:
7023  $c = 0;
7024  $cRecTypes = array();
7025  foreach ($currentRecord as $col => $val) {
7026  $cRecTypes[$col] = $this->databaseConnection->sql_field_type($res, $c);
7027  $c++;
7028  }
7029  // Free result:
7030  $this->databaseConnection->sql_free_result($res);
7031  // Unset the fields which are similar:
7032  foreach ($fieldArray as $col => $val) {
7033  $fieldConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
7034  $isNullField = (!empty($fieldConfiguration['eval']) && GeneralUtility::inList($fieldConfiguration['eval'], 'null'));
7035 
7036  // Unset fields if stored and submitted values are equal - except the current field holds MM relations.
7037  // In general this avoids to store superfluous data which also will be visualized in the editing history.
7038  if (!$fieldConfiguration['MM'] && $this->isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $cRecTypes[$col], $isNullField)) {
7039  unset($fieldArray[$col]);
7040  } else {
7041  if (!isset($this->mmHistoryRecords[($table . ':' . $id)]['oldRecord'][$col])) {
7042  $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
7043  } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
7044  $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
7045  }
7046  if (!isset($this->mmHistoryRecords[($table . ':' . $id)]['newRecord'][$col])) {
7047  $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
7048  } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
7049  $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
7050  }
7051  }
7052  }
7053  } else {
7054  // If the current record does not exist this is an error anyways and we just return an empty array here.
7055  $fieldArray = array();
7056  }
7057  return $fieldArray;
7058  }
7059 
7072  protected function isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull = false)
7073  {
7074  // No NULL values are allowed, this is the regular behaviour.
7075  // Thus, check whether strings are the same or whether integer values are empty ("0" or "").
7076  if (!$allowNull) {
7077  $result = (string)$submittedValue === (string)$storedValue || $storedType === 'int' && (int)$storedValue === (int)$submittedValue;
7078  // Null values are allowed, but currently there's a real (not NULL) value.
7079  // Thus, ensure no NULL value was submitted and fallback to the regular behaviour.
7080  } elseif ($storedValue !== null) {
7081  $result = (
7082  $submittedValue !== null
7083  && $this->isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, false)
7084  );
7085  // Null values are allowed, and currently there's a NULL value.
7086  // Thus, check whether a NULL value was submitted.
7087  } else {
7088  $result = ($submittedValue === null);
7089  }
7090 
7091  return $result;
7092  }
7093 
7101  public function assemblePermissions($string)
7102  {
7103  $keyArr = GeneralUtility::trimExplode(',', $string, true);
7104  $value = 0;
7105  foreach ($keyArr as $key) {
7106  if ($key && isset($this->pMap[$key])) {
7107  $value |= $this->pMap[$key];
7108  }
7109  }
7110  return $value;
7111  }
7112 
7119  public function rmComma($input)
7120  {
7121  return rtrim($input, ',');
7122  }
7123 
7130  public function convNumEntityToByteValue($input)
7131  {
7132  $token = md5(microtime());
7133  $parts = explode($token, preg_replace('/(&#([0-9]+);)/', $token . '\\2' . $token, $input));
7134  foreach ($parts as $k => $v) {
7135  if ($k % 2) {
7136  $v = (int)$v;
7137  // Just to make sure that control bytes are not converted.
7138  if ($v > 32) {
7139  $parts[$k] = chr((int)$v);
7140  }
7141  }
7142  }
7143  return implode('', $parts);
7144  }
7145 
7152  public function destPathFromUploadFolder($folder)
7153  {
7154  return PATH_site . $folder;
7155  }
7156 
7164  public function disableDeleteClause()
7165  {
7166  $this->disableDeleteClause = true;
7167  }
7168 
7175  public function deleteClause($table)
7176  {
7177  // Returns the proper delete-clause if any for a table from TCA
7178  if (!$this->disableDeleteClause && $GLOBALS['TCA'][$table]['ctrl']['delete']) {
7179  return ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0';
7180  } else {
7181  return '';
7182  }
7183  }
7184 
7191  public function getTCEMAIN_TSconfig($tscPID)
7192  {
7193  if (!isset($this->cachedTSconfig[$tscPID])) {
7194  $this->cachedTSconfig[$tscPID] = $this->BE_USER->getTSConfig('TCEMAIN', BackendUtility::getPagesTSconfig($tscPID));
7195  }
7196  return $this->cachedTSconfig[$tscPID]['properties'];
7197  }
7198 
7207  public function getTableEntries($table, $TSconfig)
7208  {
7209  $tA = is_array($TSconfig['table.'][$table . '.']) ? $TSconfig['table.'][$table . '.'] : array();
7210  $dA = is_array($TSconfig['default.']) ? $TSconfig['default.'] : array();
7212  return $dA;
7213  }
7214 
7222  public function getPID($table, $uid)
7223  {
7224  $res_tmp = $this->databaseConnection->exec_SELECTquery('pid', $table, 'uid=' . (int)$uid);
7225  if ($row = $this->databaseConnection->sql_fetch_assoc($res_tmp)) {
7226  return $row['pid'];
7227  }
7228  return false;
7229  }
7230 
7237  public function dbAnalysisStoreExec()
7238  {
7239  foreach ($this->dbAnalysisStore as $action) {
7240  $id = BackendUtility::wsMapId($action[4], MathUtility::canBeInterpretedAsInteger($action[2]) ? $action[2] : $this->substNEWwithIDs[$action[2]]);
7241  if ($id) {
7242  $action[0]->writeMM($action[1], $id, $action[3]);
7243  }
7244  }
7245  }
7246 
7252  public function removeRegisteredFiles()
7253  {
7254  foreach ($this->removeFilesStore as $file) {
7255  if (@is_file($file)) {
7256  $file = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
7257  $file->delete();
7258  }
7259  }
7260  }
7261 
7272  public function int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
7273  {
7274  if ($counter) {
7275  $addW = !$this->admin ? ' AND ' . $this->BE_USER->getPagePermsClause($this->pMap['show']) : '';
7276  $mres = $this->databaseConnection->exec_SELECTquery('uid', 'pages', 'pid=' . (int)$pid . $this->deleteClause('pages') . $addW, '', 'sorting DESC');
7277  while ($row = $this->databaseConnection->sql_fetch_assoc($mres)) {
7278  if ($row['uid'] != $rootID) {
7279  $CPtable[$row['uid']] = $pid;
7280  // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
7281  if ($counter - 1) {
7282  $CPtable = $this->int_pageTreeInfo($CPtable, $row['uid'], $counter - 1, $rootID);
7283  }
7284  }
7285  }
7286  $this->databaseConnection->sql_free_result($mres);
7287  }
7288  return $CPtable;
7289  }
7290 
7296  public function compileAdminTables()
7297  {
7298  return array_keys($GLOBALS['TCA']);
7299  }
7300 
7308  public function fixUniqueInPid($table, $uid)
7309  {
7310  if (empty($GLOBALS['TCA'][$table])) {
7311  return;
7312  }
7313 
7314  $curData = $this->recordInfo($table, $uid, '*');
7315  $newData = array();
7316  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7317  if ($conf['config']['type'] === 'input' && (string)$curData[$field] !== '') {
7318  $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7319  if (in_array('uniqueInPid', $evalCodesArray, true)) {
7320  $newV = $this->getUnique($table, $field, $curData[$field], $uid, $curData['pid']);
7321  if ((string)$newV !== (string)$curData[$field]) {
7322  $newData[$field] = $newV;
7323  }
7324  }
7325  }
7326  }
7327  // IF there are changed fields, then update the database
7328  if (!empty($newData)) {
7329  $this->updateDB($table, $uid, $newData);
7330  }
7331  }
7332 
7344  public function fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData = array())
7345  {
7346  if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields']) {
7347  $prevData = $this->recordInfo($table, $prevUid, '*');
7348  $theFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true);
7349  foreach ($theFields as $field) {
7350  if ($GLOBALS['TCA'][$table]['columns'][$field] && ($update || !isset($newData[$field]))) {
7351  $newData[$field] = $prevData[$field];
7352  }
7353  }
7354  if ($update && !empty($newData)) {
7355  $this->updateDB($table, $uid, $newData);
7356  }
7357  }
7358  return $newData;
7359  }
7360 
7367  public function extFileFields($table)
7368  {
7369  $listArr = array();
7370  if (isset($GLOBALS['TCA'][$table]['columns'])) {
7371  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) {
7372  if ($configArr['config']['type'] == 'group' && ($configArr['config']['internal_type'] == 'file' || $configArr['config']['internal_type'] == 'file_reference')) {
7373  $listArr[] = $field;
7374  }
7375  }
7376  }
7377  return $listArr;
7378  }
7379 
7392  protected function castReferenceValue($value, array $configuration)
7393  {
7394  if ((string)$value !== '') {
7395  return $value;
7396  }
7397 
7398  if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) {
7399  return 0;
7400  }
7401 
7402  if (array_key_exists('default', $configuration)) {
7403  return $configuration['default'];
7404  }
7405 
7406  return $value;
7407  }
7408 
7415  public function isReferenceField($conf)
7416  {
7417  return $conf['type'] == 'group' && $conf['internal_type'] == 'db' || $conf['type'] == 'select' && $conf['foreign_table'];
7418  }
7419 
7427  public function getInlineFieldType($conf)
7428  {
7429  if ($conf['type'] !== 'inline' || !$conf['foreign_table']) {
7430  return false;
7431  }
7432  if ($conf['foreign_field']) {
7433  // The reference to the parent is stored in a pointer field in the child record
7434  return 'field';
7435  } elseif ($conf['MM']) {
7436  // Regular MM intermediate table is used to store data
7437  return 'mm';
7438  } else {
7439  // An item list (separated by comma) is stored (like select type is doing)
7440  return 'list';
7441  }
7442  }
7443 
7455  public function getCopyHeader($table, $pid, $field, $value, $count, $prevTitle = '')
7456  {
7457  // Set title value to check for:
7458  if ($count) {
7459  $checkTitle = $value . rtrim(' ' . sprintf($this->prependLabel($table), $count));
7460  } else {
7461  $checkTitle = $value;
7462  }
7463  // Do check:
7464  if ($prevTitle != $checkTitle || $count < 100) {
7465  $rowCount = $this->databaseConnection->exec_SELECTcountRows('uid', $table, 'pid=' . (int)$pid . ' AND ' . $field . '=' . $this->databaseConnection->fullQuoteStr($checkTitle, $table) . $this->deleteClause($table));
7466  if ($rowCount) {
7467  return $this->getCopyHeader($table, $pid, $field, $value, $count + 1, $checkTitle);
7468  }
7469  }
7470  // Default is to just return the current input title if no other was returned before:
7471  return $checkTitle;
7472  }
7473 
7481  public function prependLabel($table)
7482  {
7483  if (is_object($GLOBALS['LANG'])) {
7484  $label = $GLOBALS['LANG']->sL($GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
7485  } else {
7486  list($label) = explode('|', $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
7487  }
7488  return $label;
7489  }
7490 
7498  public function resolvePid($table, $pid)
7499  {
7500  $pid = (int)$pid;
7501  if ($pid < 0) {
7502  $res = $this->databaseConnection->exec_SELECTquery('pid', $table, 'uid=' . abs($pid));
7503  $row = $this->databaseConnection->sql_fetch_assoc($res);
7504  $this->databaseConnection->sql_free_result($res);
7505  // Look, if the record UID happens to be an offline record. If so, find its live version.
7506  // Offline uids will be used when a page is versionized as "branch" so this is when we
7507  // must correct - otherwise a pid of "-1" and a wrong sort-row number
7508  // is returned which we don't want.
7509  if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, abs($pid), 'pid')) {
7510  $row = $lookForLiveVersion;
7511  }
7512  $pid = (int)$row['pid'];
7513  }
7514  return $pid;
7515  }
7516 
7524  public function clearPrefixFromValue($table, $value)
7525  {
7526  $regex = '/' . sprintf(quotemeta($this->prependLabel($table)), '[0-9]*') . '$/';
7527  return @preg_replace($regex, '', $value);
7528  }
7529 
7539  public function extFileFunctions($table, $field, $filelist, $func)
7540  {
7541  $uploadFolder = $GLOBALS['TCA'][$table]['columns'][$field]['config']['uploadfolder'];
7542  if ($uploadFolder && trim($filelist) && $GLOBALS['TCA'][$table]['columns'][$field]['config']['internal_type'] == 'file') {
7543  $uploadPath = $this->destPathFromUploadFolder($uploadFolder);
7544  $fileArray = explode(',', $filelist);
7545  foreach ($fileArray as $theFile) {
7546  $theFile = trim($theFile);
7547  if ($theFile) {
7548  switch ($func) {
7549  case 'deleteAll':
7550  $theFileFullPath = $uploadPath . '/' . $theFile;
7551  if (@is_file($theFileFullPath)) {
7552  $file = $this->getResourceFactory()->retrieveFileOrFolderObject($theFileFullPath);
7553  $file->delete();
7554  } elseif ($this->enableLogging) {
7555  $this->log($table, 0, 3, 0, 100, 'Delete: Referenced file that was supposed to be deleted together with it\'s record didn\'t exist');
7556  }
7557  break;
7558  }
7559  }
7560  }
7561  }
7562  }
7563 
7570  public function noRecordsFromUnallowedTables($inList)
7571  {
7572  $inList = trim($this->rmComma(trim($inList)));
7573  if ($inList && !$this->admin) {
7574  foreach ($GLOBALS['TCA'] as $table => $_) {
7575  $count = $this->databaseConnection->exec_SELECTcountRows('uid', $table, 'pid IN (' . $inList . ')' . BackendUtility::deleteClause($table));
7576  if ($count && ($this->tableReadOnly($table) || !$this->checkModifyAccessList($table))) {
7577  return false;
7578  }
7579  }
7580  }
7581  return true;
7582  }
7583 
7591  public function isRecordCopied($table, $uid)
7592  {
7593  // If the record was copied:
7594  if (isset($this->copyMappingArray[$table][$uid])) {
7595  return true;
7596  } elseif (isset($this->copyMappingArray[$table]) && in_array($uid, array_values($this->copyMappingArray[$table]))) {
7597  return true;
7598  }
7599  return false;
7600  }
7601 
7602  /******************************
7603  *
7604  * Clearing cache
7605  *
7606  ******************************/
7607 
7618  public function registerRecordIdForPageCacheClearing($table, $uid)
7619  {
7620  if (!is_array(static::$recordsToClearCacheFor[$table])) {
7621  static::$recordsToClearCacheFor[$table] = array();
7622  }
7623  static::$recordsToClearCacheFor[$table][] = (int)$uid;
7624  }
7625 
7630  protected function processClearCacheQueue()
7631  {
7632  $tagsToClear = array();
7633  $clearCacheCommands = array();
7634  foreach (static::$recordsToClearCacheFor as $table => $uids) {
7635  foreach (array_unique($uids) as $uid) {
7636  $pageUid = 0;
7637  if (!isset($GLOBALS['TCA'][$table]) || $uid <= 0) {
7638  return;
7639  }
7640  // Get Page TSconfig relevant:
7641  list($tscPID) = BackendUtility::getTSCpid($table, $uid, '');
7642  $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
7643  if (empty($TSConfig['clearCache_disable'])) {
7644  // If table is "pages":
7645  $pageIdsThatNeedCacheFlush = array();
7646  if ($table === 'pages' || $table === 'pages_language_overlay') {
7647  if ($table === 'pages_language_overlay') {
7648  $pageUid = $this->getPID($table, $uid);
7649  } else {
7650  $pageUid = $uid;
7651  }
7652  // Builds list of pages on the SAME level as this page (siblings)
7653  $res_tmp = $this->databaseConnection->exec_SELECTquery('A.pid AS pid, B.uid AS uid', 'pages A, pages B', 'A.uid=' . (int)$pageUid . ' AND B.pid=A.pid AND B.deleted=0');
7654  $pid_tmp = 0;
7655  while ($row_tmp = $this->databaseConnection->sql_fetch_assoc($res_tmp)) {
7656  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
7657  $pid_tmp = $row_tmp['pid'];
7658  // Add children as well:
7659  if ($TSConfig['clearCache_pageSiblingChildren']) {
7660  $res_tmp2 = $this->databaseConnection->exec_SELECTquery('uid', 'pages', 'pid=' . (int)$row_tmp['uid'] . ' AND deleted=0');
7661  while ($row_tmp2 = $this->databaseConnection->sql_fetch_assoc($res_tmp2)) {
7662  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
7663  }
7664  $this->databaseConnection->sql_free_result($res_tmp2);
7665  }
7666  }
7667  $this->databaseConnection->sql_free_result($res_tmp);
7668  // Finally, add the parent page as well:
7669  $pageIdsThatNeedCacheFlush[] = (int)$pid_tmp;
7670  // Add grand-parent as well:
7671  if ($TSConfig['clearCache_pageGrandParent']) {
7672  $res_tmp = $this->databaseConnection->exec_SELECTquery('pid', 'pages', 'uid=' . (int)$pid_tmp);
7673  if ($row_tmp = $this->databaseConnection->sql_fetch_assoc($res_tmp)) {
7674  $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
7675  }
7676  $this->databaseConnection->sql_free_result($res_tmp);
7677  }
7678  } else {
7679  // For other tables than "pages", delete cache for the records "parent page".
7680  $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
7681  }
7682  // Call pre-processing function for clearing of cache for page ids:
7683  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'])) {
7684  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] as $funcName) {
7685  $_params = array('pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()');
7686  // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
7687  GeneralUtility::callUserFunction($funcName, $_params, $this);
7688  }
7689  }
7690  // Delete cache for selected pages:
7691  foreach ($pageIdsThatNeedCacheFlush as $pageId) {
7692  // Workspaces always use "-1" as the page id which do not
7693  // point to real pages and caches at all. Flushing caches for
7694  // those records does not make sense and decreases performance
7695  if ($pageId >= 0) {
7696  $tagsToClear['pageId_' . $pageId] = true;
7697  }
7698  }
7699  // Queue delete cache for current table and record
7700  $tagsToClear[$table] = true;
7701  $tagsToClear[$table . '_' . $uid] = true;
7702  }
7703  // Clear cache for pages entered in TSconfig:
7704  if (!empty($TSConfig['clearCacheCmd'])) {
7705  $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
7706  $clearCacheCommands = array_unique($commands);
7707  unset($commands);
7708  }
7709  // Call post processing function for clear-cache:
7710  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'])) {
7711  $_params = array('table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig);
7712  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] as $_funcRef) {
7713  GeneralUtility::callUserFunction($_funcRef, $_params, $this);
7714  }
7715  }
7716  }
7717  }
7718 
7720  $cacheManager = $this->getCacheManager();
7721  foreach ($tagsToClear as $tag => $_) {
7722  $cacheManager->flushCachesInGroupByTag('pages', $tag);
7723  }
7724 
7725  // Execute collected clear cache commands from page TSConfig
7726  foreach ($clearCacheCommands as $command) {
7727  $this->clear_cacheCmd($command);
7728  }
7729 
7730  // Reset the cache clearing array
7731  static::$recordsToClearCacheFor = array();
7732  }
7733 
7773  public function clear_cacheCmd($cacheCmd)
7774  {
7775  if (is_object($this->BE_USER)) {
7776  $this->BE_USER->writelog(3, 1, 0, 0, 'User %s has cleared the cache (cacheCmd=%s)', array($this->BE_USER->user['username'], $cacheCmd));
7777  }
7778  // Clear cache for either ALL pages or ALL tables!
7779  switch (strtolower($cacheCmd)) {
7780  case 'pages':
7781  if ($this->admin || $this->BE_USER->getTSConfigVal('options.clearCache.pages')) {
7782  $this->getCacheManager()->flushCachesInGroup('pages');
7783  }
7784  break;
7785  case 'all':
7786  if ($this->admin || $this->BE_USER->getTSConfigVal('options.clearCache.all')) {
7787  // Clear cache group "all" of caching framework caches
7788  $this->getCacheManager()->flushCachesInGroup('all');
7789  $this->databaseConnection->exec_TRUNCATEquery('cache_treelist');
7790  }
7791 
7792  break;
7793  case 'temp_cached':
7794  case 'system':
7795  if ($this->admin || $this->BE_USER->getTSConfigVal('options.clearCache.system')
7796  || ((bool)$GLOBALS['TYPO3_CONF_VARS']['SYS']['clearCacheSystem'] === true && $this->admin)) {
7797  $this->getCacheManager()->flushCachesInGroup('system');
7798  }
7799  break;
7800  }
7801 
7802  $tagsToFlush = array();
7803  // Clear cache for a page ID!
7804  if (MathUtility::canBeInterpretedAsInteger($cacheCmd)) {
7805  $list_cache = array($cacheCmd);
7806  // Call pre-processing function for clearing of cache for page ids:
7807  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'])) {
7808  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] as $funcName) {
7809  $_params = array('pageIdArray' => &$list_cache, 'cacheCmd' => $cacheCmd, 'functionID' => 'clear_cacheCmd()');
7810  // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
7811  GeneralUtility::callUserFunction($funcName, $_params, $this);
7812  }
7813  }
7814  // Delete cache for selected pages:
7815  if (is_array($list_cache)) {
7816  foreach ($list_cache as $pageId) {
7817  $tagsToFlush[] = 'pageId_' . (int)$pageId;
7818  }
7819  }
7820  }
7821  // flush cache by tag
7822  if (GeneralUtility::isFirstPartOfStr(strtolower($cacheCmd), 'cachetag:')) {
7823  $cacheTag = substr($cacheCmd, 9);
7824  $tagsToFlush[] = $cacheTag;
7825  }
7826  // process caching framwork operations
7827  if (!empty($tagsToFlush)) {
7828  foreach (array_unique($tagsToFlush) as $tag) {
7829  $this->getCacheManager()->flushCachesInGroupByTag('pages', $tag);
7830  }
7831  }
7832 
7833  // Call post processing function for clear-cache:
7834  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'])) {
7835  $_params = array('cacheCmd' => strtolower($cacheCmd));
7836  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] as $_funcRef) {
7837  GeneralUtility::callUserFunction($_funcRef, $_params, $this);
7838  }
7839  }
7840  }
7841 
7842  /*****************************
7843  *
7844  * Logging
7845  *
7846  *****************************/
7862  public function log($table, $recuid, $action, $recpid, $error, $details, $details_nr = -1, $data = array(), $event_pid = -1, $NEWid = '')
7863  {
7864  if (!$this->enableLogging) {
7865  return 0;
7866  }
7867  // Type value for tce_db.php
7868  $type = 1;
7869  if (!$this->storeLogMessages) {
7870  $details = '';
7871  }
7872  if ($error > 0) {
7873  $detailMessage = $details;
7874  if (is_array($data)) {
7875  $detailMessage = vsprintf($details, $data);
7876  }
7877  $this->errorLog[] = '[' . $type . '.' . $action . '.' . $details_nr . ']: ' . $detailMessage;
7878  }
7879  return $this->BE_USER->writelog($type, $action, $error, $details_nr, $details, $data, $table, $recuid, $recpid, $event_pid, $NEWid);
7880  }
7881 
7890  public function newlog($message, $error = 0)
7891  {
7892  return $this->log('', 0, 0, 0, $error, '[newlog()] ' . $message, -1);
7893  }
7894 
7906  public function newlog2($message, $table, $uid, $pid = null, $error = 0)
7907  {
7908  if ($pid === false) {
7909  GeneralUtility::deprecationLog('Setting the $pid parameter of DataHandler::newlog2 to FALSE is deprecated since TYPO3 CMS 7. Either provide an integer or NULL. FALSE will not be supported any more in TYPO3 CMS 8');
7910  $pid = null;
7911  }
7912  if (is_null($pid)) {
7913  $propArr = $this->getRecordProperties($table, $uid);
7914  $pid = $propArr['pid'];
7915  }
7916  return $this->log($table, $uid, 0, 0, $error, $message, -1, array(), $this->eventPid($table, $uid, $pid));
7917  }
7918 
7925  public function printLogErrorMessages($redirect)
7926  {
7927  $res_log = $this->databaseConnection->exec_SELECTquery('*', 'sys_log', 'type=1 AND action<256 AND userid=' . (int)$this->BE_USER->user['uid'] . ' AND tstamp=' . (int)$GLOBALS['EXEC_TIME'] . ' AND error<>0');
7928  while ($row = $this->databaseConnection->sql_fetch_assoc($res_log)) {
7929  $log_data = unserialize($row['log_data']);
7930  $msg = $row['error'] . ': ' . sprintf($row['details'], $log_data[0], $log_data[1], $log_data[2], $log_data[3], $log_data[4]);
7932  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, htmlspecialchars($msg), '', FlashMessage::ERROR, true);
7934  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
7935  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
7936  $defaultFlashMessageQueue->enqueue($flashMessage);
7937  }
7938  $this->databaseConnection->sql_free_result($res_log);
7939  }
7940 
7941  /*****************************
7942  *
7943  * Internal (do not use outside Core!)
7944  *
7945  *****************************/
7946 
7956  public function insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
7957  {
7958  $result = $fieldArray;
7959  foreach ($fieldArray as $field => $value) {
7960  switch ($GLOBALS['TCA'][$table]['columns'][$field]['config']['type']) {
7961  case 'inline':
7962  if ($GLOBALS['TCA'][$table]['columns'][$field]['config']['foreign_field']) {
7964  $result[$field] = count(GeneralUtility::trimExplode(',', $value, true));
7965  }
7966  }
7967  break;
7968  }
7969  }
7970  return $result;
7971  }
7972 
7980  public function getAutoVersionId($table, $id)
7981  {
7982  $result = null;
7983  if (isset($this->autoVersionIdMap[$table][$id])) {
7984  $result = $this->autoVersionIdMap[$table][$id];
7985  }
7986  return $result;
7987  }
7988 
7996  protected function overlayAutoVersionId($table, $id)
7997  {
7998  $autoVersionId = $this->getAutoVersionId($table, $id);
7999  if (is_null($autoVersionId) === false) {
8000  $id = $autoVersionId;
8001  }
8002  return $id;
8003  }
8004 
8011  protected function addNewValuesToRemapStackChildIds(array $idValues)
8012  {
8013  foreach ($idValues as $idValue) {
8014  if (strpos($idValue, 'NEW') === 0) {
8015  $this->remapStackChildIds[$idValue] = true;
8016  }
8017  }
8018  }
8019 
8027  protected function getOuterMostInstance()
8028  {
8029  if (!isset($this->outerMostInstance)) {
8030  $stack = array_reverse(debug_backtrace());
8031  foreach ($stack as $stackItem) {
8032  if (isset($stackItem['object']) && $stackItem['object'] instanceof DataHandler) {
8033  $this->outerMostInstance = $stackItem['object'];
8034  break;
8035  }
8036  }
8037  }
8038  return $this->outerMostInstance;
8039  }
8040 
8048  public function isOuterMostInstance()
8049  {
8050  return $this->getOuterMostInstance() === $this;
8051  }
8052 
8058  protected function getRuntimeCache()
8059  {
8060  return $this->getCacheManager()->getCache('cache_runtime');
8061  }
8062 
8071  protected function isNestedElementCallRegistered($table, $id, $identifier)
8072  {
8073  $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8074  return isset($nestedElementCalls[$identifier][$table][$id]);
8075  }
8076 
8086  protected function registerNestedElementCall($table, $id, $identifier)
8087  {
8088  $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8089  $nestedElementCalls[$identifier][$table][$id] = true;
8090  $this->runtimeCache->set($this->cachePrefixNestedElementCalls, $nestedElementCalls);
8091  }
8092 
8098  protected function resetNestedElementCalls()
8099  {
8100  $this->runtimeCache->remove($this->cachePrefixNestedElementCalls);
8101  }
8102 
8114  protected function isElementToBeDeleted($table, $id)
8115  {
8116  $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8117  return isset($elementsToBeDeleted[$table][$id]);
8118  }
8119 
8126  protected function registerElementsToBeDeleted()
8127  {
8128  $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8129  $this->runtimeCache->set('core-datahandler-elementsToBeDeleted', array_merge($elementsToBeDeleted, $this->getCommandMapElements('delete')));
8130  }
8131 
8138  protected function resetElementsToBeDeleted()
8139  {
8140  $this->runtimeCache->remove('core-datahandler-elementsToBeDeleted');
8141  }
8142 
8150  protected function unsetElementsToBeDeleted(array $elements)
8151  {
8152  $elements = ArrayUtility::arrayDiffAssocRecursive($elements, $this->getCommandMapElements('delete'));
8153  foreach ($elements as $key => $value) {
8154  if (empty($value)) {
8155  unset($elements[$key]);
8156  }
8157  }
8158  return $elements;
8159  }
8160 
8167  protected function getCommandMapElements($needle)
8168  {
8169  $elements = array();
8170  foreach ($this->cmdmap as $tableName => $idArray) {
8171  foreach ($idArray as $id => $commandArray) {
8172  foreach ($commandArray as $command => $value) {
8173  if ($value && $command == $needle) {
8174  $elements[$tableName][$id] = true;
8175  }
8176  }
8177  }
8178  }
8179  return $elements;
8180  }
8181 
8188  protected function controlActiveElements()
8189  {
8190  if (!empty($this->control['active'])) {
8191  $this->setNullValues(
8192  $this->control['active'],
8193  $this->datamap
8194  );
8195  }
8196  }
8197 
8207  protected function setNullValues(array $active, array &$haystack)
8208  {
8209  foreach ($active as $key => $value) {
8210  // Nested data is processes recursively
8211  if (is_array($value)) {
8212  $this->setNullValues(
8213  $value,
8214  $haystack[$key]
8215  );
8216  // Field has not been activated in the user interface,
8217  // thus a NULL value shall be stored in the database
8218  } elseif ($value == 0) {
8219  $haystack[$key] = null;
8220  }
8221  }
8222  }
8223 
8230  protected function getFieldEvalCacheIdentifier($additionalIdentifier)
8231  {
8232  return 'core-datahandler-eval-' . md5($additionalIdentifier);
8233  }
8234 
8240  protected function createRelationHandlerInstance()
8241  {
8242  return GeneralUtility::makeInstance(RelationHandler::class);
8243  }
8244 
8250  protected function getCacheManager()
8251  {
8252  return GeneralUtility::makeInstance(CacheManager::class);
8253  }
8254 
8260  protected function getResourceFactory()
8261  {
8263  }
8264 }