Test Failed
Branch master (7b1793)
by Tymoteusz
15:35
created

flushWorkspaceCacheEntriesByWorkspaceId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace TYPO3\CMS\Workspaces\Hook;
3
4
/*
5
 * This file is part of the TYPO3 CMS project.
6
 *
7
 * It is free software; you can redistribute it and/or modify it under
8
 * the terms of the GNU General Public License, either version 2
9
 * of the License, or any later version.
10
 *
11
 * For the full copyright and license information, please read the
12
 * LICENSE.txt file that was distributed with this source code.
13
 *
14
 * The TYPO3 project - inspiring people to share!
15
 */
16
17
use Doctrine\DBAL\DBALException;
18
use Doctrine\DBAL\Platforms\SQLServerPlatform;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Core\Database\Connection;
21
use TYPO3\CMS\Core\Database\ConnectionPool;
22
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
23
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
24
use TYPO3\CMS\Core\Database\ReferenceIndex;
25
use TYPO3\CMS\Core\DataHandling\DataHandler;
26
use TYPO3\CMS\Core\Localization\LanguageService;
27
use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
28
use TYPO3\CMS\Core\Utility\ArrayUtility;
29
use TYPO3\CMS\Core\Utility\GeneralUtility;
30
use TYPO3\CMS\Core\Versioning\VersionState;
31
use TYPO3\CMS\Workspaces\Service\StagesService;
32
33
/**
34
 * Contains some parts for staging, versioning and workspaces
35
 * to interact with the TYPO3 Core Engine
36
 */
37
class DataHandlerHook
38
{
39
    /**
40
     * For accumulating information about workspace stages raised
41
     * on elements so a single mail is sent as notification.
42
     * previously called "accumulateForNotifEmail" in DataHandler
43
     *
44
     * @var array
45
     */
46
    protected $notificationEmailInfo = [];
47
48
    /**
49
     * Contains remapped IDs.
50
     *
51
     * @var array
52
     */
53
    protected $remappedIds = [];
54
55
    /**
56
     * @var \TYPO3\CMS\Workspaces\Service\WorkspaceService
57
     */
58
    protected $workspaceService;
59
60
    /****************************
61
     *****  Cmdmap  Hooks  ******
62
     ****************************/
63
    /**
64
     * hook that is called before any cmd of the commandmap is executed
65
     *
66
     * @param DataHandler $dataHandler reference to the main DataHandler object
67
     */
68
    public function processCmdmap_beforeStart(DataHandler $dataHandler)
69
    {
70
        // Reset notification array
71
        $this->notificationEmailInfo = [];
72
        // Resolve dependencies of version/workspaces actions:
73
        $dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get();
74
    }
75
76
    /**
77
     * hook that is called when no prepared command was found
78
     *
79
     * @param string $command the command to be executed
80
     * @param string $table the table of the record
81
     * @param int $id the ID of the record
82
     * @param mixed $value the value containing the data
83
     * @param bool $commandIsProcessed can be set so that other hooks or
84
     * @param DataHandler $dataHandler reference to the main DataHandler object
85
     */
86
    public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
87
    {
88
        // custom command "version"
89
        if ($command === 'version') {
90
            $commandIsProcessed = true;
91
            $action = (string)$value['action'];
92
            $comment = !empty($value['comment']) ? $value['comment'] : '';
93
            $notificationAlternativeRecipients = (isset($value['notificationAlternativeRecipients'])) && is_array($value['notificationAlternativeRecipients']) ? $value['notificationAlternativeRecipients'] : [];
94
            switch ($action) {
95
                case 'new':
96
                    $dataHandler->versionizeRecord($table, $id, $value['label']);
97
                    break;
98
                case 'swap':
99
                    $this->version_swap(
100
                        $table,
101
                        $id,
102
                        $value['swapWith'],
103
                        $value['swapIntoWS'],
104
                        $dataHandler,
105
                        $comment,
106
                        true,
107
                        $notificationAlternativeRecipients
108
                    );
109
                    break;
110
                case 'clearWSID':
111
                    $this->version_clearWSID($table, $id, false, $dataHandler);
112
                    break;
113
                case 'flush':
114
                    $this->version_clearWSID($table, $id, true, $dataHandler);
115
                    break;
116
                case 'setStage':
117
                    $elementIds = GeneralUtility::trimExplode(',', $id, true);
118
                    foreach ($elementIds as $elementId) {
119
                        $this->version_setStage(
120
                            $table,
121
                            $elementId,
122
                            $value['stageId'],
123
                            $comment,
124
                            true,
125
                            $dataHandler,
126
                            $notificationAlternativeRecipients
127
                        );
128
                    }
129
                    break;
130
                default:
131
                    // Do nothing
132
            }
133
        }
134
    }
135
136
    /**
137
     * hook that is called AFTER all commands of the commandmap was
138
     * executed
139
     *
140
     * @param DataHandler $dataHandler reference to the main DataHandler object
141
     */
142
    public function processCmdmap_afterFinish(DataHandler $dataHandler)
143
    {
144
        // Empty accumulation array:
145
        foreach ($this->notificationEmailInfo as $notifItem) {
146
            $this->notifyStageChange($notifItem['shared'][0], $notifItem['shared'][1], implode(', ', $notifItem['elements']), 0, $notifItem['shared'][2], $dataHandler, $notifItem['alternativeRecipients']);
147
        }
148
        // Reset notification array
149
        $this->notificationEmailInfo = [];
150
        // Reset remapped IDs
151
        $this->remappedIds = [];
152
153
        $this->flushWorkspaceCacheEntriesByWorkspaceId($dataHandler->BE_USER->workspace);
154
    }
155
156
    /**
157
     * hook that is called when an element shall get deleted
158
     *
159
     * @param string $table the table of the record
160
     * @param int $id the ID of the record
161
     * @param array $record The accordant database record
162
     * @param bool $recordWasDeleted can be set so that other hooks or
163
     * @param DataHandler $dataHandler reference to the main DataHandler object
164
     */
165
    public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler)
166
    {
167
        // only process the hook if it wasn't processed
168
        // by someone else before
169
        if ($recordWasDeleted) {
170
            return;
171
        }
172
        $recordWasDeleted = true;
173
        // For Live version, try if there is a workspace version because if so, rather "delete" that instead
174
        // Look, if record is an offline version, then delete directly:
175
        if ($record['pid'] != -1) {
176
            if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) {
177
                $record = $wsVersion;
178
                $id = $record['uid'];
179
            }
180
        }
181
        $recordVersionState = VersionState::cast($record['t3ver_state']);
182
        // Look, if record is an offline version, then delete directly:
183
        if ($record['pid'] == -1) {
184
            if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
185
                // In Live workspace, delete any. In other workspaces there must be match.
186
                if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) {
187
                    $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
188
                    // Processing can be skipped if a delete placeholder shall be swapped/published
189
                    // during the current request. Thus it will be deleted later on...
190
                    $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']);
191
                    if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid'])
192
                        && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'])
193
                        && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
194
                        && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
195
                        && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id
196
                    ) {
197
                        return null;
198
                    }
199
200
                    if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) {
201
                        // Change normal versioned record to delete placeholder
202
                        // Happens when an edited record is deleted
203
                        GeneralUtility::makeInstance(ConnectionPool::class)
204
                            ->getConnectionForTable($table)
205
                            ->update(
206
                                $table,
207
                                [
208
                                    't3ver_label' => 'DELETED!',
209
                                    't3ver_state' => 2,
210
                                ],
211
                                ['uid' => $id]
212
                            );
213
214
                        // Delete localization overlays:
215
                        $dataHandler->deleteL10nOverlayRecords($table, $id);
216
                    } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) {
217
                        // Delete those in WS 0 + if their live records state was not "Placeholder".
218
                        $dataHandler->deleteEl($table, $id);
219
                        // Delete move-placeholder if current version record is a move-to-pointer
220 View Code Duplication
                        if ($recordVersionState->equals(VersionState::MOVE_POINTER)) {
221
                            $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid', $record['t3ver_wsid']);
222
                            if (!empty($movePlaceholder)) {
223
                                $dataHandler->deleteEl($table, $movePlaceholder['uid']);
224
                            }
225
                        }
226
                    } else {
227
                        // If live record was placeholder (new/deleted), rather clear
228
                        // it from workspace (because it clears both version and placeholder).
229
                        $this->version_clearWSID($table, $id, false, $dataHandler);
230
                    }
231
                } else {
232
                    $dataHandler->newlog('Tried to delete record from another workspace', 1);
233
                }
234
            } else {
235
                $dataHandler->newlog('Versioning not enabled for record with PID = -1!', 2);
236
            }
