DataHandlerHook::moveRecord()   F
last analyzed

Complexity

Conditions 22
Paths 577

Size

Total Lines 60
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 22
eloc 35
nc 577
nop 8
dl 0
loc 60
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\Exception as DBALException;
19
use Doctrine\DBAL\Platforms\SQLServer2012Platform as 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\RelationHandler;
28
use TYPO3\CMS\Core\DataHandling\DataHandler;
29
use TYPO3\CMS\Core\Localization\LanguageService;
30
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
31
use TYPO3\CMS\Core\SysLog\Action\Database as DatabaseAction;
32
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
33
use TYPO3\CMS\Core\Type\Bitmask\Permission;
34
use TYPO3\CMS\Core\Utility\ArrayUtility;
35
use TYPO3\CMS\Core\Utility\GeneralUtility;
36
use TYPO3\CMS\Core\Versioning\VersionState;
37
use TYPO3\CMS\Workspaces\DataHandler\CommandMap;
38
use TYPO3\CMS\Workspaces\Notification\StageChangeNotification;
39
use TYPO3\CMS\Workspaces\Service\StagesService;
40
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
41
42
/**
43
 * Contains some parts for staging, versioning and workspaces
44
 * to interact with the TYPO3 Core Engine
45
 * @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
46
 */
47
class DataHandlerHook
48
{
49
    /**
50
     * For accumulating information about workspace stages raised
51
     * on elements so a single mail is sent as notification.
52
     *
53
     * @var array
54
     */
55
    protected $notificationEmailInfo = [];
56
57
    /**
58
     * Contains remapped IDs.
59
     *
60
     * @var array
61
     */
62
    protected $remappedIds = [];
63
64
    /****************************
65
     *****  Cmdmap  Hooks  ******
66
     ****************************/
67
    /**
68
     * hook that is called before any cmd of the commandmap is executed
69
     *
70
     * @param DataHandler $dataHandler reference to the main DataHandler object
71
     */
72
    public function processCmdmap_beforeStart(DataHandler $dataHandler)
73
    {
74
        // Reset notification array
75
        $this->notificationEmailInfo = [];
76
        // Resolve dependencies of version/workspaces actions:
77
        $dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get();
78
    }
79
80
    /**
81
     * hook that is called when no prepared command was found
82
     *
83
     * @param string $command the command to be executed
84
     * @param string $table the table of the record
85
     * @param int $id the ID of the record
86
     * @param mixed $value the value containing the data
87
     * @param bool $commandIsProcessed can be set so that other hooks or
88
     * @param DataHandler $dataHandler reference to the main DataHandler object
89
     */
90
    public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
91
    {
92
        // custom command "version"
93
        if ($command !== 'version') {
94
            return;
95
        }
96
        $commandIsProcessed = true;
97
        $action = (string)$value['action'];
98
        $comment = $value['comment'] ?? '';
99
        $notificationAlternativeRecipients = $value['notificationAlternativeRecipients'] ?? [];
100
        switch ($action) {
101
            case 'new':
102
                $dataHandler->versionizeRecord($table, $id, $value['label']);
103
                break;
104
            case 'swap':
105
            case 'publish':
106
                $this->version_swap(
107
                    $table,
108
                    $id,
109
                    $value['swapWith'],
110
                    $dataHandler,
111
                    $comment,
112
                    $notificationAlternativeRecipients
113
                );
114
                break;
115
            case 'clearWSID':
116
            case 'flush':
117
                $dataHandler->discard($table, (int)$id);
118
                break;
119
            case 'setStage':
120
                $elementIds = GeneralUtility::intExplode(',', (string)$id, true);
121
                foreach ($elementIds as $elementId) {
122
                    $this->version_setStage(
123
                        $table,
124
                        $elementId,
125
                        $value['stageId'],
126
                        $comment,
127
                        $dataHandler,
128
                        $notificationAlternativeRecipients
129
                    );
130
                }
131
                break;
132
            default:
133
                // Do nothing
134
        }
135
    }
136
137
    /**
138
     * hook that is called AFTER all commands of the commandmap was
139
     * executed
140
     *
141
     * @param DataHandler $dataHandler reference to the main DataHandler object
142
     */
143
    public function processCmdmap_afterFinish(DataHandler $dataHandler)
144
    {
145
        // Empty accumulation array
146
        $emailNotificationService = GeneralUtility::makeInstance(StageChangeNotification::class);
147
        $this->sendStageChangeNotification(
148
            $this->notificationEmailInfo,
149
            $emailNotificationService,
150
            $dataHandler
151
        );
152
153
        // Reset notification array
154
        $this->notificationEmailInfo = [];
155
        // Reset remapped IDs
156
        $this->remappedIds = [];
157
158
        $this->flushWorkspaceCacheEntriesByWorkspaceId((int)$dataHandler->BE_USER->workspace);
159
    }
160
161
    protected function sendStageChangeNotification(
162
        array $accumulatedNotificationInformation,
163
        StageChangeNotification $notificationService,
164
        DataHandler $dataHandler
165
    ): void {
166
        foreach ($accumulatedNotificationInformation as $groupedNotificationInformation) {
167
            $emails = (array)$groupedNotificationInformation['recipients'];
168
            if (empty($emails)) {
169
                continue;
170
            }
171
            $workspaceRec = $groupedNotificationInformation['shared'][0];
172
            if (!is_array($workspaceRec)) {
173
                continue;
174
            }
175
            $notificationService->notifyStageChange(
176
                $workspaceRec,
177
                (int)$groupedNotificationInformation['shared'][1],
178
                $groupedNotificationInformation['elements'],
179
                $groupedNotificationInformation['shared'][2],
180
                $emails,
181
                $dataHandler->BE_USER
182
            );
183
184
            if ($dataHandler->enableLogging) {
185
                [$elementTable, $elementUid] = reset($groupedNotificationInformation['elements']);
186
                $propertyArray = $dataHandler->getRecordProperties($elementTable, $elementUid);
187
                $pid = $propertyArray['pid'];
188
                $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));
189
            }
190
        }
191
    }
192
193
    /**
194
     * hook that is called when an element shall get deleted
195
     *
196
     * @param string $table the table of the record
197
     * @param int $id the ID of the record
198
     * @param array $record The accordant database record
199
     * @param bool $recordWasDeleted can be set so that other hooks or
200
     * @param DataHandler $dataHandler reference to the main DataHandler object
201
     */
202
    public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler)
203
    {
204
        // only process the hook if it wasn't processed
205
        // by someone else before
206
        if ($recordWasDeleted) {
207
            return;
208
        }
209
        $recordWasDeleted = true;
210
        // For Live version, try if there is a workspace version because if so, rather "delete" that instead
211
        // Look, if record is an offline version, then delete directly:
212
        if ((int)($record['t3ver_oid'] ?? 0) === 0) {
213
            if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) {
214
                $record = $wsVersion;
215
                $id = $record['uid'];
216
            }
217
        }
218
        $recordVersionState = VersionState::cast($record['t3ver_state'] ?? 0);
219
        // Look, if record is an offline version, then delete directly:
