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

FluidTYPO3 / flux / 12934396798

23 Jan 2025 05:10PM UTC coverage: 93.278% (-0.01%) from 93.29%
12934396798

push

github

NamelessCoder
[BUGFIX] Respect Provider stopping subsequent Providers' processing

0 of 2 new or added lines in 1 file covered. (0.0%)

7035 of 7542 relevant lines covered (93.28%)

56.31 hits per line

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

48.18
/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\DoctrineQueryProxy;
20
use FluidTYPO3\Flux\Utility\ExtensionConfigurationUtility;
21
use TYPO3\CMS\Backend\Utility\BackendUtility;
22
use TYPO3\CMS\Core\Cache\CacheManager;
23
use TYPO3\CMS\Core\Database\Connection;
24
use TYPO3\CMS\Core\Database\ConnectionPool;
25
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
26
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
28
use TYPO3\CMS\Core\DataHandling\DataHandler;
29
use TYPO3\CMS\Core\Utility\GeneralUtility;
30

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

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

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

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

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

79
            foreach ($providers as $provider) {
×
NEW
80
                if ($provider->postProcessRecord($command, (integer) $id, $record, $reference, [])) {
×
NEW
81
                    break;
×
82
                }
83
            }
84
        }
85

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

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

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

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

171
        static::$copiedRecords[$fieldArray['t3_origuid']] = true;
×
172
    }
173

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

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

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

239
        if ($table !== 'tt_content' || !is_integer($id)) {
18✔
240
            return;
12✔
241
        }
242

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

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

277
        if (empty($childRecords)) {
30✔
278
            return;
30✔
279
        }
280

281
        foreach ($childRecords as $childRecord) {
30✔
282
            $childRecordUid = $childRecord['uid'];
30✔
283
            $dataHandler->cmdmap[$table][$childRecordUid][$command] = $value;
30✔
284
            $this->cascadeCommandToChildRecords($table, $childRecordUid, $command, $value, $dataHandler);
30✔
285
        }
286
    }
287

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

301
            if ($table !== 'tt_content') {
48✔
302
                continue;
6✔
303
            }
304

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

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

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

387
                $oldParentUid = ColumnNumberUtility::calculateParentUid($copiedRecord['colPos']);
388
                $newParentUid = $reference->copyMappingArray['tt_content'][$oldParentUid];
389

390
                $overrideArray['colPos'] = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
391
                    $newParentUid,
392
                    ColumnNumberUtility::calculateLocalColumnNumber((int) $copiedRecord['colPos'])
393
                );
394

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

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

411
        if ($table !== 'tt_content' || $command !== 'move') {
24✔
412
            return;
6✔
413
        }
414

415
        [$originalRecord, $recordsToProcess] = $this->getParentAndRecordsNestedInGrid(
18✔
416
            $table,
18✔
417
            (integer) $id,
18✔
418
            'uid, pid, colPos',
18✔
419
            false,
18✔
420
            $command
18✔
421
        );
18✔
422

423
        if (empty($recordsToProcess)) {
18✔
424
            return;
6✔
425
        }
426

427
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
12✔
428
        $languageUid = (int)($reference->cmdmap[$table][$id][$command]['update'][$languageField]
12✔
429
            ?? $originalRecord[$languageField]);
12✔
430

431
        if ($relativeTo > 0) {
12✔
432
            $destinationPid = $relativeTo;
6✔
433
        } else {
434
            $relativeRecord = $this->getSingleRecordWithoutRestrictions(
6✔
435
                $table,
6✔
436
                (integer) abs((integer) $relativeTo),
6✔
437
                'pid'
6✔
438
            );
6✔
439
            $destinationPid = $relativeRecord['pid'] ?? $relativeTo;
6✔
440
        }
441

442
        $this->recursivelyMoveChildRecords(
12✔
443
            $table,
12✔
444
            $recordsToProcess,
12✔
445
            (integer) $destinationPid,
12✔
446
            $languageUid,
12✔
447
            $reference
12✔
448
        );
12✔
449
    }
450

451
    protected function fetchAllColumnNumbersBeneathParent(int $parentUid): array
452
    {
453
        [, $recordsToProcess, $bannedColumnNumbers] = $this->getParentAndRecordsNestedInGrid(
×
454
            'tt_content',
×
455
            $parentUid,
×
456
            'uid, colPos'
×
457
        );
×
458
        $invalidColumnPositions = $bannedColumnNumbers;
×
459
        foreach ($recordsToProcess as $childRecord) {
×
460
            $invalidColumnPositions += $this->fetchAllColumnNumbersBeneathParent($childRecord['uid']);
×
461
        }
462
        return (array) $invalidColumnPositions;
×
463
    }
