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

DataHandlerHook::moveRecord()   F

Complexity

Conditions 24
Paths 577

Size

Total Lines 68
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 39
nc 577
nop 8
dl 0
loc 68
rs 0.5875
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Workspaces\Hook;
17
18
use Doctrine\DBAL\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