Completed
Push — master ( d193fe...01e2c1 )
by
unknown
13:23
created

DataHandlerHook::moveRecord()   F

Complexity

Conditions 24
Paths 769

Size

Total Lines 76
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 43
c 0
b 0
f 0
nc 769
nop 8
dl 0
loc 76
rs 0.3207

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\Database\Connection;
25
use TYPO3\CMS\Core\Database\ConnectionPool;
26
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27
use TYPO3\CMS\Core\Database\ReferenceIndex;
28
use TYPO3\CMS\Core\Database\RelationHandler;
29
use TYPO3\CMS\Core\DataHandling\DataHandler;
30
use TYPO3\CMS\Core\DataHandling\PlaceholderShadowColumnsResolver;
31
use TYPO3\CMS\Core\Localization\LanguageService;
32
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
33
use TYPO3\CMS\Core\SysLog\Action\Database as DatabaseAction;
34
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
35
use TYPO3\CMS\Core\Type\Bitmask\Permission;
36
use TYPO3\CMS\Core\Utility\ArrayUtility;
37
use TYPO3\CMS\Core\Utility\GeneralUtility;
38
use TYPO3\CMS\Core\Versioning\VersionState;
39
use TYPO3\CMS\Workspaces\DataHandler\CommandMap;
40
use TYPO3\CMS\Workspaces\Notification\StageChangeNotification;
41
use TYPO3\CMS\Workspaces\Service\StagesService;
42
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
43
44
/**
45
 * Contains some parts for staging, versioning and workspaces
46
 * to interact with the TYPO3 Core Engine
47
 * @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
48
 */
49
class DataHandlerHook
50
{
51
    /**
52
     * For accumulating information about workspace stages raised
53
     * on elements so a single mail is sent as notification.
54
     *
55
     * @var array
56
     */
57
    protected $notificationEmailInfo = [];
58
59
    /**
60
     * Contains remapped IDs.
61
     *
62
     * @var array
63
     */
64
    protected $remappedIds = [];
65
66
    /****************************
67
     *****  Cmdmap  Hooks  ******
68
     ****************************/
69
    /**
70
     * hook that is called before any cmd of the commandmap is executed
71
     *
72
     * @param DataHandler $dataHandler reference to the main DataHandler object
73
     */
74
    public function processCmdmap_beforeStart(DataHandler $dataHandler)
75
    {
76
        // Reset notification array
77
        $this->notificationEmailInfo = [];
78
        // Resolve dependencies of version/workspaces actions:
79
        $dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get();
80
    }
81
82
    /**
83
     * hook that is called when no prepared command was found
84
     *
85
     * @param string $command the command to be executed
86
     * @param string $table the table of the record
87
     * @param int $id the ID of the record
88
     * @param mixed $value the value containing the data
89
     * @param bool $commandIsProcessed can be set so that other hooks or
90
     * @param DataHandler $dataHandler reference to the main DataHandler object
91
     */
92
    public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
93
    {
94
        // custom command "version"
95
        if ($command !== 'version') {
96
            return;
97
        }
98
        $commandIsProcessed = true;
99
        $action = (string)$value['action'];
100
        $comment = $value['comment'] ?: '';
101
        $notificationAlternativeRecipients = $value['notificationAlternativeRecipients'] ?? [];
102
        switch ($action) {
103
            case 'new':
104
                $dataHandler->versionizeRecord($table, $id, $value['label']);
105
                break;
106
            case 'swap':
107
            case 'publish':
108
                $this->version_swap(
109
                    $table,
110
                    $id,
111
                    $value['swapWith'],
112
                    $dataHandler,
113
                    $comment,
114
                    $notificationAlternativeRecipients
115
                );
116
                break;
117
            case 'clearWSID':
118
            case 'flush':
119
                $dataHandler->discard($table, (int)$id);
120
                break;
121
            case 'setStage':
122
                $elementIds = GeneralUtility::intExplode(',', (string)$id, true);
123
                foreach ($elementIds as $elementId) {
124
                    $this->version_setStage(
125
                        $table,
126
                        $elementId,
127
                        $value['stageId'],
128
                        $comment,
129
                        $dataHandler,
130
                        $notificationAlternativeRecipients
131
                    );
132
                }
133
                break;
134
            default:
135
                // Do nothing
136
        }
137
    }
138
139
    /**
140
     * hook that is called AFTER all commands of the commandmap was
141
     * executed
142
     *
143
     * @param DataHandler $dataHandler reference to the main DataHandler object
144
     */
145
    public function processCmdmap_afterFinish(DataHandler $dataHandler)
146
    {
147
        // Empty accumulation array
148
        $emailNotificationService = GeneralUtility::makeInstance(StageChangeNotification::class);
149
        $this->sendStageChangeNotification(
150
            $this->notificationEmailInfo,
151
            $emailNotificationService,
152
            $dataHandler
153
        );
154
155
        // Reset notification array
156
        $this->notificationEmailInfo = [];
157
        // Reset remapped IDs
158
        $this->remappedIds = [];
159
160
        $this->flushWorkspaceCacheEntriesByWorkspaceId((int)$dataHandler->BE_USER->workspace);
161
    }
162
163
    protected function sendStageChangeNotification(
164
        array $accumulatedNotificationInformation,
165
        StageChangeNotification $notificationService,
166
        DataHandler $dataHandler
167
    ): void {
168
        foreach ($accumulatedNotificationInformation as $groupedNotificationInformation) {
169
            $emails = (array)$groupedNotificationInformation['recipients'];
170
            if (empty($emails)) {
171
                continue;
172
            }
173
            $workspaceRec = $groupedNotificationInformation['shared'][0];
174
            if (!is_array($workspaceRec)) {
175
                continue;
176
            }
177
            $notificationService->notifyStageChange(
178
                $workspaceRec,
179
                (int)$groupedNotificationInformation['shared'][1],
180
                $groupedNotificationInformation['elements'],
181
                $groupedNotificationInformation['shared'][2],
182
                $emails,
183
                $dataHandler->BE_USER
184
            );
185
186
            if ($dataHandler->enableLogging) {
187
                [$elementTable, $elementUid] = reset($groupedNotificationInformation['elements']);
188
                $propertyArray = $dataHandler->getRecordProperties($elementTable, $elementUid);
189
                $pid = $propertyArray['pid'];
190
                $dataHandler->log($elementTable, $elementUid, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Notification email for stage change was sent to "' . implode('", "', $emails) . '"', -1, [], $dataHandler->eventPid($elementTable, $elementUid, $pid));
191
            }
192
        }
193
    }
194
195
    /**
196
     * hook that is called when an element shall get deleted
197
     *
198
     * @param string $table the table of the record
199
     * @param int $id the ID of the record
200
     * @param array $record The accordant database record
201
     * @param bool $recordWasDeleted can be set so that other hooks or
202
     * @param DataHandler $dataHandler reference to the main DataHandler object
203
     */
204
    public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler)
205
    {
206
        // only process the hook if it wasn't processed
207
        // by someone else before
208
        if ($recordWasDeleted) {
209
            return;
210
        }
211
        $recordWasDeleted = true;
212
        // For Live version, try if there is a workspace version because if so, rather "delete" that instead
213
        // Look, if record is an offline version, then delete directly:
214
        if ((int)($record['t3ver_oid'] ?? 0) === 0) {
215
            if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) {
216
                $record = $wsVersion;
217
                $id = $record['uid'];
218
            }
219
        }
220
        $recordVersionState = VersionState::cast($record['t3ver_state']);
221
        // Look, if record is an offline version, then delete directly:
222
        if ((int)($record['t3ver_oid'] ?? 0) > 0) {
223
            if (BackendUtility::isTableWorkspaceEnabled($table)) {
224
                // In Live workspace, delete any. In other workspaces there must be match.
225
                if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) {
226
                    $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
227
                    // Processing can be skipped if a delete placeholder shall be published
228
                    // during the current request. Thus it will be deleted later on...
229
                    $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']);
230
                    if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid'])
231
                        && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'])
232
                        && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
233
                        && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
234
                        && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id
235
                    ) {
236
                        return null;
237
                    }
238
239
                    if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) {
240
                        // Change normal versioned record to delete placeholder
241
                        // Happens when an edited record is deleted
242
                        GeneralUtility::makeInstance(ConnectionPool::class)
243
                            ->getConnectionForTable($table)
244
                            ->update(
245
                                $table,
246
                                ['t3ver_state' => VersionState::DELETE_PLACEHOLDER],
247
                                ['uid' => $id]
248
                            );
249
250
                        // Delete localization overlays:
251
                        $dataHandler->deleteL10nOverlayRecords($table, $id);
252
                    } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) {
253
                        // Delete those in WS 0 + if their live records state was not "Placeholder".
254
                        $dataHandler->deleteEl($table, $id);
255
                        if ($recordVersionState->equals(VersionState::MOVE_POINTER)) {
256
                            // Delete move-placeholder if current version record is a move-to-pointer.
257
                            // deleteEl() can't be used here: The deleteEl() for the MOVE_POINTER record above
258
                            // already triggered a delete cascade for children (inline, ...). If we'd
259
                            // now call deleteEl() again, we'd trigger adding delete placeholder records for children.
260
                            // Thus, it's safe here to just set the MOVE_PLACEHOLDER to deleted (or drop row) straight ahead.
261
                            $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid', $record['t3ver_wsid']);
262
                            if (!empty($movePlaceholder)) {
263
                                $this->softOrHardDeleteSingleRecord($table, (int)$movePlaceholder['uid']);
264
                            }
265
                        }
266
                    } elseif ($recordVersionState->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
267
                        $placeholderRecord = BackendUtility::getLiveVersionOfRecord($table, (int)$id);
268
                        $dataHandler->deleteEl($table, (int)$id);
269
                        if (is_array($placeholderRecord)) {
270
                            $this->softOrHardDeleteSingleRecord($table, (int)$placeholderRecord['uid']);
271
                            $dataHandler->updateRefIndex($table, (int)$placeholderRecord['uid']);
272
                        }
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)
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
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
            $versionedRecordUid = (int)$workspaceVersion['uid'];
