Completed
Push — master ( 69442d...0bd76b )
by
unknown
16:42
created

DataHandlerHook::moveRecord()   F

Complexity

Conditions 24
Paths 577

Size

Total Lines 68
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 39
nc 577
nop 8
dl 0
loc 68
rs 0.5875
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Workspaces\Hook;
17
18
use Doctrine\DBAL\DBALException;
19
use Doctrine\DBAL\Platforms\SQLServerPlatform;
20
use TYPO3\CMS\Backend\Utility\BackendUtility;
21
use TYPO3\CMS\Core\Cache\CacheManager;
22
use TYPO3\CMS\Core\Core\Environment;
23
use TYPO3\CMS\Core\Database\Connection;
24
use TYPO3\CMS\Core\Database\ConnectionPool;
25
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
26
use TYPO3\CMS\Core\Database\ReferenceIndex;
27
use TYPO3\CMS\Core\Database\RelationHandler;
28
use TYPO3\CMS\Core\DataHandling\DataHandler;
29
use TYPO3\CMS\Core\DataHandling\PlaceholderShadowColumnsResolver;
30
use TYPO3\CMS\Core\Localization\LanguageService;
31
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
32
use TYPO3\CMS\Core\SysLog\Action\Database as DatabaseAction;
33
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
34
use TYPO3\CMS\Core\Type\Bitmask\Permission;
35
use TYPO3\CMS\Core\Utility\ArrayUtility;
36
use TYPO3\CMS\Core\Utility\GeneralUtility;
37
use TYPO3\CMS\Core\Versioning\VersionState;
38
use TYPO3\CMS\Workspaces\DataHandler\CommandMap;
39
use TYPO3\CMS\Workspaces\Notification\StageChangeNotification;
40
use TYPO3\CMS\Workspaces\Service\StagesService;
41
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
42
43
/**
44
 * Contains some parts for staging, versioning and workspaces
45
 * to interact with the TYPO3 Core Engine
46
 * @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
47
 */
48
class DataHandlerHook
49
{
50
    /**
51
     * For accumulating information about workspace stages raised
52
     * on elements so a single mail is sent as notification.
53
     *
54
     * @var array
55
     */
56
    protected $notificationEmailInfo = [];
57
58
    /**
59
     * Contains remapped IDs.
60
     *
61
     * @var array
62
     */
63
    protected $remappedIds = [];
64
65
    /****************************
66
     *****  Cmdmap  Hooks  ******
67
     ****************************/
68
    /**
69
     * hook that is called before any cmd of the commandmap is executed
70
     *
71
     * @param DataHandler $dataHandler reference to the main DataHandler object
72
     */
73
    public function processCmdmap_beforeStart(DataHandler $dataHandler)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::processCmdmap_beforeStart" is not in camel caps format
Loading history...
74
    {
75
        // Reset notification array
76
        $this->notificationEmailInfo = [];
77
        // Resolve dependencies of version/workspaces actions:
78
        $dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get();
79
    }
80
81
    /**
82
     * hook that is called when no prepared command was found
83
     *
84
     * @param string $command the command to be executed
85
     * @param string $table the table of the record
86
     * @param int $id the ID of the record
87
     * @param mixed $value the value containing the data
88
     * @param bool $commandIsProcessed can be set so that other hooks or
89
     * @param DataHandler $dataHandler reference to the main DataHandler object
90
     */
91
    public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
92
    {
93
        // custom command "version"
94
        if ($command !== 'version') {
95
            return;
96
        }
97
        $commandIsProcessed = true;
98
        $action = (string)$value['action'];
99
        $comment = $value['comment'] ?: '';
100
        $notificationAlternativeRecipients = $value['notificationAlternativeRecipients'] ?? [];
101
        switch ($action) {
102
            case 'new':
103
                $dataHandler->versionizeRecord($table, $id, $value['label']);
104
                break;
105
            case 'swap':
106
                $this->version_swap(
107
                    $table,
108
                    $id,
109
                    $value['swapWith'],
110
                    (bool)$value['swapIntoWS'],
111
                    $dataHandler,
112
                    $comment,
113
                    $notificationAlternativeRecipients
114
                );
115
                break;
116
            case 'clearWSID':
117
                $this->version_clearWSID($table, (int)$id, false, $dataHandler);
118
                break;
119
            case 'flush':
120
                $this->version_clearWSID($table, (int)$id, true, $dataHandler);
121
                break;
122
            case 'setStage':
123
                $elementIds = GeneralUtility::trimExplode(',', $id, true);
124
                foreach ($elementIds as $elementId) {
125
                    $this->version_setStage(
126
                        $table,
127
                        $elementId,
0 ignored issues
show
Bug introduced by
$elementId of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Workspaces\Hoo...ook::version_setStage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

127
                        /** @scrutinizer ignore-type */ $elementId,
Loading history...
128
                        $value['stageId'],
129
                        $comment,
130
                        $dataHandler,
131
                        $notificationAlternativeRecipients
132
                    );
133
                }
134
                break;
135
            default:
136
                // Do nothing
137
        }
138
    }
139
140
    /**
141
     * hook that is called AFTER all commands of the commandmap was
142
     * executed
143
     *
144
     * @param DataHandler $dataHandler reference to the main DataHandler object
145
     */
146
    public function processCmdmap_afterFinish(DataHandler $dataHandler)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::processCmdmap_afterFinish" is not in camel caps format
Loading history...
147
    {
148
        // Empty accumulation array
149
        $emailNotificationService = GeneralUtility::makeInstance(StageChangeNotification::class);
150
        $this->sendStageChangeNotification(
151
            $this->notificationEmailInfo,
152
            $emailNotificationService,
153
            $dataHandler
154
        );
155
156
        // Reset notification array
157
        $this->notificationEmailInfo = [];
158
        // Reset remapped IDs
159
        $this->remappedIds = [];
160
161
        $this->flushWorkspaceCacheEntriesByWorkspaceId((int)$dataHandler->BE_USER->workspace);
162
    }
163
164
    protected function sendStageChangeNotification(
165
        array $accumulatedNotificationInformation,
166
        StageChangeNotification $notificationService,
167
        DataHandler $dataHandler
168
    ): void {
169
        foreach ($accumulatedNotificationInformation as $groupedNotificationInformation) {
170
            $emails = (array)$groupedNotificationInformation['recipients'];
171
            if (empty($emails)) {
172
                continue;
173
            }
174
            $workspaceRec = BackendUtility::getRecord('sys_workspace', $groupedNotificationInformation['shared'][0]);
175
            if (!is_array($workspaceRec)) {
176
                continue;
177
            }
178
            $notificationService->notifyStageChange(
179
                $workspaceRec,
180
                (int)$groupedNotificationInformation['shared'][1],
181
                $groupedNotificationInformation['elements'],
182
                $groupedNotificationInformation['shared'][2],
183
                $emails,
184
                $dataHandler->BE_USER
185
            );
186
187
            if ($dataHandler->enableLogging) {
188
                [$elementTable, $elementUid] = reset($groupedNotificationInformation['elements']);
189
                $propertyArray = $dataHandler->getRecordProperties($elementTable, $elementUid);
190
                $pid = $propertyArray['pid'];
191
                $dataHandler->log($elementTable, $elementUid, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Notification email for stage change was sent to "' . implode('", "', $emails) . '"', -1, [], $dataHandler->eventPid($elementTable, $elementUid, $pid));
192
            }
193
        }
194
    }
195
196
    /**
197
     * hook that is called when an element shall get deleted
198
     *
199
     * @param string $table the table of the record
200
     * @param int $id the ID of the record
201
     * @param array $record The accordant database record
202
     * @param bool $recordWasDeleted can be set so that other hooks or
203
     * @param DataHandler $dataHandler reference to the main DataHandler object
204
     */
205
    public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::processCmdmap_deleteAction" is not in camel caps format
Loading history...
206
    {
207
        // only process the hook if it wasn't processed
208
        // by someone else before
209
        if ($recordWasDeleted) {
210
            return;
211
        }
212
        $recordWasDeleted = true;
213
        // For Live version, try if there is a workspace version because if so, rather "delete" that instead
214
        // Look, if record is an offline version, then delete directly:
215
        if ((int)($record['t3ver_oid'] ?? 0) === 0) {
216
            if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) {
217
                $record = $wsVersion;
218
                $id = $record['uid'];
219
            }
220
        }
221
        $recordVersionState = VersionState::cast($record['t3ver_state']);
222
        // Look, if record is an offline version, then delete directly:
223
        if ((int)($record['t3ver_oid'] ?? 0) > 0) {
224
            if (BackendUtility::isTableWorkspaceEnabled($table)) {
225
                // In Live workspace, delete any. In other workspaces there must be match.
226
                if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) {
227
                    $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
228
                    // Processing can be skipped if a delete placeholder shall be swapped/published
229
                    // during the current request. Thus it will be deleted later on...
230
                    $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']);
231
                    if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid'])
232
                        && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'])
233
                        && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
234
                        && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
235
                        && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id
236
                    ) {
237
                        return null;
238
                    }
239
240
                    if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) {
241
                        // Change normal versioned record to delete placeholder
242
                        // Happens when an edited record is deleted
243
                        GeneralUtility::makeInstance(ConnectionPool::class)
244
                            ->getConnectionForTable($table)
245
                            ->update(
246
                                $table,
247
                                ['t3ver_state' => VersionState::DELETE_PLACEHOLDER],
248
                                ['uid' => $id]
249
                            );
250
251
                        // Delete localization overlays:
252
                        $dataHandler->deleteL10nOverlayRecords($table, $id);
253
                    } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) {
254
                        // Delete those in WS 0 + if their live records state was not "Placeholder".
255
                        $dataHandler->deleteEl($table, $id);
256
                        // Delete move-placeholder if current version record is a move-to-pointer
257
                        if ($recordVersionState->equals(VersionState::MOVE_POINTER)) {
258
                            $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid', $record['t3ver_wsid']);
259
                            if (!empty($movePlaceholder)) {
260
                                $dataHandler->deleteEl($table, $movePlaceholder['uid']);
261
                            }
262
                        }
263
                    } else {
264
                        // If live record was placeholder (new/deleted), rather clear
265
                        // it from workspace (because it clears both version and placeholder).
266
                        $this->version_clearWSID($table, (int)$id, false, $dataHandler);
267
                    }
268
                } else {
269
                    $dataHandler->newlog('Tried to delete record from another workspace', SystemLogErrorClassification::USER_ERROR);
270
                }
