Completed
Push — master ( 18e02f...44418d )
by
unknown
14:17
created

DataHandlerHook::moveRecord()   F

Complexity

Conditions 24
Paths 769

Size

Total Lines 76
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 43
c 0
b 0
f 0
nc 769
nop 8
dl 0
loc 76
rs 0.3207

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\Context\Context;
23
use TYPO3\CMS\Core\Context\WorkspaceAspect;
24
use TYPO3\CMS\Core\Database\Connection;
25
use TYPO3\CMS\Core\Database\ConnectionPool;
26
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27
use TYPO3\CMS\Core\Database\ReferenceIndex;
28
use TYPO3\CMS\Core\Database\RelationHandler;
29
use TYPO3\CMS\Core\DataHandling\DataHandler;
30
use TYPO3\CMS\Core\DataHandling\PlaceholderShadowColumnsResolver;
31
use TYPO3\CMS\Core\Localization\LanguageService;
32
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
33
use TYPO3\CMS\Core\SysLog\Action\Database as DatabaseAction;
34
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
35
use TYPO3\CMS\Core\Type\Bitmask\Permission;
36
use TYPO3\CMS\Core\Utility\ArrayUtility;
37
use TYPO3\CMS\Core\Utility\GeneralUtility;
38
use TYPO3\CMS\Core\Versioning\VersionState;
39
use TYPO3\CMS\Workspaces\DataHandler\CommandMap;
40
use TYPO3\CMS\Workspaces\Notification\StageChangeNotification;
41
use TYPO3\CMS\Workspaces\Service\StagesService;
42
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
43
44
/**
45
 * Contains some parts for staging, versioning and workspaces
46
 * to interact with the TYPO3 Core Engine
47
 * @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
48
 */
49
class DataHandlerHook
50
{
51
    /**
52
     * For accumulating information about workspace stages raised
53
     * on elements so a single mail is sent as notification.
54
     *
55
     * @var array
56
     */
57
    protected $notificationEmailInfo = [];
58
59
    /**
60
     * Contains remapped IDs.
61
     *
62
     * @var array
63
     */
64
    protected $remappedIds = [];
65
66
    /****************************
67
     *****  Cmdmap  Hooks  ******
68
     ****************************/
69
    /**
70
     * hook that is called before any cmd of the commandmap is executed
71
     *
72
     * @param DataHandler $dataHandler reference to the main DataHandler object
73
     */
74
    public function processCmdmap_beforeStart(DataHandler $dataHandler)
75
    {
76
        // Reset notification array
77
        $this->notificationEmailInfo = [];
78
        // Resolve dependencies of version/workspaces actions:
79
        $dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get();
80
    }
81
82
    /**
83
     * hook that is called when no prepared command was found
84
     *
85
     * @param string $command the command to be executed
86
     * @param string $table the table of the record
87
     * @param int $id the ID of the record
88
     * @param mixed $value the value containing the data
89
     * @param bool $commandIsProcessed can be set so that other hooks or
90
     * @param DataHandler $dataHandler reference to the main DataHandler object
91
     */
92
    public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
93
    {
94
        // custom command "version"
95
        if ($command !== 'version') {
96
            return;
97
        }
98
        $commandIsProcessed = true;
99
        $action = (string)$value['action'];
100
        $comment = $value['comment'] ?: '';
101
        $notificationAlternativeRecipients = $value['notificationAlternativeRecipients'] ?? [];
102
        switch ($action) {
103
            case 'new':
104
                $dataHandler->versionizeRecord($table, $id, $value['label']);
105
                break;
106
            case 'swap':
107
            case 'publish':
108
                $this->version_swap(
109
                    $table,
110
                    $id,
111
                    $value['swapWith'],
112
                    $dataHandler,
113
                    $comment,
114
                    $notificationAlternativeRecipients
115
                );
116
                break;
117
            case 'clearWSID':
118
            case 'flush':
119
                $dataHandler->discard($table, (int)$id);
120
                break;
121
            case 'setStage':
122
                $elementIds = GeneralUtility::intExplode(',', (string)$id, true);
123
                foreach ($elementIds as $elementId) {
124
                    $this->version_setStage(
125
                        $table,
126
                        $elementId,
127
                        $value['stageId'],
128
                        $comment,
129
                        $dataHandler,
130
                        $notificationAlternativeRecipients
131
                    );
132
                }
133
                break;
134
            default:
135
                // Do nothing
136
        }
137
    }
138
139
    /**
140
     * hook that is called AFTER all commands of the commandmap was
141
     * executed
142
     *
143
     * @param DataHandler $dataHandler reference to the main DataHandler object
144
     */
145
    public function processCmdmap_afterFinish(DataHandler $dataHandler)
146
    {
147
        // Empty accumulation array
148
        $emailNotificationService = GeneralUtility::makeInstance(StageChangeNotification::class);
149
        $this->sendStageChangeNotification(
150
            $this->notificationEmailInfo,
151
            $emailNotificationService,
152
            $dataHandler
153
        );
154
155
        // Reset notification array
156
        $this->notificationEmailInfo = [];
157
        // Reset remapped IDs
158
        $this->remappedIds = [];
159
160
        $this->flushWorkspaceCacheEntriesByWorkspaceId((int)$dataHandler->BE_USER->workspace);
161
    }
162
163
    protected function sendStageChangeNotification(
164
        array $accumulatedNotificationInformation,
165
        StageChangeNotification $notificationService,
166
        DataHandler $dataHandler
167
    ): void {
168
        foreach ($accumulatedNotificationInformation as $groupedNotificationInformation) {
169
            $emails = (array)$groupedNotificationInformation['recipients'];
170
            if (empty($emails)) {
171
                continue;
172
            }
173
            $workspaceRec = $groupedNotificationInformation['shared'][0];
174
            if (!is_array($workspaceRec)) {
175
                continue;
176
            }
177
            $notificationService->notifyStageChange(
178
                $workspaceRec,
179
                (int)$groupedNotificationInformation['shared'][1],
180
                $groupedNotificationInformation['elements'],
181
                $groupedNotificationInformation['shared'][2],
182
                $emails,
183
                $dataHandler->BE_USER
184
            );
185
186
            if ($dataHandler->enableLogging) {
187
                [$elementTable, $elementUid] = reset($groupedNotificationInformation['elements']);
188
                $propertyArray = $dataHandler->getRecordProperties($elementTable, $elementUid);
189
                $pid = $propertyArray['pid'];
190
                $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));
191
            }
192
        }