237
        } elseif ($res = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($record['pid'], $table)) {
238
            // Look, if record is "online" or in a versionized branch, then delete directly.
239
            if ($res > 0) {
240
                $dataHandler->deleteEl($table, $id);
241
            } else {
242
                $dataHandler->newlog('Stage of root point did not allow for deletion', 1);
243
            }
244
        } elseif ($recordVersionState->equals(VersionState::MOVE_PLACEHOLDER)) {
245
            // Placeholders for moving operations are deletable directly.
246
            // Get record which its a placeholder for and reset the t3ver_state of that:
247
            if ($wsRec = BackendUtility::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) {
248
                // Clear the state flag of the workspace version of the record
249
                // Setting placeholder state value for version (so it can know it is currently a new version...)
250
251
                GeneralUtility::makeInstance(ConnectionPool::class)
252
                    ->getConnectionForTable($table)
253
                    ->update(
254
                        $table,
255
                        [
256
                            't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE)
257
                        ],
258
                        ['uid' => (int)$wsRec['uid']]
259
                    );
260
            }
261
            $dataHandler->deleteEl($table, $id);
262
        } else {
263
            // Otherwise, try to delete by versioning:
264
            $copyMappingArray = $dataHandler->copyMappingArray;
265
            $dataHandler->versionizeRecord($table, $id, 'DELETED!', true);
266
            // Determine newly created versions:
267
            // (remove placeholders are copied and modified, thus they appear in the copyMappingArray)
268
            $versionizedElements = ArrayUtility::arrayDiffAssocRecursive($dataHandler->copyMappingArray, $copyMappingArray);
269
            // Delete localization overlays:
270
            foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) {
271
                foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) {
272
                    $dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId);
273
                }
274
            }
275
        }
276
    }
277
278
    /**
279
     * In case a sys_workspace_stage record is deleted we do a hard reset
280
     * for all existing records in that stage to avoid that any of these end up
281
     * as orphan records.
282
     *
283
     * @param string $command
284
     * @param string $table
285
     * @param string $id
286
     * @param string $value
287
     * @param \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler
288
     */
289
    public function processCmdmap_postProcess($command, $table, $id, $value, \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler)
0 ignored issues
show
Unused Code introduced by
The parameter $dataHandler is not used and could be removed. ( Ignorable by Annotation )

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

289
    public function processCmdmap_postProcess($command, $table, $id, $value, /** @scrutinizer ignore-unused */ \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler)

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

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

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

289
    public function processCmdmap_postProcess($command, $table, $id, /** @scrutinizer ignore-unused */ $value, \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler)

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

Loading history...
290
    {
291
        if ($command === 'delete') {
292
            if ($table === StagesService::TABLE_STAGE) {
293
                $this->resetStageOfElements($id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $stageId of TYPO3\CMS\Workspaces\Hoo...:resetStageOfElements(). ( Ignorable by Annotation )

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

293
                $this->resetStageOfElements(/** @scrutinizer ignore-type */ $id);
Loading history...
294
            } elseif ($table === \TYPO3\CMS\Workspaces\Service\WorkspaceService::TABLE_WORKSPACE) {
295
                $this->flushWorkspaceElements($id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $workspaceId of TYPO3\CMS\Workspaces\Hoo...lushWorkspaceElements(). ( Ignorable by Annotation )

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

295
                $this->flushWorkspaceElements(/** @scrutinizer ignore-type */ $id);
Loading history...
296
            }
297
        }
298
    }
299
300
    /**
301
     * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about
302
     * moving records that are *not* in the live workspace
303
     *
304
     * @param string $table the table of the record
305
     * @param int $uid the ID of the record
306
     * @param int $destPid Position to move to: $destPid: >=0 then it points to
307
     * @param array $propArr Record properties, like header and pid (includes workspace overlay)
308
     * @param array $moveRec Record properties, like header and pid (without workspace overlay)
309
     * @param int $resolvedPid The final page ID of the record
310
     * @param bool $recordWasMoved can be set so that other hooks or
311
     * @param DataHandler $dataHandler
312
     */
313
    public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)
0 ignored issues
show
Unused Code introduced by
The parameter $propArr is not used and could be removed. ( Ignorable by Annotation )

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

313
    public function moveRecord($table, $uid, $destPid, /** @scrutinizer ignore-unused */ array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)

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

Loading history...
314
    {
315
        // Only do something in Draft workspace
316
        if ($dataHandler->BE_USER->workspace === 0) {
317
            return;
318
        }
319 View Code Duplication
        if ($destPid < 0) {
320
            // Fetch move placeholder, since it might point to a new page in the current workspace
321
            $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

321
            $movePlaceHolder = BackendUtility::getMovePlaceholder($table, /** @scrutinizer ignore-type */ abs($destPid), 'uid,pid');
Loading history...
322
            if ($movePlaceHolder !== false) {
323
                $resolvedPid = $movePlaceHolder['pid'];
324
            }
325
        }
326
        $recordWasMoved = true;
327
        $moveRecVersionState = VersionState::cast($moveRec['t3ver_state']);
328
        // Get workspace version of the source record, if any:
329
        $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
330
        // Handle move-placeholders if the current record is not one already
331
        if (
332
            BackendUtility::isTableWorkspaceEnabled($table)
333
            && !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER)
334
        ) {
335
            // Create version of record first, if it does not exist
336
            if (empty($WSversion['uid'])) {
337
                $dataHandler->versionizeRecord($table, $uid, 'MovePointer');
338
                $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
339
                $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
340
            } elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$WSversion['uid']) {
341
                // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders
342
                $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
343
            }
344
        }
345
        // Check workspace permissions:
346
        $workspaceAccessBlocked = [];
347
        // Element was in "New/Deleted/Moved" so it can be moved...
348
        $recIsNewVersion = $moveRecVersionState->indicatesPlaceholder();
349
        $destRes = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($resolvedPid, $table);
350
        $canMoveRecord = ($recIsNewVersion || BackendUtility::isTableWorkspaceEnabled($table));
351
        // Workspace source check:
352
        if (!$recIsNewVersion) {
353
            $errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $WSversion['uid'] ? $WSversion['uid'] : $uid);
354
            if ($errorCode) {
355
                $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' ';
356
            } elseif (!$canMoveRecord && $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($moveRec['pid'], $table) <= 0) {
357
                $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" ';
358
            }
359
        }
360
        // Workspace destination check:
361
        // All records can be inserted if $destRes is greater than zero.
362
        // Only new versions can be inserted if $destRes is FALSE.
363
        // NO RECORDS can be inserted if $destRes is negative which indicates a stage
364
        //  not allowed for use. If "versioningWS" is version 2, moving can take place of versions.
365
        // since TYPO3 CMS 7, version2 is the default and the only option
366
        if (!($destRes > 0 || $canMoveRecord && !$destRes)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $destRes of type integer|false is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
367
            $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" ';
368
        } elseif ($destRes == 1 && $WSversion['uid']) {
369
            $workspaceAccessBlocked['dest2'] = 'Could not insert other versions in destination PID ';
370
        }
371
        if (empty($workspaceAccessBlocked)) {
372
            // If the move operation is done on a versioned record, which is
373
            // NOT new/deleted placeholder and versioningWS is in version 2, then...
374
            // since TYPO3 CMS 7, version2 is the default and the only option
375
            if ($WSversion['uid'] && !$recIsNewVersion && BackendUtility::isTableWorkspaceEnabled($table)) {
376
                $this->moveRecord_wsPlaceholders($table, $uid, $destPid, $WSversion['uid'], $dataHandler);
377
            } else {
378
                // moving not needed, just behave like in live workspace
379
                $recordWasMoved = false;
380
            }
381
        } else {
382
            $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), 1);
383
        }
384
    }
385
386
    /**
387
     * Processes fields of a moved record and follows references.
388
     *
389
     * @param DataHandler $dataHandler Calling DataHandler instance
390
     * @param int $resolvedPageId Resolved real destination page id
391
     * @param string $table Name of parent table
392
     * @param int $uid UID of the parent record
393
     */
394
    protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
395
    {
396
        $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid);
397
        if (empty($versionedRecord)) {
398
            return;
399
        }
400
        foreach ($versionedRecord as $field => $value) {
401
            if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
402
                continue;
403
            }
404
            $this->moveRecord_processFieldValue(
405
                $dataHandler,
406
                $resolvedPageId,
407
                $table,
408
                $uid,
409
                $field,
410
                $value,
411
                $GLOBALS['TCA'][$table]['columns'][$field]['config']
412
            );
413
        }
414
    }
415
416
    /**
417
     * Processes a single field of a moved record and follows references.
418
     *
419
     * @param DataHandler $dataHandler Calling DataHandler instance
420
     * @param int $resolvedPageId Resolved real destination page id
421
     * @param string $table Name of parent table
422
     * @param int $uid UID of the parent record
423
     * @param string $field Name of the field of the parent record
424
     * @param string $value Value of the field of the parent record
425
     * @param array $configuration TCA field configuration of the parent record
426
     */