419
            // moving not needed, just behave like in live workspace
420
            if (!$versionedRecordUid || !$tableSupportsVersioning) {
421
                $recordWasMoved = false;
422
            } elseif ($recIsNewVersion) {
423
                // A newly created record is marked to be moved, so TYPO3 Core is taking care of moving
424
                // the new placeholder.
425
                $recordWasMoved = false;
426
                // However, TYPO3 Core should move the versioned record as well, which is done directly in Core,
427
                // before the placeholder is moved.
428
                $dataHandler->moveRecord_raw($table, $versionedRecordUid, (int)$destPid);
429
            } else {
430
                // If the move operation is done on a versioned record, which is
431
                // NOT new/deleted placeholder, then also create a move placeholder
432
                $this->moveRecord_wsPlaceholders($table, (int)$uid, (int)$destPid, (int)$resolvedPid, $versionedRecordUid, $dataHandler);
433
            }
434
        } else {
435
            $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), SystemLogErrorClassification::USER_ERROR);
436
        }
437
    }
438
439
    /**
440
     * Processes fields of a moved record and follows references.
441
     *
442
     * @param DataHandler $dataHandler Calling DataHandler instance
443
     * @param int $resolvedPageId Resolved real destination page id
444
     * @param string $table Name of parent table
445
     * @param int $uid UID of the parent record
446
     */
447
    protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
448
    {
449
        $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid);
450
        if (empty($versionedRecord)) {
451
            return;
452
        }
453
        foreach ($versionedRecord as $field => $value) {
454
            if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
455
                continue;
456
            }
457
            $this->moveRecord_processFieldValue(
458
                $dataHandler,
459
                $resolvedPageId,
460
                $table,
461
                $uid,
462
                $value,
463
                $GLOBALS['TCA'][$table]['columns'][$field]['config']
464
            );