464

465
    protected function recursivelyMoveChildRecords(
466
        string $table,
467
        array $recordsToProcess,
468
        int $pageUid,
469
        int $languageUid,
470
        DataHandler $dataHandler
471
    ): void {
472
        $subCommandMap = [];
12✔
473

474
        foreach ($recordsToProcess as $recordToProcess) {
12✔
475
            $recordUid = $recordToProcess['uid'];
12✔
476
            $subCommandMap[$table][$recordUid]['move'] = [
12✔
477
                'action' => 'paste',
12✔
478
                'target' => $pageUid,
12✔
479
                'update' => [
12✔
480
                    $GLOBALS['TCA']['tt_content']['ctrl']['languageField'] => $languageUid,
12✔
481
                ],
12✔
482
            ];
12✔
483
        }
484

485
        if (!empty($subCommandMap)) {
12✔
486
            /** @var DataHandler $dataHandler */
487
            $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
12✔
488
            $dataHandler->copyMappingArray = $dataHandler->copyMappingArray;
12✔
489
            $dataHandler->start([], $subCommandMap);
12✔
490
            $dataHandler->process_cmdmap();
12✔
491
        }
492
    }
493

494
    /**
495
     * @codeCoverageIgnore
496
     */
497
    protected function getSingleRecordWithRestrictions(string $table, int $uid, string $fieldsToSelect): ?array
498
    {
499
        /** @var DeletedRestriction $deletedRestriction */
500
        $deletedRestriction = GeneralUtility::makeInstance(DeletedRestriction::class);
501
        $queryBuilder = $this->createQueryBuilderForTable($table);
502
        $queryBuilder->getRestrictions()->removeAll()->add($deletedRestriction);
503
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
504
            ->from($table)
505
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)));
506
        /** @var array|false $firstResult */
507
        $firstResult = DoctrineQueryProxy::fetchAssociative(
508
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
509
        );
510
        return $firstResult ?: null;
511
    }
512

513
    /**
514
     * @codeCoverageIgnore
515
     */
516
    protected function getSingleRecordWithoutRestrictions(string $table, int $uid, string $fieldsToSelect): ?array
517
    {
518
        $queryBuilder = $this->createQueryBuilderForTable($table);
519
        $queryBuilder->getRestrictions()->removeAll();
520
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
521
            ->from($table)
522
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)));
523
        /** @var array|false $firstResult */
524
        $firstResult = DoctrineQueryProxy::fetchAssociative(
525
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
526
        );
527
        return $firstResult ?: null;
528
    }
529

530
    /**
531
     * @codeCoverageIgnore
532
     */
533
    protected function getMostRecentCopyOfRecord(int $uid, string $fieldsToSelect = 'uid'): ?array
534
    {
535
        $queryBuilder = $this->createQueryBuilderForTable('tt_content');
536
        $queryBuilder->getRestrictions()->removeAll();
537
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
538
            ->from('tt_content')
539
            ->orderBy('uid', 'DESC')
540
            ->setMaxResults(1)
541
            ->where(
542
                $queryBuilder->expr()->eq('t3_origuid', $uid),
543
                $queryBuilder->expr()->neq('t3ver_state', -1)
544
            );
545
        /** @var array|false $firstResult */
546
        $firstResult = DoctrineQueryProxy::fetchAssociative(
547
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
548
        );
549
        return $firstResult ?: null;
550
    }
551

552
    /**
553
     * @codeCoverageIgnore
554
     */
555
    protected function getTranslatedVersionOfParentInLanguageOnPage(
556
        int $languageUid,
557
        int $pageUid,
558
        int $originalParentUid,
559
        string $fieldsToSelect = '*'
560
    ): ?array {
561
        /** @var DeletedRestriction $deletedRestriction */
562
        $deletedRestriction = GeneralUtility::makeInstance(DeletedRestriction::class);
563
        $queryBuilder = $this->createQueryBuilderForTable('tt_content');
564
        $queryBuilder->getRestrictions()->removeAll()->add($deletedRestriction);
565
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
566
            ->from('tt_content')
567
            ->setMaxResults(1)
568
            ->orderBy('uid', 'DESC')
569
            ->where(
570
                $queryBuilder->expr()->eq(
571
                    'sys_language_uid',
572
                    $queryBuilder->createNamedParameter($languageUid, Connection::PARAM_INT)
573
                ),
574
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageUid, Connection::PARAM_INT)),
575
                $queryBuilder->expr()->eq(
576
                    'l10n_source',
577
                    $queryBuilder->createNamedParameter($originalParentUid, Connection::PARAM_INT)
578
                )