271
            } else {
272
                $dataHandler->newlog('Versioning not enabled for record with an online ID (t3ver_oid) given', SystemLogErrorClassification::SYSTEM_ERROR);
273
            }
274
        } elseif ($dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
275
            // Look, if record is "online" then delete directly.
276
            $dataHandler->deleteEl($table, $id);
277
        } elseif ($recordVersionState->equals(VersionState::MOVE_PLACEHOLDER)) {
278
            // Placeholders for moving operations are deletable directly.
279
            // Get record which its a placeholder for and reset the t3ver_state of that:
280
            if ($wsRec = BackendUtility::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) {
281
                // Clear the state flag of the workspace version of the record
282
                // Setting placeholder state value for version (so it can know it is currently a new version...)
283
284
                GeneralUtility::makeInstance(ConnectionPool::class)
285
                    ->getConnectionForTable($table)
286
                    ->update(
287
                        $table,
288
                        [
289
                            't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE)
290
                        ],
291
                        ['uid' => (int)$wsRec['uid']]
292
                    );
293
            }
294
            $dataHandler->deleteEl($table, $id);
295
        } else {
296
            // Otherwise, try to delete by versioning:
297
            $copyMappingArray = $dataHandler->copyMappingArray;
298
            $dataHandler->versionizeRecord($table, $id, 'DELETED!', true);
299
            // Determine newly created versions:
300
            // (remove placeholders are copied and modified, thus they appear in the copyMappingArray)
301
            $versionizedElements = ArrayUtility::arrayDiffAssocRecursive($dataHandler->copyMappingArray, $copyMappingArray);
302
            // Delete localization overlays:
303
            foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) {
304
                foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) {
305
                    $dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId);
306
                }
307
            }
308
        }
309
    }
310
311
    /**
312
     * In case a sys_workspace_stage record is deleted we do a hard reset
313
     * for all existing records in that stage to avoid that any of these end up
314
     * as orphan records.
315
     *
316
     * @param string $command
317
     * @param string $table
318
     * @param string $id
319
     * @param string $value
320
     * @param DataHandler $dataHandler
321
     */
322
    public function processCmdmap_postProcess($command, $table, $id, $value, DataHandler $dataHandler)
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

322
    public function processCmdmap_postProcess($command, $table, $id, /** @scrutinizer ignore-unused */ $value, DataHandler $dataHandler)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $dataHandler is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

322
    public function processCmdmap_postProcess($command, $table, $id, $value, /** @scrutinizer ignore-unused */ DataHandler $dataHandler)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Coding Style introduced by
Method name "DataHandlerHook::processCmdmap_postProcess" is not in camel caps format
Loading history...
323
    {
324
        if ($command === 'delete') {
325
            if ($table === StagesService::TABLE_STAGE) {
326
                $this->resetStageOfElements((int)$id);
327
            } elseif ($table === WorkspaceService::TABLE_WORKSPACE) {
328
                $this->flushWorkspaceElements((int)$id);
329
                $this->emitUpdateTopbarSignal();
330
            }
331
        }
332
    }
333
334
    public function processDatamap_afterAllOperations(DataHandler $dataHandler): void
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::processDatamap_afterAllOperations" is not in camel caps format
Loading history...
335
    {
336
        if (isset($dataHandler->datamap[WorkspaceService::TABLE_WORKSPACE])) {
337
            $this->emitUpdateTopbarSignal();
338
        }
339
    }
340
341
    /**
342
     * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about
343
     * moving records that are *not* in the live workspace
344
     *
345
     * @param string $table the table of the record
346
     * @param int $uid the ID of the record
347
     * @param int $destPid Position to move to: $destPid: >=0 then it points to
348
     * @param array $propArr Record properties, like header and pid (includes workspace overlay)
349
     * @param array $moveRec Record properties, like header and pid (without workspace overlay)
350
     * @param int $resolvedPid The final page ID of the record
351
     * @param bool $recordWasMoved can be set so that other hooks or
352
     * @param DataHandler $dataHandler
353
     */
354
    public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)
