Passed
Branch master (6c65a4)
by Christian
16:31
created

DataHandlerHook::moveRecord()   F

Complexity

Conditions 24
Paths 865

Size

Total Lines 70
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 39
nc 865
nop 8
dl 0
loc 70
rs 2.8571
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

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

290
    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...
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

290
    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...
291
    {
292
        if ($command === 'delete') {
293
            if ($table === StagesService::TABLE_STAGE) {
294
                $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

294
                $this->resetStageOfElements(/** @scrutinizer ignore-type */ $id);
Loading history...
295
            } elseif ($table === \TYPO3\CMS\Workspaces\Service\WorkspaceService::TABLE_WORKSPACE) {
296
                $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

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

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

322
            $movePlaceHolder = BackendUtility::getMovePlaceholder($table, /** @scrutinizer ignore-type */ abs($destPid), 'uid,pid');
Loading history...
323
            if ($movePlaceHolder !== false) {
324
                $resolvedPid = $movePlaceHolder['pid'];
325
            }
326
        }
327
        $recordWasMoved = true;
328
        $moveRecVersionState = VersionState::cast($moveRec['t3ver_state']);
329
        // Get workspace version of the source record, if any:
330
        $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
331
        // Handle move-placeholders if the current record is not one already
332
        if (
333
            BackendUtility::isTableWorkspaceEnabled($table)
334
            && !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER)
335
        ) {
336
            // Create version of record first, if it does not exist
337
            if (empty($WSversion['uid'])) {
338
                $dataHandler->versionizeRecord($table, $uid, 'MovePointer');
339
                $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
340
                $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
341
            } elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$WSversion['uid']) {
342
                // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders
343
                $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid);
344
            }
345
        }
346
        // Check workspace permissions:
347
        $workspaceAccessBlocked = [];
348
        // Element was in "New/Deleted/Moved" so it can be moved...
349
        $recIsNewVersion = $moveRecVersionState->indicatesPlaceholder();
350
        $destRes = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($resolvedPid, $table);
351
        $canMoveRecord = ($recIsNewVersion || BackendUtility::isTableWorkspaceEnabled($table));
352
        // Workspace source check:
353
        if (!$recIsNewVersion) {
354
            $errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $WSversion['uid'] ? $WSversion['uid'] : $uid);
355
            if ($errorCode) {
356
                $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' ';
357
            } elseif (!$canMoveRecord && $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($moveRec['pid'], $table) <= 0) {
358
                $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" ';
359
            }
360
        }
361
        // Workspace destination check:
362
        // All records can be inserted if $destRes is greater than zero.
363
        // Only new versions can be inserted if $destRes is FALSE.
364
        // NO RECORDS can be inserted if $destRes is negative which indicates a stage
365
        //  not allowed for use. If "versioningWS" is version 2, moving can take place of versions.
366
        // since TYPO3 CMS 7, version2 is the default and the only option
367
        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...
368
            $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" ';
369
        } elseif ($destRes == 1 && $WSversion['uid']) {
370
            $workspaceAccessBlocked['dest2'] = 'Could not insert other versions in destination PID ';
371
        }
372
        if (empty($workspaceAccessBlocked)) {
373
            // If the move operation is done on a versioned record, which is
374
            // NOT new/deleted placeholder and versioningWS is in version 2, then...
375
            // since TYPO3 CMS 7, version2 is the default and the only option
376
            if ($WSversion['uid'] && !$recIsNewVersion && BackendUtility::isTableWorkspaceEnabled($table)) {
377
                $this->moveRecord_wsPlaceholders($table, $uid, $destPid, $WSversion['uid'], $dataHandler);
378
            } else {
379
                // moving not needed, just behave like in live workspace
380
                $recordWasMoved = false;
381
            }
382
        } else {
383
            $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), 1);
384
        }
385
    }
386
387
    /**
388
     * Processes fields of a moved record and follows references.
389
     *
390
     * @param DataHandler $dataHandler Calling DataHandler instance
391
     * @param int $resolvedPageId Resolved real destination page id
392
     * @param string $table Name of parent table
393
     * @param int $uid UID of the parent record
394
     */
