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

DataHandlerHook   F

Complexity

Total Complexity 259

Size/Duplication

Total Lines 1673
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1673
rs 0.6314
c 0
b 0
f 0
wmc 259

30 Methods

Rating   Name   Duplication   Size   Complexity  
A processCmdmap_afterFinish() 0 12 2
B getEmailsForStageChangeNotification() 0 19 7
C processCmdmap() 0 45 11
A updateInlineForeignFieldSorting() 0 18 3
F notifyStageChange() 0 199 28
B version_swap_processFields() 0 30 4
A createRelationHandlerInstance() 0 3 1
A moveRecord_processFields() 0 18 4
F moveRecord() 0 70 24
A processCmdmap_postProcess() 0 7 4
A flushWorkspaceCacheEntriesByWorkspaceId() 0 5 1
B resetStageOfElements() 0 25 3
F version_swap() 0 290 55
C version_clearWSID() 0 56 11
B getUniqueFields() 0 15 6
A writeRemappedForeignField() 0 8 3
A getDataHandler() 0 3 1
A processCmdmap_beforeStart() 0 6 1
A getCommandMap() 0 8 1
F moveRecord_wsPlaceholders() 0 97 16
C findPageElementsForVersionStageChange() 0 44 7
A getTcaTables() 0 3 1
B flushWorkspaceElements() 0 27 5
A findRealPageIds() 0 6 3
C version_setStage() 0 41 7
D moveRecord_processFieldValue() 0 27 10
B findPageIdsForVersionStateChange() 0 48 4
C findPageElementsForVersionSwap() 0 60 9
A getLanguageService() 0 3 1
D processCmdmap_deleteAction() 0 108 26

How to fix   Complexity   

Complex Class

Complex classes like DataHandlerHook often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataHandlerHook, and based on these observations, apply Extract Interface, too.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1291
                    ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $workspaceId, /** @scrutinizer ignore-type */ false));
Loading history...
Bug introduced by
It seems like $workspaceId can also be of type integer; however, parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance() does only seem to accept array<integer,mixed>, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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