Completed
Push — master ( 0260f9...e176d6 )
by
unknown
14:18
created

DataHandlerHook::softOrHardDeleteSingleRecord()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 14
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 17
rs 9.7998
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\Core\Environment;
25
use TYPO3\CMS\Core\Database\Connection;
26
use TYPO3\CMS\Core\Database\ConnectionPool;
27
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
28
use TYPO3\CMS\Core\Database\ReferenceIndex;
29
use TYPO3\CMS\Core\Database\RelationHandler;
30
use TYPO3\CMS\Core\DataHandling\DataHandler;
31
use TYPO3\CMS\Core\DataHandling\PlaceholderShadowColumnsResolver;
32
use TYPO3\CMS\Core\Localization\LanguageService;
33
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
34
use TYPO3\CMS\Core\SysLog\Action\Database as DatabaseAction;
35
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
36
use TYPO3\CMS\Core\Type\Bitmask\Permission;
37
use TYPO3\CMS\Core\Utility\ArrayUtility;
38
use TYPO3\CMS\Core\Utility\GeneralUtility;
39
use TYPO3\CMS\Core\Versioning\VersionState;
40
use TYPO3\CMS\Workspaces\DataHandler\CommandMap;
41
use TYPO3\CMS\Workspaces\Notification\StageChangeNotification;
42
use TYPO3\CMS\Workspaces\Service\StagesService;
43
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
44
45
/**
46
 * Contains some parts for staging, versioning and workspaces
47
 * to interact with the TYPO3 Core Engine
48
 * @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
49
 */
50
class DataHandlerHook
51
{
52
    /**
53
     * For accumulating information about workspace stages raised
54
     * on elements so a single mail is sent as notification.
55
     *
56
     * @var array
57
     */
58
    protected $notificationEmailInfo = [];
59
60
    /**
61
     * Contains remapped IDs.
62
     *
63
     * @var array
64
     */
65
    protected $remappedIds = [];
66
67
    /****************************
68
     *****  Cmdmap  Hooks  ******
69
     ****************************/
70
    /**
71
     * hook that is called before any cmd of the commandmap is executed
72
     *
73
     * @param DataHandler $dataHandler reference to the main DataHandler object
74
     */
75
    public function processCmdmap_beforeStart(DataHandler $dataHandler)
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::processCmdmap_beforeStart" is not in camel caps format
Loading history...
76
    {
77
        // Reset notification array
78
        $this->notificationEmailInfo = [];
79
        // Resolve dependencies of version/workspaces actions:
80
        $dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get();
81
    }
82
83
    /**
84
     * hook that is called when no prepared command was found
85
     *
86
     * @param string $command the command to be executed
87
     * @param string $table the table of the record
88
     * @param int $id the ID of the record
89
     * @param mixed $value the value containing the data
90
     * @param bool $commandIsProcessed can be set so that other hooks or
91
     * @param DataHandler $dataHandler reference to the main DataHandler object
92
     */
93
    public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
94
    {
95
        // custom command "version"
96
        if ($command !== 'version') {
97
            return;
98
        }
99
        $commandIsProcessed = true;
100
        $action = (string)$value['action'];
101
        $comment = $value['comment'] ?: '';
102
        $notificationAlternativeRecipients = $value['notificationAlternativeRecipients'] ?? [];
103
        switch ($action) {
104
            case 'new':
105
                $dataHandler->versionizeRecord($table, $id, $value['label']);
106
                break;
107
            case 'swap':
108
                $this->version_swap(
109
                    $table,
110
                    $id,
111
                    $value['swapWith'],
112
                    (bool)$value['swapIntoWS'],
113
                    $dataHandler,
114
                    $comment,
115
                    $notificationAlternativeRecipients
116
                );
117
                break;
118
            case 'clearWSID':
119
                $this->version_clearWSID($table, (int)$id, false, $dataHandler);
120
                break;
121
            case 'flush':
122
                $this->version_clearWSID($table, (int)$id, true, $dataHandler);
123
                break;
124
            case 'setStage':
125
                $elementIds = GeneralUtility::trimExplode(',', $id, true);
126
                foreach ($elementIds as $elementId) {
127
                    $this->version_setStage(
128
                        $table,
129
                        $elementId,
0 ignored issues
show
Bug introduced by
$elementId of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Workspaces\Hoo...ook::version_setStage(). ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

Loading history...
Coding Style introduced by
Method name "DataHandlerHook::processCmdmap_postProcess" is not in camel caps format
Loading history...
329
    {
330
        if ($command === 'delete') {
331
            if ($table === StagesService::TABLE_STAGE) {
332
                $this->resetStageOfElements((int)$id);
333
            } elseif ($table === WorkspaceService::TABLE_WORKSPACE) {
334
                $this->flushWorkspaceElements((int)$id);
335
                $this->emitUpdateTopbarSignal();
336
            }
337
        }
338
    }
339
340
    public function processDatamap_afterAllOperations(DataHandler $dataHandler): void
0 ignored issues
show
Coding Style introduced by
Method name "DataHandlerHook::processDatamap_afterAllOperations" is not in camel caps format
Loading history...
341
    {
342
        if (isset($dataHandler->datamap[WorkspaceService::TABLE_WORKSPACE])) {
343
            $this->emitUpdateTopbarSignal();
344
        }
345
    }
346
347
    /**
348
     * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about
349
     * moving records that are *not* in the live workspace
350
     *
351
     * @param string $table the table of the record
352
     * @param int $uid the ID of the record
353
     * @param int $destPid Position to move to: $destPid: >=0 then it points to
354
     * @param array $propArr Record properties, like header and pid (includes workspace overlay)
355
     * @param array $moveRec Record properties, like header and pid (without workspace overlay)
356
     * @param int $resolvedPid The final page ID of the record
357
     * @param bool $recordWasMoved can be set so that other hooks or
358
     * @param DataHandler $dataHandler
359
     */
360
    public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)
361
    {
362
        // Only do something in Draft workspace
363
        if ($dataHandler->BE_USER->workspace === 0) {
364
            return;
365
        }
366
        $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
367
        // Fetch move placeholder, since it might point to a new page in the current workspace
368
        $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

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

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

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