Completed
Push — master ( 69442d...0bd76b )
by
unknown
16:42
created

DataHandlerHook::getLanguageService()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

322
    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

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

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

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

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