427
    protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $field, $value, array $configuration)
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed. ( Ignorable by Annotation )

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

427
    protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, /** @scrutinizer ignore-unused */ $field, $value, array $configuration)

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

Loading history...
428
    {
429
        $inlineFieldType = $dataHandler->getInlineFieldType($configuration);
430
        $inlineProcessing = (
431
            ($inlineFieldType === 'list' || $inlineFieldType === 'field')
432
            && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
433
            && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent'])
434
        );
435
436
        if ($inlineProcessing) {
437
            if ($table === 'pages') {
438
                // If the inline elements are related to a page record,
439
                // make sure they reside at that page and not at its parent
440
                $resolvedPageId = $uid;
441
            }
442
443
            $dbAnalysis = $this->createRelationHandlerInstance();
444
            $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration);
445
446
            // Moving records to a positive destination will insert each
447
            // record at the beginning, thus the order is reversed here:
448
            foreach ($dbAnalysis->itemArray as $item) {
449
                $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
450
                if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) {
451
                    continue;
452
                }
453
                $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId);
454
            }
455
        }
456
    }
457
458
    /****************************
459
     *****  Notifications  ******
460
     ****************************/
461
    /**
462
     * Send an email notification to users in workspace
463
     *
464
     * @param array $stat Workspace access array from \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::checkWorkspace()
465
     * @param int $stageId New Stage number: 0 = editing, 1= just ready for review, 10 = ready for publication, -1 = rejected!
466
     * @param string $table Table name of element (or list of element names if $id is zero)
467
     * @param int $id Record uid of element (if zero, then $table is used as reference to element(s) alone)
468
     * @param string $comment User comment sent along with action
469
     * @param DataHandler $dataHandler DataHandler object
470
     * @param array $notificationAlternativeRecipients List of recipients to notify instead of be_users selected by sys_workspace, list is generated by workspace extension module
471
     */
472
    protected function notifyStageChange(array $stat, $stageId, $table, $id, $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
473
    {
474
        $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
475
        // So, if $id is not set, then $table is taken to be the complete element name!
476
        $elementName = $id ? $table . ':' . $id : $table;
477
        if (!is_array($workspaceRec)) {
478
            return;
479
        }
480
481
        // Get the new stage title
482
        $stageService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\StagesService::class);
483
        $newStage = $stageService->getStageTitle((int)$stageId);
484
        if (empty($notificationAlternativeRecipients)) {
485
            // Compile list of recipients:
486
            $emails = [];
487
            switch ((int)$stat['stagechg_notification']) {
488
                case 1:
489
                    switch ((int)$stageId) {
490
                        case 1:
491
                            $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
492
                            break;
493
                        case 10:
494
                            $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
495
                            break;
496
                        case -1:
497
                            // List of elements to reject:
498
                            $allElements = explode(',', $elementName);
499
                            // Traverse them, and find the history of each
500
                            foreach ($allElements as $elRef) {
501
                                list($eTable, $eUid) = explode(':', $elRef);
502
503
                                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
504
                                    ->getQueryBuilderForTable('sys_log');
505
506
                                $queryBuilder->getRestrictions()->removeAll();
507
508
                                $result = $queryBuilder
509
                                    ->select('log_data', 'tstamp', 'userid')
510
                                    ->from('sys_log')
511
                                    ->where(
512
                                        $queryBuilder->expr()->eq(
513
                                            'action',
514
                                            $queryBuilder->createNamedParameter(6, \PDO::PARAM_INT)
515
                                        ),
516
                                        $queryBuilder->expr()->eq(
517
                                            'details_nr',
518
                                            $queryBuilder->createNamedParameter(30, \PDO::PARAM_INT)
519
                                        ),
520
                                        $queryBuilder->expr()->eq(
521
                                            'tablename',
522
                                            $queryBuilder->createNamedParameter($eTable, \PDO::PARAM_STR)
523
                                        ),
524
                                        $queryBuilder->expr()->eq(
525
                                            'recuid',
526
                                            $queryBuilder->createNamedParameter($eUid, \PDO::PARAM_INT)
527
                                        )
528
                                    )
529
                                    ->orderBy('uid', 'DESC')
530
                                    ->execute();
531
532
                                // Find all implicated since the last stage-raise from editing to review:
533
                                while ($dat = $result->fetch()) {
534
                                    $data = unserialize($dat['log_data']);
535
                                    $emails = $this->getEmailsForStageChangeNotification($dat['userid'], true) + $emails;
536
                                    if ($data['stage'] == 1) {
537
                                        break;
538
                                    }
539
                                }
540
                            }
541
                            break;
542
                        case 0:
543
                            $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']);
544
                            break;
545
                        default:
546
                            $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
547
                    }
548
                    break;
549
                case 10:
550
                    $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
551
                    $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']) + $emails;
552
                    $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']) + $emails;
553
                    break;
554
                default:
555
                    // Do nothing
556
            }
557
        } else {
558
            $emails = $notificationAlternativeRecipients;
559
        }
560
        // prepare and then send the emails
561
        if (!empty($emails)) {
562
            // Path to record is found:
563
            list($elementTable, $elementUid) = explode(':', $elementName);
564
            $elementUid = (int)$elementUid;
565
            $elementRecord = BackendUtility::getRecord($elementTable, $elementUid);
566
            $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord);
567
            if ($elementTable === 'pages') {
568
                $pageUid = $elementUid;
569
            } else {
570
                BackendUtility::fixVersioningPid($elementTable, $elementRecord);
571
                $pageUid = ($elementUid = $elementRecord['pid']);
572
            }
573
574
            // new way, options are
575
            // pageTSconfig: tx_version.workspaces.stageNotificationEmail.subject
576
            // userTSconfig: page.tx_version.workspaces.stageNotificationEmail.subject
577
            $pageTsConfig = BackendUtility::getPagesTSconfig($pageUid);
578
            $emailConfig = $pageTsConfig['tx_version.']['workspaces.']['stageNotificationEmail.'];
579
            $markers = [
580
                '###RECORD_TITLE###' => $recordTitle,
581
                '###RECORD_PATH###' => BackendUtility::getRecordPath($elementUid, '', 20),
582
                '###SITE_NAME###' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
583
                '###SITE_URL###' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir,
584
                '###WORKSPACE_TITLE###' => $workspaceRec['title'],
585
                '###WORKSPACE_UID###' => $workspaceRec['uid'],
586
                '###ELEMENT_NAME###' => $elementName,
587
                '###NEXT_STAGE###' => $newStage,
588
                '###COMMENT###' => $comment,
589
                // See: #30212 - keep both markers for compatibility
590
                '###USER_REALNAME###' => $dataHandler->BE_USER->user['realName'],
591
                '###USER_FULLNAME###' => $dataHandler->BE_USER->user['realName'],
592
                '###USER_USERNAME###' => $dataHandler->BE_USER->user['username']
593
            ];
594
            // add marker for preview links if workspace extension is loaded
595
            $this->workspaceService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\WorkspaceService::class);
596
            // only generate the link if the marker is in the template - prevents database from getting to much entries
597
            if (GeneralUtility::isFirstPartOfStr($emailConfig['message'], 'LLL:')) {
598
                $tempEmailMessage = $this->getLanguageService()->sL($emailConfig['message']);
599
            } else {
600
                $tempEmailMessage = $emailConfig['message'];
601
            }
602
            if (strpos($tempEmailMessage, '###PREVIEW_LINK###') !== false) {
603
                $markers['###PREVIEW_LINK###'] = $this->workspaceService->generateWorkspacePreviewLink($elementUid);
604
            }
605
            unset($tempEmailMessage);
606
            $markers['###SPLITTED_PREVIEW_LINK###'] = $this->workspaceService->generateWorkspaceSplittedPreviewLink($elementUid, true);
607
            // Hook for preprocessing of the content for formmails:
608
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'] ?? [] as $className) {
609
                $_procObj = GeneralUtility::makeInstance($className);
610
                $markers = $_procObj->postModifyMarkers($markers, $this);
611
            }
612
            // send an email to each individual user, to ensure the
613
            // multilanguage version of the email
614
            $emailRecipients = [];
615
            // an array of language objects that are needed
616
            // for emails with different languages
617
            $languageObjects = [
618
                $this->getLanguageService()->lang => $this->getLanguageService()
619
            ];
620
            // loop through each recipient and send the email
