DataHandlerHook::version_swap()   F
last analyzed

Complexity

Conditions 40
Paths > 20000

Size

Total Lines 241
Code Lines 145

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 145
dl 0
loc 241
rs 0
c 0
b 0
f 0
cc 40
nc 124808
nop 6

How to fix   Long Method    Complexity   

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:

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