193
    }
194
195
    /**
196
     * hook that is called when an element shall get deleted
197
     *
198
     * @param string $table the table of the record
199
     * @param int $id the ID of the record
200
     * @param array $record The accordant database record
201
     * @param bool $recordWasDeleted can be set so that other hooks or
202
     * @param DataHandler $dataHandler reference to the main DataHandler object
203
     */
204
    public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler)
205
    {
206
        // only process the hook if it wasn't processed
207
        // by someone else before
208
        if ($recordWasDeleted) {
209
            return;
210
        }
211
        $recordWasDeleted = true;
212
        // For Live version, try if there is a workspace version because if so, rather "delete" that instead
213
        // Look, if record is an offline version, then delete directly:
214
        if ((int)($record['t3ver_oid'] ?? 0) === 0) {
215
            if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) {
216
                $record = $wsVersion;
217
                $id = $record['uid'];
218
            }
219
        }
220
        $recordVersionState = VersionState::cast($record['t3ver_state']);
221
        // Look, if record is an offline version, then delete directly:
222
        if ((int)($record['t3ver_oid'] ?? 0) > 0) {
223
            if (BackendUtility::isTableWorkspaceEnabled($table)) {
224
                // In Live workspace, delete any. In other workspaces there must be match.
225
                if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) {
226
                    $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
227
                    // Processing can be skipped if a delete placeholder shall be published
228
                    // during the current request. Thus it will be deleted later on...
229
                    $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']);
230
                    if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid'])
231
                        && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'])
232
                        && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
233
                        && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
234
                        && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id
235
                    ) {
236
                        return null;
237
                    }
238
239
                    if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) {
240
                        // Change normal versioned record to delete placeholder
241
                        // Happens when an edited record is deleted
242
                        GeneralUtility::makeInstance(ConnectionPool::class)
243
                            ->getConnectionForTable($table)
244
                            ->update(
245
                                $table,
246
                                ['t3ver_state' => VersionState::DELETE_PLACEHOLDER],
247
                                ['uid' => $id]
248
                            );
249
250
                        // Delete localization overlays:
251
                        $dataHandler->deleteL10nOverlayRecords($table, $id);
252
                    } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) {
253
                        // Delete those in WS 0 + if their live records state was not "Placeholder".
254
                        $dataHandler->deleteEl($table, $id);
255
                        if ($recordVersionState->equals(VersionState::MOVE_POINTER)) {
256
                            // Delete move-placeholder if current version record is a move-to-pointer.
257
                            // deleteEl() can't be used here: The deleteEl() for the MOVE_POINTER record above
258
                            // already triggered a delete cascade for children (inline, ...). If we'd
259
                            // now call deleteEl() again, we'd trigger adding delete placeholder records for children.
260
                            // Thus, it's safe here to just set the MOVE_PLACEHOLDER to deleted (or drop row) straight ahead.
261
                            $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid', $record['t3ver_wsid']);
262
                            if (!empty($movePlaceholder)) {
263
                                $this->softOrHardDeleteSingleRecord($table, (int)$movePlaceholder['uid']);
264
                            }
265
                        }
266
                    } elseif ($recordVersionState->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
267
                        $placeholderRecord = BackendUtility::getLiveVersionOfRecord($table, (int)$id);
268
                        $dataHandler->deleteEl($table, (int)$id);
269
                        if (is_array($placeholderRecord)) {
270
                            $this->softOrHardDeleteSingleRecord($table, (int)$placeholderRecord['uid']);
271
                        }
272
                    }
