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

DataHandlerHook::version_clearWSID()   B

Complexity

Conditions 11
Paths 16

Size

Total Lines 50
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 28
c 1
b 0
f 0
nc 16
nop 4
dl 0
loc 50
rs 7.3166

How to fix   Complexity   

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:

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