465
        }
466
    }
467
468
    /**
469
     * Processes a single field of a moved record and follows references.
470
     *
471
     * @param DataHandler $dataHandler Calling DataHandler instance
472
     * @param int $resolvedPageId Resolved real destination page id
473
     * @param string $table Name of parent table
474
     * @param int $uid UID of the parent record
475
     * @param string $value Value of the field of the parent record
476
     * @param array $configuration TCA field configuration of the parent record
477
     */
478
    protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $value, array $configuration): void
479
    {
480
        $inlineFieldType = $dataHandler->getInlineFieldType($configuration);
481
        $inlineProcessing = (
482
            ($inlineFieldType === 'list' || $inlineFieldType === 'field')
483
            && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
484
            && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent'])
485
        );
486
487
        if ($inlineProcessing) {
488
            if ($table === 'pages') {
489
                // If the inline elements are related to a page record,
490
                // make sure they reside at that page and not at its parent
491
                $resolvedPageId = $uid;
492
            }
493
494
            $dbAnalysis = $this->createRelationHandlerInstance();
495
            $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration);
496
497
            // Moving records to a positive destination will insert each
498
            // record at the beginning, thus the order is reversed here:
499
            foreach ($dbAnalysis->itemArray as $item) {
500
                $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
501
                if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) {
502
                    continue;
503
                }
504
                $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId);
505
            }
506
        }
507
    }
508
509
    /****************************
510
     *****  Stage Changes  ******
511
     ****************************/
512
    /**
513
     * Setting stage of record
514
     *
515
     * @param string $table Table name
516
     * @param int $id
517
     * @param int $stageId Stage ID to set
518
     * @param string $comment Comment that goes into log
519
     * @param DataHandler $dataHandler DataHandler object
520
     * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
521
     */
522
    protected function version_setStage($table, $id, $stageId, string $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
523
    {
524
        if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
525
            $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR);
526
        } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) {
527
            $record = BackendUtility::getRecord($table, $id);
528
            $workspaceInfo = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']);
529
            // check if the user is allowed to the current stage, so it's also allowed to send to next stage
530
            if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) {
531
                // Set stage of record:
532
                GeneralUtility::makeInstance(ConnectionPool::class)
533
                    ->getConnectionForTable($table)
534
                    ->update(
535
                        $table,
536
                        [
537
                            't3ver_stage' => $stageId,
538
                        ],
539
                        ['uid' => (int)$id]
540
                    );
541
542
                if ($dataHandler->enableLogging) {
543
                    $propertyArray = $dataHandler->getRecordProperties($table, $id);
544
                    $pid = $propertyArray['pid'];
545
                    $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));
546
                }
547
                // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
548
                $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]);
549
                if ((int)$workspaceInfo['stagechg_notification'] > 0) {
550
                    $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$workspaceInfo, $stageId, $comment];
551
                    $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = [$table, $id];
552
                    $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['recipients'] = $notificationAlternativeRecipients;
553
                }
554
            } else {
555
                $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', SystemLogErrorClassification::USER_ERROR);
556
            }
557
        } else {
558
            $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', SystemLogErrorClassification::USER_ERROR);
559
        }
560
    }
561
562
    /*****************************
563
     *****  CMD versioning  ******
564
     *****************************/
565
566
    /**
567
     * Publishing / Swapping (= switching) versions of a record
568
     * 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
569
     *
570
     * @param string $table Table name
571
     * @param int $id UID of the online record to swap
572
     * @param int $swapWith UID of the archived version to swap with!
573
     * @param DataHandler $dataHandler DataHandler object
574
     * @param string $comment Notification comment
575
     * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
576
     */
577
    protected function version_swap($table, $id, $swapWith, DataHandler $dataHandler, string $comment, $notificationAlternativeRecipients = [])
578
    {
579
        // Check prerequisites before start publishing
580
        // Skip records that have been deleted during the current execution
581
        if ($dataHandler->hasDeletedRecord($table, $id)) {
582
            return;
583
        }
584
585
        // First, check if we may actually edit the online record
586
        if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
587
            $dataHandler->newlog(
588
                sprintf(
589
                    'Error: You cannot swap versions for record %s:%d you do not have access to edit!',
590
                    $table,
591
                    $id
592
                ),
593
                SystemLogErrorClassification::USER_ERROR
594
            );
595
            return;
596
        }
597
        // Select the two versions:
598
        $curVersion = BackendUtility::getRecord($table, $id, '*');
599
        $swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
600
        $movePlh = [];
601
        $movePlhID = 0;
602
        if (!(is_array($curVersion) && is_array($swapVersion))) {
603
            $dataHandler->newlog(
604
                sprintf(
605
                    'Error: Either online or swap version for %s:%d->%d could not be selected!',
606
                    $table,
607
                    $id,
608
                    $swapWith
609
                ),
610
                SystemLogErrorClassification::SYSTEM_ERROR
611
            );
612
            return;
613
        }
614
        if (!$dataHandler->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
615
            $dataHandler->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], SystemLogErrorClassification::USER_ERROR);
616
            return;
617
        }
618
        $wsAccess = $dataHandler->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']);
619
        if (!($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === -10)) {
620
            $dataHandler->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', SystemLogErrorClassification::USER_ERROR);
621
            return;
622
        }
