• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FluidTYPO3 / flux / 7003688291

27 Nov 2023 10:26AM UTC coverage: 93.508% (-0.05%) from 93.56%
7003688291

push

github

NamelessCoder
[TER] 10.0.8

6799 of 7271 relevant lines covered (93.51%)

46.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.76
/Classes/Integration/HookSubscribers/DataHandlerSubscriber.php
1
<?php
2

3
namespace FluidTYPO3\Flux\Integration\HookSubscribers;
4

5
/*
6
 * This file is part of the FluidTYPO3/Flux project under GPLv2 or later.
7
 *
8
 * For the full copyright and license information, please read the
9
 * LICENSE.md file that was distributed with this source code.
10
 */
11

12
use FluidTYPO3\Flux\Content\ContentTypeManager;
13
use FluidTYPO3\Flux\Enum\ExtensionOption;
14
use FluidTYPO3\Flux\Provider\Interfaces\GridProviderInterface;
15
use FluidTYPO3\Flux\Provider\Interfaces\RecordProcessingProvider;
16
use FluidTYPO3\Flux\Provider\PageProvider;
17
use FluidTYPO3\Flux\Provider\ProviderResolver;
18
use FluidTYPO3\Flux\Utility\ColumnNumberUtility;
19
use FluidTYPO3\Flux\Utility\ExtensionConfigurationUtility;
20
use TYPO3\CMS\Backend\Utility\BackendUtility;
21
use TYPO3\CMS\Core\Cache\CacheManager;
22
use TYPO3\CMS\Core\Database\Connection;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
25
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
26
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
27
use TYPO3\CMS\Core\DataHandling\DataHandler;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29