621
            foreach ($emails as $recipientData) {
622
                // don't send an email twice
623
                if (isset($emailRecipients[$recipientData['email']])) {
624
                    continue;
625
                }
626
                $emailSubject = $emailConfig['subject'];
627
                $emailMessage = $emailConfig['message'];
628
                $emailRecipients[$recipientData['email']] = $recipientData['email'];
629
                // check if the email needs to be localized
630
                // in the users' language
631
                if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:') || GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
632
                    $recipientLanguage = $recipientData['lang'] ? $recipientData['lang'] : 'default';
633
                    if (!isset($languageObjects[$recipientLanguage])) {
634
                        // a LANG object in this language hasn't been
635
                        // instantiated yet, so this is done here
636
                        /** @var $languageObject \TYPO3\CMS\Core\Localization\LanguageService */
637
                        $languageObject = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Localization\LanguageService::class);
638
                        $languageObject->init($recipientLanguage);
639
                        $languageObjects[$recipientLanguage] = $languageObject;
640
                    } else {
641
                        $languageObject = $languageObjects[$recipientLanguage];
642
                    }
643
                    if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:')) {
644
                        $emailSubject = $languageObject->sL($emailSubject);
645
                    }
646
                    if (GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
647
                        $emailMessage = $languageObject->sL($emailMessage);
648
                    }
649
                }
650
                $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
651
                $emailSubject = $templateService->substituteMarkerArray($emailSubject, $markers, '', true, true);
652
                $emailMessage = $templateService->substituteMarkerArray($emailMessage, $markers, '', true, true);
653
                // Send an email to the recipient
654
                /** @var $mail \TYPO3\CMS\Core\Mail\MailMessage */
655
                $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
656
                if (!empty($recipientData['realName'])) {
657
                    $recipient = [$recipientData['email'] => $recipientData['realName']];
658
                } else {
659
                    $recipient = $recipientData['email'];
660
                }
661
                $mail->setTo($recipient)
662
                    ->setSubject($emailSubject)
663
                    ->setBody($emailMessage);
664
                $mail->send();
665
            }
666
            $emailRecipients = implode(',', $emailRecipients);
667
            if ($dataHandler->enableLogging) {
668
                $propertyArray = $dataHandler->getRecordProperties($table, $id);
669
                $pid = $propertyArray['pid'];
670
                $dataHandler->log($table, $id, 0, 0, 0, 'Notification email for stage change was sent to "' . $emailRecipients . '"', -1, [], $dataHandler->eventPid($table, $id, $pid));
671
            }
672
        }
673
    }
674
675
    /**
676
     * Return be_users that should be notified on stage change from input list.
677
     * previously called notifyStageChange_getEmails() in DataHandler
678
     *
679
     * @param string $listOfUsers List of backend users, on the form "be_users_10,be_users_2" or "10,2" in case noTablePrefix is set.
680
     * @param bool $noTablePrefix If set, the input list are integers and not strings.
681
     * @return array Array of emails
682
     */
683
    protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = false)
684
    {
685
        $users = GeneralUtility::trimExplode(',', $listOfUsers, true);
686
        $emails = [];
687
        foreach ($users as $userIdent) {
688
            if ($noTablePrefix) {
689
                $id = (int)$userIdent;
690
            } else {
691
                list($table, $id) = GeneralUtility::revExplode('_', $userIdent, 2);
692
            }
693
            if ($table === 'be_users' || $noTablePrefix) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $table does not seem to be defined for all execution paths leading up to this point.
Loading history...
694
                if ($userRecord = BackendUtility::getRecord('be_users', $id, 'uid,email,lang,realName', BackendUtility::BEenableFields('be_users'))) {
695
                    if (trim($userRecord['email']) !== '') {
696
                        $emails[$id] = $userRecord;
697
                    }
698
                }
699
            }
700
        }
701
        return $emails;
702
    }
703
704
    /****************************
705
     *****  Stage Changes  ******
706
     ****************************/
707
    /**
708
     * Setting stage of record
709
     *
710
     * @param string $table Table name
711
     * @param int $integer Record UID
712
     * @param int $stageId Stage ID to set
713
     * @param string $comment Comment that goes into log
714
     * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email?
715
     * @param DataHandler $dataHandler DataHandler object
716
     * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
717
     */
718
    protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = false, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
719
    {
720
        if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
721
            $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, 1);
722
        } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) {
723
            $record = BackendUtility::getRecord($table, $id);
724
            $stat = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']);
725
            // check if the usere is allowed to the current stage, so it's also allowed to send to next stage
726
            if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) {
727
                // Set stage of record:
728
                GeneralUtility::makeInstance(ConnectionPool::class)
729
                    ->getConnectionForTable($table)
730
                    ->update(
731
                        $table,
732
                        [
733
                            't3ver_stage' => $stageId,
734
                        ],
735
                        ['uid' => (int)$id]
736
                    );
737
738 View Code Duplication
                if ($dataHandler->enableLogging) {
739
                    $propertyArray = $dataHandler->getRecordProperties($table, $id);
740
                    $pid = $propertyArray['pid'];
741
                    $dataHandler->log($table, $id, 0, 0, 0, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid));
0 ignored issues
show
Bug introduced by
Are you sure substr($comment, 0, 100) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

741
                    $dataHandler->log($table, $id, 0, 0, 0, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . /** @scrutinizer ignore-type */ substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid));
Loading history...
742
                }
743
                // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
744
                $dataHandler->log($table, $id, 6, 0, 0, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]);
745
                if ((int)$stat['stagechg_notification'] > 0) {
746
                    if ($notificationEmailInfo) {
747
                        $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$stat, $stageId, $comment];
748
                        $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id;
749
                        $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['alternativeRecipients'] = $notificationAlternativeRecipients;
750
                    } else {
751
                        $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients);
752
                    }
753
                }
754
            } else {
755
                $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1);
756
            }
757
        } else {
758
            $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', 1);
759
        }
760
    }
761
762
    /*****************************
763
     *****  CMD versioning  ******
764
     *****************************/
765
766
    /**
767
     * Swapping versions of a record
768
     * 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
769
     *
770
     * @param string $table Table name
771
     * @param int $id UID of the online record to swap
772
     * @param int $swapWith UID of the archived version to swap with!
773
     * @param bool $swapIntoWS If set, swaps online into workspace instead of publishing out of workspace.
774
     * @param DataHandler $dataHandler DataHandler object
775
     * @param string $comment Notification comment
776
     * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email?
777
     * @param array $notificationAlternativeRecipients comma separated list of recipients to notificate instead of normal be_users
778
     */
779
    protected function version_swap($table, $id, $swapWith, $swapIntoWS = 0, DataHandler $dataHandler, $comment = '', $notificationEmailInfo = false, $notificationAlternativeRecipients = [])
780
    {
781
782
        // Check prerequisites before start swapping
783
784
        // Skip records that have been deleted during the current execution
785
        if ($dataHandler->hasDeletedRecord($table, $id)) {
786
            return;
787
        }
788
789
        // First, check if we may actually edit the online record
790
        if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
791
            $dataHandler->newlog('Error: You cannot swap versions for a record you do not have access to edit!', 1);
792
            return;
793
        }
794
        // Select the two versions:
795
        $curVersion = BackendUtility::getRecord($table, $id, '*');
796
        $swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
797
        $movePlh = [];
798
        $movePlhID = 0;
799
        if (!(is_array($curVersion) && is_array($swapVersion))) {
800
            $dataHandler->newlog('Error: Either online or swap version could not be selected!', 2);
801
            return;
802
        }
803
        if (!$dataHandler->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
804
            $dataHandler->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], 1);
805
            return;
806
        }
807
        $wsAccess = $dataHandler->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']);
808
        if (!($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === -10)) {
809
            $dataHandler->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', 1);
810
            return;
811
        }