395
    protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
396
    {
397
        $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid);
398
        if (empty($versionedRecord)) {
399
            return;
400
        }
401
        foreach ($versionedRecord as $field => $value) {
402
            if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
403
                continue;
404
            }
405
            $this->moveRecord_processFieldValue(
406
                $dataHandler,
407
                $resolvedPageId,
408
                $table,
409
                $uid,
410
                $field,
411
                $value,
412
                $GLOBALS['TCA'][$table]['columns'][$field]['config']
413
            );
414
        }
415
    }
416
417
    /**
418
     * Processes a single field of a moved record and follows references.
419
     *
420
     * @param DataHandler $dataHandler Calling DataHandler instance
421
     * @param int $resolvedPageId Resolved real destination page id
422
     * @param string $table Name of parent table
423
     * @param int $uid UID of the parent record
424
     * @param string $field Name of the field of the parent record
425
     * @param string $value Value of the field of the parent record
426
     * @param array $configuration TCA field configuration of the parent record
427
     */
428
    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

428
    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...
429
    {
430
        $inlineFieldType = $dataHandler->getInlineFieldType($configuration);
431
        $inlineProcessing = (
432
            ($inlineFieldType === 'list' || $inlineFieldType === 'field')
433
            && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
434
            && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent'])
435
        );
436
437
        if ($inlineProcessing) {
438
            if ($table === 'pages') {
439
                // If the inline elements are related to a page record,
440
                // make sure they reside at that page and not at its parent
441
                $resolvedPageId = $uid;
442
            }
443
444
            $dbAnalysis = $this->createRelationHandlerInstance();
445
            $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration);
446
447
            // Moving records to a positive destination will insert each
448
            // record at the beginning, thus the order is reversed here:
449
            foreach ($dbAnalysis->itemArray as $item) {
450
                $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
451
                if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) {
452
                    continue;
453
                }
454
                $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId);
455
            }
456
        }
457
    }
458
459
    /****************************
460
     *****  Notifications  ******
461
     ****************************/
462
    /**
463
     * Send an email notification to users in workspace
464
     *
465
     * @param array $stat Workspace access array from \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::checkWorkspace()
466
     * @param int $stageId New Stage number: 0 = editing, 1= just ready for review, 10 = ready for publication, -1 = rejected!
467
     * @param string $table Table name of element (or list of element names if $id is zero)
468
     * @param int $id Record uid of element (if zero, then $table is used as reference to element(s) alone)
469
     * @param string $comment User comment sent along with action
470
     * @param DataHandler $dataHandler DataHandler object
471
     * @param array $notificationAlternativeRecipients List of recipients to notify instead of be_users selected by sys_workspace, list is generated by workspace extension module
472
     */