30
class DataHandlerSubscriber
31
{
32
    protected static array $copiedRecords = [];
33

34
    public function clearCacheCommand(array $command): void
35
    {
36
        if (($command['cacheCmd'] ?? null) === 'all' || ($command['cacheCmd'] ?? null) === 'system') {
15✔
37
            $this->regenerateContentTypes();
10✔
38
        }
39
    }
40

41
    /**
42
     * @param string $command Command that was executed
43
     * @param string $table The table TCEmain is currently processing
44
     * @param string $id The records id (if any)
45
     * @param array $fieldArray The field names and their values to be processed
46
     * @param DataHandler $reference Reference to the parent object (TCEmain)
47
     * @return void
48
     */
49
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
50
    public function processDatamap_afterDatabaseOperations($command, $table, $id, $fieldArray, $reference)
51
    {
52
        if ($table === 'content_types') {
×
53
            // Changing records in table "content_types" has to flush the system cache to regenerate various cached
54
            // definitions of plugins etc. that are based on those "content_types" records.
55
            /** @var CacheManager $cacheManager */
56
            $cacheManager = GeneralUtility::makeInstance(CacheManager::class);
×
57
            $cacheManager->flushCachesInGroup('system');
×
58
            $this->regenerateContentTypes();
×
59
            return;
×
60
        }
61

62
        if ($GLOBALS['BE_USER']->workspace) {
×
63
            $record = BackendUtility::getRecord($table, (integer) $id);
×
64
        } else {
65
            $record = $reference->datamap[$table][$id] ?? null;
×
66
        }
67

68
        if ($record !== null) {
×
69
            /** @var RecordProcessingProvider[] $providers */
70
            $providers = $this->getProviderResolver()->resolveConfigurationProviders(
×
71
                $table,
×
72
                null,
×
73
                $record,
×
74
                null,
×
75
                [RecordProcessingProvider::class]
×
76
            );
×
77

78
            foreach ($providers as $provider) {
×
79
                $provider->postProcessRecord($command, (integer) $id, $record, $reference, []);
×
80
            }
81
        }
82

83
        if ($table !== 'tt_content'
×
84
            || $command !== 'new'
×
85
            || !isset($fieldArray['t3_origuid'])
×
86
            || !$fieldArray['t3_origuid']
×
87
        ) {
88
            // The action was not for tt_content, not a "create new" action, or not a "copy" or "copyToLanguage" action.
89
            return;
×
90
        }
91

92
        $originalRecord = $this->getSingleRecordWithoutRestrictions($table, $fieldArray['t3_origuid'], 'colPos');
×
93
        if ($originalRecord === null) {
×
94
            // Original record has been hard-deleted and can no longer be loaded. Processing must stop.
95
            return;
×
96
        }
97
        $originalParentUid = ColumnNumberUtility::calculateParentUid($originalRecord['colPos']);
×
98
        $newColumnPosition = 0;
×
99

100
        if (!empty($fieldArray['l18n_parent'])) {
×
101
            // Command was "localize", read colPos value from the translation parent and use directly
102
            $newColumnPosition = $this->getSingleRecordWithoutRestrictions(
×
103
                $table,
×
104
                $fieldArray['l18n_parent'],
×
105
                'colPos'
×
106
            )['colPos'] ?? null;
×
107
        } elseif (isset(static::$copiedRecords[$originalParentUid])) {
×
108
            // The parent of the original version of the record that was copied, was also copied in the same request;
109
            // this means the record that was copied, was copied as a recursion operation. Look up the most recent copy
110
            // of the original record's parent and create a new column position number based on the new parent.
111
            $newParentRecord = $this->getMostRecentCopyOfRecord($originalParentUid);
×
112
            if ($newParentRecord !== null) {
×
113
                $newColumnPosition = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
×
114
                    $newParentRecord['uid'],
×
115
                    ColumnNumberUtility::calculateLocalColumnNumber($originalRecord['colPos'])
×
116
                );
×
117
            }
118
        } elseif (($fieldArray['colPos'] ?? 0) >= ColumnNumberUtility::MULTIPLIER) {
×
119
            // Record is a child record, the updated field array still indicates it is a child (was not pasted outside
120
            // of parent, rather, parent was pasted somewhere else).
121
            // If language of child record is different from resolved parent (copyToLanguage occurred), resolve the
122
            // right parent for the language and update the column position accordingly.
123
            $recordUid = (integer) ($record['uid'] ?? $id);
×
124
            $originalParentUid = ColumnNumberUtility::calculateParentUid($fieldArray['colPos']);
×
125
            $originalParent = $this->getSingleRecordWithoutRestrictions($table, $originalParentUid, 'sys_language_uid');
×
126
            $currentRecordLanguageUid = $fieldArray['sys_language_uid']
×
127
                ?? $this->getSingleRecordWithoutRestrictions($table, $recordUid, 'sys_language_uid')['sys_language_uid']
×
128
                ?? 0;
×
129
            if ($originalParent !== null && $originalParent['sys_language_uid'] !== $currentRecordLanguageUid) {
×
130
                // copyToLanguage case. Resolve the most recent translated version of the parent record in language of
131
                // child record, and calculate the new column position number based on it.
132
                $newParentRecord = $this->getTranslatedVersionOfParentInLanguageOnPage(
×
133
                    (int) $currentRecordLanguageUid,
×
134
                    (int) $fieldArray['pid'],
×
135
                    $originalParentUid
×
136
                );
×
137
                if ($newParentRecord !== null) {
×
138
                    $newColumnPosition = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
×
139
                        $newParentRecord['uid'],
×
140
                        ColumnNumberUtility::calculateLocalColumnNumber($fieldArray['colPos'])
×
141
                    );
×
142
                }
143
            }
144
        }
145

146
        if ($newColumnPosition > 0) {
×
147
            $queryBuilder = $this->createQueryBuilderForTable($table);
×
148
            $queryBuilder->update($table)->set('colPos', $newColumnPosition, true, \PDO::PARAM_INT)->where(
×
149
                $queryBuilder->expr()->eq(
×
150
                    'uid',
×
151
                    $queryBuilder->createNamedParameter($reference->substNEWwithIDs[$id], \PDO::PARAM_INT)
×
152
                )
×
153
            )->orWhere(
×
154
                $queryBuilder->expr()->andX(
×
155
                    $queryBuilder->expr()->eq(
×
156
                        't3ver_oid',
×
157
                        $queryBuilder->createNamedParameter($reference->substNEWwithIDs[$id], \PDO::PARAM_INT)
×
158
                    ),
×
159
                    $queryBuilder->expr()->eq(
×
160
                        't3ver_wsid',
×
161
                        $queryBuilder->createNamedParameter($GLOBALS['BE_USER']->workspace, \PDO::PARAM_INT)
×
162
                    )
×
163
                )
×
164
            )->execute();
×
165
        }
166

167
        static::$copiedRecords[$fieldArray['t3_origuid']] = true;
×
168
    }
169