812
        if (!($dataHandler->doesRecordExist($table, $swapWith, 'show') && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) {
813
            $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', 1);
814
            return;
815
        }
816
        if ($swapIntoWS && !$dataHandler->BE_USER->workspaceSwapAccess()) {
817
            $dataHandler->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', 1);
818
            return;
819
        }
820
        // Check if the swapWith record really IS a version of the original!
821
        if (!(((int)$swapVersion['pid'] == -1 && (int)$curVersion['pid'] >= 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) {
822
            $dataHandler->newlog('In swap version, either pid was not -1 or the t3ver_oid didn\'t match the id of the online version as it must!', 2);
823
            return;
824
        }
825
        // Lock file name:
826
        $lockFileName = PATH_site . 'typo3temp/var/swap_locking/' . $table . '_' . $id . '.ser';
827
        if (@is_file($lockFileName)) {
828
            $dataHandler->newlog('A swapping lock file was present. Either another swap process is already running or a previous swap process failed. Ask your administrator to handle the situation.', 2);
829
            return;
830
        }
831
832
        // Now start to swap records by first creating the lock file
833
834
        // Write lock-file:
835
        GeneralUtility::writeFileToTypo3tempDir($lockFileName, serialize([
836
            'tstamp' => $GLOBALS['EXEC_TIME'],
837
            'user' => $dataHandler->BE_USER->user['username'],
838
            'curVersion' => $curVersion,
839
            'swapVersion' => $swapVersion
840
        ]));
841
        // Find fields to keep
842
        $keepFields = $this->getUniqueFields($table);
843
        if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
844
            $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
845
        }
846
        // l10n-fields must be kept otherwise the localization
847
        // will be lost during the publishing
848 View Code Duplication
        if ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
849
            $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
850
        }
851
        // Swap "keepfields"
852
        foreach ($keepFields as $fN) {
853
            $tmp = $swapVersion[$fN];
854
            $swapVersion[$fN] = $curVersion[$fN];
855
            $curVersion[$fN] = $tmp;
856
        }
857
        // Preserve states:
858
        $t3ver_state = [];
859
        $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
860
        $t3ver_state['curVersion'] = $curVersion['t3ver_state'];
861
        // Modify offline version to become online:
862
        $tmp_wsid = $swapVersion['t3ver_wsid'];
863
        // Set pid for ONLINE
864
        $swapVersion['pid'] = (int)$curVersion['pid'];
865
        // We clear this because t3ver_oid only make sense for offline versions
866
        // and we want to prevent unintentional misuse of this
867
        // value for online records.
868
        $swapVersion['t3ver_oid'] = 0;
869
        // In case of swapping and the offline record has a state
870
        // (like 2 or 4 for deleting or move-pointer) we set the
871
        // current workspace ID so the record is not deselected
872
        // in the interface by BackendUtility::versioningPlaceholderClause()
873
        $swapVersion['t3ver_wsid'] = 0;
874
        if ($swapIntoWS) {
875
            if ($t3ver_state['swapVersion'] > 0) {
876
                $swapVersion['t3ver_wsid'] = $dataHandler->BE_USER->workspace;
877
            } else {
878
                $swapVersion['t3ver_wsid'] = (int)$curVersion['t3ver_wsid'];
879
            }
880
        }
881
        $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
882
        $swapVersion['t3ver_stage'] = 0;
883
        if (!$swapIntoWS) {
884
            $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
885
        }
886
        // Moving element.
887
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
888
            //  && $t3ver_state['swapVersion']==4   // Maybe we don't need this?
889
            if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) {
890
                $movePlhID = $plhRec['uid'];
891
                $movePlh['pid'] = $swapVersion['pid'];
892
                $swapVersion['pid'] = (int)$plhRec['pid'];
893
                $curVersion['t3ver_state'] = (int)$swapVersion['t3ver_state'];
894
                $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
895
                if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
896
                    // sortby is a "keepFields" which is why this will work...
897
                    $movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
898
                    $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
899
                }
900
            }
901
        }
902
        // Take care of relations in each field (e.g. IRRE):
903
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
904
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
905
                if (isset($fieldConf['config']) && is_array($fieldConf['config'])) {
906
                    $this->version_swap_processFields($table, $field, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler);
907
                }
908
            }
909
        }
910
        unset($swapVersion['uid']);
911
        // Modify online version to become offline:
912
        unset($curVersion['uid']);
913
        // Set pid for OFFLINE
914
        $curVersion['pid'] = -1;
915
        $curVersion['t3ver_oid'] = (int)$id;
916
        $curVersion['t3ver_wsid'] = $swapIntoWS ? (int)$tmp_wsid : 0;
917
        $curVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
918
        $curVersion['t3ver_count'] = $curVersion['t3ver_count'] + 1;
919
        // Increment lifecycle counter
920
        $curVersion['t3ver_stage'] = 0;
921
        if (!$swapIntoWS) {
922
            $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
923
        }
924
        // Registering and swapping MM relations in current and swap records:
925
        $dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith);
926
        // Generating proper history data to prepare logging
927
        $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
928
        $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
929
930
        // Execute swapping:
931
        $sqlErrors = [];
932
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
933
934
        $platform = $connection->getDatabasePlatform();
935
        $tableDetails = null;
936
        if ($platform instanceof SQLServerPlatform) {
937
            // mssql needs to set proper PARAM_LOB and others to update fields
938
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
939
        }
940
941
        try {
942
            $types = [];
943
944 View Code Duplication
            if ($platform instanceof SQLServerPlatform) {
945
                foreach ($curVersion as $columnName => $columnValue) {
946
                    $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
947
                }
948
            }
949
950
            $connection->update(
951
                $table,
952
                $swapVersion,
953
                ['uid' => (int)$id],
954
                $types
955
            );
956
        } catch (DBALException $e) {
957
            $sqlErrors[] = $e->getPrevious()->getMessage();
958
        }
959
960
        if (empty($sqlErrors)) {
961
            try {
962
                $types = [];
963 View Code Duplication
                if ($platform instanceof SQLServerPlatform) {
964
                    foreach ($curVersion as $columnName => $columnValue) {
965
                        $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
966
                    }
967
                }
968
969
                $connection->update(
970
                    $table,
971
                    $curVersion,
972
                    ['uid' => (int)$swapWith],
973
                    $types
974
                );
975
                unlink($lockFileName);
976
            } catch (DBALException $e) {
977
                $sqlErrors[] = $e->getPrevious()->getMessage();
978
            }
979
        }
980
981
        if (!empty($sqlErrors)) {
982
            $dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
983
        } else {
984
            // Register swapped ids for later remapping:
985
            $this->remappedIds[$table][$id] = $swapWith;
986
            $this->remappedIds[$table][$swapWith] = $id;
987
            // If a moving operation took place...:
988
            if ($movePlhID) {
989
                // Remove, if normal publishing:
990
                if (!$swapIntoWS) {
991
                    // For delete + completely delete!
992
                    $dataHandler->deleteEl($table, $movePlhID, true, true);
993
                } else {
994
                    // Otherwise update the movePlaceholder:
995
                    GeneralUtility::makeInstance(ConnectionPool::class)
996
                        ->getConnectionForTable($table)
997
                        ->update(
998
                            $table,
999
                            $movePlh,
1000
                            ['uid' => (int)$movePlhID]
1001
                        );
1002
                    $dataHandler->addRemapStackRefIndex($table, $movePlhID);
1003
                }
1004
            }
1005
            // Checking for delete:
1006
            // Delete only if new/deleted placeholders are there.
1007
            if (!$swapIntoWS && ((int)$t3ver_state['swapVersion'] === 1 || (int)$t3ver_state['swapVersion'] === 2)) {
1008
                // Force delete
1009
                $dataHandler->deleteEl($table, $id, true);
1010
            }
1011
            if ($dataHandler->enableLogging) {
1012
                $dataHandler->log($table, $id, 0, 0, 0, ($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, -1, [], $dataHandler->eventPid($table, $id, $swapVersion['pid']));
1013
            }
1014
1015
            // Update reference index of the live record:
1016
            $dataHandler->addRemapStackRefIndex($table, $id);
1017
            // Set log entry for live record:
1018
            $propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion);
1019 View Code Duplication
            if ($propArr['_ORIG_pid'] == -1) {
1020
                $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
1021
            } else {
1022
                $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
1023
            }
1024
            $theLogId = $dataHandler->log($table, $id, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
1025
            $dataHandler->setHistory($table, $id, $theLogId);
1026
            // Update reference index of the offline record:
1027
            $dataHandler->addRemapStackRefIndex($table, $swapWith);
1028
            // Set log entry for offline record:
1029
            $propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion);
1030 View Code Duplication
            if ($propArr['_ORIG_pid'] == -1) {
1031
                $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
1032
            } else {
1033
                $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
1034
            }
1035
            $theLogId = $dataHandler->log($table, $swapWith, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']);
1036
            $dataHandler->setHistory($table, $swapWith, $theLogId);
1037
1038
            $stageId = -20; // \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_EXECUTE_ID;
1039
            if ($notificationEmailInfo) {
1040
                $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
1041
                $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
1042
                $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = $table . ':' . $id;
1043
                $this->notificationEmailInfo[$notificationEmailInfoKey]['alternativeRecipients'] = $notificationAlternativeRecipients;
1044
            } else {
1045
                $this->notifyStageChange($wsAccess, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients);
1046
            }
1047
            // Write to log with stageId -20
1048 View Code Duplication
            if ($dataHandler->enableLogging) {
1049
                $propArr = $dataHandler->getRecordProperties($table, $id);
1050
                $pid = $propArr['pid'];
1051
                $dataHandler->log($table, $id, 0, 0, 0, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid));
0 ignored issues
show
Bug introduced by
Are you sure substr($comment, 0, 100) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

1051
                $dataHandler->log($table, $id, 0, 0, 0, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . /** @scrutinizer ignore-type */ substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid));
Loading history...
1052
            }
1053
            $dataHandler->log($table, $id, 6, 0, 0, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]);