220
        if ((int)($record['t3ver_oid'] ?? 0) > 0) {
221
            if (BackendUtility::isTableWorkspaceEnabled($table)) {
222
                // In Live workspace, delete any. In other workspaces there must be match.
223
                if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) {
224
                    $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
225
                    // Processing can be skipped if a delete placeholder shall be published
226
                    // during the current request. Thus it will be deleted later on...
227
                    $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']);
228
                    if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid'])
229
                        && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'])
230
                        && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
231
                        && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
232
                        && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id
233
                    ) {
234
                        return null;
235
                    }
236
237
                    if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) {
238
                        // Change normal versioned record to delete placeholder
239
                        // Happens when an edited record is deleted
240
                        GeneralUtility::makeInstance(ConnectionPool::class)
241
                            ->getConnectionForTable($table)
242
                            ->update(
243
                                $table,
244
                                ['t3ver_state' => VersionState::DELETE_PLACEHOLDER],
245
                                ['uid' => $id]
246
                            );
247
248
                        // Delete localization overlays:
249
                        $dataHandler->deleteL10nOverlayRecords($table, $id);
250
                    } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) {
251
                        // Delete those in WS 0 + if their live records state was not "Placeholder".
252
                        $dataHandler->deleteEl($table, $id);
253
                    } elseif ($recordVersionState->equals(VersionState::NEW_PLACEHOLDER)) {
254
                        $placeholderRecord = BackendUtility::getLiveVersionOfRecord($table, (int)$id);
255
                        $dataHandler->deleteEl($table, (int)$id);
256
                        if (is_array($placeholderRecord)) {
257
                            $this->softOrHardDeleteSingleRecord($table, (int)$placeholderRecord['uid']);
258
                        }
259
                    }
260
                } else {
261
                    $dataHandler->newlog('Tried to delete record from another workspace', SystemLogErrorClassification::USER_ERROR);
262
                }
263
            } else {
264
                $dataHandler->newlog('Versioning not enabled for record with an online ID (t3ver_oid) given', SystemLogErrorClassification::SYSTEM_ERROR);
265
            }
266
        } elseif ($recordVersionState->equals(VersionState::NEW_PLACEHOLDER)) {
267
            // If it is a new versioned record, delete it directly.
268
            $dataHandler->deleteEl($table, $id);
269
        } elseif ($dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
270
            // Look, if record is "online" then delete directly.
271
            $dataHandler->deleteEl($table, $id);
272
        } else {
273
            // Otherwise, try to delete by versioning:
274
            $copyMappingArray = $dataHandler->copyMappingArray;
275
            $dataHandler->versionizeRecord($table, $id, 'DELETED!', true);
276
            // Determine newly created versions:
277
            // (remove placeholders are copied and modified, thus they appear in the copyMappingArray)
278
            $versionizedElements = ArrayUtility::arrayDiffKeyRecursive($dataHandler->copyMappingArray, $copyMappingArray);
279
            // Delete localization overlays:
280
            foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) {
281
                foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) {
282
                    $dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId);
283
                }
284
            }
285
        }
286
    }
287
288
    /**
289
     * In case a sys_workspace_stage record is deleted we do a hard reset
290
     * for all existing records in that stage to avoid that any of these end up
291
     * as orphan records.
292
     *
293
     * @param string $command
294
     * @param string $table
295
     * @param string $id
296
     * @param string $value
297
     * @param DataHandler $dataHandler
298
     */
299
    public function processCmdmap_postProcess($command, $table, $id, $value, DataHandler $dataHandler)
300
    {
301
        if ($command === 'delete') {
302
            if ($table === StagesService::TABLE_STAGE) {
303
                $this->resetStageOfElements((int)$id);
304
            } elseif ($table === WorkspaceService::TABLE_WORKSPACE) {
305
                $this->flushWorkspaceElements((int)$id);
306
                $this->emitUpdateTopbarSignal();
307
            }
308
        }
309
    }
310
311
    public function processDatamap_afterAllOperations(DataHandler $dataHandler): void
312
    {
313
        if (isset($dataHandler->datamap[WorkspaceService::TABLE_WORKSPACE])) {
314
            $this->emitUpdateTopbarSignal();
315
        }
316
    }
317
318
    /**
319
     * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about
320
     * moving records that are *not* in the live workspace
321
     *
322
     * @param string $table the table of the record
323
     * @param int $uid the ID of the record
324
     * @param int $destPid Position to move to: $destPid: >=0 then it points to
325
     * @param array $propArr Record properties, like header and pid (includes workspace overlay)
326
     * @param array $moveRec Record properties, like header and pid (without workspace overlay)
327
     * @param int $resolvedPid The final page ID of the record
328
     * @param bool $recordWasMoved can be set so that other hooks or
329
     * @param DataHandler $dataHandler
330
     */
331
    public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)
332
    {
333
        // Only do something in Draft workspace
334
        if ($dataHandler->BE_USER->workspace === 0) {
335
            return;
336
        }
337
        $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
338
        $recordWasMoved = true;
339
        $moveRecVersionState = VersionState::cast((int)($moveRec['t3ver_state'] ?? VersionState::DEFAULT_STATE));
340
        // Get workspace version of the source record, if any:
341
        $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
342
        if ($tableSupportsVersioning) {
343
            // Create version of record first, if it does not exist
344
            if (empty($versionedRecord['uid'])) {
345
                $dataHandler->versionizeRecord($table, $uid, 'MovePointer');
346
                $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
347
                if ((int)$resolvedPid !== (int)$propArr['pid']) {
348
                    $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
349
                }
350
            } elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$versionedRecord['uid']) {
351
                // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders
352
                if ((int)$resolvedPid !== (int)$propArr['pid']) {
353
                    $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
354
                }
355
            }
356
        }
357
        // Check workspace permissions:
358
        $workspaceAccessBlocked = [];
359
        // Element was in "New/Deleted/Moved" so it can be moved...
360
        $recIsNewVersion = $moveRecVersionState->equals(VersionState::NEW_PLACEHOLDER) || $moveRecVersionState->indicatesPlaceholder();
361
        $recordMustNotBeVersionized = $dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table);
362
        $canMoveRecord = $recIsNewVersion || $tableSupportsVersioning;
363
        // Workspace source check:
364
        if (!$recIsNewVersion) {
365
            $errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $versionedRecord['uid'] ?: $uid);
366
            if ($errorCode) {
367
                $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' ';
368
            } elseif (!$canMoveRecord && !$recordMustNotBeVersionized) {
369
                $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" ';
370
            }
371
        }
372
        // Workspace destination check:
373
        // All records can be inserted if $recordMustNotBeVersionized is true.
374
        // Only new versions can be inserted if $recordMustNotBeVersionized is FALSE.
375
        if (!($recordMustNotBeVersionized || $canMoveRecord && !$recordMustNotBeVersionized)) {
376
            $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" ';
377
        }
378
379
        if (empty($workspaceAccessBlocked)) {
380
            $versionedRecordUid = (int)$versionedRecord['uid'];
381
            // custom moving not needed, just behave like in live workspace (also for newly versioned records)
382
            if (!$versionedRecordUid || !$tableSupportsVersioning || $recIsNewVersion) {
383
                $recordWasMoved = false;
384
            } else {
385
                // If the move operation is done on a versioned record, which is
386
                // NOT new/deleted placeholder, then mark the versioned record as "moved"
387
                $this->moveRecord_moveVersionedRecord($table, (int)$uid, (int)$destPid, $versionedRecordUid, $dataHandler);
388
            }
389
        } else {
390
            $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), SystemLogErrorClassification::USER_ERROR);
391
        }
392
    }