170
    /**
171
     * @param array $fieldArray
172
     * @param string $table
173
     * @param int|string $id
174
     * @param DataHandler $dataHandler
175
     * @return void
176
     */
177
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
178
    public function processDatamap_preProcessFieldArray(array &$fieldArray, $table, $id, DataHandler $dataHandler)
179
    {
180
        $isNewRecord = strpos((string) $id, 'NEW') === 0;
15✔
181
        $isTranslatedRecord = ($fieldArray['l10n_source'] ?? 0) > 0;
15✔
182
        $pageIntegrationEnabled = ExtensionConfigurationUtility::getOption(ExtensionOption::OPTION_PAGE_INTEGRATION);
15✔
183
        $isPageRecord = $table === 'pages';
15✔
184
        if ($pageIntegrationEnabled && $isPageRecord && $isNewRecord && $isTranslatedRecord) {
15✔
185
            // Record is a newly created page and is a translation of a page. In all likelyhood (but we can't actually
186
            // know for sure since TYPO3 uses a nested DataHandler for this...) this record is the result of a blank
187
            // initial copy of the original language's record. We may want to copy the "Page Configuration" fields'
188
            // values from the original record.
189
            if (!isset($fieldArray[PageProvider::FIELD_NAME_MAIN], $fieldArray[PageProvider::FIELD_NAME_SUB])) {
5✔
190
                // To make completely sure, we only want to copy those values if both "Page Configuration" fields are
191
                // completely omitted from the incoming field array.
192
                $originalLanguageRecord = $this->getSingleRecordWithoutRestrictions(
5✔
193
                    'pages',
5✔
194
                    $fieldArray['l10n_source'],
5✔
195
                    PageProvider::FIELD_NAME_MAIN . ',' . PageProvider::FIELD_NAME_SUB
5✔
196
                );
5✔
197
                if ($originalLanguageRecord) {
5✔
198
                    $fieldArray[PageProvider::FIELD_NAME_MAIN] = $originalLanguageRecord[PageProvider::FIELD_NAME_MAIN];
5✔
199
                    $fieldArray[PageProvider::FIELD_NAME_SUB] = $originalLanguageRecord[PageProvider::FIELD_NAME_SUB];
5✔
200
                }
201
            }
202
        }
203

204
        // Handle "$table.$field" named fields where $table is the valid TCA table name and $field is an existing TCA
205
        // field. Updated value will still be subject to permission checks.
206
        $resolver = $this->getProviderResolver();
15✔
207
        foreach ($fieldArray as $fieldName => $fieldValue) {
15✔
208
            if (($GLOBALS["TCA"][$table]["columns"][$fieldName]["config"]["type"] ?? '') === 'flex') {
15✔
209
                $primaryConfigurationProvider = $resolver->resolvePrimaryConfigurationProvider(
5✔
210
                    $table,
5✔
211
                    $fieldName,
5✔
212
                    $fieldArray
5✔
213
                );
5✔
214

215
                if ($primaryConfigurationProvider
5✔
216
                    && is_array($fieldArray[$fieldName])
5✔
217
                    && array_key_exists('data', $fieldArray[$fieldName])
5✔
218
                ) {
219
                    foreach ($fieldArray[$fieldName]['data'] as $sheet) {
5✔
220
                        foreach ($sheet['lDEF'] as $key => $value) {
5✔
221
                            if (strpos($key, '.') !== false) {
5✔
222
                                [$possibleTableName, $columnName] = explode('.', $key, 2);
5✔
223
                                if ($possibleTableName === $table
5✔
224
                                    && isset($GLOBALS['TCA'][$table]['columns'][$columnName])
5✔
225
                                ) {
226
                                    $fieldArray[$columnName] = $value['vDEF'];
5✔
227
                                }
228
                            }
229
                        }
230
                    }
231
                }
232
            }
233
        }
234

235
        if ($table !== 'tt_content' || !is_integer($id)) {
15✔
236
            return;
10✔
237
        }
238

239
        // TYPO3 issue https://forge.typo3.org/issues/85013 "colPos not part of $fieldArray when dropping in top column"
240
        // TODO: remove when expected solution, the inclusion of colPos in $fieldArray, is merged and released in TYPO3
241
        if (!array_key_exists('colPos', $fieldArray)) {
5✔
242
            $record = $this->getSingleRecordWithoutRestrictions($table, (int) $id, 'pid, colPos, l18n_parent');
5✔
243
            $uidInDefaultLanguage = $record['l18n_parent'] ?? null;
5✔
244
            if ($uidInDefaultLanguage && isset($dataHandler->datamap[$table][$uidInDefaultLanguage]['colPos'])) {
5✔
245
                $fieldArray['colPos'] = (integer) $dataHandler->datamap[$table][$uidInDefaultLanguage]['colPos'];
5✔
246
            }
247
        }
248
    }