623
        if (!($dataHandler->doesRecordExist($table, $swapWith, Permission::PAGE_SHOW) && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) {
624
            $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', SystemLogErrorClassification::USER_ERROR);
625
            return;
626
        }
627
        // Check if the swapWith record really IS a version of the original!
628
        if (!(((int)$swapVersion['t3ver_oid'] > 0 && (int)$curVersion['t3ver_oid'] === 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) {
629
            $dataHandler->newlog('In offline record, either t3ver_oid was not set or the t3ver_oid didn\'t match the id of the online version as it must!', SystemLogErrorClassification::SYSTEM_ERROR);
630
            return;
631
        }
632
633
        // Find fields to keep
634
        $keepFields = $this->getUniqueFields($table);
635
        if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
636
            $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
637
        }
638
        // l10n-fields must be kept otherwise the localization
639
        // will be lost during the publishing
640
        if ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
641
            $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
642
        }
643
        // Swap "keepfields"
644
        foreach ($keepFields as $fN) {
645
            $tmp = $swapVersion[$fN];
646
            $swapVersion[$fN] = $curVersion[$fN];
647
            $curVersion[$fN] = $tmp;
648
        }
649
        // Preserve states:
650
        $t3ver_state = [];
651
        $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
652
        // Modify offline version to become online:
653
        // Set pid for ONLINE
654
        $swapVersion['pid'] = (int)$curVersion['pid'];
655
        // We clear this because t3ver_oid only make sense for offline versions
656
        // and we want to prevent unintentional misuse of this
657
        // value for online records.
658
        $swapVersion['t3ver_oid'] = 0;
659
        // In case of swapping and the offline record has a state
660
        // (like 2 or 4 for deleting or move-pointer) we set the
661
        // current workspace ID so the record is not deselected.
662
        $swapVersion['t3ver_wsid'] = 0;
663
        $swapVersion['t3ver_stage'] = 0;
664
        $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
665
        // Moving element.
666
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
667
            //  && $t3ver_state['swapVersion']==4   // Maybe we don't need this?
668
            if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) {
669
                $movePlhID = $plhRec['uid'];
670
                $movePlh['pid'] = $swapVersion['pid'];
671
                $swapVersion['pid'] = (int)$plhRec['pid'];
672
                $curVersion['t3ver_state'] = (int)$swapVersion['t3ver_state'];
673
                $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
674
                if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
675
                    // sortby is a "keepFields" which is why this will work...
676
                    $movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
677
                    $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
678
                }
679
            }
680
        }
681
        // Take care of relations in each field (e.g. IRRE):
682
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
683
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
684
                if (isset($fieldConf['config']) && is_array($fieldConf['config'])) {
685
                    $this->version_swap_processFields($table, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler);
686
                }
687
            }
688
        }
689
        unset($swapVersion['uid']);
690
        // Modify online version to become offline:
691
        unset($curVersion['uid']);
692
        // Mark curVersion to contain the oid
693
        $curVersion['t3ver_oid'] = (int)$id;
694
        $curVersion['t3ver_wsid'] = 0;
695
        // Increment lifecycle counter
696
        $curVersion['t3ver_stage'] = 0;
697
        $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
698
        // Registering and swapping MM relations in current and swap records:
699
        $dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith);
700
        // Generating proper history data to prepare logging
701
        $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
702
        $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
703
704
        // Execute swapping:
705
        $sqlErrors = [];
706
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
707
708
        $platform = $connection->getDatabasePlatform();
709
        $tableDetails = null;
710
        if ($platform instanceof SQLServerPlatform) {
711
            // mssql needs to set proper PARAM_LOB and others to update fields
712
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
713
        }
714
715
        try {
716
            $types = [];
717
718
            if ($platform instanceof SQLServerPlatform) {
719
                foreach ($curVersion as $columnName => $columnValue) {
720
                    $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
721
                }
722
            }
723
724
            $connection->update(
725
                $table,
726
                $swapVersion,
727
                ['uid' => (int)$id],
728
                $types
729
            );
730
        } catch (DBALException $e) {
731
            $sqlErrors[] = $e->getPrevious()->getMessage();
732
        }
733
734
        if (empty($sqlErrors)) {
735
            try {
736
                $types = [];
737
                if ($platform instanceof SQLServerPlatform) {
738
                    foreach ($curVersion as $columnName => $columnValue) {
739
                        $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
740
                    }
741
                }
742
743
                $connection->update(
744
                    $table,
745
                    $curVersion,
746
                    ['uid' => (int)$swapWith],
747
                    $types
748
                );
749
            } catch (DBALException $e) {
750
                $sqlErrors[] = $e->getPrevious()->getMessage();
751
            }
752
        }
753
754
        if (!empty($sqlErrors)) {
755
            $dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), SystemLogErrorClassification::SYSTEM_ERROR);
756
        } else {
757
            // Register swapped ids for later remapping:
758
            $this->remappedIds[$table][$id] = $swapWith;
759
            $this->remappedIds[$table][$swapWith] = $id;
760
            // If a moving operation took place...:
761
            if ($movePlhID) {
762
                // Remove, if normal publishing:
763
                // For delete + completely delete!
764
                $dataHandler->deleteEl($table, $movePlhID, true, true);
765
            }
766
            // Checking for delete:
767
            // Delete only if new/deleted placeholders are there.
768
            if (((int)$t3ver_state['swapVersion'] === VersionState::NEW_PLACEHOLDER || (int)$t3ver_state['swapVersion'] === VersionState::DELETE_PLACEHOLDER)) {
769
                // Force delete
770
                $dataHandler->deleteEl($table, $id, true);
771
            }
772
            if ($dataHandler->enableLogging) {
773
                $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Publishing successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, -1, [], $dataHandler->eventPid($table, $id, $swapVersion['pid']));
774
            }
775
776
            // Update reference index of the live record:
777
            $dataHandler->addRemapStackRefIndex($table, $id);
778
            // Set log entry for live record:
779
            $propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion);
780
            if (($propArr['t3ver_oid'] ?? 0) > 0) {
781
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
782
            } else {
783
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
784
            }
785
            $theLogId = $dataHandler->log($table, $id, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
786
            $dataHandler->setHistory($table, $id, $theLogId);
787
            // Update reference index of the offline record:
788
            $dataHandler->addRemapStackRefIndex($table, $swapWith);
789
            // Set log entry for offline record:
790
            $propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion);
791
            if (($propArr['t3ver_oid'] ?? 0) > 0) {
792
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
793
            } else {
794
                $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
795
            }