1054
1055
            // Clear cache:
1056
            $dataHandler->registerRecordIdForPageCacheClearing($table, $id);
1057
            // Checking for "new-placeholder" and if found, delete it (BUT FIRST after swapping!):
1058
            if (!$swapIntoWS && $t3ver_state['curVersion'] > 0) {
1059
                // For delete + completely delete!
1060
                $dataHandler->deleteEl($table, $swapWith, true, true);
1061
            }
1062
1063
            //Update reference index for live workspace too:
1064
            /** @var $refIndexObj \TYPO3\CMS\Core\Database\ReferenceIndex */
1065
            $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
1066
            $refIndexObj->setWorkspaceId(0);
1067
            $refIndexObj->updateRefIndexTable($table, $id);
1068
            $refIndexObj->updateRefIndexTable($table, $swapWith);
1069
        }
1070
    }
1071
1072
    /**
1073
     * Writes remapped foreign field (IRRE).
1074
     *
1075
     * @param \TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis Instance that holds the sorting order of child records
1076
     * @param array $configuration The TCA field configuration
1077
     * @param int $parentId The uid of the parent record
1078
     */
1079
    public function writeRemappedForeignField(\TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis, array $configuration, $parentId)
1080
    {
1081
        foreach ($dbAnalysis->itemArray as &$item) {
1082
            if (isset($this->remappedIds[$item['table']][$item['id']])) {
1083
                $item['id'] = $this->remappedIds[$item['table']][$item['id']];
1084
            }
1085
        }
1086
        $dbAnalysis->writeForeignField($configuration, $parentId);
1087
    }
1088
1089
    /**
1090
     * Processes fields of a record for the publishing/swapping process.
1091
     * Basically this takes care of IRRE (type "inline") child references.
1092
     *
1093
     * @param string $tableName Table name
1094
     * @param string $fieldName: Field name
1095
     * @param array $configuration TCA field configuration
1096
     * @param array $liveData: Live record data
1097
     * @param array $versionData: Version record data
1098
     * @param DataHandler $dataHandler Calling data-handler object
1099
     */
1100
    protected function version_swap_processFields($tableName, $fieldName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
0 ignored issues
show
Unused Code introduced by
The parameter $fieldName is not used and could be removed. ( Ignorable by Annotation )

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

1100
    protected function version_swap_processFields($tableName, /** @scrutinizer ignore-unused */ $fieldName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)

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

Loading history...
1101
    {
1102
        $inlineType = $dataHandler->getInlineFieldType($configuration);
1103
        if ($inlineType !== 'field') {
1104
            return;
1105
        }
1106
        $foreignTable = $configuration['foreign_table'];
1107
        // Read relations that point to the current record (e.g. live record):
1108
        $liveRelations = $this->createRelationHandlerInstance();
1109
        $liveRelations->setWorkspaceId(0);
1110
        $liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration);
1111
        // Read relations that point to the record to be swapped with e.g. draft record):
1112
        $versionRelations = $this->createRelationHandlerInstance();
1113
        $versionRelations->setUseLiveReferenceIds(false);
1114
        $versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration);
1115
        // Update relations for both (workspace/versioning) sites:
1116 View Code Duplication
        if (count($liveRelations->itemArray)) {
1117
            $dataHandler->addRemapAction(
1118
                $tableName,
1119
                $liveData['uid'],
1120
                [$this, 'updateInlineForeignFieldSorting'],
1121
                [$tableName, $liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace]
1122
            );
1123
        }
1124 View Code Duplication
        if (count($versionRelations->itemArray)) {
1125
            $dataHandler->addRemapAction(
1126
                $tableName,
1127
                $liveData['uid'],
1128
                [$this, 'updateInlineForeignFieldSorting'],
1129
                [$tableName, $liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0]
1130
            );
1131
        }
1132
    }
1133
1134
    /**
1135
     * Updates foreign field sorting values of versioned and live
1136
     * parents after(!) the whole structure has been published.
1137
     *
1138
     * This method is used as callback function in
1139
     * DataHandlerHook::version_swap_procBasedOnFieldType().
1140
     * Sorting fields ("sortby") are not modified during the
1141
     * workspace publishing/swapping process directly.
1142
     *
1143
     * @param string $parentTableName
1144
     * @param string $parentId
1145
     * @param string $foreignTableName
1146
     * @param int[] $foreignIds
1147
     * @param array $configuration
1148
     * @param int $targetWorkspaceId
1149
     * @internal
1150
     */
1151
    public function updateInlineForeignFieldSorting($parentTableName, $parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
1152
    {
1153
        $remappedIds = [];
1154
        // Use remapped ids (live id <-> version id)
1155
        foreach ($foreignIds as $foreignId) {
1156
            if (!empty($this->remappedIds[$foreignTableName][$foreignId])) {
1157
                $remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId];
1158
            } else {
1159
                $remappedIds[] = $foreignId;
1160
            }
1161
        }
1162
1163
        $relationHandler = $this->createRelationHandlerInstance();
1164
        $relationHandler->setWorkspaceId($targetWorkspaceId);
1165
        $relationHandler->setUseLiveReferenceIds(false);
1166
        $relationHandler->start(implode(',', $remappedIds), $foreignTableName);
1167
        $relationHandler->processDeletePlaceholder();
1168
        $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

1168
        $relationHandler->writeForeignField($configuration, /** @scrutinizer ignore-type */ $parentId);
Loading history...
1169
    }
1170
1171
    /**
1172
     * Release version from this workspace (and into "Live" workspace but as an offline version).
1173
     *
1174
     * @param string $table Table name
1175
     * @param int $id Record UID
1176
     * @param bool $flush If set, will completely delete element
1177
     * @param DataHandler $dataHandler DataHandler object
1178
     */
1179
    protected function version_clearWSID($table, $id, $flush = false, DataHandler $dataHandler)
1180
    {
1181
        if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
1182
            $dataHandler->newlog('Attempt to reset workspace for record failed: ' . $errorCode, 1);
1183
            return;
1184
        }
1185
        if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
1186
            $dataHandler->newlog('Attempt to reset workspace for record failed because you do not have edit access', 1);
1187
            return;
1188
        }
1189
        $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
1190
        if (!$liveRec) {
1191
            return;
1192
        }
1193
        // Clear workspace ID:
1194
        $updateData = [
1195
            't3ver_wsid' => 0,
1196
            't3ver_tstamp' => $GLOBALS['EXEC_TIME']
1197
        ];
1198
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
1199
        $connection->update(
1200
            $table,
1201
            $updateData,
1202
            ['uid' => (int)$id]
1203
        );
1204
1205
        // Clear workspace ID for live version AND DELETE IT as well because it is a new record!
1206
        if (
1207
            VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER)
1208
            || VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
1209
        ) {
1210
            $connection->update(
1211
                $table,
1212
                $updateData,
1213
                ['uid' => (int)$liveRec['uid']]
1214
            );
1215
1216
            // THIS assumes that the record was placeholder ONLY for ONE record (namely $id)
1217
            $dataHandler->deleteEl($table, $liveRec['uid'], true);
1218
        }
1219
        // If "deleted" flag is set for the version that got released
1220
        // it doesn't make sense to keep that "placeholder" anymore and we delete it completly.
1221
        $wsRec = BackendUtility::getRecord($table, $id);
1222
        if (
1223
            $flush
1224
            || (
1225
                VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER)
1226
                || VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
1227
            )
1228
        ) {
1229
            $dataHandler->deleteEl($table, $id, true, true);
1230
        }
1231
        // Remove the move-placeholder if found for live record.
1232
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
1233
            if ($plhRec = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid')) {
1234
                $dataHandler->deleteEl($table, $plhRec['uid'], true, true);
1235
            }
1236
        }
1237
    }
1238
1239
    /**
1240
     * In case a sys_workspace_stage record is deleted we do a hard reset
1241
     * for all existing records in that stage to avoid that any of these end up
1242
     * as orphan records.
1243
     *
1244
     * @param int $stageId Elements with this stage are resetted
1245
     */
1246
    protected function resetStageOfElements($stageId)
1247
    {
1248
        foreach ($this->getTcaTables() as $tcaTable) {
1249
            if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
1250
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1251
                    ->getQueryBuilderForTable($tcaTable);
1252
1253
                $queryBuilder
1254
                    ->update($tcaTable)
1255
                    ->set('t3ver_stage', StagesService::STAGE_EDIT_ID)
1256
                    ->where(
1257
                        $queryBuilder->expr()->eq(
1258
                            't3ver_stage',
1259
                            $queryBuilder->createNamedParameter($stageId, \PDO::PARAM_INT)
1260
                        ),
1261
                        $queryBuilder->expr()->eq(
1262
                            'pid',
1263
                            $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1264
                        ),
1265
                        $queryBuilder->expr()->gt(
1266
                            't3ver_wsid',
1267
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1268
                        )
1269
                    )
1270
                    ->execute();
1271
            }
1272
        }