249

250
    /**
251
     * @param string $table
252
     * @param int $id
253
     * @param string $command
254
     * @param mixed $value
255
     * @param DataHandler $dataHandler
256
     * @return void
257
     */
258
    protected function cascadeCommandToChildRecords(
259
        string $table,
260
        int $id,
261
        string $command,
262
        $value,
263
        DataHandler $dataHandler
264
    ) {
265
        [, $childRecords] = $this->getParentAndRecordsNestedInGrid(
25✔
266
            $table,
25✔
267
            (int)$id,
25✔
268
            'uid, pid',
25✔
269
            false,
25✔
270
            $command
25✔
271
        );
25✔
272

273
        if (empty($childRecords)) {
25✔
274
            return;
25✔
275
        }
276

277
        foreach ($childRecords as $childRecord) {
25✔
278
            $childRecordUid = $childRecord['uid'];
25✔
279
            $dataHandler->cmdmap[$table][$childRecordUid][$command] = $value;
25✔
280
            $this->cascadeCommandToChildRecords($table, $childRecordUid, $command, $value, $dataHandler);
25✔
281
        }
282
    }
283

284
    /**
285
     * @param DataHandler $dataHandler
286
     * @return void
287
     */
288
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
289
    public function processCmdmap_beforeStart(DataHandler $dataHandler)
290
    {
291
        foreach ($dataHandler->cmdmap as $table => $commandSets) {
45✔
292
            if ($table === 'content_types') {
45✔
293
                $this->regenerateContentTypes();
5✔
294
                continue;
5✔
295
            }
296

297
            if ($table !== 'tt_content') {
40✔
298
                continue;
5✔
299
            }
300

301
            foreach ($commandSets as $id => $commands) {
35✔
302
                foreach ($commands as $command => $value) {
35✔
303
                    switch ($command) {
304
                        case 'move':
35✔
305
                            // Verify that the target column is not within the element or any child hereof.
306
                            if (is_array($value) && isset($value['update']['colPos'])) {
5✔
307
                                $invalidColumnNumbers = $this->fetchAllColumnNumbersBeneathParent((integer) $id);
5✔
308
                                // Only react to move commands which contain a target colPos
309
                                if (in_array((int) $value['update']['colPos'], $invalidColumnNumbers, true)) {
5✔
310
                                    // Invalid target detected - delete the "move" command so it does not happen, and
311
                                    // dispatch an error message.
312
                                    unset($dataHandler->cmdmap[$table][$id]);
5✔
313
                                    $dataHandler->log(
5✔
314
                                        $table,
5✔
315
                                        (integer) $id,
5✔
316
                                        4,
5✔
317
                                        0,
5✔
318
                                        1,
5✔
319
                                        'Record not moved, would become child of self'
5✔
320
                                    );
5✔
321
                                }
322
                            }
323
                            break;
5✔
324
                        case 'delete':
30✔
325
                        case 'undelete':
25✔
326
                        case 'copyToLanguage':
20✔
327
                        case 'localize':
15✔
328
                            $this->cascadeCommandToChildRecords($table, (int)$id, $command, $value, $dataHandler);
20✔
329
                            break;
20✔
330
                        case 'copy':
10✔
331
                            if (is_array($value)) {
5✔
332
                                unset($value['update']['colPos']);
5✔
333
                            }
334
                            $this->cascadeCommandToChildRecords($table, (int)$id, $command, $value, $dataHandler);
5✔
335
                            break;
5✔
336
                        default:
337
                            break;
5✔
338
                    }
339
                }
340
            }
341
        }
342
    }
343