796
            $theLogId = $dataHandler->log($table, $swapWith, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']);
797
            $dataHandler->setHistory($table, $swapWith, $theLogId);
798
799
            $stageId = StagesService::STAGE_PUBLISH_EXECUTE_ID;
800
            $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
801
            $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
802
            $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = [$table, $id];
803
            $this->notificationEmailInfo[$notificationEmailInfoKey]['recipients'] = $notificationAlternativeRecipients;
804
            // Write to log with stageId -20 (STAGE_PUBLISH_EXECUTE_ID)
805
            if ($dataHandler->enableLogging) {
806
                $propArr = $dataHandler->getRecordProperties($table, $id);
807
                $pid = $propArr['pid'];
808
                $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));
809
            }
810
            $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]);
811
812
            // Clear cache:
813
            $dataHandler->registerRecordIdForPageCacheClearing($table, $id);
814
            // If published, delete the record from the database
815
            if ($table === 'pages') {
816
                // Note on fifth argument false: At this point both $curVersion and $swapVersion page records are
817
                // identical in DB. deleteEl() would now usually find all records assigned to our obsolete
818
                // page which at the same time belong to our current version page, and would delete them.
819
                // To suppress this, false tells deleteEl() to only delete the obsolete page but not its assigned records.
820
                $dataHandler->deleteEl($table, $swapWith, true, true, false);
821
            } else {
822
                $dataHandler->deleteEl($table, $swapWith, true, true);
823
            }
824
825
            // Update reference index for live workspace too:
826
            /** @var \TYPO3\CMS\Core\Database\ReferenceIndex $refIndexObj */
827
            $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
828
            $refIndexObj->setWorkspaceId(0);
829
            $refIndexObj->updateRefIndexTable($table, $id);
830
            $refIndexObj->updateRefIndexTable($table, $swapWith);
831
        }
832
    }
833
834
    /**
835
     * Processes fields of a record for the publishing/swapping process.
836
     * Basically this takes care of IRRE (type "inline") child references.
837
     *
838
     * @param string $tableName Table name
839
     * @param array $configuration TCA field configuration
840
     * @param array $liveData Live record data
841
     * @param array $versionData Version record data
842
     * @param DataHandler $dataHandler Calling data-handler object
843
     */
844
    protected function version_swap_processFields($tableName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
845
    {
846
        $inlineType = $dataHandler->getInlineFieldType($configuration);
847
        if ($inlineType !== 'field') {
848
            return;
849
        }
850
        $foreignTable = $configuration['foreign_table'];
851
        // Read relations that point to the current record (e.g. live record):
852
        $liveRelations = $this->createRelationHandlerInstance();
853
        $liveRelations->setWorkspaceId(0);
854
        $liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration);
855
        // Read relations that point to the record to be swapped with e.g. draft record):
856
        $versionRelations = $this->createRelationHandlerInstance();
857
        $versionRelations->setUseLiveReferenceIds(false);
858
        $versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration);
859
        // Update relations for both (workspace/versioning) sites:
860
        if (!empty($liveRelations->itemArray)) {
861
            $dataHandler->addRemapAction(
862
                $tableName,
863
                $liveData['uid'],
864
                [$this, 'updateInlineForeignFieldSorting'],
865
                [$liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace]
866
            );
867
        }
868
        if (!empty($versionRelations->itemArray)) {
869
            $dataHandler->addRemapAction(
870
                $tableName,
871
                $liveData['uid'],
872
                [$this, 'updateInlineForeignFieldSorting'],
873
                [$liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0]
874
            );
875
        }
876
    }
877
878
    /**
879
     * Updates foreign field sorting values of versioned and live
880
     * parents after(!) the whole structure has been published.
881
     *
882
     * This method is used as callback function in
883
     * DataHandlerHook::version_swap_procBasedOnFieldType().
884
     * Sorting fields ("sortby") are not modified during the
885
     * workspace publishing/swapping process directly.
886
     *
887
     * @param string $parentId
888
     * @param string $foreignTableName
889
     * @param int[] $foreignIds
890
     * @param array $configuration
891
     * @param int $targetWorkspaceId
892
     * @internal
893
     */
894
    public function updateInlineForeignFieldSorting($parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
895
    {
896
        $remappedIds = [];
897
        // Use remapped ids (live id <-> version id)
898
        foreach ($foreignIds as $foreignId) {
899
            if (!empty($this->remappedIds[$foreignTableName][$foreignId])) {
900
                $remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId];
901
            } else {
902
                $remappedIds[] = $foreignId;
903
            }
904
        }
905
906
        $relationHandler = $this->createRelationHandlerInstance();
907
        $relationHandler->setWorkspaceId($targetWorkspaceId);
908
        $relationHandler->setUseLiveReferenceIds(false);
909
        $relationHandler->start(implode(',', $remappedIds), $foreignTableName);
910
        $relationHandler->processDeletePlaceholder();
911
        $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

911
        $relationHandler->writeForeignField($configuration, /** @scrutinizer ignore-type */ $parentId);
Loading history...
912
    }
913
914
    /**
915
     * In case a sys_workspace_stage record is deleted we do a hard reset
916
     * for all existing records in that stage to avoid that any of these end up
917
     * as orphan records.
918
     *
919
     * @param int $stageId Elements with this stage are reset
920
     */
921
    protected function resetStageOfElements(int $stageId): void
922
    {
923
        foreach ($this->getTcaTables() as $tcaTable) {
924
            if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
925
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
926
                    ->getQueryBuilderForTable($tcaTable);
927
928
                $queryBuilder
929
                    ->update($tcaTable)
930
                    ->set('t3ver_stage', StagesService::STAGE_EDIT_ID)
931
                    ->where(
932
                        $queryBuilder->expr()->eq(
933
                            't3ver_stage',
934
                            $queryBuilder->createNamedParameter($stageId, \PDO::PARAM_INT)
935
                        ),
936
                        $queryBuilder->expr()->gt(
937
                            't3ver_wsid',
938
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
939
                        )
940
                    )
941
                    ->execute();
942
            }
943
        }
944
    }