473
    protected function notifyStageChange(array $stat, $stageId, $table, $id, $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
474
    {
475
        $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
476
        // So, if $id is not set, then $table is taken to be the complete element name!
477
        $elementName = $id ? $table . ':' . $id : $table;
478
        if (!is_array($workspaceRec)) {
479
            return;
480
        }
481
482
        // Get the new stage title
483
        $stageService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\StagesService::class);
484
        $newStage = $stageService->getStageTitle((int)$stageId);
485
        if (empty($notificationAlternativeRecipients)) {
486
            // Compile list of recipients:
487
            $emails = [];
488
            switch ((int)$stat['stagechg_notification']) {
489
                case 1:
490
                    switch ((int)$stageId) {
491
                        case 1:
492
                            $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
493
                            break;
494
                        case 10:
495
                            $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
496
                            break;
497
                        case -1:
498
                            // List of elements to reject:
499
                            $allElements = explode(',', $elementName);
500
                            // Traverse them, and find the history of each
501
                            foreach ($allElements as $elRef) {
502
                                list($eTable, $eUid) = explode(':', $elRef);
503
504
                                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
505
                                    ->getQueryBuilderForTable('sys_log');
506
507
                                $queryBuilder->getRestrictions()->removeAll();
508
509
                                $result = $queryBuilder
510
                                    ->select('log_data', 'tstamp', 'userid')
511
                                    ->from('sys_log')
512
                                    ->where(
513
                                        $queryBuilder->expr()->eq(
514
                                            'action',
515
                                            $queryBuilder->createNamedParameter(6, \PDO::PARAM_INT)
516
                                        ),
517
                                        $queryBuilder->expr()->eq(
518
                                            'details_nr',
519
                                            $queryBuilder->createNamedParameter(30, \PDO::PARAM_INT)
520
                                        ),
521
                                        $queryBuilder->expr()->eq(
522
                                            'tablename',
523
                                            $queryBuilder->createNamedParameter($eTable, \PDO::PARAM_STR)
524
                                        ),
525
                                        $queryBuilder->expr()->eq(
526
                                            'recuid',
527
                                            $queryBuilder->createNamedParameter($eUid, \PDO::PARAM_INT)
528
                                        )
529
                                    )
530
                                    ->orderBy('uid', 'DESC')
531
                                    ->execute();
532
533
                                // Find all implicated since the last stage-raise from editing to review:
534
                                while ($dat = $result->fetch()) {
535
                                    $data = unserialize($dat['log_data']);
536
                                    $emails = $this->getEmailsForStageChangeNotification($dat['userid'], true) + $emails;
537
                                    if ($data['stage'] == 1) {
538
                                        break;
539
                                    }
540
                                }
541
                            }
542
                            break;
543
                        case 0:
544
                            $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']);
545
                            break;
546
                        default:
547
                            $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
548
                    }
549
                    break;
550
                case 10:
551
                    $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true);
552
                    $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']) + $emails;
553
                    $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']) + $emails;
554
                    break;
555
                default:
556
                    // Do nothing
557
            }
558
        } else {
559
            $emails = $notificationAlternativeRecipients;
560
        }
561
        // prepare and then send the emails
562
        if (!empty($emails)) {
563
            // Path to record is found:
564
            list($elementTable, $elementUid) = explode(':', $elementName);
565
            $elementUid = (int)$elementUid;
566
            $elementRecord = BackendUtility::getRecord($elementTable, $elementUid);
567
            $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord);
568
            if ($elementTable === 'pages') {
569
                $pageUid = $elementUid;
570
            } else {
571
                BackendUtility::fixVersioningPid($elementTable, $elementRecord);
572
                $pageUid = ($elementUid = $elementRecord['pid']);
573
            }
574
575
            // new way, options are
576
            // pageTSconfig: tx_version.workspaces.stageNotificationEmail.subject
577
            // userTSconfig: page.tx_version.workspaces.stageNotificationEmail.subject
578
            $pageTsConfig = BackendUtility::getPagesTSconfig($pageUid);
579
            $emailConfig = $pageTsConfig['tx_version.']['workspaces.']['stageNotificationEmail.'];
580
            $markers = [
581
                '###RECORD_TITLE###' => $recordTitle,
582
                '###RECORD_PATH###' => BackendUtility::getRecordPath($elementUid, '', 20),
583
                '###SITE_NAME###' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
584
                '###SITE_URL###' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir,
585
                '###WORKSPACE_TITLE###' => $workspaceRec['title'],
586
                '###WORKSPACE_UID###' => $workspaceRec['uid'],
587
                '###ELEMENT_NAME###' => $elementName,
588
                '###NEXT_STAGE###' => $newStage,
589
                '###COMMENT###' => $comment,
590
                // See: #30212 - keep both markers for compatibility
591
                '###USER_REALNAME###' => $dataHandler->BE_USER->user['realName'],
592
                '###USER_FULLNAME###' => $dataHandler->BE_USER->user['realName'],
593
                '###USER_USERNAME###' => $dataHandler->BE_USER->user['username']
594
            ];
595
            // add marker for preview links if workspace extension is loaded
596
            $this->workspaceService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\WorkspaceService::class);
597
            // only generate the link if the marker is in the template - prevents database from getting to much entries
598
            if (GeneralUtility::isFirstPartOfStr($emailConfig['message'], 'LLL:')) {
599
                $tempEmailMessage = $this->getLanguageService()->sL($emailConfig['message']);
600
            } else {
601
                $tempEmailMessage = $emailConfig['message'];
602
            }