393
394
    /**
395
     * Processes fields of a moved record and follows references.
396
     *
397
     * @param DataHandler $dataHandler Calling DataHandler instance
398
     * @param int $resolvedPageId Resolved real destination page id
399
     * @param string $table Name of parent table
400
     * @param int $uid UID of the parent record
401
     */
402
    protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
403
    {
404
        $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid);
405
        if (empty($versionedRecord)) {
406
            return;
407
        }
408
        foreach ($versionedRecord as $field => $value) {
409
            if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
410
                continue;
411
            }
412
            $this->moveRecord_processFieldValue(
413
                $dataHandler,
414
                $resolvedPageId,
415
                $table,
416
                $uid,
417
                $value,
418
                $GLOBALS['TCA'][$table]['columns'][$field]['config']
419
            );
420
        }
421
    }
422
423
    /**
424
     * Processes a single field of a moved record and follows references.
425
     *
426
     * @param DataHandler $dataHandler Calling DataHandler instance
427
     * @param int $resolvedPageId Resolved real destination page id
428
     * @param string $table Name of parent table
429
     * @param int $uid UID of the parent record
430
     * @param string $value Value of the field of the parent record
431
     * @param array $configuration TCA field configuration of the parent record
432
     */
433
    protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $value, array $configuration): void
434
    {
435
        $inlineFieldType = $dataHandler->getInlineFieldType($configuration);
436
        $inlineProcessing = (
437
            ($inlineFieldType === 'list' || $inlineFieldType === 'field')
438
            && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
439
            && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent'])
440
        );
441
442
        if ($inlineProcessing) {
443
            if ($table === 'pages') {
444
                // If the inline elements are related to a page record,
445
                // make sure they reside at that page and not at its parent
446
                $resolvedPageId = $uid;
447
            }
448
449
            $dbAnalysis = $this->createRelationHandlerInstance();
450
            $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration);
451
452
            // Moving records to a positive destination will insert each
453
            // record at the beginning, thus the order is reversed here:
454
            foreach ($dbAnalysis->itemArray as $item) {
455
                $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
456
                if (empty($versionedRecord)) {
457
                    continue;
458
                }
459
                $versionState = VersionState::cast($versionedRecord['t3ver_state']);
460
                if ($versionState->indicatesPlaceholder()) {
461
                    continue;
462
                }
463
                $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId);
464
            }
465
        }
466
    }
467
468
    /****************************
469
     *****  Stage Changes  ******
470
     ****************************/
471
    /**
472
     * Setting stage of record
473
     *
474
     * @param string $table Table name
475
     * @param int $id
476
     * @param int $stageId Stage ID to set
477
     * @param string $comment Comment that goes into log
478
     * @param DataHandler $dataHandler DataHandler object
479
     * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
480
     */
481
    protected function version_setStage($table, $id, $stageId, string $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
482
    {
483
        if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
484
            $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR);
485
        } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) {
486
            $record = BackendUtility::getRecord($table, $id);
487
            $workspaceInfo = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']);
488
            // check if the user is allowed to the current stage, so it's also allowed to send to next stage
489
            if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) {
490
                // Set stage of record:
491
                GeneralUtility::makeInstance(ConnectionPool::class)
492
                    ->getConnectionForTable($table)
493
                    ->update(
494
                        $table,
495
                        [
496
                            't3ver_stage' => $stageId,
497
                        ],
498
                        ['uid' => (int)$id]
499
                    );
500
501
                if ($dataHandler->enableLogging) {
502
                    $propertyArray = $dataHandler->getRecordProperties($table, $id);
503
                    $pid = $propertyArray['pid'];
504
                    $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));
505
                }
506
                // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
507
                $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]);
508
                if ((int)$workspaceInfo['stagechg_notification'] > 0) {
509
                    $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$workspaceInfo, $stageId, $comment];
510
                    $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = [$table, $id];
511
                    $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['recipients'] = $notificationAlternativeRecipients;
512
                }
513
            } else {
514
                $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', SystemLogErrorClassification::USER_ERROR);
515
            }
516
        } else {
517
            $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', SystemLogErrorClassification::USER_ERROR);
518
        }
519
    }
520
521
    /*****************************
522
     *****  CMD versioning  ******
523
     *****************************/
524
525
    /**
526
     * Publishing / Swapping (= switching) versions of a record
527
     * 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
528
     *
529
     * @param string $table Table name
530
     * @param int $id UID of the online record to swap
531
     * @param int $swapWith UID of the archived version to swap with!
532
     * @param DataHandler $dataHandler DataHandler object
533
     * @param string $comment Notification comment
534
     * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
535
     */
536
    protected function version_swap($table, $id, $swapWith, DataHandler $dataHandler, string $comment, $notificationAlternativeRecipients = [])
537
    {
538
        // Check prerequisites before start publishing
539
        // Skip records that have been deleted during the current execution
540
        if ($dataHandler->hasDeletedRecord($table, $id)) {
541
            return;
542
        }
543
544
        // First, check if we may actually edit the online record
545
        if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
546
            $dataHandler->newlog(
547
                sprintf(
548
                    'Error: You cannot swap versions for record %s:%d you do not have access to edit!',
549
                    $table,
550
                    $id
551
                ),
552
                SystemLogErrorClassification::USER_ERROR
553
            );
554
            return;
555
        }
556
        // Select the two versions:
557
        // Currently live version, contents will be removed.
558
        $curVersion = BackendUtility::getRecord($table, $id, '*');
559
        // Versioned records which contents will be moved into $curVersion
560
        $isNewRecord = ((int)($curVersion['t3ver_state'] ?? 0) === VersionState::NEW_PLACEHOLDER);
561
        if ($isNewRecord && is_array($curVersion)) {
562
            $this->publishNewRecord($table, $curVersion, $dataHandler, $comment, (array)$notificationAlternativeRecipients);
563
            return;
564
        }
565
        $swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
566
        if (!(is_array($curVersion) && is_array($swapVersion))) {
567
            $dataHandler->newlog(
568
                sprintf(
569
                    'Error: Either online or swap version for %s:%d->%d could not be selected!',
570
                    $table,
571
                    $id,
572
                    $swapWith
573
                ),
574
                SystemLogErrorClassification::SYSTEM_ERROR
575
            );
576
            return;
577
        }
578
        $workspaceId = (int)$swapVersion['t3ver_wsid'];
579
        if (!$dataHandler->BE_USER->workspacePublishAccess($workspaceId)) {
580
            $dataHandler->newlog('User could not publish records from workspace #' . $workspaceId, SystemLogErrorClassification::USER_ERROR);
581
            return;
582
        }
583
        $wsAccess = $dataHandler->BE_USER->checkWorkspace($workspaceId);
584
        if (!($workspaceId <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === StagesService::STAGE_PUBLISH_ID)) {
585
            $dataHandler->newlog('Records in workspace #' . $workspaceId . ' can only be published when in "Publish" stage.', SystemLogErrorClassification::USER_ERROR);
586
            return;
587
        }