945
946
    /**
947
     * Flushes (remove, no soft delete!) elements of a particular workspace to avoid orphan records.
948
     * This is used if an admin deletes a sys_workspace record.
949
     *
950
     * @param int $workspaceId The workspace to be flushed
951
     */
952
    protected function flushWorkspaceElements(int $workspaceId): void
953
    {
954
        $command = [];
955
        foreach ($this->getTcaTables() as $tcaTable) {
956
            if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
957
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
958
                    ->getQueryBuilderForTable($tcaTable);
959
                $queryBuilder->getRestrictions()->removeAll();
960
                $result = $queryBuilder
961
                    ->select('uid')
962
                    ->from($tcaTable)
963
                    ->where(
964
                        $queryBuilder->expr()->eq(
965
                            't3ver_wsid',
966
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
967
                        ),
968
                        // t3ver_oid >= 0 basically omits placeholder records here, those would otherwise
969
                        // fail to delete later in DH->discard() and would create "can't do that" log entries.
970
                        $queryBuilder->expr()->gt(
971
                            't3ver_oid',
972
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
973
                        )
974
                    )
975
                    ->orderBy('uid')
976
                    ->execute();
977
978
                while (($recordId = $result->fetchColumn()) !== false) {
979
                    $command[$tcaTable][$recordId]['version']['action'] = 'flush';
980
                }
981
            }
982
        }
983
        if (!empty($command)) {
984
            // Execute the command array via DataHandler to flush all records from this workspace.
985
            // Switch to target workspace temporarily, otherwise DH->discard() do not
986
            // operate on correct workspace if fetching additional records.
987
            $backendUser = $GLOBALS['BE_USER'];
988
            $savedWorkspace = $backendUser->workspace;
989
            $backendUser->workspace = $workspaceId;
990
            $context = GeneralUtility::makeInstance(Context::class);
991
            $savedWorkspaceContext = $context->getAspect('workspace');
992
            $context->setAspect('workspace', new WorkspaceAspect($workspaceId));
993
994
            $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
995
            $dataHandler->start([], $command, $backendUser);
996
            $dataHandler->process_cmdmap();
997
998
            $backendUser->workspace = $savedWorkspace;
999
            $context->setAspect('workspace', $savedWorkspaceContext);
1000
        }
1001
    }
1002
1003
    /**
1004
     * Gets all defined TCA tables.
1005
     *
1006
     * @return array
1007
     */
1008
    protected function getTcaTables(): array
1009
    {
1010
        return array_keys($GLOBALS['TCA']);
1011
    }
1012
1013
    /**
1014
     * Flushes the workspace cache for current workspace and for the virtual "all workspaces" too.
1015
     *
1016
     * @param int $workspaceId The workspace to be flushed in cache
1017
     */
1018
    protected function flushWorkspaceCacheEntriesByWorkspaceId(int $workspaceId): void
1019
    {
1020
        $workspacesCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('workspaces_cache');
1021
        $workspacesCache->flushByTag($workspaceId);
1022
    }
1023
1024
    /*******************************
1025
     *****  helper functions  ******
1026
     *******************************/
1027
1028
    /**
1029
     * Finds all elements for swapping versions in workspace
1030
     *
1031
     * @param string $table Table name of the original element to swap
1032
     * @param int $id UID of the original element to swap (online)
1033
     * @param int $offlineId As above but offline
1034
     * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID
1035
     */
1036
    public function findPageElementsForVersionSwap($table, $id, $offlineId)
1037
    {
1038
        $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid');
1039
        $workspaceId = (int)$rec['t3ver_wsid'];
1040
        $elementData = [];
1041
        if ($workspaceId === 0) {
1042
            return $elementData;
1043
        }
1044
        // Get page UID for LIVE and workspace
1045
        if ($table !== 'pages') {
1046
            $rec = BackendUtility::getRecord($table, $id, 'pid');
1047
            $pageId = $rec['pid'];
1048
            $rec = BackendUtility::getRecord('pages', $pageId);
1049
            BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1050
            $offlinePageId = $rec['_ORIG_uid'];
1051
        } else {
1052
            $pageId = $id;
1053
            $offlinePageId = $offlineId;
1054
        }
1055
        // Traversing all tables supporting versioning:
1056
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
0 ignored issues
show
introduced by
$table is overwriting one of the parameters of this function.
Loading history...
1057
            if (BackendUtility::isTableWorkspaceEnabled($table) && $table !== 'pages') {
1058
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1059
                    ->getQueryBuilderForTable($table);
1060
1061
                $queryBuilder->getRestrictions()
1062
                    ->removeAll()
1063
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1064
1065
                $statement = $queryBuilder
1066
                    ->select('A.uid AS offlineUid', 'B.uid AS uid')
1067
                    ->from($table, 'A')
1068
                    ->from($table, 'B')
1069
                    ->where(
1070
                        $queryBuilder->expr()->gt(
1071
                            'A.t3ver_oid',
1072
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1073
                        ),
1074
                        $queryBuilder->expr()->eq(
1075
                            'B.pid',
1076
                            $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
1077
                        ),
1078
                        $queryBuilder->expr()->eq(
1079
                            'A.t3ver_wsid',
1080
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1081
                        ),
1082
                        $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1083
                    )
1084
                    ->execute();
1085
1086
                while ($row = $statement->fetch()) {
1087
                    $elementData[$table][] = [$row['uid'], $row['offlineUid']];
1088
                }
1089
            }
1090
        }
1091
        if ($offlinePageId && $offlinePageId != $pageId) {
1092
            $elementData['pages'][] = [$pageId, $offlinePageId];
1093
        }
1094
1095
        return $elementData;
1096
    }
1097
1098
    /**
1099
     * Searches for all elements from all tables on the given pages in the same workspace.
1100
     *
1101
     * @param array $pageIdList List of PIDs to search
1102
     * @param int $workspaceId Workspace ID
1103
     * @param array $elementList List of found elements. Key is table name, value is array of element UIDs
1104
     */
1105
    public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList)