603
            if (strpos($tempEmailMessage, '###PREVIEW_LINK###') !== false) {
604
                $markers['###PREVIEW_LINK###'] = $this->workspaceService->generateWorkspacePreviewLink($elementUid);
605
            }
606
            unset($tempEmailMessage);
607
            $markers['###SPLITTED_PREVIEW_LINK###'] = $this->workspaceService->generateWorkspaceSplittedPreviewLink($elementUid, true);
608
            // Hook for preprocessing of the content for formmails:
609
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'] ?? [] as $className) {
610
                $_procObj = GeneralUtility::makeInstance($className);
611
                $markers = $_procObj->postModifyMarkers($markers, $this);
612
            }
613
            // send an email to each individual user, to ensure the
614
            // multilanguage version of the email
615
            $emailRecipients = [];
616
            // an array of language objects that are needed
617
            // for emails with different languages
618
            $languageObjects = [
619
                $this->getLanguageService()->lang => $this->getLanguageService()
620
            ];
621
            // loop through each recipient and send the email
622
            foreach ($emails as $recipientData) {
623
                // don't send an email twice
624
                if (isset($emailRecipients[$recipientData['email']])) {
625
                    continue;
626
                }
627
                $emailSubject = $emailConfig['subject'];
628
                $emailMessage = $emailConfig['message'];
629
                $emailRecipients[$recipientData['email']] = $recipientData['email'];
630
                // check if the email needs to be localized
631
                // in the users' language
632
                if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:') || GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
633
                    $recipientLanguage = $recipientData['lang'] ? $recipientData['lang'] : 'default';
634
                    if (!isset($languageObjects[$recipientLanguage])) {
635
                        // a LANG object in this language hasn't been
636
                        // instantiated yet, so this is done here
637
                        /** @var $languageObject \TYPO3\CMS\Core\Localization\LanguageService */
638
                        $languageObject = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Localization\LanguageService::class);
639
                        $languageObject->init($recipientLanguage);
640
                        $languageObjects[$recipientLanguage] = $languageObject;
641
                    } else {
642
                        $languageObject = $languageObjects[$recipientLanguage];
643
                    }
644
                    if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:')) {
645
                        $emailSubject = $languageObject->sL($emailSubject);
646
                    }
647
                    if (GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
648
                        $emailMessage = $languageObject->sL($emailMessage);
649
                    }
650
                }
651
                $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
652
                $emailSubject = $templateService->substituteMarkerArray($emailSubject, $markers, '', true, true);
653
                $emailMessage = $templateService->substituteMarkerArray($emailMessage, $markers, '', true, true);
654
                // Send an email to the recipient
655
                /** @var $mail \TYPO3\CMS\Core\Mail\MailMessage */
656
                $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
657
                if (!empty($recipientData['realName'])) {
658
                    $recipient = [$recipientData['email'] => $recipientData['realName']];
659
                } else {
660
                    $recipient = $recipientData['email'];
661
                }
662
                $mail->setTo($recipient)
663
                    ->setSubject($emailSubject)
664
                    ->setBody($emailMessage);
665
                $mail->send();
666
            }
667
            $emailRecipients = implode(',', $emailRecipients);
668
            if ($dataHandler->enableLogging) {
669
                $propertyArray = $dataHandler->getRecordProperties($table, $id);
670
                $pid = $propertyArray['pid'];
671
                $dataHandler->log($table, $id, 0, 0, 0, 'Notification email for stage change was sent to "' . $emailRecipients . '"', -1, [], $dataHandler->eventPid($table, $id, $pid));
672
            }
673
        }
674
    }
675
676
    /**
677
     * Return be_users that should be notified on stage change from input list.
678
     * previously called notifyStageChange_getEmails() in DataHandler
679
     *
680
     * @param string $listOfUsers List of backend users, on the form "be_users_10,be_users_2" or "10,2" in case noTablePrefix is set.
681
     * @param bool $noTablePrefix If set, the input list are integers and not strings.
682
     * @return array Array of emails
683
     */
684
    protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = false)