588
        if (!($dataHandler->doesRecordExist($table, $swapWith, Permission::PAGE_SHOW) && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) {
589
            $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', SystemLogErrorClassification::USER_ERROR);
590
            return;
591
        }
592
        // Check if the swapWith record really IS a version of the original!
593
        if (!(((int)$swapVersion['t3ver_oid'] > 0 && (int)$curVersion['t3ver_oid'] === 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) {
594
            $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);
595
            return;
596
        }
597
        $versionState = new VersionState($swapVersion['t3ver_state']);
598
599
        // Find fields to keep
600
        $keepFields = $this->getUniqueFields($table);
601
        // Sorting needs to be exchanged for moved records
602
        if (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby']) && !$versionState->equals(VersionState::MOVE_POINTER)) {
603
            $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
604
        }
605
        // l10n-fields must be kept otherwise the localization
606
        // will be lost during the publishing
607
        if ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
608
            $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
609
        }
610
        // Swap "keepfields"
611
        foreach ($keepFields as $fN) {
612
            $tmp = $swapVersion[$fN];
613
            $swapVersion[$fN] = $curVersion[$fN];
614
            $curVersion[$fN] = $tmp;
615
        }
616
        // Preserve states:
617
        $t3ver_state = [];
618
        $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
619
        // Modify offline version to become online:
620
        // Set pid for ONLINE (but not for moved records)
621
        if (!$versionState->equals(VersionState::MOVE_POINTER)) {
622
            $swapVersion['pid'] = (int)$curVersion['pid'];
623
        }
624
        // We clear this because t3ver_oid only make sense for offline versions
625
        // and we want to prevent unintentional misuse of this
626
        // value for online records.
627
        $swapVersion['t3ver_oid'] = 0;
628
        // In case of swapping and the offline record has a state
629
        // (like 2 or 4 for deleting or move-pointer) we set the
630
        // current workspace ID so the record is not deselected.
631
        $swapVersion['t3ver_wsid'] = 0;
632
        $swapVersion['t3ver_stage'] = 0;
633
        $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
634
        // Take care of relations in each field (e.g. IRRE):
635
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
636
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
637
                if (isset($fieldConf['config']) && is_array($fieldConf['config'])) {
638
                    $this->version_swap_processFields($table, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler);
639
                }
640
            }
641
        }
642
        unset($swapVersion['uid']);
643
        // Modify online version to become offline:
644
        unset($curVersion['uid']);
645
        // Mark curVersion to contain the oid
646
        $curVersion['t3ver_oid'] = (int)$id;
647
        $curVersion['t3ver_wsid'] = 0;
648
        // Increment lifecycle counter
649
        $curVersion['t3ver_stage'] = 0;
650
        $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
651
        // Registering and swapping MM relations in current and swap records:
652
        $dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith);
653
        // Generating proper history data to prepare logging
654
        $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
655
        $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
656
657
        // Execute swapping:
658
        $sqlErrors = [];
659
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
660
661
        $platform = $connection->getDatabasePlatform();
662
        $tableDetails = null;
663
        if ($platform instanceof SQLServerPlatform) {
664
            // mssql needs to set proper PARAM_LOB and others to update fields
665
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
666
        }
667
668
        try {
669
            $types = [];
670
671
            if ($platform instanceof SQLServerPlatform) {
672
                foreach ($curVersion as $columnName => $columnValue) {
673
                    $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
674
                }
675
            }
676
677
            $connection->update(
678
                $table,
679
                $swapVersion,
680
                ['uid' => (int)$id],
681
                $types
682
            );
683
        } catch (DBALException $e) {
684
            $sqlErrors[] = $e->getPrevious()->getMessage();
685
        }
686
687
        if (empty($sqlErrors)) {
688
            try {
689
                $types = [];
690
                if ($platform instanceof SQLServerPlatform) {
691
                    foreach ($curVersion as $columnName => $columnValue) {
692
                        $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
693
                    }
694
                }
695
696
                $connection->update(
697
                    $table,
698
                    $curVersion,
699
                    ['uid' => (int)$swapWith],
700
                    $types
701
                );
702
            } catch (DBALException $e) {
703
                $sqlErrors[] = $e->getPrevious()->getMessage();
704
            }
705
        }
706
707
        if (!empty($sqlErrors)) {
708
            $dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), SystemLogErrorClassification::SYSTEM_ERROR);
709
        } else {
710
            // Update localized elements to use the live l10n_parent now
711
            $this->updateL10nOverlayRecordsOnPublish($table, $id, $swapWith, $workspaceId, $dataHandler);
712
            // Register swapped ids for later remapping:
713
            $this->remappedIds[$table][$id] = $swapWith;
714
            $this->remappedIds[$table][$swapWith] = $id;
715
            if ((int)$t3ver_state['swapVersion'] === VersionState::DELETE_PLACEHOLDER) {
716
                // We're publishing a delete placeholder t3ver_state = 2. This means the live record should
717
                // be set to deleted. We're currently in some workspace and deal with a live record here. Thus,
718
                // we temporarily set backend user workspace to 0 so all operations happen as in live.
719
                $currentUserWorkspace = $dataHandler->BE_USER->workspace;
720
                $dataHandler->BE_USER->workspace = 0;
721
                $dataHandler->deleteEl($table, $id, true);
722
                $dataHandler->BE_USER->workspace = $currentUserWorkspace;
723
            }
724
            if ($dataHandler->enableLogging) {
725
                $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Publishing successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, -1, [], $dataHandler->eventPid($table, $id, $swapVersion['pid']));
726
            }
727
728
            // Set log entry for live record:
729
            $propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion);
730
            if (($propArr['t3ver_oid'] ?? 0) > 0) {
731
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
732
            } else {
733
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
734
            }
735
            $dataHandler->log($table, $id, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
736
            $dataHandler->setHistory($table, $id);
737
            // Set log entry for offline record:
738
            $propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion);
739
            if (($propArr['t3ver_oid'] ?? 0) > 0) {
740
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
741
            } else {
742
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
743
            }
744
            $dataHandler->log($table, $swapWith, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']);
745
            $dataHandler->setHistory($table, $swapWith);
746
747
            $stageId = StagesService::STAGE_PUBLISH_EXECUTE_ID;
748
            $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
749
            $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
750
            $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = [$table, $id];
751
            $this->notificationEmailInfo[$notificationEmailInfoKey]['recipients'] = $notificationAlternativeRecipients;
752
            // Write to log with stageId -20 (STAGE_PUBLISH_EXECUTE_ID)
753
            if ($dataHandler->enableLogging) {
754
                $propArr = $dataHandler->getRecordProperties($table, $id);
755
                $pid = $propArr['pid'];
756
                $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));
757
            }
758
            $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]);
759
760
            // Clear cache:
761
            $dataHandler->registerRecordIdForPageCacheClearing($table, $id);
762
            // If published, delete the record from the database
763
            if ($table === 'pages') {
764
                // Note on fifth argument false: At this point both $curVersion and $swapVersion page records are
765
                // identical in DB. deleteEl() would now usually find all records assigned to our obsolete
766
                // page which at the same time belong to our current version page, and would delete them.
767
                // To suppress this, false tells deleteEl() to only delete the obsolete page but not its assigned records.
768
                $dataHandler->deleteEl($table, $swapWith, true, true, false);
769
            } else {
770
                $dataHandler->deleteEl($table, $swapWith, true, true);
771
            }
772
773
            // Update reference index of the live record - which could have been a workspace record in case 'new'
774
            $dataHandler->updateRefIndex($table, $id, 0);
775
            // The 'swapWith' record has been deleted, so we can drop any reference index the record is involved in
776
            $dataHandler->registerReferenceIndexRowsForDrop($table, $swapWith, (int)$dataHandler->BE_USER->workspace);
777
        }
778
    }