1106
    {
1107
        if ($workspaceId == 0) {
1108
            return;
1109
        }
1110
        // Traversing all tables supporting versioning:
1111
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
1112
            if (BackendUtility::isTableWorkspaceEnabled($table) && $table !== 'pages') {
1113
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1114
                    ->getQueryBuilderForTable($table);
1115
1116
                $queryBuilder->getRestrictions()
1117
                    ->removeAll()
1118
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1119
1120
                $statement = $queryBuilder
1121
                    ->select('A.uid')
1122
                    ->from($table, 'A')
1123
                    ->from($table, 'B')
1124
                    ->where(
1125
                        $queryBuilder->expr()->gt(
1126
                            'A.t3ver_oid',
1127
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1128
                        ),
1129
                        $queryBuilder->expr()->in(
1130
                            'B.pid',
1131
                            $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY)
1132
                        ),
1133
                        $queryBuilder->expr()->eq(
1134
                            'A.t3ver_wsid',
1135
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1136
                        ),
1137
                        $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1138
                    )
1139
                    ->groupBy('A.uid')
1140
                    ->execute();
1141
1142
                while ($row = $statement->fetch()) {
1143
                    $elementList[$table][] = $row['uid'];
1144
                }
1145
                if (is_array($elementList[$table])) {
1146
                    // Yes, it is possible to get non-unique array even with DISTINCT above!
1147
                    // It happens because several UIDs are passed in the array already.
1148
                    $elementList[$table] = array_unique($elementList[$table]);
1149
                }
1150
            }
1151
        }
1152
    }
1153
1154
    /**
1155
     * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1156
     *
1157
     * @param string $table Table to search
1158
     * @param array $idList List of records' UIDs
1159
     * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publish DRAFT from ws module!
1160
     * @param array $pageIdList List of found page UIDs
1161
     * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs
1162
     */
1163
    public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList)
1164
    {
1165
        if ($workspaceId == 0) {
1166
            return;
1167
        }
1168
1169
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1170
            ->getQueryBuilderForTable($table);
1171
        $queryBuilder->getRestrictions()
1172
            ->removeAll()
1173
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1174
1175
        $statement = $queryBuilder
1176
            ->select('B.pid')
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
                    'A.t3ver_wsid',
1186
                    $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1187
                ),
1188
                $queryBuilder->expr()->in(
1189
                    'A.uid',
1190
                    $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY)
1191
                ),
1192
                $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1193
            )
1194
            ->groupBy('B.pid')
1195
            ->execute();
1196
1197
        while ($row = $statement->fetch()) {
1198
            $pageIdList[] = $row['pid'];
1199
            // Find ws version
1200
            // Note: cannot use BackendUtility::getRecordWSOL()
1201
            // here because it does not accept workspace id!
1202
            $rec = BackendUtility::getRecord('pages', $row[0]);
1203
            BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1204
            if ($rec['_ORIG_uid']) {
1205
                $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1206
            }
1207
        }
1208
        // The line below is necessary even with DISTINCT
1209
        // because several elements can be passed by caller
1210
        $pageIdList = array_unique($pageIdList);
1211
    }
1212
1213
    /**
1214
     * Finds real page IDs for state change.
1215
     *
1216
     * @param array $idList List of page UIDs, possibly versioned
1217
     */
1218
    public function findRealPageIds(array &$idList): void
1219
    {
1220
        foreach ($idList as $key => $id) {
1221
            $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid');
1222
            if ($rec['t3ver_oid'] > 0) {
1223
                $idList[$key] = $rec['t3ver_oid'];
1224
            }
1225
        }
1226
    }
1227
1228
    /**
1229
     * Creates a move placeholder for workspaces.
1230
     * USE ONLY INTERNALLY
1231
     * Moving placeholder: Can be done because the system sees it as a placeholder for NEW elements like t3ver_state=VersionState::NEW_PLACEHOLDER
1232
     * Moving original: Will either create the placeholder if it doesn't exist or move existing placeholder in workspace.
1233
     *
1234
     * @param string $table Table name to move
1235
     * @param int $uid Record uid to move (online record)
1236
     * @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
1237
     * @param int $resolvedId Effective page ID
1238
     * @param int $offlineUid UID of offline version of online record
1239
     * @param DataHandler $dataHandler DataHandler object
1240
     * @see moveRecord()
1241
     */
1242
    protected function moveRecord_wsPlaceholders(string $table, int $uid, int $destPid, int $resolvedId, int $offlineUid, DataHandler $dataHandler): void
1243
    {
1244
        // If a record gets moved after a record that already has a placeholder record
1245
        // then the new placeholder record needs to be after the existing one
1246
        $originalRecordDestinationPid = $destPid;
1247
        $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

1247
        $movePlaceHolder = BackendUtility::getMovePlaceholder($table, /** @scrutinizer ignore-type */ abs($destPid), 'uid');
Loading history...
1248
        if ($movePlaceHolder !== false && $destPid < 0) {
1249
            $destPid = -$movePlaceHolder['uid'];
1250
        }
1251
        if ($plh = BackendUtility::getMovePlaceholder($table, $uid, 'uid')) {
1252
            // If already a placeholder exists, move it:
1253
            $dataHandler->moveRecord_raw($table, $plh['uid'], $destPid);
1254
        } else {
1255
            // First, we create a placeholder record in the Live workspace that
1256
            // represents the position to where the record is eventually moved to.
1257
            $newVersion_placeholderFieldArray = [];
1258
1259
            $factory = GeneralUtility::makeInstance(
1260
                PlaceholderShadowColumnsResolver::class,
1261
                $table,
1262
                $GLOBALS['TCA'][$table] ?? []
1263
            );
1264
            $shadowColumns = $factory->forMovePlaceholder();
1265
            // Set values from the versioned record to the move placeholder
1266
            if (!empty($shadowColumns)) {
1267
                $versionedRecord = BackendUtility::getRecord($table, $offlineUid);
1268
                foreach ($shadowColumns as $shadowColumn) {
1269
                    if (isset($versionedRecord[$shadowColumn])) {
1270
                        $newVersion_placeholderFieldArray[$shadowColumn] = $versionedRecord[$shadowColumn];
1271
                    }
1272
                }
1273
            }
1274
1275
            if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1276
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1277
            }
1278
            if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1279
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $dataHandler->userid;
1280
            }