355
    {
356
        // Only do something in Draft workspace
357
        if ($dataHandler->BE_USER->workspace === 0) {
358
            return;
359
        }
360
        $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
361
        // Fetch move placeholder, since it might point to a new page in the current workspace
362
        $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid,pid');
0 ignored issues
show
Bug introduced by
It seems like abs($destPid) can also be of type double; however, parameter $uid of TYPO3\CMS\Backend\Utilit...y::getMovePlaceholder() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

362
        $movePlaceHolder = BackendUtility::getMovePlaceholder($table, /** @scrutinizer ignore-type */ abs($destPid), 'uid,pid');
Loading history...
363
        if ($movePlaceHolder !== false && $destPid < 0) {
364
            $resolvedPid = $movePlaceHolder['pid'];
365
        }
366
        $recordWasMoved = true;
367
        $moveRecVersionState = VersionState::cast($moveRec['t3ver_state']);
368
        // Get workspace version of the source record, if any:
369
        $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
370
        // Handle move-placeholders if the current record is not one already
371
        if (
372
            $tableSupportsVersioning
373
            && !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER)
374
        ) {
375
            // Create version of record first, if it does not exist
376
            if (empty($workspaceVersion['uid'])) {
377
                $dataHandler->versionizeRecord($table, $uid, 'MovePointer');
378
                $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
379
                if ((int)$resolvedPid !== (int)$propArr['pid']) {
380
                    $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
381
                }
382
            } elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$workspaceVersion['uid']) {
383
                // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders
384
                if ((int)$resolvedPid !== (int)$propArr['pid']) {
385
                    $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
386
                }
387
            }
388
        }
389
        // Check workspace permissions:
390
        $workspaceAccessBlocked = [];
391
        // Element was in "New/Deleted/Moved" so it can be moved...
392
        $recIsNewVersion = $moveRecVersionState->indicatesPlaceholder();
393
        $recordMustNotBeVersionized = $dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table);
394
        $canMoveRecord = $recIsNewVersion || $tableSupportsVersioning;
395
        // Workspace source check:
396
        if (!$recIsNewVersion) {
397
            $errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $workspaceVersion['uid'] ?: $uid);
398
            if ($errorCode) {
399
                $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' ';
400
            } elseif (!$canMoveRecord && !$recordMustNotBeVersionized) {
401
                $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" ';
402
            }
403
        }
404
        // Workspace destination check:
405
        // All records can be inserted if $recordMustNotBeVersionized is true.
406
        // Only new versions can be inserted if $recordMustNotBeVersionized is FALSE.
407
        if (!($recordMustNotBeVersionized || $canMoveRecord && !$recordMustNotBeVersionized)) {
408
            $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" ';
409
        }
410
411
        if (empty($workspaceAccessBlocked)) {
412
            // If the move operation is done on a versioned record, which is
413
            // NOT new/deleted placeholder, then also create a move placeholder
414
            if ($workspaceVersion['uid'] && !$recIsNewVersion && BackendUtility::isTableWorkspaceEnabled($table)) {
415
                $this->moveRecord_wsPlaceholders($table, (int)$uid, (int)$destPid, (int)$resolvedPid, (int)$workspaceVersion['uid'], $dataHandler);
416
            } else {
417
                // moving not needed, just behave like in live workspace
418
                $recordWasMoved = false;
419
            }
420
        } else {
421
            $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), SystemLogErrorClassification::USER_ERROR);
422
        }
423
    }
424
425
    /**
426
     * Processes fields of a moved record and follows references.
427
     *
428
     * @param DataHandler $dataHandler Calling DataHandler instance
429
     * @param int $resolvedPageId Resolved real destination page id
430
     * @param string $table Name of parent table
431
     * @param int $uid UID of the parent record
432
     */
433
    protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::moveRecord_processFields" is not in camel caps format
Loading history...
434
    {
435
        $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid);
436
        if (empty($versionedRecord)) {
437
            return;
438
        }
439
        foreach ($versionedRecord as $field => $value) {
440
            if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
441
                continue;
442
            }
443
            $this->moveRecord_processFieldValue(
444
                $dataHandler,
445
                $resolvedPageId,
446
                $table,
447
                $uid,
448
                $value,
449
                $GLOBALS['TCA'][$table]['columns'][$field]['config']
450
            );
451
        }
452
    }
453
454
    /**
455
     * Processes a single field of a moved record and follows references.
456
     *
457
     * @param DataHandler $dataHandler Calling DataHandler instance
458
     * @param int $resolvedPageId Resolved real destination page id
459
     * @param string $table Name of parent table
460
     * @param int $uid UID of the parent record
461
     * @param string $value Value of the field of the parent record
462
     * @param array $configuration TCA field configuration of the parent record
463
     */
464
    protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $value, array $configuration): void
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::moveRecord_processFieldValue" is not in camel caps format
Loading history...
465
    {
466
        $inlineFieldType = $dataHandler->getInlineFieldType($configuration);
467
        $inlineProcessing = (
468
            ($inlineFieldType === 'list' || $inlineFieldType === 'field')
469
            && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
470
            && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent'])
471
        );
472
473
        if ($inlineProcessing) {
474
            if ($table === 'pages') {
475
                // If the inline elements are related to a page record,
476
                // make sure they reside at that page and not at its parent
477
                $resolvedPageId = $uid;
478
            }
479
480
            $dbAnalysis = $this->createRelationHandlerInstance();
481
            $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration);
482
483
            // Moving records to a positive destination will insert each
484
            // record at the beginning, thus the order is reversed here:
485
            foreach ($dbAnalysis->itemArray as $item) {
486
                $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
487
                if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) {
488
                    continue;
489
                }
490
                $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId);
491
            }
492
        }
493
    }
494
495
    /****************************
496
     *****  Stage Changes  ******
497
     ****************************/
498
    /**
499
     * Setting stage of record
500
     *
501
     * @param string $table Table name
502
     * @param int $id
503
     * @param int $stageId Stage ID to set
504
     * @param string $comment Comment that goes into log
505
     * @param DataHandler $dataHandler DataHandler object
506
     * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
507
     */
508
    protected function version_setStage($table, $id, $stageId, string $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::version_setStage" is not in camel caps format
Loading history...
509
    {
510
        if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
511
            $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR);
512
        } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) {
513
            $record = BackendUtility::getRecord($table, $id);
514
            $workspaceInfo = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']);
515
            // check if the user is allowed to the current stage, so it's also allowed to send to next stage
516
            if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) {
517
                // Set stage of record:
518
                GeneralUtility::makeInstance(ConnectionPool::class)
519
                    ->getConnectionForTable($table)
520
                    ->update(
521
                        $table,
522
                        [
523
                            't3ver_stage' => $stageId,
524
                        ],
525
                        ['uid' => (int)$id]
526
                    );
527
528
                if ($dataHandler->enableLogging) {
529
                    $propertyArray = $dataHandler->getRecordProperties($table, $id);
530
                    $pid = $propertyArray['pid'];
531
                    $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid));
532
                }
533
                // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
534
                $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]);
535
                if ((int)$workspaceInfo['stagechg_notification'] > 0) {
536
                    $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$workspaceInfo, $stageId, $comment];
537
                    $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = [$table, $id];
538
                    $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['recipients'] = $notificationAlternativeRecipients;
539
                }
540
            } else {
541
                $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', SystemLogErrorClassification::USER_ERROR);
542
            }
543
        } else {
544
            $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', SystemLogErrorClassification::USER_ERROR);
545
        }
546
    }
547
548
    /*****************************
549
     *****  CMD versioning  ******
550
     *****************************/
551
552
    /**
553
     * Swapping versions of a record
554
     * Version from archive (future/past, called "swap version") will get the uid of the "t3ver_oid", the official element with uid = "t3ver_oid" will get the new versions old uid. PIDs are swapped also
555
     *
556
     * @param string $table Table name
557
     * @param int $id UID of the online record to swap
558
     * @param int $swapWith UID of the archived version to swap with!
559
     * @param bool $swapIntoWS If set, swaps online into workspace instead of publishing out of workspace.
560
     * @param DataHandler $dataHandler DataHandler object
561
     * @param string $comment Notification comment
562
     * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
563
     */