685
    {
686
        $users = GeneralUtility::trimExplode(',', $listOfUsers, true);
687
        $emails = [];
688
        foreach ($users as $userIdent) {
689
            if ($noTablePrefix) {
690
                $id = (int)$userIdent;
691
            } else {
692
                list($table, $id) = GeneralUtility::revExplode('_', $userIdent, 2);
693
            }
694
            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...
695
                if ($userRecord = BackendUtility::getRecord('be_users', $id, 'uid,email,lang,realName', BackendUtility::BEenableFields('be_users'))) {
696
                    if (trim($userRecord['email']) !== '') {
697
                        $emails[$id] = $userRecord;
698
                    }
699
                }
700
            }
701
        }
702
        return $emails;
703
    }
704
705
    /****************************
706
     *****  Stage Changes  ******
707
     ****************************/
708
    /**
709
     * Setting stage of record
710
     *
711
     * @param string $table Table name
712
     * @param int $integer Record UID
713
     * @param int $stageId Stage ID to set
714
     * @param string $comment Comment that goes into log
715
     * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email?
716
     * @param DataHandler $dataHandler DataHandler object
717
     * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
718
     */
719
    protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = false, DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
720
    {
721
        if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
722
            $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, 1);
723
        } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) {
724
            $record = BackendUtility::getRecord($table, $id);
725
            $stat = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']);
726
            // check if the usere is allowed to the current stage, so it's also allowed to send to next stage
727
            if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) {
728
                // Set stage of record:
729
                GeneralUtility::makeInstance(ConnectionPool::class)
730
                    ->getConnectionForTable($table)
731
                    ->update(
732
                        $table,
733
                        [
734
                            't3ver_stage' => $stageId,
735
                        ],
736
                        ['uid' => (int)$id]
737
                    );
738
739
                if ($dataHandler->enableLogging) {
740
                    $propertyArray = $dataHandler->getRecordProperties($table, $id);
741
                    $pid = $propertyArray['pid'];
742
                    $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));
743
                }
744
                // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
745
                $dataHandler->log($table, $id, 6, 0, 0, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]);
746
                if ((int)$stat['stagechg_notification'] > 0) {
747
                    if ($notificationEmailInfo) {
748
                        $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$stat, $stageId, $comment];
749
                        $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id;
750
                        $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['alternativeRecipients'] = $notificationAlternativeRecipients;
751
                    } else {
752
                        $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients);
753
                    }
754
                }
755
            } else {
756
                $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1);
757
            }
758
        } else {
759
            $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', 1);
760
        }
761
    }
762
763
    /*****************************
764
     *****  CMD versioning  ******
765
     *****************************/
766
767
    /**
768
     * Swapping versions of a record
769
     * 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
770
     *
771
     * @param string $table Table name
772
     * @param int $id UID of the online record to swap
773
     * @param int $swapWith UID of the archived version to swap with!
774
     * @param bool $swapIntoWS If set, swaps online into workspace instead of publishing out of workspace.
775
     * @param DataHandler $dataHandler DataHandler object
776
     * @param string $comment Notification comment
777
     * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email?
778
     * @param array $notificationAlternativeRecipients comma separated list of recipients to notificate instead of normal be_users
779
     */
780
    protected function version_swap($table, $id, $swapWith, $swapIntoWS = 0, DataHandler $dataHandler, $comment = '', $notificationEmailInfo = false, $notificationAlternativeRecipients = [])
781
    {
782
783
        // Check prerequisites before start swapping
784
785
        // Skip records that have been deleted during the current execution
786
        if ($dataHandler->hasDeletedRecord($table, $id)) {
787
            return;
788
        }
789
790
        // First, check if we may actually edit the online record
791
        if (!$dataHandler->checkRecordUpdateAccess($table, $id)) {
792
            $dataHandler->newlog('Error: You cannot swap versions for a record you do not have access to edit!', 1);
793
            return;
794
        }
795
        // Select the two versions:
796
        $curVersion = BackendUtility::getRecord($table, $id, '*');
797
        $swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
798
        $movePlh = [];
799
        $movePlhID = 0;
800
        if (!(is_array($curVersion) && is_array($swapVersion))) {
801
            $dataHandler->newlog('Error: Either online or swap version could not be selected!', 2);
802
            return;
803
        }
804
        if (!$dataHandler->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
805
            $dataHandler->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], 1);