579
            );
580
        /** @var array|false $firstResult */
581
        $firstResult = DoctrineQueryProxy::fetchAssociative(
582
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
583
        );
584
        return $firstResult ?: null;
585
    }
586

587
    protected function getParentAndRecordsNestedInGrid(
588
        string $table,
589
        int $parentUid,
590
        string $fieldsToSelect,
591
        bool $respectPid = false,
592
        ?string $command = null
593
    ):array {
594
        // A Provider must be resolved which implements the GridProviderInterface
595
        $resolver = $this->getProviderResolver();
×
596
        $originalRecord = (array) $this->getSingleRecordWithoutRestrictions($table, $parentUid, '*');
×
597

598
        $primaryProvider = $resolver->resolvePrimaryConfigurationProvider(
×
599
            $table,
×
600
            null,
×
601
            $originalRecord,
×
602
            null,
×
603
            [GridProviderInterface::class]
×
604
        );
×
605

606
        if (!$primaryProvider) {
×
607
            return [
×
608
                $originalRecord,
×
609
                [],
×
610
                [],
×
611
            ];
×
612
        }
613

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

617
        if (empty($childColPosValues)) {
×
618
            return [
×
619
                $originalRecord,
×
620
                [],
×
621
                [],
×
622
            ];
×
623
        }
624

625
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
×
626

627
        $queryBuilder = $this->createQueryBuilderForTable($table);
×
628
        if ($command === 'undelete') {
×
629
            $queryBuilder->getRestrictions()->removeAll();
×
630
        } else {
631
            $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
×
632
        }
633

634
        $query = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
×
635
            ->from($table)
×
636
            ->andWhere(
×
637
                $queryBuilder->expr()->in(
×
638
                    'colPos',
×
639
                    $queryBuilder->createNamedParameter($childColPosValues, Connection::PARAM_INT_ARRAY)
×
640
                ),
×
641
                $queryBuilder->expr()->eq($languageField, (int)$originalRecord[$languageField]),
×
642
                $queryBuilder->expr()->in(
×
643
                    't3ver_wsid',
×
644
                    $queryBuilder->createNamedParameter(
×
645
                        [0, $GLOBALS['BE_USER']->workspace],
×
646
                        Connection::PARAM_INT_ARRAY
×
647
                    )
×
648
                )
×
649
            )->orderBy('sorting', 'DESC');
×
650

651
        if ($respectPid) {
×
652
            $query->andWhere($queryBuilder->expr()->eq('pid', $originalRecord['pid']));
×
653
        } else {
654
            $query->andWhere($queryBuilder->expr()->neq('pid', -1));
×
655
        }
656

657
        $records = DoctrineQueryProxy::fetchAllAssociative(DoctrineQueryProxy::executeQueryOnQueryBuilder($query));
×
658

659
        // Selecting records to return. The "sorting DESC" is very intentional; copy operations will place records
660
        // into the top of columns which means reading records in reverse order causes the correct final order.
661
        return [
×
662
            $originalRecord,
×
663
            $records,
×
664
            $childColPosValues
×
665
        ];
×
666
    }
667

668
    /**
669
     * @codeCoverageIgnore
670
     */
671
    protected function getProviderResolver(): ProviderResolver
672
    {
673
        /** @var ProviderResolver $providerResolver */
674
        $providerResolver = GeneralUtility::makeInstance(ProviderResolver::class);
675
        return $providerResolver;
676
    }
677

678
    /**
679
     * @codeCoverageIgnore
680
     */
681
    protected function createQueryBuilderForTable(string $table): QueryBuilder
682
    {
683
        /** @var ConnectionPool $connectionPool */
684
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
685
        return $connectionPool->getQueryBuilderForTable($table);
686
    }
687

688
    /**
689
     * @codeCoverageIgnore
690
     */
691
    protected function regenerateContentTypes(): void
692
    {
693
        /** @var ContentTypeManager $contentTypeManager */
694
        $contentTypeManager = GeneralUtility::makeInstance(ContentTypeManager::class);
695
        $contentTypeManager->regenerate();
696
    }
697
}
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