779
780
    /**
781
     * If an editor is doing "partial" publishing, the translated children need to be "linked" to the now pointed
782
     * live record, as if the versioned record (which is deleted) would have never existed.
783
     *
784
     * This is related to the l10n_source and l10n_parent fields.
785
     *
786
     * This needs to happen before the hook calls DataHandler->deleteEl() otherwise the children get deleted as well.
787
     *
788
     * @param string $table the database table of the published record
789
     * @param int $liveId the live version / online version of the record that was just published
790
     * @param int $previouslyUsedVersionId the versioned record ID (wsid>0) which is about to be deleted
791
     * @param int $workspaceId the workspace ID
792
     * @param DataHandler $dataHandler
793
     */
794
    protected function updateL10nOverlayRecordsOnPublish(string $table, int $liveId, int $previouslyUsedVersionId, int $workspaceId, DataHandler $dataHandler): void
795
    {
796
        if (!BackendUtility::isTableLocalizable($table)) {
797
            return;
798
        }
799
        if (!BackendUtility::isTableWorkspaceEnabled($table)) {
800
            return;
801
        }
802
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
803
        $queryBuilder = $connection->createQueryBuilder();
804
        $queryBuilder->getRestrictions()->removeAll();
805
806
        $l10nParentFieldName = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
807
        $constraints = $queryBuilder->expr()->eq(
808
            $l10nParentFieldName,
809
            $queryBuilder->createNamedParameter($previouslyUsedVersionId, \PDO::PARAM_INT)
810
        );
811
        $translationSourceFieldName = $GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null;
812
        if ($translationSourceFieldName) {
813
            $constraints = $queryBuilder->expr()->orX(
814
                $constraints,
815
                $queryBuilder->expr()->eq(
816
                    $translationSourceFieldName,
817
                    $queryBuilder->createNamedParameter($previouslyUsedVersionId, \PDO::PARAM_INT)
818
                )
819
            );
820
        }
821
822
        $queryBuilder
823
            ->select('uid', $l10nParentFieldName)
824
            ->from($table)
825
            ->where(
826
                $constraints,
827
                $queryBuilder->expr()->eq(
828
                    't3ver_wsid',
829
                    $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
830
                )
831
            );
832
833
        if ($translationSourceFieldName) {
834
            $queryBuilder->addSelect($translationSourceFieldName);
835
        }
836
837
        $statement = $queryBuilder->execute();
838
        while ($record = $statement->fetch()) {
839
            $updateFields = [];
840
            $dataTypes = [\PDO::PARAM_INT];
841
            if ((int)$record[$l10nParentFieldName] === $previouslyUsedVersionId) {
842
                $updateFields[$l10nParentFieldName] = $liveId;
843
                $dataTypes[] = \PDO::PARAM_INT;
844
            }
845
            if ($translationSourceFieldName && (int)$record[$translationSourceFieldName] === $previouslyUsedVersionId) {
846
                $updateFields[$translationSourceFieldName] = $liveId;
847
                $dataTypes[] = \PDO::PARAM_INT;
848
            }
849
850
            if (empty($updateFields)) {
851
                continue;
852
            }
853
854
            $connection->update(
855
                $table,
856
                $updateFields,
857
                ['uid' => (int)$record['uid']],
858
                $dataTypes
859
            );
860
            $dataHandler->updateRefIndex($table, $record['uid']);
861
        }
862
    }
863
864
    /**
865
     * Processes fields of a record for the publishing/swapping process.
866
     * Basically this takes care of IRRE (type "inline") child references.
867
     *
868
     * @param string $tableName Table name
869
     * @param array $configuration TCA field configuration
870
     * @param array $liveData Live record data
871
     * @param array $versionData Version record data
872
     * @param DataHandler $dataHandler Calling data-handler object
873
     */
874
    protected function version_swap_processFields($tableName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
875
    {
876
        $inlineType = $dataHandler->getInlineFieldType($configuration);
877
        if ($inlineType !== 'field') {
878
            return;
879
        }
880
        $foreignTable = $configuration['foreign_table'];
881
        // Read relations that point to the current record (e.g. live record):
882
        $liveRelations = $this->createRelationHandlerInstance();
883
        $liveRelations->setWorkspaceId(0);
884
        $liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration);
885
        // Read relations that point to the record to be swapped with e.g. draft record):
886
        $versionRelations = $this->createRelationHandlerInstance();
887
        $versionRelations->setUseLiveReferenceIds(false);
888
        $versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration);
889
        // Update relations for both (workspace/versioning) sites:
890
        if (!empty($liveRelations->itemArray)) {
891
            $dataHandler->addRemapAction(
892
                $tableName,
893
                $liveData['uid'],
894
                [$this, 'updateInlineForeignFieldSorting'],
895
                [$liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace]
896
            );
897
        }
898
        if (!empty($versionRelations->itemArray)) {
899
            $dataHandler->addRemapAction(
900
                $tableName,
901
                $liveData['uid'],
902
                [$this, 'updateInlineForeignFieldSorting'],
903
                [$liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0]
904
            );
905
        }
906
    }
907
908
    /**
909
     * When a new record in a workspace is published, there is no "replacing" the online version with
910
     * the versioned record, but instead the workspace ID and the state is changed.
911
     *
912
     * @param string $table
913
     * @param array $newRecordInWorkspace
914
     * @param DataHandler $dataHandler
915
     * @param string $comment
916
     * @param array $notificationAlternativeRecipients
917
     */
918
    protected function publishNewRecord(string $table, array $newRecordInWorkspace, DataHandler $dataHandler, string $comment, array $notificationAlternativeRecipients): void
919
    {
920
        $id = (int)$newRecordInWorkspace['uid'];
921
        $workspaceId = (int)$newRecordInWorkspace['t3ver_wsid'];
922
        if (!$dataHandler->BE_USER->workspacePublishAccess($workspaceId)) {
923
            $dataHandler->newlog('User could not publish records from workspace #' . $workspaceId, SystemLogErrorClassification::USER_ERROR);
924
            return;
925
        }
926
        $wsAccess = $dataHandler->BE_USER->checkWorkspace($workspaceId);
927
        if (!($workspaceId <= 0 || !($wsAccess['publish_access'] & 1) || (int)$newRecordInWorkspace['t3ver_stage'] === StagesService::STAGE_PUBLISH_ID)) {
928
            $dataHandler->newlog('Records in workspace #' . $workspaceId . ' can only be published when in "Publish" stage.', SystemLogErrorClassification::USER_ERROR);
929
            return;
930
        }
931
        if (!($dataHandler->doesRecordExist($table, $id, Permission::PAGE_SHOW) && $dataHandler->checkRecordUpdateAccess($table, $id))) {
932
            $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', SystemLogErrorClassification::USER_ERROR);
933
            return;
934
        }
935
936
        // Modify versioned record to become online
937
        $updatedFields = [
938
            't3ver_oid' => 0,
939
            't3ver_wsid' => 0,
940
            't3ver_stage' => 0,
941
            't3ver_state' => VersionState::DEFAULT_STATE
942
        ];
943
944
        try {
945
            $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
946
            $connection->update(
947
                $table,
948
                $updatedFields,
949
                [
950
                    'uid' => (int)$id
951
                ],
952
                [
953
                    \PDO::PARAM_INT,
954
                    \PDO::PARAM_INT,
955
                    \PDO::PARAM_INT,
956
                    \PDO::PARAM_INT,
957
                    \PDO::PARAM_INT
958
                ]
959
            );
960
        } catch (DBALException $e) {
961
            $dataHandler->newlog('During Publishing: SQL errors happened: ' . $e->getPrevious()->getMessage(), SystemLogErrorClassification::SYSTEM_ERROR);
962
        }
963
964
        if ($dataHandler->enableLogging) {
965
            $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Publishing successful for table "' . $table . '" uid ' . $id . ' (new record)', -1, [], $dataHandler->eventPid($table, $id, $newRecordInWorkspace['pid']));
966
        }
967
968
        // Set log entry for record
969
        $propArr = $dataHandler->getRecordPropertiesFromRow($table, $newRecordInWorkspace);
970
        $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
971
        $dataHandler->log($table, $id, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
972
        $dataHandler->setHistory($table, $id);
973
974
        $stageId = StagesService::STAGE_PUBLISH_EXECUTE_ID;
975
        $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
976
        $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
977
        $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = [$table, $id];
978
        $this->notificationEmailInfo[$notificationEmailInfoKey]['recipients'] = $notificationAlternativeRecipients;
979
        // Write to log with stageId -20 (STAGE_PUBLISH_EXECUTE_ID)
980
        if ($dataHandler->enableLogging) {
981
            $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, $newRecordInWorkspace['pid']));
982
        }
983
        $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]);
984
985
        // Clear cache
986
        $dataHandler->registerRecordIdForPageCacheClearing($table, $id);
987
        // Update the reference index: Drop the references in the workspace, but update them in the live workspace
988
        $dataHandler->registerReferenceIndexRowsForDrop($table, $id, $workspaceId);
989
        $dataHandler->updateRefIndex($table, $id, 0);
990
        $this->updateReferenceIndexForL10nOverlays($table, $id, $workspaceId, $dataHandler);
991
    }
992
993
    /**
994
     * A new record was just published, but the reference index for the localized elements needs
995
     * an update too.
996
     *
997
     * @param string $table
998
     * @param int $newVersionedRecordId
999
     * @param int $workspaceId
1000
     * @param DataHandler $dataHandler
1001
     */
1002
    protected function updateReferenceIndexForL10nOverlays(string $table, int $newVersionedRecordId, int $workspaceId, DataHandler $dataHandler): void
1003
    {
1004
        if (!BackendUtility::isTableLocalizable($table)) {
1005
            return;
1006
        }
1007
        if (!BackendUtility::isTableWorkspaceEnabled($table)) {
1008
            return;
1009
        }
1010
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
1011
        $queryBuilder = $connection->createQueryBuilder();
1012
        $queryBuilder->getRestrictions()->removeAll();
1013
1014
        $l10nParentFieldName = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1015
        $constraints = $queryBuilder->expr()->eq(
1016
            $l10nParentFieldName,
1017
            $queryBuilder->createNamedParameter($newVersionedRecordId, \PDO::PARAM_INT)
1018
        );
1019
        $translationSourceFieldName = $GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null;
1020
        if ($translationSourceFieldName) {
1021
            $constraints = $queryBuilder->expr()->orX(
1022
                $constraints,
1023
                $queryBuilder->expr()->eq(
1024
                    $translationSourceFieldName,
1025
                    $queryBuilder->createNamedParameter($newVersionedRecordId, \PDO::PARAM_INT)
1026
                )
1027
            );
1028
        }
1029
1030
        $queryBuilder
1031
            ->select('uid', $l10nParentFieldName)
1032
            ->from($table)
1033
            ->where(
1034
                $constraints,
1035
                $queryBuilder->expr()->eq(
1036
                    't3ver_wsid',
1037
                    $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1038
                )
1039
            );
1040
1041
        if ($translationSourceFieldName) {
1042
            $queryBuilder->addSelect($translationSourceFieldName);
1043
        }
1044
1045
        $statement = $queryBuilder->execute();
1046
        while ($record = $statement->fetch()) {
1047
            $dataHandler->updateRefIndex($table, $record['uid']);
1048
        }
1049
    }
1050
1051
    /**
1052
     * Updates foreign field sorting values of versioned and live
1053
     * parents after(!) the whole structure has been published.
1054
     *
1055
     * This method is used as callback function in
1056
     * DataHandlerHook::version_swap_procBasedOnFieldType().
1057
     * Sorting fields ("sortby") are not modified during the
1058
     * workspace publishing/swapping process directly.
1059
     *
1060
     * @param string $parentId
1061
     * @param string $foreignTableName
1062
     * @param int[] $foreignIds
1063
     * @param array $configuration
1064
     * @param int $targetWorkspaceId
1065
     * @internal
1066
     */
1067
    public function updateInlineForeignFieldSorting($parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
1068
    {
1069
        $remappedIds = [];
1070
        // Use remapped ids (live id <-> version id)
1071
        foreach ($foreignIds as $foreignId) {
1072
            if (!empty($this->remappedIds[$foreignTableName][$foreignId])) {
1073
                $remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId];
1074
            } else {
1075
                $remappedIds[] = $foreignId;
1076
            }
1077
        }
1078
1079
        $relationHandler = $this->createRelationHandlerInstance();
1080
        $relationHandler->setWorkspaceId($targetWorkspaceId);
1081
        $relationHandler->setUseLiveReferenceIds(false);
1082
        $relationHandler->start(implode(',', $remappedIds), $foreignTableName);
1083
        $relationHandler->processDeletePlaceholder();
1084
        $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

1084
        $relationHandler->writeForeignField($configuration, /** @scrutinizer ignore-type */ $parentId);
Loading history...
1085
    }
1086
1087
    /**
1088
     * In case a sys_workspace_stage record is deleted we do a hard reset
1089
     * for all existing records in that stage to avoid that any of these end up
1090
     * as orphan records.
1091
     *
1092
     * @param int $stageId Elements with this stage are reset
1093
     */
1094
    protected function resetStageOfElements(int $stageId): void
1095
    {
1096
        foreach ($this->getTcaTables() as $tcaTable) {
1097
            if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
1098
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1099
                    ->getQueryBuilderForTable($tcaTable);
1100
1101
                $queryBuilder
1102
                    ->update($tcaTable)
1103
                    ->set('t3ver_stage', StagesService::STAGE_EDIT_ID)
1104
                    ->where(
1105
                        $queryBuilder->expr()->eq(
1106
                            't3ver_stage',
1107
                            $queryBuilder->createNamedParameter($stageId, \PDO::PARAM_INT)
1108
                        ),
1109
                        $queryBuilder->expr()->gt(
1110
                            't3ver_wsid',
1111
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1112
                        )
1113
                    )
1114
                    ->execute();
1115
            }
1116
        }
1117
    }
1118
1119
    /**
1120
     * Flushes (remove, no soft delete!) elements of a particular workspace to avoid orphan records.
1121
     * This is used if an admin deletes a sys_workspace record.
1122
     *
1123
     * @param int $workspaceId The workspace to be flushed
1124
     */
1125
    protected function flushWorkspaceElements(int $workspaceId): void
