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

DataHandlerHook   F

Complexity

Total Complexity 212

Size/Duplication

Total Lines 1460
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 713
dl 0
loc 1460
rs 1.887
c 4
b 0
f 0
wmc 212

30 Methods

Rating   Name   Duplication   Size   Complexity  
A processCmdmap_beforeStart() 0 6 1
B findPageElementsForVersionStageChange() 0 44 7
A findRealPageIds() 0 6 3
A findPageIdsForVersionStateChange() 0 48 4
B findPageElementsForVersionSwap() 0 60 9
A flushWorkspaceCacheEntriesByWorkspaceId() 0 4 1
A resetStageOfElements() 0 21 3
A getUniqueFields() 0 12 5
A getCommandMap() 0 8 1
F moveRecord_wsPlaceholders() 0 96 15
A getTcaTables() 0 3 1
A flushWorkspaceElements() 0 48 5
A emitUpdateTopbarSignal() 0 3 1
A moveRecord_processFields() 0 17 4
F moveRecord() 0 76 24
A processCmdmap_postProcess() 0 8 4
A processDatamap_afterAllOperations() 0 4 2
B version_setStage() 0 37 6
B moveRecord_processFieldValue() 0 27 10
A processCmdmap_afterFinish() 0 16 1
B processCmdmap() 0 43 10
A sendStageChangeNotification() 0 28 5
D processCmdmap_deleteAction() 0 107 27
A updateInlineForeignFieldSorting() 0 18 3
A version_swap_processFields() 0 30 4
A createRelationHandlerInstance() 0 3 1
B updateL10nOverlayRecordsOnPublish() 0 67 10
F version_swap() 0 257 42
A getLanguageService() 0 3 1
A softOrHardDeleteSingleRecord() 0 18 2

How to fix   Complexity   

Complex Class

Complex classes like DataHandlerHook often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataHandlerHook, and based on these observations, apply Extract Interface, too.

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

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

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

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

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

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

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