564
    protected function version_swap($table, $id, $swapWith, bool $swapIntoWS, DataHandler $dataHandler, string $comment, $notificationAlternativeRecipients = [])
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::version_swap" is not in camel caps format
Loading history...
565
    {
566
        // Check prerequisites before start swapping
567
568
        // Skip records that have been deleted during the current execution
569
        if ($dataHandler->hasDeletedRecord($table, $id)) {
570
            return;
571
        }
572
573
        // First, check if we may actually edit the online record
574
        if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
575
            $dataHandler->newlog(
576
                sprintf(
577
                    'Error: You cannot swap versions for record %s:%d you do not have access to edit!',
578
                    $table,
579
                    $id
580
                ),
581
                SystemLogErrorClassification::USER_ERROR
582
            );
583
            return;
584
        }
585
        // Select the two versions:
586
        $curVersion = BackendUtility::getRecord($table, $id, '*');
587
        $swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
588
        $movePlh = [];
589
        $movePlhID = 0;
590
        if (!(is_array($curVersion) && is_array($swapVersion))) {
591
            $dataHandler->newlog(
592
                sprintf(
593
                    'Error: Either online or swap version for %s:%d->%d could not be selected!',
594
                    $table,
595
                    $id,
596
                    $swapWith
597
                ),
598
                SystemLogErrorClassification::SYSTEM_ERROR
599
            );
600
            return;
601
        }
602
        if (!$dataHandler->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
603
            $dataHandler->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], SystemLogErrorClassification::USER_ERROR);
604
            return;
605
        }
606
        $wsAccess = $dataHandler->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']);
607
        if (!($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === -10)) {
608
            $dataHandler->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', SystemLogErrorClassification::USER_ERROR);
609
            return;
610
        }
611
        if (!($dataHandler->doesRecordExist($table, $swapWith, Permission::PAGE_SHOW) && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) {
612
            $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', SystemLogErrorClassification::USER_ERROR);
613
            return;
614
        }
615
        if ($swapIntoWS && !$dataHandler->BE_USER->workspaceSwapAccess()) {
616
            $dataHandler->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', SystemLogErrorClassification::USER_ERROR);
617
            return;
618
        }
619
        // Check if the swapWith record really IS a version of the original!
620
        if (!(((int)$swapVersion['t3ver_oid'] > 0 && (int)$curVersion['t3ver_oid'] === 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) {
621
            $dataHandler->newlog('In swap version, either t3ver_oid was not set or the t3ver_oid didn\'t match the id of the online version as it must!', SystemLogErrorClassification::SYSTEM_ERROR);
622
            return;
623
        }
624
        // Lock file name:
625
        $lockFileName = Environment::getVarPath() . '/lock/workspaces_swap' . $table . '_' . $id . '.json';
626
        if (@is_file($lockFileName)) {
627
            $lockFileContents = file_get_contents($lockFileName);
628
            $lockFileContents = json_decode($lockFileContents ?: '', true);
629
            // Only skip if the lock file is newer than the last 1h (a publishing process should not be running longer than 60mins)
630
            if (isset($lockFileContents['tstamp']) && $lockFileContents['tstamp'] > ($GLOBALS['EXEC_TIME']-3600)) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "-"; 0 found
Loading history...
Coding Style introduced by
Expected 1 space after "-"; 0 found
Loading history...
631
                $dataHandler->newlog('A swapping lock file was present. Either another swap process is already running or a previous swap process failed. Ask your administrator to handle the situation.', SystemLogErrorClassification::SYSTEM_ERROR);
632
                return;
633
            }
634
        }
635
636
        // Now start to swap records by first creating the lock file
637
638
        // Write lock-file:
639
        GeneralUtility::writeFileToTypo3tempDir($lockFileName, json_encode([
640
            'tstamp' => $GLOBALS['EXEC_TIME'],
641
            'user' => $dataHandler->BE_USER->user['username'],
642
            'curVersion' => $curVersion,
643
            'swapVersion' => $swapVersion
644
        ]));
645
        // Find fields to keep
646
        $keepFields = $this->getUniqueFields($table);
647
        if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
648
            $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
649
        }
650
        // l10n-fields must be kept otherwise the localization
651
        // will be lost during the publishing
652
        if ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
653
            $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
654
        }
655
        // Swap "keepfields"
656
        foreach ($keepFields as $fN) {
657
            $tmp = $swapVersion[$fN];
658
            $swapVersion[$fN] = $curVersion[$fN];
659
            $curVersion[$fN] = $tmp;
660
        }
661
        // Preserve states:
662
        $t3ver_state = [];
663
        $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
664
        // Modify offline version to become online:
665
        $tmp_wsid = $swapVersion['t3ver_wsid'];
666
        // Set pid for ONLINE
667
        $swapVersion['pid'] = (int)$curVersion['pid'];
668
        // We clear this because t3ver_oid only make sense for offline versions
669
        // and we want to prevent unintentional misuse of this
670
        // value for online records.
671
        $swapVersion['t3ver_oid'] = 0;
672
        // In case of swapping and the offline record has a state
673
        // (like 2 or 4 for deleting or move-pointer) we set the
674
        // current workspace ID so the record is not deselected
675
        // in the interface by BackendUtility::versioningPlaceholderClause()
676
        $swapVersion['t3ver_wsid'] = 0;
677
        if ($swapIntoWS) {
678
            if ($t3ver_state['swapVersion'] > 0) {
679
                $swapVersion['t3ver_wsid'] = $dataHandler->BE_USER->workspace;
680
            } else {
681
                $swapVersion['t3ver_wsid'] = (int)$curVersion['t3ver_wsid'];
682
            }
683
        }
684
        $swapVersion['t3ver_stage'] = 0;
685
        if (!$swapIntoWS) {
686
            $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
687
        }
688
        // Moving element.
689
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
690
            //  && $t3ver_state['swapVersion']==4   // Maybe we don't need this?
691
            if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) {
692
                $movePlhID = $plhRec['uid'];
693
                $movePlh['pid'] = $swapVersion['pid'];
694
                $swapVersion['pid'] = (int)$plhRec['pid'];
695
                $curVersion['t3ver_state'] = (int)$swapVersion['t3ver_state'];
696
                $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
697
                if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
698
                    // sortby is a "keepFields" which is why this will work...
699
                    $movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
700
                    $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
701
                }
702
            }
703
        }
704
        // Take care of relations in each field (e.g. IRRE):
705
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
706
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
707
                if (isset($fieldConf['config']) && is_array($fieldConf['config'])) {
708
                    $this->version_swap_processFields($table, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler);
709
                }
710
            }
711
        }
712
        unset($swapVersion['uid']);
713
        // Modify online version to become offline:
714
        unset($curVersion['uid']);
715
        // Mark curVersion to contain the oid
716
        $curVersion['t3ver_oid'] = (int)$id;
717
        $curVersion['t3ver_wsid'] = $swapIntoWS ? (int)$tmp_wsid : 0;
718
        // Increment lifecycle counter
719
        $curVersion['t3ver_stage'] = 0;
720
        if (!$swapIntoWS) {
721
            $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
722
        }
723
        // Registering and swapping MM relations in current and swap records:
724
        $dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith);
725
        // Generating proper history data to prepare logging
726
        $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
727
        $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
728
729
        // Execute swapping:
730
        $sqlErrors = [];
731
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
732
733
        $platform = $connection->getDatabasePlatform();
734
        $tableDetails = null;
735
        if ($platform instanceof SQLServerPlatform) {
736
            // mssql needs to set proper PARAM_LOB and others to update fields
737
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
738
        }