344
    /**
345
     * Command post processing method
346
     *
347
     * Like other pre/post methods this method calls the corresponding
348
     * method on Providers which match the table/id(record) being passed.
349
     *
350
     * In addition, this method also listens for paste commands executed
351
     * via the TYPO3 clipboard, since such methods do not necessarily
352
     * trigger the "normal" record move hooks (which we also subscribe
353
     * to and react to in moveRecord_* methods).
354
     *
355
     * @param string $command The TCEmain operation status, fx. 'update'
356
     * @param string $table The table TCEmain is currently processing
357
     * @param string $id The records id (if any)
358
     * @param string $relativeTo Filled if command is relative to another element
359
     * @param DataHandler $reference Reference to the parent object (TCEmain)
360
     * @param array $pasteUpdate
361
     * @param array $pasteDataMap
362
     * @return void
363
     */
364
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
365
    public function processCmdmap_postProcess(
366
        &$command,
367
        $table,
368
        $id,
369
        &$relativeTo,
370
        &$reference,
371
        &$pasteUpdate,
372
        &$pasteDataMap
373
    ) {
374

375
        /*
376
        if ($table === 'pages' && $command === 'copy') {
377
            foreach ($reference->copyMappingArray['tt_content'] ?? [] as $originalRecordUid => $copiedRecordUid) {
378
                $copiedRecord = $this->getSingleRecordWithoutRestrictions('tt_content', $copiedRecordUid, 'colPos');
379
                if ($copiedRecord['colPos'] < ColumnNumberUtility::MULTIPLIER) {
380
                    continue;
381
                }
382

383
                $oldParentUid = ColumnNumberUtility::calculateParentUid($copiedRecord['colPos']);
384
                $newParentUid = $reference->copyMappingArray['tt_content'][$oldParentUid];
385

386
                $overrideArray['colPos'] = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
387
                    $newParentUid,
388
                    ColumnNumberUtility::calculateLocalColumnNumber((int) $copiedRecord['colPos'])
389
                );
390

391
                // Note here: it is safe to directly update the DB in this case, since we filtered out any
392
                // non-"copy" actions, and "copy" is the only action which requires adjustment.
393
                $reference->updateDB('tt_content', $copiedRecordUid, $overrideArray);
394

395
                // But if we also have a workspace version of the record recorded, it too must be updated:
396
                if (isset($reference->autoVersionIdMap['tt_content'][$copiedRecordUid])) {
397
                    $reference->updateDB(
398
                        'tt_content',
399
                        $reference->autoVersionIdMap['tt_content'][$copiedRecordUid],
400
                        $overrideArray
401
                    );
402
                }
403
            }
404
        }
405
        */
406

407
        if ($table !== 'tt_content' || $command !== 'move') {
20✔
408
            return;
5✔
409
        }
410

411
        [$originalRecord, $recordsToProcess] = $this->getParentAndRecordsNestedInGrid(
15✔
412
            $table,
15✔
413
            (integer) $id,
15✔
414
            'uid, pid, colPos',
15✔
415
            false,
15✔
416
            $command
15✔
417
        );
15✔
418

419
        if (empty($recordsToProcess)) {
15✔
420
            return;
5✔
421
        }
422

423
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
10✔
424
        $languageUid = (int)($reference->cmdmap[$table][$id][$command]['update'][$languageField]
10✔
425
            ?? $originalRecord[$languageField]);
10✔
426

427
        if ($relativeTo > 0) {
10✔
428
            $destinationPid = $relativeTo;
5✔
429
        } else {
430
            $relativeRecord = $this->getSingleRecordWithoutRestrictions($table, (integer) abs($relativeTo), 'pid');
5✔
431
            $destinationPid = $relativeRecord['pid'] ?? $relativeTo;
5✔
432
        }
433

434
        $this->recursivelyMoveChildRecords(
10✔
435
            $table,
10✔
436
            $recordsToProcess,
10✔
437
            (integer) $destinationPid,
10✔
438
            $languageUid,
10✔
439
            $reference
10✔
440
        );
10✔
441
    }
442

443
    protected function fetchAllColumnNumbersBeneathParent(int $parentUid): array
444
    {
445
        [, $recordsToProcess, $bannedColumnNumbers] = $this->getParentAndRecordsNestedInGrid(
×
446
            'tt_content',
×
447
            $parentUid,
×
448
            'uid, colPos'
×
449
        );
×
450
        $invalidColumnPositions = $bannedColumnNumbers;
×
451
        foreach ($recordsToProcess as $childRecord) {
×
452
            $invalidColumnPositions += $this->fetchAllColumnNumbersBeneathParent($childRecord['uid']);
×
453
        }
454
        return (array) $invalidColumnPositions;
×
455
    }