273
                } else {
274
                    $dataHandler->newlog('Tried to delete record from another workspace', SystemLogErrorClassification::USER_ERROR);
275
                }
276
            } else {
277
                $dataHandler->newlog('Versioning not enabled for record with an online ID (t3ver_oid) given', SystemLogErrorClassification::SYSTEM_ERROR);
278
            }
279
        } elseif ($dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
280
            // Look, if record is "online" then delete directly.
281
            $dataHandler->deleteEl($table, $id);
282
        } elseif ($recordVersionState->equals(VersionState::MOVE_PLACEHOLDER)) {
283
            // Placeholders for moving operations are deletable directly.
284
            // Get record which its a placeholder for and reset the t3ver_state of that:
285
            if ($wsRec = BackendUtility::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) {
286
                // Clear the state flag of the workspace version of the record
287
                // Setting placeholder state value for version (so it can know it is currently a new version...)
288
289
                GeneralUtility::makeInstance(ConnectionPool::class)
290
                    ->getConnectionForTable($table)
291
                    ->update(
292
                        $table,
293
                        [
294
                            't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE)
295
                        ],
296
                        ['uid' => (int)$wsRec['uid']]
297
                    );
298
            }
299
            $dataHandler->deleteEl($table, $id);
300
        } else {
301
            // Otherwise, try to delete by versioning:
302
            $copyMappingArray = $dataHandler->copyMappingArray;
303
            $dataHandler->versionizeRecord($table, $id, 'DELETED!', true);
304
            // Determine newly created versions:
305
            // (remove placeholders are copied and modified, thus they appear in the copyMappingArray)
306
            $versionizedElements = ArrayUtility::arrayDiffAssocRecursive($dataHandler->copyMappingArray, $copyMappingArray);
307
            // Delete localization overlays:
308
            foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) {
309
                foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) {
310
                    $dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId);
311
                }
312
            }
313
        }
314
    }
315
316
    /**
317
     * In case a sys_workspace_stage record is deleted we do a hard reset
318
     * for all existing records in that stage to avoid that any of these end up
319
     * as orphan records.
320
     *
321
     * @param string $command
322
     * @param string $table
323
     * @param string $id
324
     * @param string $value
325
     * @param DataHandler $dataHandler
326
     */
327
    public function processCmdmap_postProcess($command, $table, $id, $value, DataHandler $dataHandler)
328
    {
329
        if ($command === 'delete') {
330
            if ($table === StagesService::TABLE_STAGE) {
331
                $this->resetStageOfElements((int)$id);
332
            } elseif ($table === WorkspaceService::TABLE_WORKSPACE) {
333
                $this->flushWorkspaceElements((int)$id);
334
                $this->emitUpdateTopbarSignal();
335
            }
336
        }
337
    }
338
339
    public function processDatamap_afterAllOperations(DataHandler $dataHandler): void
340
    {
341
        if (isset($dataHandler->datamap[WorkspaceService::TABLE_WORKSPACE])) {
342
            $this->emitUpdateTopbarSignal();
343
        }
344
    }
345
346
    /**
347
     * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about
348
     * moving records that are *not* in the live workspace
349
     *
350
     * @param string $table the table of the record
351
     * @param int $uid the ID of the record
352
     * @param int $destPid Position to move to: $destPid: >=0 then it points to
353
     * @param array $propArr Record properties, like header and pid (includes workspace overlay)
354
     * @param array $moveRec Record properties, like header and pid (without workspace overlay)
355
     * @param int $resolvedPid The final page ID of the record
356
     * @param bool $recordWasMoved can be set so that other hooks or
357
     * @param DataHandler $dataHandler
358
     */
359
    public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)
360
    {
361
        // Only do something in Draft workspace
362
        if ($dataHandler->BE_USER->workspace === 0) {
363
            return;
364
        }
365
        $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
366
        // Fetch move placeholder, since it might point to a new page in the current workspace
367
        $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

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

997
        $relationHandler->writeForeignField($configuration, /** @scrutinizer ignore-type */ $parentId);
Loading history...
998
    }
999
1000
    /**
1001
     * In case a sys_workspace_stage record is deleted we do a hard reset
1002
     * for all existing records in that stage to avoid that any of these end up
1003
     * as orphan records.
1004
     *
1005
     * @param int $stageId Elements with this stage are reset
1006
     */
1007
    protected function resetStageOfElements(int $stageId): void
1008
    {
1009
        foreach ($this->getTcaTables() as $tcaTable) {
1010
            if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
1011
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1012
                    ->getQueryBuilderForTable($tcaTable);
1013
1014
                $queryBuilder
1015
                    ->update($tcaTable)
1016
                    ->set('t3ver_stage', StagesService::STAGE_EDIT_ID)
1017
                    ->where(
1018
                        $queryBuilder->expr()->eq(
1019
                            't3ver_stage',
1020
                            $queryBuilder->createNamedParameter($stageId, \PDO::PARAM_INT)
1021
                        ),
1022
                        $queryBuilder->expr()->gt(
1023
                            't3ver_wsid',
1024
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1025
                        )
1026
                    )
1027
                    ->execute();
1028
            }
1029
        }
1030
    }
1031
1032
    /**
1033
     * Flushes (remove, no soft delete!) elements of a particular workspace to avoid orphan records.
1034
     * This is used if an admin deletes a sys_workspace record.
1035
     *
1036
     * @param int $workspaceId The workspace to be flushed
1037
     */
1038
    protected function flushWorkspaceElements(int $workspaceId): void
1039
    {
1040
        $command = [];
1041
        foreach ($this->getTcaTables() as $tcaTable) {
1042
            if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
1043
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1044
                    ->getQueryBuilderForTable($tcaTable);
1045
                $queryBuilder->getRestrictions()->removeAll();
1046
                $result = $queryBuilder
1047
                    ->select('uid')
1048
                    ->from($tcaTable)
1049
                    ->where(
1050
                        $queryBuilder->expr()->eq(
1051
                            't3ver_wsid',
1052
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1053
                        ),
1054
                        // t3ver_oid >= 0 basically omits placeholder records here, those would otherwise
1055
                        // fail to delete later in DH->discard() and would create "can't do that" log entries.
1056
                        $queryBuilder->expr()->gt(
1057
                            't3ver_oid',
1058
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1059
                        )
1060
                    )
1061
                    ->orderBy('uid')
1062
                    ->execute();
1063
1064
                while (($recordId = $result->fetchColumn()) !== false) {
1065
                    $command[$tcaTable][$recordId]['version']['action'] = 'flush';
1066
                }
1067
            }
1068
        }
1069
        if (!empty($command)) {
1070
            // Execute the command array via DataHandler to flush all records from this workspace.
1071
            // Switch to target workspace temporarily, otherwise DH->discard() do not
1072
            // operate on correct workspace if fetching additional records.
1073
            $backendUser = $GLOBALS['BE_USER'];
1074
            $savedWorkspace = $backendUser->workspace;
1075
            $backendUser->workspace = $workspaceId;
1076
            $context = GeneralUtility::makeInstance(Context::class);
1077
            $savedWorkspaceContext = $context->getAspect('workspace');
1078
            $context->setAspect('workspace', new WorkspaceAspect($workspaceId));
1079
1080
            $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
1081
            $dataHandler->start([], $command, $backendUser);
1082
            $dataHandler->process_cmdmap();
1083
1084
            $backendUser->workspace = $savedWorkspace;
1085
            $context->setAspect('workspace', $savedWorkspaceContext);
1086
        }