739
740
        try {
741
            $types = [];
742
743
            if ($platform instanceof SQLServerPlatform) {
744
                foreach ($curVersion as $columnName => $columnValue) {
745
                    $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
746
                }
747
            }
748
749
            $connection->update(
750
                $table,
751
                $swapVersion,
752
                ['uid' => (int)$id],
753
                $types
754
            );
755
        } catch (DBALException $e) {
756
            $sqlErrors[] = $e->getPrevious()->getMessage();
757
        }
758
759
        if (empty($sqlErrors)) {
760
            try {
761
                $types = [];
762
                if ($platform instanceof SQLServerPlatform) {
763
                    foreach ($curVersion as $columnName => $columnValue) {
764
                        $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
765
                    }
766
                }
767
768
                $connection->update(
769
                    $table,
770
                    $curVersion,
771
                    ['uid' => (int)$swapWith],
772
                    $types
773
                );
774
                unlink($lockFileName);
775
            } catch (DBALException $e) {
776
                $sqlErrors[] = $e->getPrevious()->getMessage();
777
            }
778
        }
779
780
        if (!empty($sqlErrors)) {
781
            $dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), SystemLogErrorClassification::SYSTEM_ERROR);
782
        } else {
783
            // Register swapped ids for later remapping:
784
            $this->remappedIds[$table][$id] = $swapWith;
785
            $this->remappedIds[$table][$swapWith] = $id;
786
            // If a moving operation took place...:
787
            if ($movePlhID) {
788
                // Remove, if normal publishing:
789
                if (!$swapIntoWS) {
790
                    // For delete + completely delete!
791
                    $dataHandler->deleteEl($table, $movePlhID, true, true);
792
                } else {
793
                    // Otherwise update the movePlaceholder:
794
                    GeneralUtility::makeInstance(ConnectionPool::class)
795
                        ->getConnectionForTable($table)
796
                        ->update(
797
                            $table,
798
                            $movePlh,
799
                            ['uid' => (int)$movePlhID]
800
                        );
801
                    $dataHandler->addRemapStackRefIndex($table, $movePlhID);
802
                }
803
            }
804
            // Checking for delete:
805
            // Delete only if new/deleted placeholders are there.
806
            if (!$swapIntoWS && ((int)$t3ver_state['swapVersion'] === VersionState::NEW_PLACEHOLDER || (int)$t3ver_state['swapVersion'] === VersionState::DELETE_PLACEHOLDER)) {
807
                // Force delete
808
                $dataHandler->deleteEl($table, $id, true);
809
            }
810
            if ($dataHandler->enableLogging) {
811
                $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, ($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, -1, [], $dataHandler->eventPid($table, $id, $swapVersion['pid']));
812
            }
813
814
            // Update reference index of the live record:
815
            $dataHandler->addRemapStackRefIndex($table, $id);
816
            // Set log entry for live record:
817
            $propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion);
818
            if ($propArr['t3ver_oid'] ?? 0 > 0) {
819
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
820
            } else {
821
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
822
            }
823
            $theLogId = $dataHandler->log($table, $id, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
824
            $dataHandler->setHistory($table, $id, $theLogId);
825
            // Update reference index of the offline record:
826
            $dataHandler->addRemapStackRefIndex($table, $swapWith);
827
            // Set log entry for offline record:
828
            $propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion);
829
            if ($propArr['t3ver_oid'] ?? 0 > 0) {
830
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
831
            } else {
832
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
833
            }
834
            $theLogId = $dataHandler->log($table, $swapWith, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']);
835
            $dataHandler->setHistory($table, $swapWith, $theLogId);
836
837
            $stageId = StagesService::STAGE_PUBLISH_EXECUTE_ID;
838
            $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
839
            $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
840
            $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = [$table, $id];
841
            $this->notificationEmailInfo[$notificationEmailInfoKey]['recipients'] = $notificationAlternativeRecipients;
842
            // Write to log with stageId -20 (STAGE_PUBLISH_EXECUTE_ID)
843
            if ($dataHandler->enableLogging) {
844
                $propArr = $dataHandler->getRecordProperties($table, $id);
845
                $pid = $propArr['pid'];
846
                $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid));
847
            }
848
            $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]);
849
850
            // Clear cache:
851
            $dataHandler->registerRecordIdForPageCacheClearing($table, $id);
852
            // If not swapped, delete the record from the database
853
            if (!$swapIntoWS) {
854
                if ($table === 'pages') {
855
                    // Note on fifth argument false: At this point both $curVersion and $swapVersion page records are
856
                    // identical in DB. deleteEl() would now usually find all records assigned to our obsolete
857
                    // page which at the same time belong to our current version page, and would delete them.
858
                    // To suppress this, false tells deleteEl() to only delete the obsolete page but not its assigned records.
859
                    $dataHandler->deleteEl($table, $swapWith, true, true, false);
860
                } else {
861
                    $dataHandler->deleteEl($table, $swapWith, true, true);
862
                }
863
            }
864
865
            //Update reference index for live workspace too:
866
            /** @var \TYPO3\CMS\Core\Database\ReferenceIndex $refIndexObj */
867
            $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
868
            $refIndexObj->setWorkspaceId(0);
869
            $refIndexObj->updateRefIndexTable($table, $id);
870
            $refIndexObj->updateRefIndexTable($table, $swapWith);
871
        }
872
    }
873
874
    /**
875
     * Processes fields of a record for the publishing/swapping process.
876
     * Basically this takes care of IRRE (type "inline") child references.
877
     *
878
     * @param string $tableName Table name
879
     * @param array $configuration TCA field configuration
880
     * @param array $liveData Live record data
881
     * @param array $versionData Version record data
882
     * @param DataHandler $dataHandler Calling data-handler object
883
     */
884
    protected function version_swap_processFields($tableName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::version_swap_processFields" is not in camel caps format
Loading history...
885
    {
886
        $inlineType = $dataHandler->getInlineFieldType($configuration);
887
        if ($inlineType !== 'field') {
888
            return;
889
        }
890
        $foreignTable = $configuration['foreign_table'];
891
        // Read relations that point to the current record (e.g. live record):
892
        $liveRelations = $this->createRelationHandlerInstance();
893
        $liveRelations->setWorkspaceId(0);
894
        $liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration);
895
        // Read relations that point to the record to be swapped with e.g. draft record):
896
        $versionRelations = $this->createRelationHandlerInstance();
897
        $versionRelations->setUseLiveReferenceIds(false);
898
        $versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration);
899
        // Update relations for both (workspace/versioning) sites:
900
        if (!empty($liveRelations->itemArray)) {
901
            $dataHandler->addRemapAction(
902
                $tableName,
903
                $liveData['uid'],
904
                [$this, 'updateInlineForeignFieldSorting'],
905
                [$liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace]
906
            );
907
        }
908
        if (!empty($versionRelations->itemArray)) {
909
            $dataHandler->addRemapAction(
910
                $tableName,
911
                $liveData['uid'],
912
                [$this, 'updateInlineForeignFieldSorting'],
913
                [$liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0]
914
            );
915
        }
916
    }
917
918
    /**
919
     * Updates foreign field sorting values of versioned and live
920
     * parents after(!) the whole structure has been published.
921
     *
922
     * This method is used as callback function in
923
     * DataHandlerHook::version_swap_procBasedOnFieldType().
924
     * Sorting fields ("sortby") are not modified during the
925
     * workspace publishing/swapping process directly.
926
     *
927
     * @param string $parentId
928
     * @param string $foreignTableName
929
     * @param int[] $foreignIds
930
     * @param array $configuration
931
     * @param int $targetWorkspaceId
932
     * @internal
933
     */
934
    public function updateInlineForeignFieldSorting($parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
935
    {
936
        $remappedIds = [];
937
        // Use remapped ids (live id <-> version id)
938
        foreach ($foreignIds as $foreignId) {
939
            if (!empty($this->remappedIds[$foreignTableName][$foreignId])) {
940
                $remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId];
941
            } else {
942
                $remappedIds[] = $foreignId;
943
            }
944
        }
945
946
        $relationHandler = $this->createRelationHandlerInstance();
947
        $relationHandler->setWorkspaceId($targetWorkspaceId);
948
        $relationHandler->setUseLiveReferenceIds(false);
949
        $relationHandler->start(implode(',', $remappedIds), $foreignTableName);
950
        $relationHandler->processDeletePlaceholder();
951
        $relationHandler->writeForeignField($configuration, $parentId);
0 ignored issues
show
Bug introduced by
$parentId of type string is incompatible with the type integer expected by parameter $parentUid of TYPO3\CMS\Core\Database\...er::writeForeignField(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

951
        $relationHandler->writeForeignField($configuration, /** @scrutinizer ignore-type */ $parentId);
Loading history...
952
    }
953
954
    /**
955
     * Remove a versioned record from this workspace. Often referred to as "discarding a version" = throwing away a version.
956
     * This means to delete the record and remove any placeholders that are not needed anymore.
957
     *
958
     * In previous versions, this meant that the versioned record was marked as deleted and moved into "live" workspace.
959
     *
960
     * @param string $table Database table name
961
     * @param int $versionId Version record uid
962
     * @param bool $flush If set, will completely delete element
963
     * @param DataHandler $dataHandler DataHandler object
964
     */
965
    protected function version_clearWSID(string $table, int $versionId, bool $flush, DataHandler $dataHandler): void
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::version_clearWSID" is not in camel caps format
Loading history...
966
    {
967
        if ($dataHandler->hasDeletedRecord($table, $versionId)) {
968
            // If discarding pages and records at once, deleting the page record may have already deleted
969
            // records on the page, rendering a call to delete single elements of this page bogus. The
970
            // data handler tracks which records have been deleted in the same process, so ignore
971
            // the record in question if its in the list.
972
            return;
973
        }
974
        if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $versionId)) {
975
            $dataHandler->newlog('Attempt to reset workspace for record ' . $table . ':' . $versionId . ' failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR);
976
            return;
977
        }
978
        if (!$dataHandler->checkRecordUpdateAccess($table, $versionId)) {
979
            $dataHandler->newlog('Attempt to reset workspace for record ' . $table . ':' . $versionId . ' failed because you do not have edit access', SystemLogErrorClassification::USER_ERROR);
980
            return;
981
        }
982
        $liveRecord = BackendUtility::getLiveVersionOfRecord($table, $versionId, 'uid,t3ver_state');
983
        if (!$liveRecord) {
984
            // Attempting to discard a record that has no live version, don't do anything
985
            return;
986
        }
987
988
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
989
        $liveState = VersionState::cast($liveRecord['t3ver_state']);
990
        $versionRecord = BackendUtility::getRecord($table, $versionId);
991
        $versionState = VersionState::cast($versionRecord['t3ver_state']);
992
        $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
993
994
        // purge delete placeholder since it would not contain any modified information
995
        if ($flush || $versionState->equals(VersionState::DELETE_PLACEHOLDER)) {
996
            $dataHandler->deleteEl($table, $versionRecord['uid'], true, true);
997
        // let DataHandler decide how to delete the record that does not have a deleted field
998
        } elseif ($deleteField === null) {
999
            $dataHandler->deleteEl($table, $versionRecord['uid'], true);
1000
        // update record directly in order to avoid delete cascades on this version
1001
        } else {
1002
            $connection->update(
1003
                $table,
1004
                [$deleteField => 1],
1005
                ['uid' => (int)$versionId]
1006
            );
1007
        }
1008
1009
        // purge move placeholder as it has been created just for the sake of pointing to a version
1010
        if ($liveState->equals(VersionState::MOVE_PLACEHOLDER)) {
1011
            $dataHandler->deleteEl($table, $liveRecord['uid'], true, true);
1012
        // purge new placeholder as it has been created just for the sake of pointing to a version
1013
        } elseif ($liveState->equals(VersionState::NEW_PLACEHOLDER)) {
1014
            // THIS assumes that the record was placeholder ONLY for ONE record (namely $id)
1015
            $dataHandler->deleteEl($table, $liveRecord['uid'], true);
1016
        }
1017
    }
1018
1019
    /**
1020
     * In case a sys_workspace_stage record is deleted we do a hard reset
1021
     * for all existing records in that stage to avoid that any of these end up
1022
     * as orphan records.
1023
     *
1024
     * @param int $stageId Elements with this stage are reset
1025
     */
1026
    protected function resetStageOfElements(int $stageId): void
1027
    {
1028
        foreach ($this->getTcaTables() as $tcaTable) {
1029
            if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
1030
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1031
                    ->getQueryBuilderForTable($tcaTable);
1032
1033
                $queryBuilder
1034
                    ->update($tcaTable)
1035
                    ->set('t3ver_stage', StagesService::STAGE_EDIT_ID)
1036
                    ->where(
1037
                        $queryBuilder->expr()->eq(
1038
                            't3ver_stage',
1039
                            $queryBuilder->createNamedParameter($stageId, \PDO::PARAM_INT)
1040
                        ),
1041
                        $queryBuilder->expr()->gt(
1042
                            't3ver_wsid',
1043
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1044
                        )
1045
                    )
1046
                    ->execute();
1047
            }
1048
        }
1049
    }
1050
1051
    /**
1052
     * Flushes elements of a particular workspace to avoid orphan records.
1053
     *
1054
     * @param int $workspaceId The workspace to be flushed
1055
     */
1056
    protected function flushWorkspaceElements(int $workspaceId): void
1057
    {
1058
        $command = [];
1059
        foreach ($this->getTcaTables() as $tcaTable) {
1060
            if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
1061
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1062
                    ->getQueryBuilderForTable($tcaTable);
1063
                $queryBuilder->getRestrictions()->removeAll();
1064
                $result = $queryBuilder
1065
                    ->select('uid')
1066
                    ->from($tcaTable)
1067
                    ->where(
1068
                        $queryBuilder->expr()->eq(
1069
                            't3ver_wsid',
1070
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1071
                        ),
1072
                        // t3ver_oid >= 0 basically omits placeholder records here, those would otherwise
1073
                        // fail to delete later in version_clearWSID() and would create "can't do that" log entries.
1074
                        $queryBuilder->expr()->gt(
1075
                            't3ver_oid',
1076
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1077
                        )
1078
                    )
1079
                    ->orderBy('uid')
1080
                    ->execute();
1081
1082
                while (($recordId = $result->fetchColumn()) !== false) {
1083
                    $command[$tcaTable][$recordId]['version']['action'] = 'flush';
1084
                }
1085
            }
1086
        }
1087
        if (!empty($command)) {
1088
            $dataHandler = $this->getDataHandler();
1089
            $dataHandler->start([], $command);
1090
            $dataHandler->process_cmdmap();
1091
        }
1092
    }
1093
1094
    /**
1095
     * Gets all defined TCA tables.
1096
     *
1097
     * @return array
1098
     */
1099
    protected function getTcaTables(): array
1100
    {
1101
        return array_keys($GLOBALS['TCA']);
1102
    }
1103
1104
    /**
1105
     * @return DataHandler
1106
     */
1107
    protected function getDataHandler(): DataHandler
1108
    {
1109
        return GeneralUtility::makeInstance(DataHandler::class);
1110
    }