456

457
    protected function recursivelyMoveChildRecords(
458
        string $table,
459
        array $recordsToProcess,
460
        int $pageUid,
461
        int $languageUid,
462
        DataHandler $dataHandler
463
    ): void {
464
        $subCommandMap = [];
10✔
465

466
        foreach ($recordsToProcess as $recordToProcess) {
10✔
467
            $recordUid = $recordToProcess['uid'];
10✔
468
            $subCommandMap[$table][$recordUid]['move'] = [
10✔
469
                'action' => 'paste',
10✔
470
                'target' => $pageUid,
10✔
471
                'update' => [
10✔
472
                    $GLOBALS['TCA']['tt_content']['ctrl']['languageField'] => $languageUid,
10✔
473
                ],
10✔
474
            ];
10✔
475
        }
476

477
        if (!empty($subCommandMap)) {
10✔
478
            /** @var DataHandler $dataHandler */
479
            $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
10✔
480
            $dataHandler->copyMappingArray = $dataHandler->copyMappingArray;
10✔
481
            $dataHandler->start([], $subCommandMap);
10✔
482
            $dataHandler->process_cmdmap();
10✔
483
        }
484
    }
485

486
    /**
487
     * @codeCoverageIgnore
488
     */
489
    protected function getSingleRecordWithRestrictions(string $table, int $uid, string $fieldsToSelect): ?array
490
    {
491
        /** @var DeletedRestriction $deletedRestriction */
492
        $deletedRestriction = GeneralUtility::makeInstance(DeletedRestriction::class);
493
        $queryBuilder = $this->createQueryBuilderForTable($table);
494
        $queryBuilder->getRestrictions()->removeAll()->add($deletedRestriction);
495
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
496
            ->from($table)
497
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
498
        /** @var array|false $firstResult */
499
        $firstResult = $queryBuilder->execute()->fetch();
500
        return $firstResult ?: null;
501
    }
502

503
    /**
504
     * @codeCoverageIgnore
505
     */
506
    protected function getSingleRecordWithoutRestrictions(string $table, int $uid, string $fieldsToSelect): ?array
507
    {
508
        $queryBuilder = $this->createQueryBuilderForTable($table);
509
        $queryBuilder->getRestrictions()->removeAll();
510
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
511
            ->from($table)
512
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
513
        /** @var array|false $firstResult */
514
        $firstResult = $queryBuilder->execute()->fetch();
515
        return $firstResult ?: null;
516
    }
517

518
    /**
519
     * @codeCoverageIgnore
520
     */
521
    protected function getMostRecentCopyOfRecord(int $uid, string $fieldsToSelect = 'uid'): ?array
522
    {
523
        $queryBuilder = $this->createQueryBuilderForTable('tt_content');
524
        $queryBuilder->getRestrictions()->removeAll();
525
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
526
            ->from('tt_content')
527
            ->orderBy('uid', 'DESC')
528
            ->setMaxResults(1)
529
            ->where(
530
                $queryBuilder->expr()->eq('t3_origuid', $uid),
531
                $queryBuilder->expr()->neq('t3ver_state', -1)
532
            );
533
        /** @var array|false $firstResult */
534
        $firstResult = $queryBuilder->execute()->fetch();
535
        return $firstResult ?: null;
536
    }
537

538
    /**
539
     * @codeCoverageIgnore
540
     */
541
    protected function getTranslatedVersionOfParentInLanguageOnPage(
542
        int $languageUid,
543
        int $pageUid,
544
        int $originalParentUid,
545
        string $fieldsToSelect = '*'
546
    ): ?array {
547
        /** @var DeletedRestriction $deletedRestriction */
548
        $deletedRestriction = GeneralUtility::makeInstance(DeletedRestriction::class);
549
        $queryBuilder = $this->createQueryBuilderForTable('tt_content');
550
        $queryBuilder->getRestrictions()->removeAll()->add($deletedRestriction);
551
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
552
            ->from('tt_content')
553
            ->setMaxResults(1)
554
            ->orderBy('uid', 'DESC')
555
            ->where(
556
                $queryBuilder->expr()->eq(
557
                    'sys_language_uid',
558
                    $queryBuilder->createNamedParameter($languageUid, \PDO::PARAM_INT)
559
                ),
560
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)),
561
                $queryBuilder->expr()->eq(
562
                    'l10n_source',
563
                    $queryBuilder->createNamedParameter($originalParentUid, \PDO::PARAM_INT)
564
                )