1126
    {
1127
        $command = [];
1128
        foreach ($this->getTcaTables() as $tcaTable) {
1129
            if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
1130
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1131
                    ->getQueryBuilderForTable($tcaTable);
1132
                $queryBuilder->getRestrictions()->removeAll();
1133
                $result = $queryBuilder
1134
                    ->select('uid')
1135
                    ->from($tcaTable)
1136
                    ->where(
1137
                        $queryBuilder->expr()->eq(
1138
                            't3ver_wsid',
1139
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1140
                        ),
1141
                        // t3ver_oid >= 0 basically omits placeholder records here, those would otherwise
1142
                        // fail to delete later in DH->discard() and would create "can't do that" log entries.
1143
                        $queryBuilder->expr()->orX(
1144
                            $queryBuilder->expr()->gt(
1145
                                't3ver_oid',
1146
                                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1147
                            ),
1148
                            $queryBuilder->expr()->eq(
1149
                                't3ver_state',
1150
                                $queryBuilder->createNamedParameter(VersionState::NEW_PLACEHOLDER, \PDO::PARAM_INT)
1151
                            )
1152
                        )
1153
                    )
1154
                    ->orderBy('uid')
1155
                    ->execute();
1156
1157
                while (($recordId = $result->fetchOne()) !== false) {
1158
                    $command[$tcaTable][$recordId]['version']['action'] = 'flush';
1159
                }
1160
            }
1161
        }
1162
        if (!empty($command)) {
1163
            // Execute the command array via DataHandler to flush all records from this workspace.
1164
            // Switch to target workspace temporarily, otherwise DH->discard() do not
1165
            // operate on correct workspace if fetching additional records.
1166
            $backendUser = $GLOBALS['BE_USER'];
1167
            $savedWorkspace = $backendUser->workspace;
1168
            $backendUser->workspace = $workspaceId;
1169
            $context = GeneralUtility::makeInstance(Context::class);
1170
            $savedWorkspaceContext = $context->getAspect('workspace');
1171
            $context->setAspect('workspace', new WorkspaceAspect($workspaceId));
1172
1173
            $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
1174
            $dataHandler->start([], $command, $backendUser);
1175
            $dataHandler->process_cmdmap();
1176
1177
            $backendUser->workspace = $savedWorkspace;
1178
            $context->setAspect('workspace', $savedWorkspaceContext);
1179
        }
1180
    }
1181
1182
    /**
1183
     * Gets all defined TCA tables.
1184
     *
1185
     * @return array
1186
     */
1187
    protected function getTcaTables(): array
1188
    {
1189
        return array_keys($GLOBALS['TCA']);
1190
    }
1191
1192
    /**
1193
     * Flushes the workspace cache for current workspace and for the virtual "all workspaces" too.
1194
     *
1195
     * @param int $workspaceId The workspace to be flushed in cache
1196
     */
1197
    protected function flushWorkspaceCacheEntriesByWorkspaceId(int $workspaceId): void
1198
    {
1199
        $workspacesCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('workspaces_cache');
1200
        $workspacesCache->flushByTag($workspaceId);
1201
    }
1202
1203
    /*******************************
1204
     *****  helper functions  ******
1205
     *******************************/
1206
1207
    /**
1208
     * Finds all elements for swapping versions in workspace
1209
     *
1210
     * @param string $table Table name of the original element to swap
1211
     * @param int $id UID of the original element to swap (online)
1212
     * @param int $offlineId As above but offline
1213
     * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID
1214
     */
1215
    public function findPageElementsForVersionSwap($table, $id, $offlineId)
1216
    {
1217
        $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid');
1218
        $workspaceId = (int)$rec['t3ver_wsid'];
1219
        $elementData = [];
1220
        if ($workspaceId === 0) {
1221
            return $elementData;
1222
        }
1223
        // Get page UID for LIVE and workspace
1224
        if ($table !== 'pages') {
1225
            $rec = BackendUtility::getRecord($table, $id, 'pid');
1226
            $pageId = $rec['pid'];
1227
            $rec = BackendUtility::getRecord('pages', $pageId);
1228
            BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1229
            $offlinePageId = $rec['_ORIG_uid'];
1230
        } else {
1231
            $pageId = $id;
1232
            $offlinePageId = $offlineId;
1233
        }
1234
        // Traversing all tables supporting versioning:
1235
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
0 ignored issues
show
introduced by
$table is overwriting one of the parameters of this function.
Loading history...
1236
            if (BackendUtility::isTableWorkspaceEnabled($table) && $table !== 'pages') {
1237
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1238
                    ->getQueryBuilderForTable($table);
1239
1240
                $queryBuilder->getRestrictions()
1241
                    ->removeAll()
1242
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1243
1244
                $statement = $queryBuilder
1245
                    ->select('A.uid AS offlineUid', 'B.uid AS uid')
1246
                    ->from($table, 'A')
1247
                    ->from($table, 'B')
1248
                    ->where(
1249
                        $queryBuilder->expr()->gt(
1250
                            'A.t3ver_oid',
1251
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1252
                        ),
1253
                        $queryBuilder->expr()->eq(
1254
                            'B.pid',
1255
                            $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
1256
                        ),
1257
                        $queryBuilder->expr()->eq(
1258
                            'A.t3ver_wsid',
1259
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1260
                        ),
1261
                        $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1262
                    )
1263
                    ->execute();
1264
1265
                while ($row = $statement->fetch()) {
1266
                    $elementData[$table][] = [$row['uid'], $row['offlineUid']];
1267
                }
1268
            }
1269
        }
1270
        if ($offlinePageId && $offlinePageId != $pageId) {
1271
            $elementData['pages'][] = [$pageId, $offlinePageId];
1272
        }
1273
1274
        return $elementData;
1275
    }
1276
1277
    /**
1278
     * Searches for all elements from all tables on the given pages in the same workspace.
1279
     *
1280
     * @param array $pageIdList List of PIDs to search
1281
     * @param int $workspaceId Workspace ID
1282
     * @param array $elementList List of found elements. Key is table name, value is array of element UIDs
1283
     */
1284
    public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList)
1285
    {
1286
        if ($workspaceId == 0) {
1287
            return;
1288
        }
1289
        // Traversing all tables supporting versioning:
1290
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
1291
            if (BackendUtility::isTableWorkspaceEnabled($table) && $table !== 'pages') {
1292
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1293
                    ->getQueryBuilderForTable($table);
1294
1295
                $queryBuilder->getRestrictions()
1296
                    ->removeAll()
1297
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1298
1299
                $statement = $queryBuilder
1300
                    ->select('A.uid')
1301
                    ->from($table, 'A')
1302
                    ->from($table, 'B')
1303
                    ->where(
1304
                        $queryBuilder->expr()->gt(
1305
                            'A.t3ver_oid',
1306
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1307
                        ),
1308
                        $queryBuilder->expr()->in(
1309
                            'B.pid',
1310
                            $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY)
1311
                        ),
1312
                        $queryBuilder->expr()->eq(
1313
                            'A.t3ver_wsid',
1314
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1315
                        ),
1316
                        $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1317
                    )
1318
                    ->groupBy('A.uid')
1319
                    ->execute();
1320
1321
                while ($row = $statement->fetch()) {
1322
                    $elementList[$table][] = $row['uid'];
1323
                }
1324
                if (is_array($elementList[$table])) {
1325
                    // Yes, it is possible to get non-unique array even with DISTINCT above!
1326
                    // It happens because several UIDs are passed in the array already.
1327
                    $elementList[$table] = array_unique($elementList[$table]);
1328
                }
1329
            }
1330
        }
1331
    }
1332
1333
    /**
1334
     * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1335
     *
1336
     * @param string $table Table to search
1337
     * @param array $idList List of records' UIDs
1338
     * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publish DRAFT from ws module!
1339
     * @param array $pageIdList List of found page UIDs
1340
     * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs
1341
     */
1342
    public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList)
