[BUGFIX] Fix message "Translate to"
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / DataHandling / DataHandler.php
index be55372..f1db6f3 100644 (file)
@@ -339,7 +339,6 @@ class DataHandler
      */
     public $pagetreeNeedsRefresh = false;
 
-
     // *********************
     // Internal Variables, do not touch.
     // *********************
@@ -671,6 +670,14 @@ class DataHandler
     protected static $recordsToClearCacheFor = array();
 
     /**
+     * Internal cache for pids of records which were deleted. It's not possible
+     * to retrieve the parent folder/page at a later stage
+     *
+     * @var array
+     */
+    protected static $recordPidsForDeletedRecords = array();
+
+    /**
      * Database layer. Identical to $GLOBALS['TYPO3_DB']
      *
      * @var DatabaseConnection
@@ -922,7 +929,7 @@ class DataHandler
                 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] as $classData) {
                     $hookObject = GeneralUtility::getUserObj($classData);
                     if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
-                        throw new \UnexpectedValueException('$hookObject must implement interface \\TYPO3\\CMS\\Core\\DataHandling\\DataHandlerCheckModifyAccessListHookInterface', 1251892472);
+                        throw new \UnexpectedValueException($classData . ' must implement interface ' . DataHandlerCheckModifyAccessListHookInterface::class, 1251892472);
                     }
                     $this->checkModifyAccessListHookObjects[] = $hookObject;
                 }
@@ -1168,6 +1175,7 @@ class DataHandler
                                 /** @var $tce DataHandler */
                                 $tce = GeneralUtility::makeInstance(__CLASS__);
                                 $tce->stripslashes_values = false;
+                                $tce->enableLogging = $this->enableLogging;
                                 // Setting up command for creating a new version of the record:
                                 $cmd = array();
                                 $cmd[$table][$id]['version'] = array(
@@ -1548,7 +1556,7 @@ class DataHandler
             $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = serialize($originalLanguage_diffStorage);
         }
         // Checking for RTE-transformations of fields:
-        $types_fieldConfig = BackendUtility::getTCAtypes($table, $currentRecord);
+        $types_fieldConfig = BackendUtility::getTCAtypes($table, $this->checkValue_currentRecord);
         $theTypeString = null;
         if (is_array($types_fieldConfig)) {
             foreach ($types_fieldConfig as $vconf) {
@@ -1558,14 +1566,14 @@ class DataHandler
                 }
 
                 // Look for transformation flag:
-                if ((string)$incomingFieldArray[('_TRANSFORM_' . $vconf['field'])] === 'RTE') {
+                if ((string)$incomingFieldArray['_TRANSFORM_' . $vconf['field']] === 'RTE') {
                     if ($theTypeString === null) {
-                        $theTypeString = BackendUtility::getTCAtypeValue($table, $currentRecord);
+                        $theTypeString = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
                     }
                     $RTEsetup = $this->BE_USER->getTSConfig('RTE', BackendUtility::getPagesTSconfig($tscPID));
                     $thisConfig = BackendUtility::RTEsetup($RTEsetup['properties'], $table, $vconf['field'], $theTypeString);
                     $fieldArray[$vconf['field']] = $this->transformRichtextContentToDatabase(
-                        $fieldArray[$vconf['field']], $table, $vconf['field'], $vconf['spec'], $thisConfig, $currentRecord['pid']
+                        $fieldArray[$vconf['field']], $table, $vconf['field'], $vconf['spec'], $thisConfig, $this->checkValue_currentRecord['pid']
                     );
                 }
             }
@@ -1602,7 +1610,6 @@ class DataHandler
         return $value;
     }
 