1273
    }
1274
1275
    /**
1276
     * Flushes elements of a particular workspace to avoid orphan records.
1277
     *
1278
     * @param int $workspaceId The workspace to be flushed
1279
     */
1280
    protected function flushWorkspaceElements($workspaceId)
1281
    {
1282
        $command = [];
1283
        foreach ($this->getTcaTables() as $tcaTable) {
1284
            if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
1285
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1286
                    ->getQueryBuilderForTable($tcaTable);
1287
                $queryBuilder->getRestrictions()
1288
                    ->removeAll()
1289
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1290
                    ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $workspaceId, false));
0 ignored issues
show
Bug introduced by
It seems like $workspaceId can also be of type integer; however, parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance() does only seem to accept array<integer,mixed>, 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

1290
                    ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, /** @scrutinizer ignore-type */ $workspaceId, false));
Loading history...
Bug introduced by
false of type false is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1290
                    ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $workspaceId, /** @scrutinizer ignore-type */ false));
Loading history...
1291
1292
                $result = $queryBuilder
1293
                    ->select('uid')
1294
                    ->from($tcaTable)
1295
                    ->orderBy('uid')
1296
                    ->execute();
1297
1298
                while (($recordId = $result->fetchColumn()) !== false) {
1299
                    $command[$tcaTable][$recordId]['version']['action'] = 'flush';
1300
                }
1301
            }
1302
        }
1303
        if (!empty($command)) {
1304
            $dataHandler = $this->getDataHandler();
1305
            $dataHandler->start([], $command);
1306
            $dataHandler->process_cmdmap();
1307
        }
1308
    }
1309
1310
    /**
1311
     * Gets all defined TCA tables.
1312
     *
1313
     * @return array
1314
     */
1315
    protected function getTcaTables()
1316
    {
1317
        return array_keys($GLOBALS['TCA']);
1318
    }
1319
1320
    /**
1321
     * @return \TYPO3\CMS\Core\DataHandling\DataHandler
1322
     */
1323
    protected function getDataHandler()
1324
    {
1325
        return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
1326
    }
1327
1328
    /**
1329
     * Flushes the workspace cache for current workspace and for the virtual "all workspaces" too.
1330
     *
1331
     * @param int $workspaceId The workspace to be flushed in cache
1332
     */
1333
    protected function flushWorkspaceCacheEntriesByWorkspaceId($workspaceId)
1334
    {
1335
        $workspacesCache = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class)->getCache('workspaces_cache');
1336
        $workspacesCache->flushByTag($workspaceId);
1337
        $workspacesCache->flushByTag(\TYPO3\CMS\Workspaces\Service\WorkspaceService::SELECT_ALL_WORKSPACES);
1338
    }
1339
1340
    /*******************************
1341
     *****  helper functions  ******
1342
     *******************************/
1343
1344
    /**
1345
     * Finds all elements for swapping versions in workspace
1346
     *
1347
     * @param string $table Table name of the original element to swap
1348
     * @param int $id UID of the original element to swap (online)
1349
     * @param int $offlineId As above but offline
1350
     * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID
1351
     */
1352
    public function findPageElementsForVersionSwap($table, $id, $offlineId)
1353
    {
1354
        $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid');
1355
        $workspaceId = (int)$rec['t3ver_wsid'];
1356
        $elementData = [];
1357
        if ($workspaceId === 0) {
1358
            return $elementData;
1359
        }
1360
        // Get page UID for LIVE and workspace
1361
        if ($table !== 'pages') {
1362
            $rec = BackendUtility::getRecord($table, $id, 'pid');
1363
            $pageId = $rec['pid'];
1364
            $rec = BackendUtility::getRecord('pages', $pageId);
1365
            BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1366
            $offlinePageId = $rec['_ORIG_uid'];
1367
        } else {
1368
            $pageId = $id;
1369
            $offlinePageId = $offlineId;
1370
        }
1371
        // Traversing all tables supporting versioning:
1372
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
1373
            if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') {
1374
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1375
                    ->getQueryBuilderForTable($table);
1376
1377
                $queryBuilder->getRestrictions()
1378
                    ->removeAll()
1379
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1380
1381
                $statement = $queryBuilder
1382
                    ->select('A.uid AS offlineUid', 'B.uid AS uid')
1383
                    ->from($table, 'A')
1384
                    ->from($table, 'B')
1385
                    ->where(
1386
                        $queryBuilder->expr()->eq(
1387
                            'A.pid',
1388
                            $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1389
                        ),
1390
                        $queryBuilder->expr()->eq(
1391
                            'B.pid',
1392
                            $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
1393
                        ),
1394
                        $queryBuilder->expr()->eq(
1395
                            'A.t3ver_wsid',
1396
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1397
                        ),
1398
                        $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1399
                    )
1400
                    ->execute();
1401
1402
                while ($row = $statement->fetch()) {
1403
                    $elementData[$table][] = [$row['uid'], $row['offlineUid']];
1404
                }
1405
            }
1406
        }
1407
        if ($offlinePageId && $offlinePageId != $pageId) {
1408
            $elementData['pages'][] = [$pageId, $offlinePageId];
1409
        }
1410
1411
        return $elementData;
1412
    }
1413
1414
    /**
1415
     * Searches for all elements from all tables on the given pages in the same workspace.
1416
     *
1417
     * @param array $pageIdList List of PIDs to search
1418
     * @param int $workspaceId Workspace ID
1419
     * @param array $elementList List of found elements. Key is table name, value is array of element UIDs
1420
     */
1421
    public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList)
1422
    {
1423
        if ($workspaceId == 0) {
1424
            return;
1425
        }
1426
        // Traversing all tables supporting versioning:
1427
        foreach ($GLOBALS['TCA'] as $table => $cfg) {
1428
            if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') {
1429
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1430
                    ->getQueryBuilderForTable($table);
1431
1432
                $queryBuilder->getRestrictions()
1433
                    ->removeAll()
1434
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1435
1436
                $statement = $queryBuilder
1437
                    ->select('A.uid')
1438
                    ->from($table, 'A')
1439
                    ->from($table, 'B')
1440
                    ->where(
1441
                        $queryBuilder->expr()->eq(
1442
                            'A.pid',
1443
                            $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1444
                        ),
1445
                        $queryBuilder->expr()->in(
1446
                            'B.pid',
1447
                            $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY)
1448
                        ),
1449
                        $queryBuilder->expr()->eq(
1450
                            'A.t3ver_wsid',
1451
                            $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1452
                        ),
1453
                        $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1454
                    )
1455
                    ->groupBy('A.uid')
1456
                    ->execute();
1457
1458
                while ($row = $statement->fetch()) {
1459
                    $elementList[$table][] = $row['uid'];
1460
                }
1461
                if (is_array($elementList[$table])) {
1462
                    // Yes, it is possible to get non-unique array even with DISTINCT above!
1463
                    // It happens because several UIDs are passed in the array already.
1464
                    $elementList[$table] = array_unique($elementList[$table]);
1465
                }
1466
            }
1467
        }
1468
    }
1469
1470
    /**
1471
     * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1472
     *
1473
     * @param string $table Table to search
1474
     * @param array $idList List of records' UIDs
1475
     * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publisg DRAFT from ws module!
1476
     * @param array $pageIdList List of found page UIDs
1477
     * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs
1478
     */
1479
    public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList)
1480
    {
1481
        if ($workspaceId == 0) {
1482
            return;
1483
        }
1484
1485
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1486
            ->getQueryBuilderForTable($table);
1487
        $queryBuilder->getRestrictions()
1488
            ->removeAll()
1489
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1490
1491
        $statement = $queryBuilder
1492
            ->select('B.pid')
1493
            ->from($table, 'A')
1494
            ->from($table, 'B')
1495
            ->where(
1496
                $queryBuilder->expr()->eq(
1497
                    'A.pid',
1498
                    $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1499
                ),
1500
                $queryBuilder->expr()->eq(
1501
                    'A.t3ver_wsid',
1502
                    $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
1503
                ),
1504
                $queryBuilder->expr()->in(
1505
                    'A.uid',
1506
                    $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY)
1507
                ),
1508
                $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid'))
1509
            )
1510
            ->groupBy('B.pid')
1511
            ->execute();
1512
1513
        while ($row = $statement->fetch()) {
1514
            $pageIdList[] = $row['pid'];
1515
            // Find ws version
1516
            // Note: cannot use BackendUtility::getRecordWSOL()
1517
            // here because it does not accept workspace id!
1518
            $rec = BackendUtility::getRecord('pages', $row[0]);
1519
            BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1520
            if ($rec['_ORIG_uid']) {
1521
                $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1522
            }
1523
        }