1281
            if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
1282
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1283
            }
1284
            if ($table === 'pages') {
1285
                // Copy page access settings from original page to placeholder
1286
                $perms_clause = $dataHandler->BE_USER->getPagePermsClause(Permission::PAGE_SHOW);
1287
                $access = BackendUtility::readPageAccess($uid, $perms_clause);
1288
                $newVersion_placeholderFieldArray['perms_userid'] = $access['perms_userid'];
1289
                $newVersion_placeholderFieldArray['perms_groupid'] = $access['perms_groupid'];
1290
                $newVersion_placeholderFieldArray['perms_user'] = $access['perms_user'];
1291
                $newVersion_placeholderFieldArray['perms_group'] = $access['perms_group'];
1292
                $newVersion_placeholderFieldArray['perms_everybody'] = $access['perms_everybody'];
1293
            }
1294
            $newVersion_placeholderFieldArray['t3ver_move_id'] = $uid;
1295
            // Setting placeholder state value for temporary record
1296
            $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER);
1297
            // Setting workspace - only so display of place holders can filter out those from other workspaces.
1298
            $newVersion_placeholderFieldArray['t3ver_wsid'] = $dataHandler->BE_USER->workspace;
1299
            $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1300
            if ($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['type'] === 'input') {
1301
                $newVersion_placeholderFieldArray[$labelField] = $dataHandler->getPlaceholderTitleForTableLabel($table, 'MOVE-TO PLACEHOLDER for #' . $uid);
1302
            }
1303
            // moving localized records requires to keep localization-settings for the placeholder too
1304
            if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
1305
                $l10nParentRec = BackendUtility::getRecord($table, $uid);
1306
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
1307
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
1308
                if (isset($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])) {
1309
                    $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']];
1310
                }
1311
                unset($l10nParentRec);
1312
            }
1313
            // @todo Check why $destPid cannot be used directly
1314
            // Initially, create at root level.
1315
            $newVersion_placeholderFieldArray['pid'] = 0;
1316
            $id = 'NEW_MOVE_PLH';
1317
            // Saving placeholder as 'original'
1318
            $dataHandler->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1319
            // Move the new placeholder from temporary root-level to location:
1320
            $dataHandler->moveRecord_raw($table, $dataHandler->substNEWwithIDs[$id], $destPid);
1321
            // Move the workspace-version of the original to be the version of the move-to-placeholder:
1322
            // Setting placeholder state value for version (so it can know it is currently a new version...)
1323
            $updateFields = [
1324
                'pid' => $resolvedId,
1325
                't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER)
1326
            ];
1327
1328
            GeneralUtility::makeInstance(ConnectionPool::class)
1329
                ->getConnectionForTable($table)
1330
                ->update(
1331
                    $table,
1332
                    $updateFields,
1333
                    ['uid' => (int)$offlineUid]
1334
                );
1335
        }
1336
        // Check for the localizations of that element and move them as well
1337
        $dataHandler->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
1338
    }
1339
1340
    /**
1341
     * Gets an instance of the command map helper.
1342
     *
1343
     * @param DataHandler $dataHandler DataHandler object
1344
     * @return CommandMap
1345
     */
1346
    public function getCommandMap(DataHandler $dataHandler): CommandMap
1347
    {
1348
        return GeneralUtility::makeInstance(
1349
            CommandMap::class,
1350
            $this,
1351
            $dataHandler,
1352
            $dataHandler->cmdmap,
1353
            $dataHandler->BE_USER->workspace
1354
        );
1355
    }
1356
1357
    protected function emitUpdateTopbarSignal(): void
1358
    {
1359
        BackendUtility::setUpdateSignal('updateTopbar');
1360
    }
1361
1362
    /**
1363
     * Returns all fieldnames from a table which have the unique evaluation type set.
1364
     *
1365
     * @param string $table Table name
1366
     * @return array Array of fieldnames
1367
     */
1368
    protected function getUniqueFields($table): array
1369
    {
1370
        $listArr = [];
1371
        foreach ($GLOBALS['TCA'][$table]['columns'] ?? [] as $field => $configArr) {
1372
            if ($configArr['config']['type'] === 'input') {
1373
                $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'], true);
1374
                if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) {
1375
                    $listArr[] = $field;
1376
                }
1377
            }
1378
        }
1379
        return $listArr;
1380
    }
1381
1382
    /**
1383
     * Straight db based record deletion: sets deleted = 1 for soft-delete
1384
     * enabled tables, or removes row from table. Used for move placeholder
1385
     * records sometimes.
1386
     */
1387
    protected function softOrHardDeleteSingleRecord(string $table, int $uid): void
1388
    {
1389
        $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
1390
        if ($deleteField) {
1391
            GeneralUtility::makeInstance(ConnectionPool::class)
1392
                ->getConnectionForTable($table)
1393
                ->update(
1394
                    $table,
1395
                    [$deleteField => 1],
1396
                    ['uid' => $uid],
1397
                    [\PDO::PARAM_INT]
1398
                );
1399
        } else {
1400
            GeneralUtility::makeInstance(ConnectionPool::class)
1401
                ->getConnectionForTable($table)
1402
                ->delete(
1403
                    $table,
1404
                    ['uid' => $uid]
1405
                );
1406
        }
1407
    }
1408
1409
    /**
1410
     * @return RelationHandler
1411
     */
1412
    protected function createRelationHandlerInstance(): RelationHandler
1413
    {
1414
        return GeneralUtility::makeInstance(RelationHandler::class);
1415
    }
1416
1417
    /**
1418
     * @return LanguageService
1419
     */
1420
    protected function getLanguageService(): LanguageService
1421
    {
1422
        return $GLOBALS['LANG'];
1423
    }
1424
}
1425