1087
    }
1088
1089
    /**
1090
     * Gets all defined TCA tables.
1091
     *
1092
     * @return array
1093
     */
1094
    protected function getTcaTables(): array
1095
    {
1096
        return array_keys($GLOBALS['TCA']);
1097
    }
1098
1099
    /**
1100
     * Flushes the workspace cache for current workspace and for the virtual "all workspaces" too.
1101
     *
1102
     * @param int $workspaceId The workspace to be flushed in cache
1103
     */
1104
    protected function flushWorkspaceCacheEntriesByWorkspaceId(int $workspaceId): void
1105
    {
1106
        $workspacesCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('workspaces_cache');
1107
        $workspacesCache->flushByTag($workspaceId);
1108
    }
1109
1110
    /*******************************
1111
     *****  helper functions  ******
1112
     *******************************/
1113
1114
    /**
1115
     * Finds all elements for swapping versions in workspace
1116
     *
1117
     * @param string $table Table name of the original element to swap
1118
     * @param int $id UID of the original element to swap (online)
1119
     * @param int $offlineId As above but offline
1120
     * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID
1121
     */
1122
    public function findPageElementsForVersionSwap($table, $id, $offlineId)
1123
    {
1124
        $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid');
1125
        $workspaceId = (int)$rec['t3ver_wsid'];
1126
        $elementData = [];
1127
        if ($workspaceId === 0) {
1128
            return $elementData;
1129
        }
1130
        // Get page UID for LIVE and workspace
1131
        if ($table !== 'pages') {
1132
            $rec = BackendUtility::getRecord($table, $id, 'pid');
1133
            $pageId = $rec['pid'];
1134
            $rec = BackendUtility::getRecord('pages', $pageId);
1135
            BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1136
            $offlinePageId = $rec['_ORIG_uid'];
1137
        } else {
1138
            $pageId = $id;
1139
            $offlinePageId = $offlineId;
1140
        }
1141
        // Traversing all tables supporting versioning:
1142
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
0 ignored issues
show
introduced by
$table is overwriting one of the parameters of this function.
Loading history...
1143
            if (BackendUtility::isTableWorkspaceEnabled($table) && $table !== 'pages') {
1144
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1145
                    ->getQueryBuilderForTable($table);
1146
1147
                $queryBuilder->getRestrictions()
1148
                    ->removeAll()
1149
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1150
1151
                $statement = $queryBuilder
1152
                    ->select('A.uid AS offlineUid', 'B.uid AS uid')
1153
                    ->from($table, 'A')
1154
                    ->from($table, 'B')
1155
                    ->where(
1156
                        $queryBuilder->expr()->gt(
1157
                            'A.t3ver_oid',
1158
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1159
                        ),
1160
                        $queryBuilder->expr()->eq(
1161
                            'B.pid',
1162
                            $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
1163
                        ),
1164
                        $queryBuilder->expr()->eq(
1165
                            'A.t3ver_wsid',
1166
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1167
                        ),
1168
                        $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1169
                    )
1170
                    ->execute();
1171
1172
                while ($row = $statement->fetch()) {
1173
                    $elementData[$table][] = [$row['uid'], $row['offlineUid']];
1174
                }
1175
            }
1176
        }
1177
        if ($offlinePageId && $offlinePageId != $pageId) {
1178
            $elementData['pages'][] = [$pageId, $offlinePageId];
1179
        }
1180
1181
        return $elementData;
1182
    }
1183
1184
    /**
1185
     * Searches for all elements from all tables on the given pages in the same workspace.
1186
     *
1187
     * @param array $pageIdList List of PIDs to search
1188
     * @param int $workspaceId Workspace ID
1189
     * @param array $elementList List of found elements. Key is table name, value is array of element UIDs
1190
     */
1191
    public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList)