806
            return;
807
        }
808
        $wsAccess = $dataHandler->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']);
809
        if (!($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === -10)) {
810
            $dataHandler->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', 1);
811
            return;
812
        }
813
        if (!($dataHandler->doesRecordExist($table, $swapWith, 'show') && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) {
814
            $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', 1);
815
            return;
816
        }
817
        if ($swapIntoWS && !$dataHandler->BE_USER->workspaceSwapAccess()) {
818
            $dataHandler->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', 1);
819
            return;
820
        }
821
        // Check if the swapWith record really IS a version of the original!
822
        if (!(((int)$swapVersion['pid'] == -1 && (int)$curVersion['pid'] >= 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) {
823
            $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);
824
            return;
825
        }
826
        // Lock file name:
827
        $lockFileName = PATH_site . 'typo3temp/var/swap_locking/' . $table . '_' . $id . '.ser';
828
        if (@is_file($lockFileName)) {
829
            $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);
830
            return;
831
        }
832
833
        // Now start to swap records by first creating the lock file
834
835
        // Write lock-file:
836
        GeneralUtility::writeFileToTypo3tempDir($lockFileName, serialize([
837
            'tstamp' => $GLOBALS['EXEC_TIME'],
838
            'user' => $dataHandler->BE_USER->user['username'],
839
            'curVersion' => $curVersion,
840
            'swapVersion' => $swapVersion
841
        ]));
842
        // Find fields to keep
843
        $keepFields = $this->getUniqueFields($table);
844
        if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
845
            $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
846
        }
847
        // l10n-fields must be kept otherwise the localization
848
        // will be lost during the publishing
849
        if ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
850
            $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
851
        }
852
        // Swap "keepfields"
853
        foreach ($keepFields as $fN) {
854
            $tmp = $swapVersion[$fN];
855
            $swapVersion[$fN] = $curVersion[$fN];
856
            $curVersion[$fN] = $tmp;
857
        }
858
        // Preserve states:
859
        $t3ver_state = [];
860
        $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
861
        $t3ver_state['curVersion'] = $curVersion['t3ver_state'];
862
        // Modify offline version to become online:
863
        $tmp_wsid = $swapVersion['t3ver_wsid'];
864
        // Set pid for ONLINE
865
        $swapVersion['pid'] = (int)$curVersion['pid'];
866
        // We clear this because t3ver_oid only make sense for offline versions
867
        // and we want to prevent unintentional misuse of this
868
        // value for online records.
869
        $swapVersion['t3ver_oid'] = 0;
870
        // In case of swapping and the offline record has a state
871
        // (like 2 or 4 for deleting or move-pointer) we set the
872
        // current workspace ID so the record is not deselected
873
        // in the interface by BackendUtility::versioningPlaceholderClause()
874
        $swapVersion['t3ver_wsid'] = 0;
875
        if ($swapIntoWS) {
876
            if ($t3ver_state['swapVersion'] > 0) {
877
                $swapVersion['t3ver_wsid'] = $dataHandler->BE_USER->workspace;
878
            } else {
879
                $swapVersion['t3ver_wsid'] = (int)$curVersion['t3ver_wsid'];
880
            }
881
        }
882
        $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
883
        $swapVersion['t3ver_stage'] = 0;
884
        if (!$swapIntoWS) {
885
            $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
886
        }
887
        // Moving element.
888
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
889
            //  && $t3ver_state['swapVersion']==4   // Maybe we don't need this?
890
            if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) {
891
                $movePlhID = $plhRec['uid'];
892
                $movePlh['pid'] = $swapVersion['pid'];
893
                $swapVersion['pid'] = (int)$plhRec['pid'];
894
                $curVersion['t3ver_state'] = (int)$swapVersion['t3ver_state'];
895
                $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
896
                if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
897
                    // sortby is a "keepFields" which is why this will work...
898
                    $movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
899
                    $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
900
                }
901
            }
902
        }