1524
        // The line below is necessary even with DISTINCT
1525
        // because several elements can be passed by caller
1526
        $pageIdList = array_unique($pageIdList);
1527
    }
1528
1529
    /**
1530
     * Finds real page IDs for state change.
1531
     *
1532
     * @param array $idList List of page UIDs, possibly versioned
1533
     */
1534
    public function findRealPageIds(array &$idList)
1535
    {
1536
        foreach ($idList as $key => $id) {
1537
            $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid');
1538
            if ($rec['t3ver_oid'] > 0) {
1539
                $idList[$key] = $rec['t3ver_oid'];
1540
            }
1541
        }
1542
    }
1543
1544
    /**
1545
     * Creates a move placeholder for workspaces.
1546
     * USE ONLY INTERNALLY
1547
     * Moving placeholder: Can be done because the system sees it as a placeholder for NEW elements like t3ver_state=VersionState::NEW_PLACEHOLDER
1548
     * Moving original: Will either create the placeholder if it doesn't exist or move existing placeholder in workspace.
1549
     *
1550
     * @param string $table Table name to move
1551
     * @param int $uid Record uid to move (online record)
1552
     * @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
1553
     * @param int $wsUid UID of offline version of online record
1554
     * @param DataHandler $dataHandler DataHandler object
1555
     * @see moveRecord()
1556
     */
1557
    protected function moveRecord_wsPlaceholders($table, $uid, $destPid, $wsUid, DataHandler $dataHandler)
1558
    {
1559
        // If a record gets moved after a record that already has a placeholder record
1560
        // then the new placeholder record needs to be after the existing one
1561
        $originalRecordDestinationPid = $destPid;
1562 View Code Duplication
        if ($destPid < 0) {
1563
            $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

1563
            $movePlaceHolder = BackendUtility::getMovePlaceholder($table, /** @scrutinizer ignore-type */ abs($destPid), 'uid');
Loading history...
1564
            if ($movePlaceHolder !== false) {
1565
                $destPid = -$movePlaceHolder['uid'];
1566
            }
1567
        }
1568
        if ($plh = BackendUtility::getMovePlaceholder($table, $uid, 'uid')) {
1569
            // If already a placeholder exists, move it:
1570
            $dataHandler->moveRecord_raw($table, $plh['uid'], $destPid);
1571
        } else {
1572
            // First, we create a placeholder record in the Live workspace that
1573
            // represents the position to where the record is eventually moved to.
1574
            $newVersion_placeholderFieldArray = [];
1575
1576
            // Use property for move placeholders if set (since TYPO3 CMS 6.2)
1577
            if (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'])) {
1578
                $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'];
1579 View Code Duplication
            } elseif (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'])) {
1580
                // Fallback to property for new placeholder (existed long time before TYPO3 CMS 6.2)
1581
                $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'];
1582
            }
1583
1584
            // Set values from the versioned record to the move placeholder
1585
            if (!empty($shadowColumnsForMovePlaceholder)) {
1586
                $versionedRecord = BackendUtility::getRecord($table, $wsUid);
1587
                $shadowColumns = GeneralUtility::trimExplode(',', $shadowColumnsForMovePlaceholder, true);
1588
                foreach ($shadowColumns as $shadowColumn) {
1589
                    if (isset($versionedRecord[$shadowColumn])) {
1590
                        $newVersion_placeholderFieldArray[$shadowColumn] = $versionedRecord[$shadowColumn];
1591
                    }
1592
                }
1593
            }
1594
1595
            if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1596
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1597
            }
1598
            if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1599
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $dataHandler->userid;
1600
            }
1601
            if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
1602
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1603
            }
1604
            if ($table === 'pages') {
1605
                // Copy page access settings from original page to placeholder
1606
                $perms_clause = $dataHandler->BE_USER->getPagePermsClause(1);
1607
                $access = BackendUtility::readPageAccess($uid, $perms_clause);
1608
                $newVersion_placeholderFieldArray['perms_userid'] = $access['perms_userid'];
1609
                $newVersion_placeholderFieldArray['perms_groupid'] = $access['perms_groupid'];
1610
                $newVersion_placeholderFieldArray['perms_user'] = $access['perms_user'];
1611
                $newVersion_placeholderFieldArray['perms_group'] = $access['perms_group'];
1612
                $newVersion_placeholderFieldArray['perms_everybody'] = $access['perms_everybody'];
1613
            }
1614
            $newVersion_placeholderFieldArray['t3ver_label'] = 'MovePlaceholder #' . $uid;
1615
            $newVersion_placeholderFieldArray['t3ver_move_id'] = $uid;
1616
            // Setting placeholder state value for temporary record
1617
            $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER);
1618
            // Setting workspace - only so display of place holders can filter out those from other workspaces.
1619
            $newVersion_placeholderFieldArray['t3ver_wsid'] = $dataHandler->BE_USER->workspace;
1620
            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $dataHandler->getPlaceholderTitleForTableLabel($table, 'MOVE-TO PLACEHOLDER for #' . $uid);
1621
            // moving localized records requires to keep localization-settings for the placeholder too
1622
            if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
1623
                $l10nParentRec = BackendUtility::getRecord($table, $uid);
1624
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
1625
                $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
1626
                if (isset($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])) {
1627
                    $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']];
1628
                }
1629
                unset($l10nParentRec);
1630
            }
1631
            // Initially, create at root level.
1632
            $newVersion_placeholderFieldArray['pid'] = 0;
1633
            $id = 'NEW_MOVE_PLH';
1634
            // Saving placeholder as 'original'
1635
            $dataHandler->insertDB($table, $id, $newVersion_placeholderFieldArray, false);
1636
            // Move the new placeholder from temporary root-level to location:
1637
            $dataHandler->moveRecord_raw($table, $dataHandler->substNEWwithIDs[$id], $destPid);
1638
            // Move the workspace-version of the original to be the version of the move-to-placeholder:
1639
            // Setting placeholder state value for version (so it can know it is currently a new version...)
1640
            $updateFields = [
1641
                't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER)
1642
            ];
1643
1644
            GeneralUtility::makeInstance(ConnectionPool::class)
1645
                ->getConnectionForTable($table)
1646
                ->update(
1647
                    $table,
1648
                    $updateFields,
1649
                    ['uid' => (int)$wsUid]
1650
                );
1651
        }
1652
        // Check for the localizations of that element and move them as well
1653
        $dataHandler->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
1654
    }
1655
1656
    /**
1657
     * Gets an instance of the command map helper.
1658
     *
1659
     * @param DataHandler $dataHandler DataHandler object
1660
     * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap
1661
     */
1662
    public function getCommandMap(DataHandler $dataHandler)
1663
    {
1664
        return GeneralUtility::makeInstance(
1665
            \TYPO3\CMS\Workspaces\DataHandler\CommandMap::class,
1666
            $this,
0 ignored issues
show
Bug introduced by
$this of type TYPO3\CMS\Workspaces\Hook\DataHandlerHook is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1666
            /** @scrutinizer ignore-type */ $this,
Loading history...
1667
            $dataHandler,
0 ignored issues
show
Bug introduced by
$dataHandler of type TYPO3\CMS\Core\DataHandling\DataHandler is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1667
            /** @scrutinizer ignore-type */ $dataHandler,
Loading history...
1668
            $dataHandler->cmdmap,
1669
            $dataHandler->BE_USER->workspace
0 ignored issues
show
Bug introduced by
$dataHandler->BE_USER->workspace of type integer is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1669
            /** @scrutinizer ignore-type */ $dataHandler->BE_USER->workspace
Loading history...
1670
        );
1671
    }
1672
1673
    /**
1674
     * Returns all fieldnames from a table which have the unique evaluation type set.
1675
     *
1676
     * @param string $table Table name
1677
     * @return array Array of fieldnames
1678
     */
1679
    protected function getUniqueFields($table)
1680
    {
1681
        $listArr = [];
1682
        if (empty($GLOBALS['TCA'][$table]['columns'])) {
1683
            return $listArr;
1684
        }
1685
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) {
1686
            if ($configArr['config']['type'] === 'input') {
1687
                $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'], true);
1688
                if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) {
1689
                    $listArr[] = $field;
1690
                }
1691
            }
1692
        }
1693
        return $listArr;
1694
    }
1695
1696
    /**
1697
     * @return \TYPO3\CMS\Core\Database\RelationHandler
1698
     */
1699
    protected function createRelationHandlerInstance()
1700
    {
1701
        return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\RelationHandler::class);
1702
    }
1703
1704
    /**
1705
     * @return LanguageService
1706
     */
1707
    protected function getLanguageService()
1708
    {
1709
        return $GLOBALS['LANG'];
1710
    }
1711
}
1712