Test Failed
Branch master (137376)
by Tymoteusz
20:39
created

DataHandlerHook::version_swap()   F

Complexity

Conditions 55
Paths > 20000

Size

Total Lines 290
Code Lines 177

Duplication

Lines 28
Ratio 9.66 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
314
    {
315
        // Only do something in Draft workspace
316
        if ($dataHandler->BE_USER->workspace === 0) {
317
            return;
318
        }
319 View Code Duplication
        if ($destPid < 0) {
320
            // Fetch move placeholder, since it might point to a new page in the current workspace
321
            $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid,pid');
0 ignored issues
show
Bug introduced by
It seems like abs($destPid) can also be of type double; however, parameter $uid of TYPO3\CMS\Backend\Utilit...y::getMovePlaceholder() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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