1343
    {
1344
        if ($workspaceId == 0) {
1345
            return;
1346
        }
1347
1348
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1349
            ->getQueryBuilderForTable($table);
1350
        $queryBuilder->getRestrictions()
1351
            ->removeAll()
1352
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1353
1354
        $statement = $queryBuilder
1355
            ->select('B.pid')
1356
            ->from($table, 'A')
1357
            ->from($table, 'B')
1358
            ->where(
1359
                $queryBuilder->expr()->gt(
1360
                    'A.t3ver_oid',
1361
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1362
                ),
1363
                $queryBuilder->expr()->eq(
1364
                    'A.t3ver_wsid',
1365
                    $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1366
                ),
1367
                $queryBuilder->expr()->in(
1368
                    'A.uid',
1369
                    $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY)
1370
                ),
1371
                $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1372
            )
1373
            ->groupBy('B.pid')
1374
            ->execute();
1375
1376
        while ($row = $statement->fetch()) {
1377
            $pageIdList[] = $row['pid'];
1378
            // Find ws version
1379
            // Note: cannot use BackendUtility::getRecordWSOL()
1380
            // here because it does not accept workspace id!
1381
            $rec = BackendUtility::getRecord('pages', $row[0]);
1382
            BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1383
            if ($rec['_ORIG_uid']) {
1384
                $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1385
            }
1386
        }
1387
        // The line below is necessary even with DISTINCT
1388
        // because several elements can be passed by caller
1389
        $pageIdList = array_unique($pageIdList);
1390
    }
1391
1392
    /**
1393
     * Finds real page IDs for state change.
1394
     *
1395
     * @param array $idList List of page UIDs, possibly versioned
1396
     */
1397
    public function findRealPageIds(array &$idList): void
1398
    {
1399
        foreach ($idList as $key => $id) {
1400
            $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid');
1401
            if ($rec['t3ver_oid'] > 0) {
1402
                $idList[$key] = $rec['t3ver_oid'];
1403
            }
1404
        }
1405
    }
1406
1407
    /**
1408
     * Moves a versioned record, which is not new or deleted.
1409
     *
1410
     * This is critical for a versioned record to be marked as MOVED (t3ver_state=4)
1411
     *
1412
     * @param string $table Table name to move
1413
     * @param int $liveUid Record uid to move (online record)
1414
     * @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
1415
     * @param int $versionedRecordUid UID of offline version of online record
1416
     * @param DataHandler $dataHandler DataHandler object
1417
     * @see moveRecord()
1418
     */
1419
    protected function moveRecord_moveVersionedRecord(string $table, int $liveUid, int $destPid, int $versionedRecordUid, DataHandler $dataHandler): void
1420
    {
1421
        // If a record gets moved after a record that already has a versioned record
1422
        // then the versioned record needs to be placed after the existing one
1423
        $originalRecordDestinationPid = $destPid;
1424
        $movedTargetRecordInWorkspace = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $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...kspaceVersionOfRecord() 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

1424
        $movedTargetRecordInWorkspace = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, /** @scrutinizer ignore-type */ abs($destPid), 'uid');
Loading history...
1425
        if (is_array($movedTargetRecordInWorkspace) && $destPid < 0) {
1426
            $destPid = -$movedTargetRecordInWorkspace['uid'];
1427
        }
1428
        $dataHandler->moveRecord_raw($table, $versionedRecordUid, $destPid);
1429
1430
        $versionedRecord = BackendUtility::getRecord($table, $versionedRecordUid, 'uid,t3ver_state');
1431
        if (!VersionState::cast($versionedRecord['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
1432
            // Update the state of this record to a move placeholder. This is allowed if the
1433
            // record is a 'changed' (t3ver_state=0) record: Changing a record and moving it
1434
            // around later, should switch it from 'changed' to 'moved'. Deleted placeholders
1435
            // however are an 'end-state', they should not be switched to a move placeholder.
1436
            // Scenario: For a live page that has a localization, the localization is first
1437
            // marked as to-delete in workspace, creating a delete placeholder for that
1438
            // localization. Later, the page is moved around, moving the localization along
1439
            // with the default language record. The localization should then NOT be switched
1440
            // from 'to-delete' to 'moved', this would loose the 'to-delete' information.
1441
            GeneralUtility::makeInstance(ConnectionPool::class)
1442
                ->getConnectionForTable($table)
1443
                ->update(
1444
                    $table,
1445
                    [
1446
                        't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER)
1447
                    ],
1448
                    [
1449
                        'uid' => (int)$versionedRecordUid
1450
                    ]
1451
                );
1452
        }
1453
1454
        // Check for the localizations of that element and move them as well
1455
        $dataHandler->moveL10nOverlayRecords($table, $liveUid, $destPid, $originalRecordDestinationPid);
1456
    }
1457
1458
    /**
1459
     * Gets an instance of the command map helper.
1460
     *
1461
     * @param DataHandler $dataHandler DataHandler object
1462
     * @return CommandMap
1463
     */
1464
    public function getCommandMap(DataHandler $dataHandler): CommandMap
1465
    {
1466
        return GeneralUtility::makeInstance(
1467
            CommandMap::class,
1468
            $this,
1469
            $dataHandler,
1470
            $dataHandler->cmdmap,
1471
            $dataHandler->BE_USER->workspace
1472
        );
1473
    }
1474
1475
    protected function emitUpdateTopbarSignal(): void
1476
    {
1477
        BackendUtility::setUpdateSignal('updateTopbar');
1478
    }
1479
1480
    /**
1481
     * Returns all fieldnames from a table which have the unique evaluation type set.
1482
     *
1483
     * @param string $table Table name
1484
     * @return array Array of fieldnames
1485
     */
1486
    protected function getUniqueFields($table): array
1487
    {
1488
        $listArr = [];
1489
        foreach ($GLOBALS['TCA'][$table]['columns'] ?? [] as $field => $configArr) {
1490
            if ($configArr['config']['type'] === 'input') {
1491
                $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'] ?? '', true);
1492
                if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) {
1493
                    $listArr[] = $field;
1494
                }
1495
            }
1496
        }
1497
        return $listArr;
1498
    }
1499
1500
    /**
1501
     * Straight db based record deletion: sets deleted = 1 for soft-delete
1502
     * enabled tables, or removes row from table. Used for move placeholder
1503
     * records sometimes.
1504
     */
1505
    protected function softOrHardDeleteSingleRecord(string $table, int $uid): void
1506
    {
1507
        $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
1508
        if ($deleteField) {
1509
            GeneralUtility::makeInstance(ConnectionPool::class)
1510
                ->getConnectionForTable($table)
1511
                ->update(
1512
                    $table,
1513
                    [$deleteField => 1],
1514
                    ['uid' => $uid],
1515
                    [\PDO::PARAM_INT]
1516
                );
1517
        } else {
1518
            GeneralUtility::makeInstance(ConnectionPool::class)
1519
                ->getConnectionForTable($table)
1520
                ->delete(
1521
                    $table,
1522
                    ['uid' => $uid]
1523
                );
1524
        }
1525
    }
1526
1527
    /**
1528
     * @return RelationHandler
1529
     */
1530
    protected function createRelationHandlerInstance(): RelationHandler
1531
    {
1532
        return GeneralUtility::makeInstance(RelationHandler::class);
1533
    }
1534
1535
    /**
1536
     * @return LanguageService
1537
     */
1538
    protected function getLanguageService(): LanguageService
1539
    {
1540
        return $GLOBALS['LANG'];
1541
    }
1542
}
1543