-
     /*********************************************
      *
      * Evaluation of input values
@@ -1911,16 +1918,31 @@ class DataHandler
      */
     protected function checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field)
     {
-        $itemC = count($tcaFieldConf['items']);
+        $items = $tcaFieldConf['items'];
+        if ($tcaFieldConf['itemsProcFunc']) {
+            /** @var ItemProcessingService $processingService */
+            $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
+            $items = $processingService->getProcessingItems($table, $realPid, $field,
+                $this->checkValue_currentRecord,
+                $tcaFieldConf, $tcaFieldConf['items']);
+        }
+
+        $itemC = count($items);
         if (!$itemC) {
             $itemC = 1;
         }
         $maxV = pow(2, $itemC) - 1;
         if ($value < 0) {
+            // @todo: throw LogicException here? Negative values for checkbox items do not make sense and indicate a coding error.
             $value = 0;
         }
         if ($value > $maxV) {
-            $value = $maxV;
+            // @todo: This case is pretty ugly: If there is an itemsProcFunc registered, and if it returns a dynamic,
+            // @todo: changing list of items, then it may happen that a value is transformed and vanished checkboxes
+            // @todo: are permanently removed from the value.
+            // @todo: Suggestion: Throw an exception instead? Maybe a specific, catchable exception that generates a
+            // @todo: error message to the user - dynamic item sets via itemProcFunc on check would be a bad idea anyway.
+            $value = $value & $maxV;
         }
         if ($field && $realPid >= 0 && $value > 0 && !empty($tcaFieldConf['eval'])) {
             $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
@@ -2084,31 +2106,30 @@ class DataHandler
         if ($tcaFieldConf['type'] == 'group') {
             switch ($tcaFieldConf['internal_type']) {
                 case 'file_reference':
-
                 case 'file':
                     $valueArray = $this->checkValue_group_select_file($valueArray, $tcaFieldConf, $curValue, $uploadedFiles, $status, $table, $id, $recFID);
                     break;
-                case 'db':
-                    $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'group', $table, $field);
-                    break;
             }
         }
         // For select types which has a foreign table attached:
         $unsetResult = false;
-        if ($tcaFieldConf['type'] == 'select' && $tcaFieldConf['foreign_table']) {
+        if (
+            $tcaFieldConf['type'] === 'group' && $tcaFieldConf['internal_type'] === 'db'
+            || $tcaFieldConf['type'] === 'select' && ($tcaFieldConf['foreign_table'] || isset($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages')
+        ) {
             // check, if there is a NEW... id in the value, that should be substituted later
             if (strpos($value, 'NEW') !== false) {
                 $this->remapStackRecords[$table][$id] = array('remapStackIndex' => count($this->remapStack));
                 $this->addNewValuesToRemapStackChildIds($valueArray);
                 $this->remapStack[] = array(
                     'func' => 'checkValue_group_select_processDBdata',
-                    'args' => array($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field),
+                    'args' => array($valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field),
                     'pos' => array('valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 5),
                     'field' => $field
                 );
                 $unsetResult = true;
             } else {
-                $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
+                $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field);
             }
         }
         if (!$unsetResult) {
@@ -2297,7 +2318,7 @@ class DataHandler
                                             foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processUpload'] as $classRef) {
                                                 $hookObject = GeneralUtility::getUserObj($classRef);
                                                 if (!$hookObject instanceof DataHandlerProcessUploadHookInterface) {
-                                                    throw new \UnexpectedValueException('$hookObject must implement interface TYPO3\\CMS\\Core\\DataHandling\\DataHandlerProcessUploadHookInterface', 1279962349);
+                                                    throw new \UnexpectedValueException($classRef . ' must implement interface ' . DataHandlerProcessUploadHookInterface::class, 1279962349);
                                                 }
                                                 $hookObject->processUpload_postProcessAction($theDestFile, $this);
                                             }
@@ -2804,7 +2825,7 @@ class DataHandler
                 case 'date':
                 case 'datetime':
                     $value = (int)$value;
-                    if ($value > 0 && !$this->dontProcessTransformations) {
+                    if ($value !== 0 && !$this->dontProcessTransformations) {
                         $value -= date('Z', $value);
                     }
                     break;
@@ -2876,7 +2897,9 @@ class DataHandler
                     }
                     break;
                 case 'email':
-                    $this->checkValue_input_ValidateEmail($value, $set);
+                    if ((string)$value !== '') {
+                        $this->checkValue_input_ValidateEmail($value, $set);
+                    }
                     break;
                 default:
                     if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
@@ -2939,7 +2962,13 @@ class DataHandler
      */
     public function checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
     {
-        $tables = $type == 'group' ? $tcaFieldConf['allowed'] : $tcaFieldConf['foreign_table'];
+        if ($type === 'group') {
+            $tables = $tcaFieldConf['allowed'];
+        } elseif (!empty($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages') {
+            $tables = 'sys_language';
+        } else {
+            $tables = $tcaFieldConf['foreign_table'];
+        }
         $prep = $type == 'group' ? $tcaFieldConf['prepend_tname'] : '';
         $newRelations = implode(',', $valueArray);
         /** @var $dbAnalysis RelationHandler */
@@ -3001,17 +3030,18 @@ class DataHandler
      * @param array $dataStructArray Data structure for the form (might be sheets or not). Only values in the data array which has a configuration in the data structure will be processed.
      * @param array $pParams A set of parameters to pass through for the calling of the evaluation functions
      * @param string $callBackFunc Optional call back function, see checkValue_flex_procInData_travDS()  DEPRECATED, use \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools instead for traversal!
+     * @param array $workspaceOptions
      * @return array The modified 'data' part.
      * @see checkValue_flex_procInData_travDS()
      */
-    public function checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructArray, $pParams, $callBackFunc = '')
+    public function checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructArray, $pParams, $callBackFunc = '', array $workspaceOptions = array())
     {
         if (is_array($dataPart)) {
             foreach ($dataPart as $sKey => $sheetDef) {
                 list($dataStruct, $actualSheet) = GeneralUtility::resolveSheetDefInDS($dataStructArray, $sKey);
                 if (is_array($dataStruct) && $actualSheet == $sKey && is_array($sheetDef)) {
                     foreach ($sheetDef as $lKey => $lData) {
-                        $this->checkValue_flex_procInData_travDS($dataPart[$sKey][$lKey], $dataPart_current[$sKey][$lKey], $uploadedFiles[$sKey][$lKey], $dataStruct['ROOT']['el'], $pParams, $callBackFunc, $sKey . '/' . $lKey . '/');
+                        $this->checkValue_flex_procInData_travDS($dataPart[$sKey][$lKey], $dataPart_current[$sKey][$lKey], $uploadedFiles[$sKey][$lKey], $dataStruct['ROOT']['el'], $pParams, $callBackFunc, $sKey . '/' . $lKey . '/', $workspaceOptions);
                     }
                 }
             }
@@ -3030,10 +3060,11 @@ class DataHandler
      * @param array $pParams A set of parameters to pass through for the calling of the evaluation functions / call back function
      * @param string $callBackFunc Call back function, default is checkValue_SW(). If $this->callBackObj is set to an object, the callback function in that object is called instead.
      * @param string $structurePath
+     * @param array $workspaceOptions
      * @return void
      * @see checkValue_flex_procInData()
      */
-    public function checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath)
+    public function checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions = array())
     {
         if (!is_array($DSelements)) {
             return;
@@ -3062,7 +3093,7 @@ class DataHandler
                             continue;
                         }
 
-                        $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/');
+                        $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/', $workspaceOptions);
                         // 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:
                         // The fact that the order changes is not important since order is controlled by a separately submitted index.
                         if (substr($ik, 0, 3) == 'ID-') {
@@ -3079,7 +3110,7 @@ class DataHandler
                     if (!isset($dataValues[$key]['el'])) {
                         $dataValues[$key]['el'] = array();
                     }
-                    $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $uploadedFiles[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/');
+                    $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $uploadedFiles[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/', $workspaceOptions);
                 }
             } else {
                 if (!is_array($dsConf['TCEforms']['config']) || !is_array($dataValues[$key])) {
@@ -3089,9 +3120,9 @@ class DataHandler
                 foreach ($dataValues[$key] as $vKey => $data) {
                     if ($callBackFunc) {
                         if (is_object($this->callBackObj)) {
-                            $res = $this->callBackObj->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/');
+                            $res = $this->callBackObj->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
                         } else {
-                            $res = $this->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/');
+                            $res = $this->{$callBackFunc}($pParams, $dsConf['TCEforms']['config'], $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
                         }
                     } else {
                         // Default
@@ -3131,7 +3162,7 @@ class DataHandler
                         // @deprecated: flexFormXMLincludeDiffBase is only enabled by ext:compatibility6 since TYPO3 CMS 7, vDEFbase can be unset / ignored with TYPO3 CMS 8
                         if ($this->clear_flexFormData_vDEFbase) {
                             $dataValues[$key][$vKey . '.vDEFbase'] = '';
-                        } 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')) {
+                        } 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')) {
                             // 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:
                             if (isset($dataValues[$key]['vDEF'])) {
                                 $diffValue = $dataValues[$key]['vDEF'];
@@ -3388,7 +3419,7 @@ class DataHandler
         }
         if ($this->isRecordCopied($table, $uid)) {
             if (!empty($overrideValues)) {
-                $this->log($table, $uid, 5, 0, 1, 'Repeated attempt to copy record "' . $table . ':' . $uid . '" with override values');
+                $this->log($table, $uid, 1, 0, 1, 'Repeated attempt to copy record "%s:%s" with override values', -1, array($table, $uid));
             }
             return null;
         }
@@ -3396,7 +3427,7 @@ class DataHandler
         // This checks if the record can be selected which is all that a copy action requires.
         if (!$this->doesRecordExist($table, $uid, 'show')) {
             if ($this->enableLogging) {
-                $this->log($table, $uid, 3, 0, 1, 'Attempt to copy record without permission');
+                $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record "%s:%s" without permission', -1, array($table, $uid));
             }
             return null;
         }
@@ -3404,7 +3435,7 @@ class DataHandler
         // Check if table is allowed on destination page
         if ($destPid >= 0 && !$this->isTableAllowedForThisPage($destPid, $table)) {
             if ($this->enableLogging) {
-                $this->log($table, $uid, 3, 0, 1, 'Attempt to insert record on a page that can\'t store record type.');
+                $this->log($table, $uid, 1, 0, 1, 'Attempt to insert record "%s:%s" on a page (%s) that can\'t store record type.', -1, array($table, $uid, $destPid));
             }
             return null;
         }
@@ -3413,7 +3444,7 @@ class DataHandler
         //Used to check language and general editing rights
         if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded)) {
             if ($this->enableLogging) {
-                $this->log($table, $uid, 3, 0, 1, 'Attempt to copy record without having permissions to do so. [' . $this->BE_USER->errorMsg . '].');
+                $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record "%s:%s" without having permissions to do so. [' . $this->BE_USER->errorMsg . '].', -1, array($table, $uid));
             }
             return null;
         }
@@ -3424,7 +3455,7 @@ class DataHandler
         $row = BackendUtility::getRecordWSOL($table, $uid);
         if (!is_array($row)) {
             if ($this->enableLogging) {
-                $this->log($table, $uid, 3, 0, 1, 'Attempt to copy record that did not exist!');
+                $this->log($table, $uid, 1, 0, 1, 'Attempt to copy record that did not exist!');
             }
             return null;
         }
@@ -3593,24 +3624,48 @@ class DataHandler
                         $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
                         $fields .= ',' . $languageField . ',' . $transOrigPointerField;
                     }
+                    if (!BackendUtility::isTableWorkspaceEnabled($table)) {
+                        $workspaceStatement = '';
+                    } elseif ((int)$this->BE_USER->workspace === 0) {
+                        $workspaceStatement = ' AND t3ver_wsid=0';
+                    } else {
+                        $workspaceStatement = ' AND t3ver_wsid IN (0,' . (int)$this->BE_USER->workspace . ')';
+                    }
+                    // Fetch records
                     $rows = $this->databaseConnection->exec_SELECTgetRows(
                         $fields,
                         $table,
-                        'pid=' . (int)$uid . $this->deleteClause($table),
+                        'pid=' . (int)$uid . $this->deleteClause($table) . $workspaceStatement,
                         '',
                         (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby']) ? $GLOBALS['TCA'][$table]['ctrl']['sortby'] . ' DESC' : ''),
                         '',
                         'uid'
                     );
-                    foreach ($rows as $row) {
-                        // Skip localized records that will be processed in
-                        // copyL10nOverlayRecords() on copying the default language record
-                        $transOrigPointer = $row[$transOrigPointerField];
-                        if ($row[$languageField] > 0 && $transOrigPointer > 0 && isset($rows[$transOrigPointer])) {
-                            continue;
+                    // Resolve placeholders of workspace versions
+                    if (!empty($rows) && (int)$this->BE_USER->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
+                        $rows = array_reverse(
+                            $this->resolveVersionedRecords(
+                                $table,
+                                $fields,
+                                $GLOBALS['TCA'][$table]['ctrl']['sortby'],
+                                array_keys($rows)
+                            ),
+                            true
+                        );
+                    }
+                    if (is_array($rows)) {
+                        foreach ($rows as $row) {
+                            // Skip localized records that will be processed in
+                            // copyL10nOverlayRecords() on copying the default language record
+                            $transOrigPointer = $row[$transOrigPointerField];
+                            if ($row[$languageField] > 0 && $transOrigPointer > 0 && isset($rows[$transOrigPointer])) {
+                                continue;
+                            }
+                            // Copying each of the underlying records...
+                            $this->copyRecord($table, $row['uid'], $theNewRootID);
                         }
-                        // Copying each of the underlying records...
-                        $this->copyRecord($table, $row['uid'], $theNewRootID);
+                    } elseif ($this->enableLogging) {
+                        $this->log($table, $uid, 5, 0, 1, 'An SQL error occurred: ' . $this->databaseConnection->sql_error());
                     }
                 }
             }
@@ -3779,7 +3834,7 @@ class DataHandler
             $currentValueArray = GeneralUtility::xml2array($value);
             // Traversing the XML structure, processing files:
             if (is_array($currentValueArray)) {
-                $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], array(), array(), $dataStructArray, array($table, $uid, $field, $realDestPid), 'copyRecord_flexFormCallBack');
+                $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], array(), array(), $dataStructArray, array($table, $uid, $field, $realDestPid), 'copyRecord_flexFormCallBack', $workspaceOptions);
                 // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
                 $value = $currentValueArray;
             }
@@ -3938,12 +3993,14 @@ class DataHandler
      * @param array $pParams Array of parameters in num-indexes: table, uid, field
      * @param array $dsConf TCA field configuration (from Data Structure XML)
      * @param string $dataValue The value of the flexForm field
-     * @param string $dataValue_ext1 Not used.
-     * @param string $dataValue_ext2 Not used.
+     * @param string $_1 Not used.
+     * @param string $_2 Not used.
+     * @param string $_3 Not used.
+     * @param array $workspaceOptions
      * @return array Result array with key "value" containing the value of the processing.
      * @see copyRecord(), checkValue_flex_procInData_travDS()
      */
-    public function copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
+    public function copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $_3, $workspaceOptions)
     {
         // Extract parameters:
         list($table, $uid, $field, $realDestPid) = $pParams;
@@ -3951,7 +4008,7 @@ class DataHandler
         $dataValue = $this->copyRecord_procFilesRefs($dsConf, $uid, $dataValue);
         // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
         if (($this->isReferenceField($dsConf) || $this->getInlineFieldType($dsConf) !== false) && (string)$dataValue !== '') {
-            $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, array(), $dsConf, $realDestPid);
+            $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, array(), $dsConf, $realDestPid, 0, $workspaceOptions);
             $this->registerDBList[$table][$uid][$field] = 'FlexForm_reference';
         }
         // Return
@@ -4272,7 +4329,8 @@ class DataHandler
         if ($destPid >= 0) {
             if ($table != 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
                 // Clear cache before moving
-                $this->registerRecordIdForPageCacheClearing($table, $uid);
+                list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
+                $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
                 // Setting PID
                 $updateFields['pid'] = $destPid;
                 // Table is sorted by 'sortby'
@@ -4575,6 +4633,7 @@ class DataHandler
         }
         // Set exclude Fields:
         foreach ($GLOBALS['TCA'][$Ttable]['columns'] as $fN => $fCfg) {
+            $translateToMsg = '';
             // Check if we are just prefixing:
             if ($fCfg['l10n_mode'] == 'prefixLangTitle') {
                 if (($fCfg['config']['type'] == 'text' || $fCfg['config']['type'] == 'input') && (string)$row[$fN] !== '') {
@@ -4625,37 +4684,76 @@ class DataHandler
             }
         }
 
-
         return $newId;
     }
 
     /**
      * Performs localization or synchronization of child records.
+     * The $command argument expects an array, but supports a string for backward-compatibility.
+     *
+     * $command = array(
+     *   'field' => 'tx_myfieldname',
+     *   'language' => 2,
+     *   // either the key 'action' or 'ids' must be set
+     *   'action' => 'synchronize', // or 'localize'
+     *   'ids' => array(1, 2, 3, 4) // child element ids
+     * );
      *
      * @param string $table The table of the localized parent record
      * @param int $id The uid of the localized parent record
-     * @param string $command Defines the type 'localize' or 'synchronize' (string) or a single uid to be localized (int)
+     * @param array|string $command Defines the command to be performed (see example above)
      * @return void
      */
     protected function inlineLocalizeSynchronize($table, $id, $command)
     {
-        // <field>, (localize | synchronize | <uid>):
-        $parts = GeneralUtility::trimExplode(',', $command);
-        $field = $parts[0];
-        $type = $parts[1];
-        if (!$field || (($type !== 'localize' && $type !== 'synchronize') && !MathUtility::canBeInterpretedAsInteger($type)) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
+        $parentRecord = BackendUtility::getRecordWSOL($table, $id);
+
+        // Backward-compatibility handling
+        if (!is_array($command)) {
+            // <field>, (localize | synchronize | <uid>):
+            $parts = GeneralUtility::trimExplode(',', $command);
+            $command = array();
+            $command['field'] = $parts[0];
+            // The previous process expected $id to point to the localized record already
+            $command['language'] = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
+
+            if (!MathUtility::canBeInterpretedAsInteger($parts[1])) {
+                $command['action'] = $parts[1];
+            } else {
+                $command['ids'] = array($parts[1]);
+            }
+        }
+
+        // In case the parent record is the default language record, fetch the localization
+        if (empty($parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
+            // Fetch the live record
+            $parentRecordLocalization = BackendUtility::getRecordLocalization($table, $id, $command['language'], 'AND pid<>-1');
+            if (empty($parentRecordLocalization)) {
+                $this->newlog2('Localization for parent record ' . $table . ':' . $id . '" cannot be fetched', $table, $id, $parentRecord['pid']);
+                return;
+            }
+            $parentRecord = $parentRecordLocalization[0];
+            $id = $parentRecord['uid'];
+            // Process overlay for current selected workspace
+            BackendUtility::workspaceOL($table, $parentRecord);
+        }
+
+        $field = $command['field'];
+        $language = $command['language'];
+        $action = $command['action'];
+        $ids = $command['ids'];
+
+        if (!$field || !($action === 'localize' || $action === 'synchronize') && empty($ids) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
             return;
         }
 
         $config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
         $foreignTable = $config['foreign_table'];
         $localizationMode = BackendUtility::getInlineLocalizationMode($table, $config);
-        if ($localizationMode != 'select') {
+        if ($localizationMode !== 'select') {
             return;
         }
 
-        $parentRecord = BackendUtility::getRecordWSOL($table, $id);
-        $language = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
         $transOrigPointer = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
         $transOrigTable = BackendUtility::getOriginalTranslationTable($table);
         $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
@@ -4687,7 +4785,7 @@ class DataHandler
         $dbAnalysisCurrent = $this->createRelationHandlerInstance();
         $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config);
         // Perform synchronization: Possibly removal of already localized records:
-        if ($type == 'synchronize') {
+        if ($action === 'synchronize') {
             foreach ($dbAnalysisCurrent->itemArray as $index => $item) {
                 $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']);
                 if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) {
@@ -4701,17 +4799,22 @@ class DataHandler
             }
         }
         // Perform synchronization/localization: Possibly add unlocalized records for original language:
-        if (MathUtility::canBeInterpretedAsInteger($type) && isset($elementsOriginal[$type])) {
-            $item = $elementsOriginal[$type];
-            $item['id'] = $this->localize($item['table'], $item['id'], $language);
-            $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
-            $dbAnalysisCurrent->itemArray[] = $item;
-        } elseif ($type === 'localize' || $type === 'synchronize') {
+        if ($action === 'localize' || $action === 'synchronize') {
             foreach ($elementsOriginal as $originalId => $item) {
                 $item['id'] = $this->localize($item['table'], $item['id'], $language);
                 $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
                 $dbAnalysisCurrent->itemArray[] = $item;
             }
+        } elseif (!empty($ids)) {
+            foreach ($ids as $childId) {
+                if (!MathUtility::canBeInterpretedAsInteger($childId) || !isset($elementsOriginal[$childId])) {
+                    continue;
+                }
+                $item = $elementsOriginal[$childId];
+                $item['id'] = $this->localize($item['table'], $item['id'], $language);
+                $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
+                $dbAnalysisCurrent->itemArray[] = $item;
+            }
         }
         // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
         $value = implode(',', $dbAnalysisCurrent->getValueArray());
@@ -4721,6 +4824,7 @@ class DataHandler
             /** @var DataHandler $tce */
             $tce = GeneralUtility::makeInstance(__CLASS__);
             $tce->stripslashes_values = false;
+            $tce->enableLogging = $this->enableLogging;
             $tce->start(array(), $removeArray);
             $tce->process_cmdmap();
             unset($tce);
@@ -4818,7 +4922,9 @@ class DataHandler
                     $versionState = VersionState::cast($verRec['t3ver_state']);
                     if ($versionState->equals(VersionState::MOVE_POINTER)) {
                         $versionMovePlaceholder = BackendUtility::getMovePlaceholder($table, $uid, 'uid', $verRec['t3ver_wsid']);
-                        $this->deleteEl($table, $versionMovePlaceholder['uid'], true, $forceHardDelete);
+                        if (!empty($versionMovePlaceholder)) {
+                            $this->deleteEl($table, $versionMovePlaceholder['uid'], true, $forceHardDelete);
+                        }
                     }
                 }
             }
@@ -4876,7 +4982,8 @@ class DataHandler
         }
 
         // Clear cache before deleting the record, else the correct page cannot be identified by clear_cache
-        $this->registerRecordIdForPageCacheClearing($table, $uid);
+        list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
+        $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
         $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
         if ($deleteField && !$forceHardDelete) {
             $updateFields = array(
@@ -5000,6 +5107,11 @@ class DataHandler
      */
     public function deletePages($uid, $force = false, $forceHardDelete = false)
     {
+        $uid = (int)$uid;
+        if ($uid === 0) {
+            $this->newlog2('Deleting all pages starting from the root-page is disabled.', 'pages', 0, 0, 2);
+            return;
+        }
         // Getting list of pages to delete:
         if ($force) {
             // Returns the branch WITHOUT permission checks (0 secures that)
@@ -5015,7 +5127,7 @@ class DataHandler
             }
         } else {
             /** @var FlashMessage $flashMessage */
-            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, htmlspecialchars($res), '', FlashMessage::ERROR, true);
+            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, true);
             /** @var $flashMessageService FlashMessageService */
             $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
             /** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */
@@ -5073,9 +5185,11 @@ class DataHandler
     protected function copyMovedRecordToNewLocation($table, $uid)
     {
         if ($this->BE_USER->workspace > 0) {
-            // Check move placeholder at workspace
+            $originalRecord = BackendUtility::getRecord($table, $uid);
             $movePlaceholder = BackendUtility::getMovePlaceholder($table, $uid);
-            if ($movePlaceholder !== false) {
+            // Check whether target page to copied to is different to current page
+            // Cloning on the same page is superfluous and does not help at all
+            if (!empty($originalRecord) && !empty($movePlaceholder) && (int)$originalRecord['pid'] !== (int)$movePlaceholder['pid']) {
                 // If move placeholder exists, copy to new location
                 // This will create a New placeholder on the new location
                 // and a version for this new placeholder
@@ -5089,6 +5203,7 @@ class DataHandler
                 /** @var DataHandler $dataHandler */
                 $dataHandler = GeneralUtility::makeInstance(__CLASS__);
                 $dataHandler->stripslashes_values = false;
+                $dataHandler->enableLogging = $this->enableLogging;
                 $dataHandler->neverHideAtCopy = true;
                 $dataHandler->start(array(), $command);
                 $dataHandler->process_cmdmap();
@@ -5209,6 +5324,9 @@ class DataHandler
     {
         $conf = $GLOBALS['TCA'][$table]['columns'];
         $row = BackendUtility::getRecord($table, $uid, '*', '', false);
+        if (empty($row)) {
+            return;
+        }
         foreach ($row as $field => $value) {
             $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
         }
@@ -5544,6 +5662,7 @@ class DataHandler
         $copyTCE = GeneralUtility::makeInstance(__CLASS__);
         $copyTCE->stripslashes_values = $stripslashesValues;
         $copyTCE->copyTree = $this->copyTree;
+        $copyTCE->enableLogging = $this->enableLogging;
         // Copy forth the cached TSconfig
         $copyTCE->cachedTSconfig = $this->cachedTSconfig;
         // Transformations should NOT be carried out during copy
@@ -5794,16 +5913,27 @@ class DataHandler
                 }
                 // Replace relations to NEW...-IDs in field value (uids of child records):
                 if (is_array($valueArray)) {
-                    $foreign_table = $tcaFieldConf['foreign_table'];
                     foreach ($valueArray as $key => $value) {
                         if (strpos($value, 'NEW') !== false) {
+                            if (strpos($value, '_') === false) {
+                                $affectedTable = $tcaFieldConf['foreign_table'];
+                                $prependTable = false;
+                            } else {
+                                $parts = explode('_', $value);
+                                $value = array_pop($parts);
+                                $affectedTable = implode('_', $parts);
+                                $prependTable = true;
+                            }
                             $value = $this->substNEWwithIDs[$value];
                             // The record is new, but was also auto-versionized and has another new id:
-                            if (isset($this->autoVersionIdMap[$foreign_table][$value])) {
-                                $value = $this->autoVersionIdMap[$foreign_table][$value];
+                            if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
+                                $value = $this->autoVersionIdMap[$affectedTable][$value];
+                            }
+                            if ($prependTable) {
+                                $value = $affectedTable . '_' . $value;
                             }
                             // Set a hint that this was a new child record:
-                            $this->newRelatedIDs[$foreign_table][] = $value;
+                            $this->newRelatedIDs[$affectedTable][] = $value;
                             $valueArray[$key] = $value;
                         }
                     }
@@ -6043,7 +6173,7 @@ class DataHandler
      */
     public function isRecordInWebMount($table, $id)
     {
-        if (!isset($this->isRecordInWebMount_Cache[($table . ':' . $id)])) {
+        if (!isset($this->isRecordInWebMount_Cache[$table . ':' . $id])) {
             $recP = $this->getRecordProperties($table, $id);
             $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->isInWebMount($recP['event_pid']);
         }
@@ -7038,12 +7168,12 @@ class DataHandler
                 if (!$fieldConfiguration['MM'] && $this->isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $cRecTypes[$col], $isNullField)) {
                     unset($fieldArray[$col]);
                 } else {
-                    if (!isset($this->mmHistoryRecords[($table . ':' . $id)]['oldRecord'][$col])) {
+                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col])) {
                         $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
                     } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
                         $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
                     }
-                    if (!isset($this->mmHistoryRecords[($table . ':' . $id)]['newRecord'][$col])) {
+                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col])) {
                         $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
                     } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
                         $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
@@ -7183,6 +7313,23 @@ class DataHandler
     }
 
     /**
+     * Gets UID of parent record. If record is deleted it will be looked up in
+     * an array built before the record was deleted
+     *
+     * @param string $table Table where record lives/lived
+     * @param int $uid Record UID
+     * @return int[] Parent UIDs
+     */
+    protected function getOriginalParentOfRecord($table, $uid)
+    {
+        if (isset(self::$recordPidsForDeletedRecords[$table][$uid])) {
+            return self::$recordPidsForDeletedRecords[$table][$uid];
+        }
+        list($parentUid) = BackendUtility::getTSCpid($table, $uid, '');
+        return array($parentUid);
+    }
+
+    /**
      * Return TSconfig for a page id
      *
      * @param int $tscPID Page id (PID) from which to get configuration.
@@ -7272,18 +7419,45 @@ class DataHandler
     public function int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
     {
         if ($counter) {
+            if ((int)$this->BE_USER->workspace === 0) {
+                $workspaceStatement = ' AND t3ver_wsid=0';
+            } else {
+                $workspaceStatement = ' AND t3ver_wsid IN (0,' . (int)$this->BE_USER->workspace . ')';
+            }
+
             $addW = !$this->admin ? ' AND ' . $this->BE_USER->getPagePermsClause($this->pMap['show']) : '';
-            $mres = $this->databaseConnection->exec_SELECTquery('uid', 'pages', 'pid=' . (int)$pid . $this->deleteClause('pages') . $addW, '', 'sorting DESC');
-            while ($row = $this->databaseConnection->sql_fetch_assoc($mres)) {
-                if ($row['uid'] != $rootID) {
-                    $CPtable[$row['uid']] = $pid;
+            $pages = $this->databaseConnection->exec_SELECTgetRows(
+                'uid',
+                'pages',
+                'pid=' . (int)$pid . $this->deleteClause('pages') . $workspaceStatement . $addW,
+                '',
+                'sorting DESC',
+                '',
+                'uid'
+            );
+
+            // Resolve placeholders of workspace versions
+            if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
+                $pages = array_reverse(
+                    $this->resolveVersionedRecords(
+                        'pages',
+                        'uid',
+                        'sorting',
+                        array_keys($pages)
+                    ),
+                    true
+                );
+            }
+
+            foreach ($pages as $page) {
+                if ($page['uid'] != $rootID) {
+                    $CPtable[$page['uid']] = $pid;
                     // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
                     if ($counter - 1) {
-                        $CPtable = $this->int_pageTreeInfo($CPtable, $row['uid'], $counter - 1, $rootID);
+                        $CPtable = $this->int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
                     }
                 }
             }
-            $this->databaseConnection->sql_free_result($mres);
         }
         return $CPtable;
     }
@@ -7612,15 +7786,22 @@ class DataHandler
      *
      * @param string $table Table name of record that was just updated.
      * @param int $uid UID of updated / inserted record
+     * @param int $pid REAL PID of page of a deleted/moved record to get TSconfig in ClearCache.
      * @return void
      * @internal This method is not meant to be called directly but only from the core itself or from hooks
      */
-    public function registerRecordIdForPageCacheClearing($table, $uid)
+    public function registerRecordIdForPageCacheClearing($table, $uid, $pid = null)
     {
         if (!is_array(static::$recordsToClearCacheFor[$table])) {
             static::$recordsToClearCacheFor[$table] = array();
         }
         static::$recordsToClearCacheFor[$table][] = (int)$uid;
+        if ($pid !== null) {
+            if (!is_array(static::$recordPidsForDeletedRecords[$table])) {
+                static::$recordPidsForDeletedRecords[$table] = array();
+            }
+            static::$recordPidsForDeletedRecords[$table][$uid][] = (int)$pid;
+        }
     }
 
     /**
@@ -7631,87 +7812,19 @@ class DataHandler
     {
         $tagsToClear = array();
         $clearCacheCommands = array();
+
         foreach (static::$recordsToClearCacheFor as $table => $uids) {
             foreach (array_unique($uids) as $uid) {
-                $pageUid = 0;
                 if (!isset($GLOBALS['TCA'][$table]) || $uid <= 0) {
                     return;
                 }
-                // Get Page TSconfig relevant:
-                list($tscPID) = BackendUtility::getTSCpid($table, $uid, '');
-                $TSConfig = $this->getTCEMAIN_TSconfig($tscPID);
-                if (empty($TSConfig['clearCache_disable'])) {
-                    // If table is "pages":
-                    $pageIdsThatNeedCacheFlush = array();
-                    if ($table === 'pages' || $table === 'pages_language_overlay') {
-                        if ($table === 'pages_language_overlay') {
-                            $pageUid = $this->getPID($table, $uid);
-                        } else {
-                            $pageUid = $uid;
-                        }
-                        // Builds list of pages on the SAME level as this page (siblings)
-                        $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');
-                        $pid_tmp = 0;
-                        while ($row_tmp = $this->databaseConnection->sql_fetch_assoc($res_tmp)) {
-                            $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
-                            $pid_tmp = $row_tmp['pid'];
-                            // Add children as well:
-                            if ($TSConfig['clearCache_pageSiblingChildren']) {
-                                $res_tmp2 = $this->databaseConnection->exec_SELECTquery('uid', 'pages', 'pid=' . (int)$row_tmp['uid'] . ' AND deleted=0');
-                                while ($row_tmp2 = $this->databaseConnection->sql_fetch_assoc($res_tmp2)) {
-                                    $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
-                                }
-                                $this->databaseConnection->sql_free_result($res_tmp2);
-                            }
-                        }
-                        $this->databaseConnection->sql_free_result($res_tmp);
-                        // Finally, add the parent page as well:
-                        $pageIdsThatNeedCacheFlush[] = (int)$pid_tmp;
-                        // Add grand-parent as well:
-                        if ($TSConfig['clearCache_pageGrandParent']) {
-                            $res_tmp = $this->databaseConnection->exec_SELECTquery('pid', 'pages', 'uid=' . (int)$pid_tmp);
-                            if ($row_tmp = $this->databaseConnection->sql_fetch_assoc($res_tmp)) {
-                                $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
-                            }
-                            $this->databaseConnection->sql_free_result($res_tmp);
-                        }
-                    } else {
-                        // For other tables than "pages", delete cache for the records "parent page".
-                        $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
-                    }
-                    // Call pre-processing function for clearing of cache for page ids:
-                    if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'])) {
-                        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] as $funcName) {
-                            $_params = array('pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()');
-                            // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
-                            GeneralUtility::callUserFunction($funcName, $_params, $this);
-                        }
-                    }
-                    // Delete cache for selected pages:
-                    foreach ($pageIdsThatNeedCacheFlush as $pageId) {
-                        // Workspaces always use "-1" as the page id which do not
-                        // point to real pages and caches at all. Flushing caches for
-                        // those records does not make sense and decreases performance
-                        if ($pageId >= 0) {
-                            $tagsToClear['pageId_' . $pageId] = true;
-                        }
-                    }
-                    // Queue delete cache for current table and record
-                    $tagsToClear[$table] = true;
-                    $tagsToClear[$table . '_' . $uid] = true;
-                }
-                // Clear cache for pages entered in TSconfig:
-                if (!empty($TSConfig['clearCacheCmd'])) {
-                    $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
-                    $clearCacheCommands = array_unique($commands);
-                    unset($commands);
-                }
-                // Call post processing function for clear-cache:
-                if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'])) {
-                    $_params = array('table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig);
-                    foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] as $_funcRef) {
-                        GeneralUtility::callUserFunction($_funcRef, $_params, $this);
-                    }
+                // For move commands we may get more then 1 parent.
+                $pageUids = $this->getOriginalParentOfRecord($table, $uid);
+                foreach ($pageUids as $originalParent) {
+                    list($tagsToClearFromPrepare, $clearCacheCommandsFromPrepare)
+                        = $this->prepareCacheFlush($table, $uid, $originalParent);
+                    $tagsToClear = array_merge($tagsToClear, $tagsToClearFromPrepare);
+                    $clearCacheCommands = array_merge($clearCacheCommands, $clearCacheCommandsFromPrepare);
                 }
             }
         }
@@ -7729,6 +7842,103 @@ class DataHandler
 
         // Reset the cache clearing array
         static::$recordsToClearCacheFor = array();
+
+        // Reset the original pid array
+        static::$recordPidsForDeletedRecords = array();
+    }
+
+    /**
+     * Prepare the cache clearing
+     *
+     * @param string $table Table name of record that needs to be cleared
+     * @param int $uid UID of record for which the cache needs to be cleared
+     * @param int $pid Original pid of the page of the record which the cache needs to be cleared
+     * @return array Array with tagsToClear and clearCacheCommands
+     * @internal This function is internal only it may be changed/removed also in minor version numbers.
+     */
+    protected function prepareCacheFlush($table, $uid, $pid)
+    {
+        $tagsToClear = array();
+        $clearCacheCommands = array();
+        $pageUid = 0;
+        // Get Page TSconfig relevant:
+        $TSConfig = $this->getTCEMAIN_TSconfig($pid);
+        if (empty($TSConfig['clearCache_disable'])) {
+            // If table is "pages":
+            $pageIdsThatNeedCacheFlush = array();
+            if ($table === 'pages' || $table === 'pages_language_overlay') {
+                if ($table === 'pages_language_overlay') {
+                    $pageUid = $this->getPID($table, $uid);
+                } else {
+                    $pageUid = $uid;
+                }
+                // Builds list of pages on the SAME level as this page (siblings)
+                $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');
+                $pid_tmp = 0;
+                while ($row_tmp = $this->databaseConnection->sql_fetch_assoc($res_tmp)) {
+                    $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
+                    $pid_tmp = $row_tmp['pid'];
+                    // Add children as well:
+                    if ($TSConfig['clearCache_pageSiblingChildren']) {
+                        $res_tmp2 = $this->databaseConnection->exec_SELECTquery('uid', 'pages', 'pid=' . (int)$row_tmp['uid'] . ' AND deleted=0');
+                        while ($row_tmp2 = $this->databaseConnection->sql_fetch_assoc($res_tmp2)) {
+                            $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
+                        }
+                        $this->databaseConnection->sql_free_result($res_tmp2);
+                    }
+                }
+                $this->databaseConnection->sql_free_result($res_tmp);
+                // Finally, add the parent page as well:
+                $pageIdsThatNeedCacheFlush[] = (int)$pid_tmp;
+                // Add grand-parent as well:
+                if ($TSConfig['clearCache_pageGrandParent']) {
+                    $res_tmp = $this->databaseConnection->exec_SELECTquery('pid', 'pages', 'uid=' . (int)$pid_tmp);
+                    if ($row_tmp = $this->databaseConnection->sql_fetch_assoc($res_tmp)) {
+                        $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
+                    }
+                    $this->databaseConnection->sql_free_result($res_tmp);
+                }
+            } else {
+                // For other tables than "pages", delete cache for the records "parent page".
+                $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
+            }
+            // Call pre-processing function for clearing of cache for page ids:
+            if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'])) {
+                foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] as $funcName) {
+                    $_params = array('pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()');
+                    // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
+                    GeneralUtility::callUserFunction($funcName, $_params, $this);
+                }
+            }
+            // Delete cache for selected pages:
+            foreach ($pageIdsThatNeedCacheFlush as $pageId) {
+                // Workspaces always use "-1" as the page id which do not
+                // point to real pages and caches at all. Flushing caches for
+                // those records does not make sense and decreases performance
+                if ($pageId >= 0) {
+                    $tagsToClear['pageId_' . $pageId] = true;
+                }
+            }
+            // Queue delete cache for current table and record
+            $tagsToClear[$table] = true;
+            $tagsToClear[$table . '_' . $uid] = true;
+        }
+        // Clear cache for pages entered in TSconfig:
+        if (!empty($TSConfig['clearCacheCmd'])) {
+            $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
+            $clearCacheCommands = array_unique($commands);
+        }
+        // Call post processing function for clear-cache:
+        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'])) {
+            $_params = array('table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig);
+            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] as $_funcRef) {
+                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
+            }
+        }
+        return array(
+            $tagsToClear,
+            $clearCacheCommands
+        );
     }
 
     /**
@@ -7929,7 +8139,7 @@ class DataHandler
             $log_data = unserialize($row['log_data']);
             $msg = $row['error'] . ': ' . sprintf($row['details'], $log_data[0], $log_data[1], $log_data[2], $log_data[3], $log_data[4]);
             /** @var FlashMessage $flashMessage */
-            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, htmlspecialchars($msg), '', FlashMessage::ERROR, true);
+            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', FlashMessage::ERROR, true);
             /** @var $flashMessageService FlashMessageService */
             $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
             $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
@@ -8018,6 +8228,40 @@ class DataHandler
     }
 
     /**
+     * Resolves versioned records for the current workspace scope.
+     * Delete placeholders and move placeholders are substituted and removed.
+     *
+     * @param string $tableName Name of the table to be processed
+     * @param string $fieldNames List of the field names to be fetched
+     * @param string $sortingField Name of the sorting field to be used
+     * @param array $liveIds Flat array of (live) record ids
+     * @return array
+     */
+    protected function resolveVersionedRecords($tableName, $fieldNames, $sortingField, array $liveIds)
+    {
+        /** @var PlainDataResolver $resolver */
+        $resolver = GeneralUtility::makeInstance(
+            PlainDataResolver::class,
+            $tableName,
+            $liveIds,
+            $sortingField
+        );
+
+        $resolver->setWorkspaceId($this->BE_USER->workspace);
+        $resolver->setKeepDeletePlaceholder(false);
+        $resolver->setKeepMovePlaceholder(false);
+        $resolver->setKeepLiveIds(true);
+        $recordIds = $resolver->get();
+
+        $records = array();
+        foreach ($recordIds as $recordId) {
+            $records[$recordId] = BackendUtility::getRecord($tableName, $recordId, $fieldNames);
+        }
+
+        return $records;
+    }
+
+    /**
      * Gets the outer most instance of \TYPO3\CMS\Core\DataHandling\DataHandler
      * Since \TYPO3\CMS\Core\DataHandling\DataHandler can create nested objects of itself,
      * this method helps to determine the first (= outer most) one.
@@ -8027,7 +8271,7 @@ class DataHandler
     protected function getOuterMostInstance()
     {
         if (!isset($this->outerMostInstance)) {
-            $stack = array_reverse(debug_backtrace());
+            $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS));
             foreach ($stack as $stackItem) {
                 if (isset($stackItem['object']) && $stackItem['object'] instanceof DataHandler) {
                     $this->outerMostInstance = $stackItem['object'];