903
        // Take care of relations in each field (e.g. IRRE):
904
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
905
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
906
                if (isset($fieldConf['config']) && is_array($fieldConf['config'])) {
907
                    $this->version_swap_processFields($table, $field, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler);
908
                }
909
            }
910
        }
911
        unset($swapVersion['uid']);
912
        // Modify online version to become offline:
913
        unset($curVersion['uid']);
914
        // Set pid for OFFLINE
915
        $curVersion['pid'] = -1;
916
        $curVersion['t3ver_oid'] = (int)$id;
917
        $curVersion['t3ver_wsid'] = $swapIntoWS ? (int)$tmp_wsid : 0;
918
        $curVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
919
        $curVersion['t3ver_count'] = $curVersion['t3ver_count'] + 1;
920
        // Increment lifecycle counter
921
        $curVersion['t3ver_stage'] = 0;
922
        if (!$swapIntoWS) {
923
            $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
924
        }
925
        // Registering and swapping MM relations in current and swap records:
926
        $dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith);
927
        // Generating proper history data to prepare logging
928
        $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
929
        $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
930
931
        // Execute swapping:
932
        $sqlErrors = [];
933
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
934
935
        $platform = $connection->getDatabasePlatform();
936
        $tableDetails = null;
937
        if ($platform instanceof SQLServerPlatform) {
938
            // mssql needs to set proper PARAM_LOB and others to update fields
939
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
940
        }
941
942
        try {
943
            $types = [];
944
945
            if ($platform instanceof SQLServerPlatform) {
946
                foreach ($curVersion as $columnName => $columnValue) {
947
                    $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
948
                }
949
            }
950
951
            $connection->update(
952
                $table,
953
                $swapVersion,
954
                ['uid' => (int)$id],
955
                $types
956
            );
957
        } catch (DBALException $e) {
958
            $sqlErrors[] = $e->getPrevious()->getMessage();
959
        }
960
961
        if (empty($sqlErrors)) {
962
            try {
963
                $types = [];
964
                if ($platform instanceof SQLServerPlatform) {
965
                    foreach ($curVersion as $columnName => $columnValue) {
966
                        $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
967
                    }
968
                }
969
970
                $connection->update(
971
                    $table,
972
                    $curVersion,
973
                    ['uid' => (int)$swapWith],
974
                    $types
975
                );
976
                unlink($lockFileName);
977
            } catch (DBALException $e) {
978
                $sqlErrors[] = $e->getPrevious()->getMessage();
979
            }
980
        }
981
982
        if (!empty($sqlErrors)) {
983
            $dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
984
        } else {
985
            // Register swapped ids for later remapping:
986
            $this->remappedIds[$table][$id] = $swapWith;
987
            $this->remappedIds[$table][$swapWith] = $id;
988
            // If a moving operation took place...:
989
            if ($movePlhID) {
990
                // Remove, if normal publishing:
991
                if (!$swapIntoWS) {
992
                    // For delete + completely delete!
993
                    $dataHandler->deleteEl($table, $movePlhID, true, true);
994
                } else {
995
                    // Otherwise update the movePlaceholder:
996
                    GeneralUtility::makeInstance(ConnectionPool::class)
997
                        ->getConnectionForTable($table)
998
                        ->update(
999
                            $table,
1000
                            $movePlh,
1001
                            ['uid' => (int)$movePlhID]
1002
                        );
1003
                    $dataHandler->addRemapStackRefIndex($table, $movePlhID);
1004
                }
1005
            }
1006
            // Checking for delete:
1007
            // Delete only if new/deleted placeholders are there.
1008
            if (!$swapIntoWS && ((int)$t3ver_state['swapVersion'] === 1 || (int)$t3ver_state['swapVersion'] === 2)) {
1009
                // Force delete
1010
                $dataHandler->deleteEl($table, $id, true);
1011
            }
1012
            if ($dataHandler->enableLogging) {
1013
                $dataHandler->log($table, $id, 0, 0, 0, ($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, -1, [], $dataHandler->eventPid($table, $id, $swapVersion['pid']));
1014
            }
1015
1016
            // Update reference index of the live record:
1017
            $dataHandler->addRemapStackRefIndex($table, $id);
1018
            // Set log entry for live record:
1019
            $propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion);
1020
            if ($propArr['_ORIG_pid'] == -1) {
1021
                $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
1022
            } else {
1023
                $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
1024
            }
1025
            $theLogId = $dataHandler->log($table, $id, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
1026
            $dataHandler->setHistory($table, $id, $theLogId);
1027
            // Update reference index of the offline record:
1028
            $dataHandler->addRemapStackRefIndex($table, $swapWith);
1029
            // Set log entry for offline record:
1030
            $propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion);
1031
            if ($propArr['_ORIG_pid'] == -1) {
1032
                $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated');
1033
            } else {
1034
                $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated');
1035
            }
1036
            $theLogId = $dataHandler->log($table, $swapWith, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']);
1037
            $dataHandler->setHistory($table, $swapWith, $theLogId);
1038
1039
            $stageId = -20; // \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_EXECUTE_ID;
1040
            if ($notificationEmailInfo) {
1041
                $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
1042
                $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
1043
                $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = $table . ':' . $id;
1044
                $this->notificationEmailInfo[$notificationEmailInfoKey]['alternativeRecipients'] = $notificationAlternativeRecipients;
1045
            } else {
1046
                $this->notifyStageChange($wsAccess, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients);
1047
            }
1048
            // Write to log with stageId -20
1049
            if ($dataHandler->enableLogging) {
1050
                $propArr = $dataHandler->getRecordProperties($table, $id);
1051
                $pid = $propArr['pid'];
1052
                $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));
1053
            }