1111
1112
    /**
1113
     * Flushes the workspace cache for current workspace and for the virtual "all workspaces" too.
1114
     *
1115
     * @param int $workspaceId The workspace to be flushed in cache
1116
     */
1117
    protected function flushWorkspaceCacheEntriesByWorkspaceId(int $workspaceId): void
1118
    {
1119
        $workspacesCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('workspaces_cache');
1120
        $workspacesCache->flushByTag($workspaceId);
1121
        $workspacesCache->flushByTag(WorkspaceService::SELECT_ALL_WORKSPACES);
1122
    }
1123
1124
    /*******************************
1125
     *****  helper functions  ******
1126
     *******************************/
1127
1128
    /**
1129
     * Finds all elements for swapping versions in workspace
1130
     *
1131
     * @param string $table Table name of the original element to swap
1132
     * @param int $id UID of the original element to swap (online)
1133
     * @param int $offlineId As above but offline
1134
     * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID
1135
     */
1136
    public function findPageElementsForVersionSwap($table, $id, $offlineId)
1137
    {
1138
        $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid');
1139
        $workspaceId = (int)$rec['t3ver_wsid'];
1140
        $elementData = [];
1141
        if ($workspaceId === 0) {
1142
            return $elementData;
1143
        }
1144
        // Get page UID for LIVE and workspace
1145
        if ($table !== 'pages') {
1146
            $rec = BackendUtility::getRecord($table, $id, 'pid');
1147
            $pageId = $rec['pid'];
1148
            $rec = BackendUtility::getRecord('pages', $pageId);
1149
            BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1150
            $offlinePageId = $rec['_ORIG_uid'];
1151
        } else {
1152
            $pageId = $id;
1153
            $offlinePageId = $offlineId;
1154
        }
1155
        // Traversing all tables supporting versioning:
1156
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
0 ignored issues
show
introduced by
$table is overwriting one of the parameters of this function.
Loading history...
1157
            if (BackendUtility::isTableWorkspaceEnabled($table) && $table !== 'pages') {
1158
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1159
                    ->getQueryBuilderForTable($table);
1160
1161
                $queryBuilder->getRestrictions()
1162
                    ->removeAll()
1163
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1164
1165
                $statement = $queryBuilder
1166
                    ->select('A.uid AS offlineUid', 'B.uid AS uid')
1167
                    ->from($table, 'A')
1168
                    ->from($table, 'B')
1169
                    ->where(
1170
                        $queryBuilder->expr()->gt(
1171
                            'A.t3ver_oid',
1172
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1173
                        ),
1174
                        $queryBuilder->expr()->eq(
1175
                            'B.pid',
1176
                            $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
1177
                        ),
1178
                        $queryBuilder->expr()->eq(
1179
                            'A.t3ver_wsid',
1180
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1181
                        ),
1182
                        $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1183
                    )
1184
                    ->execute();
1185
1186
                while ($row = $statement->fetch()) {
1187
                    $elementData[$table][] = [$row['uid'], $row['offlineUid']];
1188
                }
1189
            }
1190
        }
1191
        if ($offlinePageId && $offlinePageId != $pageId) {
1192
            $elementData['pages'][] = [$pageId, $offlinePageId];
1193
        }
1194
1195
        return $elementData;
1196
    }
1197
1198
    /**
1199
     * Searches for all elements from all tables on the given pages in the same workspace.
1200
     *
1201
     * @param array $pageIdList List of PIDs to search
1202
     * @param int $workspaceId Workspace ID
1203
     * @param array $elementList List of found elements. Key is table name, value is array of element UIDs
1204
     */
1205
    public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList)
1206
    {
1207
        if ($workspaceId == 0) {
1208
            return;
1209
        }
1210
        // Traversing all tables supporting versioning:
1211
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
1212
            if (BackendUtility::isTableWorkspaceEnabled($table) && $table !== 'pages') {
1213
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1214
                    ->getQueryBuilderForTable($table);
1215
1216
                $queryBuilder->getRestrictions()
1217
                    ->removeAll()
1218
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1219
1220
                $statement = $queryBuilder
1221
                    ->select('A.uid')
1222
                    ->from($table, 'A')
1223
                    ->from($table, 'B')
1224
                    ->where(
1225
                        $queryBuilder->expr()->gt(
1226
                            'A.t3ver_oid',
1227
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1228
                        ),
1229
                        $queryBuilder->expr()->in(
1230
                            'B.pid',
1231
                            $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY)
1232
                        ),
1233
                        $queryBuilder->expr()->eq(
1234
                            'A.t3ver_wsid',
1235
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1236
                        ),
1237
                        $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1238
                    )
1239
                    ->groupBy('A.uid')
1240
                    ->execute();
1241
1242
                while ($row = $statement->fetch()) {
1243
                    $elementList[$table][] = $row['uid'];
1244
                }
1245
                if (is_array($elementList[$table])) {
1246
                    // Yes, it is possible to get non-unique array even with DISTINCT above!
1247
                    // It happens because several UIDs are passed in the array already.
1248
                    $elementList[$table] = array_unique($elementList[$table]);
1249
                }
1250
            }
1251
        }
1252
    }
1253
1254
    /**
1255
     * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1256
     *
1257
     * @param string $table Table to search
1258
     * @param array $idList List of records' UIDs
1259
     * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publish DRAFT from ws module!
1260
     * @param array $pageIdList List of found page UIDs
1261
     * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs
1262
     */
1263
    public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList)
1264
    {
1265
        if ($workspaceId == 0) {
1266
            return;
1267
        }
1268
1269
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1270
            ->getQueryBuilderForTable($table);
1271
        $queryBuilder->getRestrictions()
1272
            ->removeAll()
1273
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1274
1275
        $statement = $queryBuilder
1276
            ->select('B.pid')
1277
            ->from($table, 'A')
1278
            ->from($table, 'B')
1279
            ->where(
1280
                $queryBuilder->expr()->gt(
1281
                    'A.t3ver_oid',
1282
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1283
                ),
1284
                $queryBuilder->expr()->eq(
1285
                    'A.t3ver_wsid',
1286
                    $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1287
                ),
1288
                $queryBuilder->expr()->in(
1289
                    'A.uid',
1290
                    $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY)
1291
                ),
1292
                $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1293
            )
1294
            ->groupBy('B.pid')
1295
            ->execute();
1296
1297
        while ($row = $statement->fetch()) {
1298
            $pageIdList[] = $row['pid'];
1299
            // Find ws version
1300
            // Note: cannot use BackendUtility::getRecordWSOL()
1301
            // here because it does not accept workspace id!
1302
            $rec = BackendUtility::getRecord('pages', $row[0]);
1303
            BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1304
            if ($rec['_ORIG_uid']) {
1305
                $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1306
            }
1307
        }
1308
        // The line below is necessary even with DISTINCT
1309
        // because several elements can be passed by caller
1310
        $pageIdList = array_unique($pageIdList);
1311
    }
1312
1313
    /**
1314
     * Finds real page IDs for state change.
1315
     *
1316
     * @param array $idList List of page UIDs, possibly versioned
1317
     */
1318
    public function findRealPageIds(array &$idList): void
1319
    {
1320
        foreach ($idList as $key => $id) {
1321
            $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid');
1322
            if ($rec['t3ver_oid'] > 0) {
1323
                $idList[$key] = $rec['t3ver_oid'];
1324
            }
1325
        }
1326
    }
1327
1328
    /**
1329
     * Creates a move placeholder for workspaces.
1330
     * USE ONLY INTERNALLY
1331
     * Moving placeholder: Can be done because the system sees it as a placeholder for NEW elements like t3ver_state=VersionState::NEW_PLACEHOLDER
1332
     * Moving original: Will either create the placeholder if it doesn't exist or move existing placeholder in workspace.
1333
     *
1334
     * @param string $table Table name to move
1335
     * @param int $uid Record uid to move (online record)
1336
     * @param int $destPid Position to move to: $destPid: >=0 then it points to a page-id on which to insert the record (as the first element). <0 then it points to a uid from its own table after which to insert it (works if
1337
     * @param int $resolvedId Effective page ID
1338
     * @param int $offlineUid UID of offline version of online record
1339
     * @param DataHandler $dataHandler DataHandler object
1340
     * @see moveRecord()
1341
     */
1342
    protected function moveRecord_wsPlaceholders(string $table, int $uid, int $destPid, int $resolvedId, int $offlineUid, DataHandler $dataHandler): void
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::moveRecord_wsPlaceholders" is not in camel caps format
Loading history...
1343
    {
1344
        // If a record gets moved after a record that already has a placeholder record
1345
        // then the new placeholder record needs to be after the existing one
1346
        $originalRecordDestinationPid = $destPid;
1347
        $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid');
0 ignored issues
show
Bug introduced by
It seems like abs($destPid) can also be of type double; however, parameter $uid of TYPO3\CMS\Backend\Utilit...y::getMovePlaceholder() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1347
        $movePlaceHolder = BackendUtility::getMovePlaceholder($table, /** @scrutinizer ignore-type */ abs($destPid), 'uid');
Loading history...
1348
        if ($movePlaceHolder !== false && $destPid < 0) {
1349
            $destPid = -$movePlaceHolder['uid'];
1350
        }
1351
        if ($plh = BackendUtility::getMovePlaceholder($table, $uid, 'uid')) {
1352
            // If already a placeholder exists, move it:
1353
            $dataHandler->moveRecord_raw($table, $plh['uid'], $destPid);
1354
        } else {
1355
            // First, we create a placeholder record in the Live workspace that
1356
            // represents the position to where the record is eventually moved to.
1357
            $newVersion_placeholderFieldArray = [];
1358
1359
            $factory = GeneralUtility::makeInstance(
1360
                PlaceholderShadowColumnsResolver::class,
1361
                $table,
1362
                $GLOBALS['TCA'][$table] ?? []
1363
            );
1364
            $shadowColumns = $factory->forMovePlaceholder();
1365
            // Set values from the versioned record to the move placeholder
1366
            if (!empty($shadowColumns)) {
1367
                $versionedRecord = BackendUtility::getRecord($table, $offlineUid);
1368
                foreach ($shadowColumns as $shadowColumn) {
1369
                    if (isset($versionedRecord[$shadowColumn])) {
1370
                        $newVersion_placeholderFieldArray[$shadowColumn] = $versionedRecord[$shadowColumn];
1371
                    }
1372
                }
1373
            }
1374
1375
            if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1376
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1377
            }
1378
            if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1379
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $dataHandler->userid;
1380
            }
1381
            if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
1382
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1383
            }
1384
            if ($table === 'pages') {
1385
                // Copy page access settings from original page to placeholder
1386
                $perms_clause = $dataHandler->BE_USER->getPagePermsClause(Permission::PAGE_SHOW);
1387
                $access = BackendUtility::readPageAccess($uid, $perms_clause);
1388
                $newVersion_placeholderFieldArray['perms_userid'] = $access['perms_userid'];
1389
                $newVersion_placeholderFieldArray['perms_groupid'] = $access['perms_groupid'];
1390
                $newVersion_placeholderFieldArray['perms_user'] = $access['perms_user'];
1391
                $newVersion_placeholderFieldArray['perms_group'] = $access['perms_group'];
1392
                $newVersion_placeholderFieldArray['perms_everybody'] = $access['perms_everybody'];
1393
            }
1394
            $newVersion_placeholderFieldArray['t3ver_move_id'] = $uid;
1395
            // Setting placeholder state value for temporary record
1396
            $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER);
1397
            // Setting workspace - only so display of place holders can filter out those from other workspaces.
1398
            $newVersion_placeholderFieldArray['t3ver_wsid'] = $dataHandler->BE_USER->workspace;
1399
            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $dataHandler->getPlaceholderTitleForTableLabel($table, 'MOVE-TO PLACEHOLDER for #' . $uid);
1400
            // moving localized records requires to keep localization-settings for the placeholder too
1401
            if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
1402
                $l10nParentRec = BackendUtility::getRecord($table, $uid);
1403
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
1404
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
1405
                if (isset($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])) {
1406
                    $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']];
1407
                }
1408
                unset($l10nParentRec);
1409
            }
1410
            // @todo Check why $destPid cannot be used directly
1411
            // Initially, create at root level.
1412
            $newVersion_placeholderFieldArray['pid'] = 0;
1413
            $id = 'NEW_MOVE_PLH';
1414
            // Saving placeholder as 'original'
1415
            $dataHandler->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1416
            // Move the new placeholder from temporary root-level to location:
1417
            $dataHandler->moveRecord_raw($table, $dataHandler->substNEWwithIDs[$id], $destPid);
1418
            // Move the workspace-version of the original to be the version of the move-to-placeholder:
1419
            // Setting placeholder state value for version (so it can know it is currently a new version...)
1420
            $updateFields = [
1421
                'pid' => $resolvedId,
1422
                't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER)
1423
            ];
1424
1425
            GeneralUtility::makeInstance(ConnectionPool::class)
1426
                ->getConnectionForTable($table)
1427
                ->update(
1428
                    $table,
1429
                    $updateFields,
1430
                    ['uid' => (int)$offlineUid]
1431
                );
1432
        }
1433
        // Check for the localizations of that element and move them as well
1434
        $dataHandler->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
1435
    }
1436
1437
    /**
1438
     * Gets an instance of the command map helper.
1439
     *
1440
     * @param DataHandler $dataHandler DataHandler object
1441
     * @return CommandMap
1442
     */
1443
    public function getCommandMap(DataHandler $dataHandler): CommandMap
1444
    {
1445
        return GeneralUtility::makeInstance(
1446
            CommandMap::class,
1447
            $this,
1448
            $dataHandler,
1449
            $dataHandler->cmdmap,
1450
            $dataHandler->BE_USER->workspace
1451
        );
1452
    }
1453
1454
    protected function emitUpdateTopbarSignal(): void
1455
    {
1456
        BackendUtility::setUpdateSignal('updateTopbar');
1457
    }
1458
1459
    /**
1460
     * Returns all fieldnames from a table which have the unique evaluation type set.
1461
     *
1462
     * @param string $table Table name
1463
     * @return array Array of fieldnames
1464
     */
1465
    protected function getUniqueFields($table): array
1466
    {
1467
        $listArr = [];
1468
        foreach ($GLOBALS['TCA'][$table]['columns'] ?? [] as $field => $configArr) {
1469
            if ($configArr['config']['type'] === 'input') {
1470
                $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'], true);
1471
                if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) {
1472
                    $listArr[] = $field;
1473
                }
1474
            }
1475
        }
1476
        return $listArr;
1477
    }
1478
1479
    /**
1480
     * @return RelationHandler
1481
     */
1482
    protected function createRelationHandlerInstance(): RelationHandler
1483
    {
1484
        return GeneralUtility::makeInstance(RelationHandler::class);
1485
    }
1486
1487
    /**
1488
     * @return LanguageService
1489
     */
1490
    protected function getLanguageService(): LanguageService
1491
    {
1492
        return $GLOBALS['LANG'];
1493
    }
1494
}
1495