1192
    {
1193
        if ($workspaceId == 0) {
1194
            return;
1195
        }
1196
        // Traversing all tables supporting versioning:
1197
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
1198
            if (BackendUtility::isTableWorkspaceEnabled($table) && $table !== 'pages') {
1199
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1200
                    ->getQueryBuilderForTable($table);
1201
1202
                $queryBuilder->getRestrictions()
1203
                    ->removeAll()
1204
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1205
1206
                $statement = $queryBuilder
1207
                    ->select('A.uid')
1208
                    ->from($table, 'A')
1209
                    ->from($table, 'B')
1210
                    ->where(
1211
                        $queryBuilder->expr()->gt(
1212
                            'A.t3ver_oid',
1213
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1214
                        ),
1215
                        $queryBuilder->expr()->in(
1216
                            'B.pid',
1217
                            $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY)
1218
                        ),
1219
                        $queryBuilder->expr()->eq(
1220
                            'A.t3ver_wsid',
1221
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1222
                        ),
1223
                        $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1224
                    )
1225
                    ->groupBy('A.uid')
1226
                    ->execute();
1227
1228
                while ($row = $statement->fetch()) {
1229
                    $elementList[$table][] = $row['uid'];
1230
                }
1231
                if (is_array($elementList[$table])) {
1232
                    // Yes, it is possible to get non-unique array even with DISTINCT above!
1233
                    // It happens because several UIDs are passed in the array already.
1234
                    $elementList[$table] = array_unique($elementList[$table]);
1235
                }
1236
            }
1237
        }
1238
    }
1239
1240
    /**
1241
     * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1242
     *
1243
     * @param string $table Table to search
1244
     * @param array $idList List of records' UIDs
1245
     * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publish DRAFT from ws module!
1246
     * @param array $pageIdList List of found page UIDs
1247
     * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs
1248
     */
1249
    public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList)
1250
    {
1251
        if ($workspaceId == 0) {
1252
            return;
1253
        }
1254
1255
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1256
            ->getQueryBuilderForTable($table);
1257
        $queryBuilder->getRestrictions()
1258
            ->removeAll()
1259
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1260
1261
        $statement = $queryBuilder
1262
            ->select('B.pid')
1263
            ->from($table, 'A')
1264
            ->from($table, 'B')
1265
            ->where(
1266
                $queryBuilder->expr()->gt(
1267
                    'A.t3ver_oid',
1268
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1269
                ),
1270
                $queryBuilder->expr()->eq(
1271
                    'A.t3ver_wsid',
1272
                    $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1273
                ),
1274
                $queryBuilder->expr()->in(
1275
                    'A.uid',
1276
                    $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY)
1277
                ),
1278
                $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1279
            )
1280
            ->groupBy('B.pid')
1281
            ->execute();
1282
1283
        while ($row = $statement->fetch()) {
1284
            $pageIdList[] = $row['pid'];
1285
            // Find ws version
1286
            // Note: cannot use BackendUtility::getRecordWSOL()
1287
            // here because it does not accept workspace id!
1288
            $rec = BackendUtility::getRecord('pages', $row[0]);
1289
            BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1290
            if ($rec['_ORIG_uid']) {
1291
                $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1292
            }
1293
        }
1294
        // The line below is necessary even with DISTINCT
1295
        // because several elements can be passed by caller
1296
        $pageIdList = array_unique($pageIdList);
1297
    }
1298
1299
    /**
1300
     * Finds real page IDs for state change.
1301
     *
1302
     * @param array $idList List of page UIDs, possibly versioned
1303
     */
1304
    public function findRealPageIds(array &$idList): void
1305
    {
1306
        foreach ($idList as $key => $id) {
1307
            $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid');
1308
            if ($rec['t3ver_oid'] > 0) {
1309
                $idList[$key] = $rec['t3ver_oid'];
1310
            }
1311
        }
1312
    }
1313
1314
    /**
1315
     * Creates a move placeholder for workspaces.
1316
     * USE ONLY INTERNALLY
1317
     * Moving placeholder: Can be done because the system sees it as a placeholder for NEW elements like t3ver_state=VersionState::NEW_PLACEHOLDER
1318
     * Moving original: Will either create the placeholder if it doesn't exist or move existing placeholder in workspace.
1319
     *
1320
     * @param string $table Table name to move
1321
     * @param int $uid Record uid to move (online record)
1322
     * @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
1323
     * @param int $resolvedId Effective page ID
1324
     * @param int $offlineUid UID of offline version of online record
1325
     * @param DataHandler $dataHandler DataHandler object
1326
     * @see moveRecord()
1327
     */
1328
    protected function moveRecord_wsPlaceholders(string $table, int $uid, int $destPid, int $resolvedId, int $offlineUid, DataHandler $dataHandler): void
1329
    {
1330
        // If a record gets moved after a record that already has a placeholder record
1331
        // then the new placeholder record needs to be after the existing one
1332
        $originalRecordDestinationPid = $destPid;
1333
        $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

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