1054
            $dataHandler->log($table, $id, 6, 0, 0, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]);
1055
1056
            // Clear cache:
1057
            $dataHandler->registerRecordIdForPageCacheClearing($table, $id);
1058
            // Checking for "new-placeholder" and if found, delete it (BUT FIRST after swapping!):
1059
            if (!$swapIntoWS && $t3ver_state['curVersion'] > 0) {
1060
                // For delete + completely delete!
1061
                $dataHandler->deleteEl($table, $swapWith, true, true);
1062
            }
1063
1064
            //Update reference index for live workspace too:
1065
            /** @var $refIndexObj \TYPO3\CMS\Core\Database\ReferenceIndex */
1066
            $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
1067
            $refIndexObj->setWorkspaceId(0);
1068
            $refIndexObj->updateRefIndexTable($table, $id);
1069
            $refIndexObj->updateRefIndexTable($table, $swapWith);
1070
        }
1071
    }
1072
1073
    /**
1074
     * Writes remapped foreign field (IRRE).
1075
     *
1076
     * @param \TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis Instance that holds the sorting order of child records
1077
     * @param array $configuration The TCA field configuration
1078
     * @param int $parentId The uid of the parent record
1079
     */
1080
    public function writeRemappedForeignField(\TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis, array $configuration, $parentId)
1081
    {
1082
        foreach ($dbAnalysis->itemArray as &$item) {
1083
            if (isset($this->remappedIds[$item['table']][$item['id']])) {
1084
                $item['id'] = $this->remappedIds[$item['table']][$item['id']];
1085
            }
1086
        }
1087
        $dbAnalysis->writeForeignField($configuration, $parentId);
1088
    }
1089
1090
    /**
1091
     * Processes fields of a record for the publishing/swapping process.
1092
     * Basically this takes care of IRRE (type "inline") child references.
1093
     *
1094
     * @param string $tableName Table name
1095
     * @param string $fieldName: Field name
1096
     * @param array $configuration TCA field configuration
1097
     * @param array $liveData: Live record data
1098
     * @param array $versionData: Version record data
1099
     * @param DataHandler $dataHandler Calling data-handler object
1100
     */
1101
    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

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

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

1291
                    ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $workspaceId, /** @scrutinizer ignore-type */ false));
Loading history...
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

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

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

1667
            /** @scrutinizer ignore-type */ $this,
Loading history...
1668
            $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

1668
            /** @scrutinizer ignore-type */ $dataHandler,
Loading history...
1669
            $dataHandler->cmdmap,
1670
            $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

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