565
            );
566
        /** @var array|false $firstResult */
567
        $firstResult = $queryBuilder->execute()->fetch();
568
        return $firstResult ?: null;
569
    }
570

571
    protected function getParentAndRecordsNestedInGrid(
572
        string $table,
573
        int $parentUid,
574
        string $fieldsToSelect,
575
        bool $respectPid = false,
576
        ?string $command = null
577
    ):array {
578
        // A Provider must be resolved which implements the GridProviderInterface
579
        $resolver = $this->getProviderResolver();
×
580
        $originalRecord = (array) $this->getSingleRecordWithoutRestrictions($table, $parentUid, '*');
×
581

582
        $primaryProvider = $resolver->resolvePrimaryConfigurationProvider(
×
583
            $table,
×
584
            null,
×
585
            $originalRecord,
×
586
            null,
×
587
            [GridProviderInterface::class]
×
588
        );
×
589

590
        if (!$primaryProvider) {
×
591
            return [
×
592
                $originalRecord,
×
593
                [],
×
594
                [],
×
595
            ];
×
596
        }
597

598
        // The Grid this Provider returns must contain at least one column
599
        $childColPosValues = $primaryProvider->getGrid($originalRecord)->buildColumnPositionValues($originalRecord);
×
600

601
        if (empty($childColPosValues)) {
×
602
            return [
×
603
                $originalRecord,
×
604
                [],
×
605
                [],
×
606
            ];
×
607
        }
608

609
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
×
610

611
        $queryBuilder = $this->createQueryBuilderForTable($table);
×
612
        if ($command === 'undelete') {
×
613
            $queryBuilder->getRestrictions()->removeAll();
×
614
        } else {
615
            $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
×
616
        }
617

618
        $query = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
×
619
            ->from($table)
×
620
            ->andWhere(
×
621
                $queryBuilder->expr()->in(
×
622
                    'colPos',
×
623
                    $queryBuilder->createNamedParameter($childColPosValues, Connection::PARAM_INT_ARRAY)
×
624
                ),
×
625
                $queryBuilder->expr()->eq($languageField, (int)$originalRecord[$languageField]),
×
626
                $queryBuilder->expr()->in(
×
627
                    't3ver_wsid',
×
628
                    $queryBuilder->createNamedParameter(
×
629
                        [0, $GLOBALS['BE_USER']->workspace],
×
630
                        Connection::PARAM_INT_ARRAY
×
631
                    )
×
632
                )
×
633
            )->orderBy('sorting', 'DESC');
×
634

635
        if ($respectPid) {
×
636
            $query->andWhere($queryBuilder->expr()->eq('pid', $originalRecord['pid']));
×
637
        } else {
638
            $query->andWhere($queryBuilder->expr()->neq('pid', -1));
×
639
        }
640

641
        $records = $query->execute()->fetchAll();
×
642

643
        // Selecting records to return. The "sorting DESC" is very intentional; copy operations will place records
644
        // into the top of columns which means reading records in reverse order causes the correct final order.
645
        return [
×
646
            $originalRecord,
×
647
            $records,
×
648
            $childColPosValues
×
649
        ];
×
650
    }
651

652
    /**
653
     * @codeCoverageIgnore
654
     */
655
    protected function getProviderResolver(): ProviderResolver
656
    {
657
        /** @var ProviderResolver $providerResolver */
658
        $providerResolver = GeneralUtility::makeInstance(ProviderResolver::class);
659
        return $providerResolver;
660
    }
661

662
    /**
663
     * @codeCoverageIgnore
664
     */
665
    protected function createQueryBuilderForTable(string $table): QueryBuilder
666
    {
667
        /** @var ConnectionPool $connectionPool */
668
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
669
        return $connectionPool->getQueryBuilderForTable($table);
670
    }
671

672
    /**
673
     * @codeCoverageIgnore
674
     */
675
    protected function regenerateContentTypes(): void
676
    {
677
        /** @var ContentTypeManager $contentTypeManager */
678
        $contentTypeManager = GeneralUtility::makeInstance(ContentTypeManager::class);
679
        $contentTypeManager->regenerate();
680
    }
681
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc