Passed
Push — master ( a7d46b...0a8eff )
by
unknown
14:11
created

overlayLanguageAndWorkspaceForJoinedSelect()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 19
rs 9.9
c 0
b 0
f 0
cc 4
nc 4
nop 4
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
19
20
use Doctrine\DBAL\Exception as DBALException;
21
use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform;
22
use Psr\Http\Message\ServerRequestInterface;
23
use TYPO3\CMS\Backend\Utility\BackendUtility;
24
use TYPO3\CMS\Core\Context\Context;
25
use TYPO3\CMS\Core\Context\WorkspaceAspect;
26
use TYPO3\CMS\Core\Database\ConnectionPool;
27
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
28
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
29
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
30
use TYPO3\CMS\Core\Http\ApplicationType;
31
use TYPO3\CMS\Core\SingletonInterface;
32
use TYPO3\CMS\Core\Utility\GeneralUtility;
33
use TYPO3\CMS\Core\Utility\MathUtility;
34
use TYPO3\CMS\Core\Versioning\VersionState;
35
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
36
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
37
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
38
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
39
use TYPO3\CMS\Extbase\Persistence\Generic\Qom;
40
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface;
41
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface;
42
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface;
43
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement;
44
use TYPO3\CMS\Extbase\Persistence\Generic\Query;
45
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException;
46
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException;
47
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
48
use TYPO3\CMS\Extbase\Service\CacheService;
49
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
50
51
/**
52
 * A Storage backend
53
 * @internal only to be used within Extbase, not part of TYPO3 Core API.
54
 */
55
class Typo3DbBackend implements BackendInterface, SingletonInterface
56
{
57
    /**
58
     * @var ConnectionPool
59
     */
60
    protected $connectionPool;
61
62
    /**
63
     * @var ConfigurationManagerInterface
64
     */
65
    protected $configurationManager;
66
67
    /**
68
     * @var CacheService
69
     */
70
    protected $cacheService;
71
72
    /**
73
     * @var ObjectManagerInterface
74
     */
75
    protected $objectManager;
76
77
    /**
78
     * As determining the table columns is a costly operation this is done only once per table during runtime and cached then
79
     *
80
     * @var array
81
     * @see clearPageCache()
82
     */
83
    protected $hasPidColumn = [];
84
85
    /**
86
     * @param ConfigurationManagerInterface $configurationManager
87
     */
88
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
89
    {
90
        $this->configurationManager = $configurationManager;
91
    }
92
93
    /**
94
     * @param CacheService $cacheService
95
     */
96
    public function injectCacheService(CacheService $cacheService): void
97
    {
98
        $this->cacheService = $cacheService;
99
    }
100
101
    /**
102
     * @param ObjectManagerInterface $objectManager
103
     */
104
    public function injectObjectManager(ObjectManagerInterface $objectManager): void
105
    {
106
        $this->objectManager = $objectManager;
107
    }
108
109
    /**
110
     * Constructor.
111
     */
112
    public function __construct()
113
    {
114
        $this->connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
115
    }
116
117
    /**
118
     * Adds a row to the storage
119
     *
120
     * @param string $tableName The database table name
121
     * @param array $fieldValues The row to be inserted
122
     * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
123
     * @return int The uid of the inserted row
124
     * @throws SqlErrorException
125
     */
126
    public function addRow(string $tableName, array $fieldValues, bool $isRelation = false): int
127
    {
128
        if (isset($fieldValues['uid'])) {
129
            unset($fieldValues['uid']);
130
        }
131
        try {
132
            $connection = $this->connectionPool->getConnectionForTable($tableName);
133
134
            $types = [];
135
            $platform = $connection->getDatabasePlatform();
136
            if ($platform instanceof SQLServerPlatform) {
137
                // mssql needs to set proper PARAM_LOB and others to update fields
138
                $tableDetails = $connection->getSchemaManager()->listTableDetails($tableName);
139
                foreach ($fieldValues as $columnName => $columnValue) {
140
                    $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
141
                }
142
            }
143
144
            $connection->insert($tableName, $fieldValues, $types);
145
        } catch (DBALException $e) {
146
            throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230766, $e);
147
        }
148
149
        $uid = 0;
150
        if (!$isRelation) {
151
            // Relation tables have no auto_increment column, so no retrieval must be tried.
152
            $uid = (int)$connection->lastInsertId($tableName);
153
            $this->clearPageCache($tableName, $uid);
154
        }
155
        return $uid;
156
    }
157
158
    /**
159
     * Updates a row in the storage
160
     *
161
     * @param string $tableName The database table name
162
     * @param array $fieldValues The row to be updated
163
     * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
164
     * @throws \InvalidArgumentException
165
     * @throws SqlErrorException
166
     */
167
    public function updateRow(string $tableName, array $fieldValues, bool $isRelation = false): void
168
    {
169
        if (!isset($fieldValues['uid'])) {
170
            throw new \InvalidArgumentException('The given row must contain a value for "uid".', 1476045164);
171
        }
172
173
        $uid = (int)$fieldValues['uid'];
174
        unset($fieldValues['uid']);
175
176
        try {
177
            $connection = $this->connectionPool->getConnectionForTable($tableName);
178
179
            $types = [];
180
            $platform = $connection->getDatabasePlatform();
181
            if ($platform instanceof SQLServerPlatform) {
182
                // mssql needs to set proper PARAM_LOB and others to update fields
183
                $tableDetails = $connection->getSchemaManager()->listTableDetails($tableName);
184
                foreach ($fieldValues as $columnName => $columnValue) {
185
                    $columnName = (string)$columnName;
186
                    $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
187
                }
188
            }
189
190
            $connection->update($tableName, $fieldValues, ['uid' => $uid], $types);
191
        } catch (DBALException $e) {
192
            throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230767, $e);
193
        }
194
195
        if (!$isRelation) {
196
            $this->clearPageCache($tableName, $uid);
197
        }
198
    }
199
200
    /**
201
     * Updates a relation row in the storage.
202
     *
203
     * @param string $tableName The database relation table name
204
     * @param array $fieldValues The row to be updated
205
     * @throws SqlErrorException
206
     * @throws \InvalidArgumentException
207
     */
208
    public function updateRelationTableRow(string $tableName, array $fieldValues): void
209
    {
210
        if (!isset($fieldValues['uid_local']) && !isset($fieldValues['uid_foreign'])) {
211
            throw new \InvalidArgumentException(
212
                'The given fieldValues must contain a value for "uid_local" and "uid_foreign".',
213
                1360500126
214
            );
215
        }
216
217
        $where = [];
218
        $where['uid_local'] = (int)$fieldValues['uid_local'];
219
        $where['uid_foreign'] = (int)$fieldValues['uid_foreign'];
220
        unset($fieldValues['uid_local']);
221
        unset($fieldValues['uid_foreign']);
222
223
        if (!empty($fieldValues['tablenames'])) {
224
            $where['tablenames'] = $fieldValues['tablenames'];
225
            unset($fieldValues['tablenames']);
226
        }
227
        if (!empty($fieldValues['fieldname'])) {
228
            $where['fieldname'] = $fieldValues['fieldname'];
229
            unset($fieldValues['fieldname']);
230
        }
231
232
        try {
233
            $this->connectionPool->getConnectionForTable($tableName)->update($tableName, $fieldValues, $where);
234
        } catch (DBALException $e) {
235
            throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230768, $e);
236
        }
237
    }
238
239
    /**
240
     * Deletes a row in the storage
241
     *
242
     * @param string $tableName The database table name
243
     * @param array $where An array of where array('fieldname' => value).
244
     * @param bool $isRelation TRUE if we are currently manipulating a relation table, FALSE by default
245
     * @throws SqlErrorException
246
     */
247
    public function removeRow(string $tableName, array $where, bool $isRelation = false): void
248
    {
249
        try {
250
            $this->connectionPool->getConnectionForTable($tableName)->delete($tableName, $where);
251
        } catch (DBALException $e) {
252
            throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230769, $e);
253
        }
254
255
        if (!$isRelation && isset($where['uid'])) {
256
            $this->clearPageCache($tableName, (int)$where['uid']);
257
        }
258
    }
259
260
    /**
261
     * Returns the object data matching the $query.
262
     *
263
     * @param QueryInterface $query
264
     * @return array
265
     * @throws SqlErrorException
266
     */
267
    public function getObjectDataByQuery(QueryInterface $query): array
268
    {
269
        $statement = $query->getStatement();
270
        // todo: remove instanceof checks as soon as getStatement() strictly returns Qom\Statement only
271
        if ($statement instanceof Statement
272
            && !$statement->getStatement() instanceof QueryBuilder
273
        ) {
274
            $rows = $this->getObjectDataByRawQuery($statement);
275
        } else {
276
            /** @var Typo3DbQueryParser $queryParser */
277
            $queryParser = $this->objectManager->get(Typo3DbQueryParser::class);
278
            if ($statement instanceof Statement
279
                && $statement->getStatement() instanceof QueryBuilder
280
            ) {
281
                $queryBuilder = $statement->getStatement();
282
            } else {
283
                $queryBuilder = $queryParser->convertQueryToDoctrineQueryBuilder($query);
284
            }
285
            $selectParts = $queryBuilder->getQueryPart('select');
286
            if ($queryParser->isDistinctQuerySuggested() && !empty($selectParts)) {
287
                $selectParts[0] = 'DISTINCT ' . $selectParts[0];
288
                $queryBuilder->selectLiteral(...$selectParts);
289
            }
290
            if ($query->getOffset()) {
291
                $queryBuilder->setFirstResult($query->getOffset());
292
            }
293
            if ($query->getLimit()) {
294
                $queryBuilder->setMaxResults($query->getLimit());
295
            }
296
            try {
297
                $rows = $queryBuilder->execute()->fetchAll();
298
            } catch (DBALException $e) {
299
                throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074485, $e);
300
            }
301
        }
302
303
        if (!empty($rows)) {
304
            $rows = $this->overlayLanguageAndWorkspace($query->getSource(), $rows, $query);
305
        }
306
307
        return $rows;
308
    }
309
310
    /**
311
     * Returns the object data using a custom statement
312
     *
313
     * @param Qom\Statement $statement
314
     * @return array
315
     * @throws SqlErrorException when the raw SQL statement fails in the database
316
     */
317
    protected function getObjectDataByRawQuery(Statement $statement): array
318
    {
319
        $realStatement = $statement->getStatement();
320
        $parameters = $statement->getBoundVariables();
321
322
        // The real statement is an instance of the Doctrine DBAL QueryBuilder, so fetching
323
        // this directly is possible
324
        if ($realStatement instanceof QueryBuilder) {
325
            try {
326
                $result = $realStatement->execute();
327
            } catch (DBALException $e) {
328
                throw new SqlErrorException($e->getPrevious()->getMessage(), 1472064721, $e);
329
            }
330
            $rows = $result->fetchAll();
331
        } elseif ($realStatement instanceof \Doctrine\DBAL\Statement) {
332
            try {
333
                $realStatement->execute($parameters);
334
            } catch (DBALException $e) {
335
                throw new SqlErrorException($e->getPrevious()->getMessage(), 1481281404, $e);
336
            }
337
            $rows = $realStatement->fetchAll();
338
        } else {
339
            // Do a real raw query. This is very stupid, as it does not allow to use DBAL's real power if
340
            // several tables are on different databases, so this is used with caution and could be removed
341
            // in the future
342
            try {
343
                $connection = $this->connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
344
                $statement = $connection->executeQuery($realStatement, $parameters);
345
            } catch (DBALException $e) {
346
                throw new SqlErrorException($e->getPrevious()->getMessage(), 1472064775, $e);
347
            }
348
349
            $rows = $statement->fetchAll();
350
        }
351
352
        return $rows;
353
    }
354
355
    /**
356
     * Returns the number of tuples matching the query.
357
     *
358
     * @param QueryInterface $query
359
     * @return int The number of matching tuples
360
     * @throws BadConstraintException
361
     * @throws SqlErrorException
362
     */
363
    public function getObjectCountByQuery(QueryInterface $query): int
364
    {
365
        if ($query->getConstraint() instanceof Statement) {
366
            throw new BadConstraintException('Could not execute count on queries with a constraint of type TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Statement', 1256661045);
367
        }
368
369
        $statement = $query->getStatement();
370
        if ($statement instanceof Statement
371
            && !$statement->getStatement() instanceof QueryBuilder
372
        ) {
373
            $rows = $this->getObjectDataByQuery($query);
374
            $count = count($rows);
375
        } else {
376
            /** @var Typo3DbQueryParser $queryParser */
377
            $queryParser  = $this->objectManager->get(Typo3DbQueryParser::class);
378
            $queryBuilder = $queryParser
379
                ->convertQueryToDoctrineQueryBuilder($query)
380
                ->resetQueryPart('orderBy');
381
382
            if ($queryParser->isDistinctQuerySuggested()) {
383
                $source = $queryBuilder->getQueryPart('from')[0];
384
                // Tablename is already quoted for the DBMS, we need to treat table and field names separately
385
                $tableName = $source['alias'] ?: $source['table'];
386
                $fieldName = $queryBuilder->quoteIdentifier('uid');
387
                $queryBuilder->resetQueryPart('groupBy')
388
                    ->selectLiteral(sprintf('COUNT(DISTINCT %s.%s)', $tableName, $fieldName));
389
            } else {
390
                $queryBuilder->count('*');
391
            }
392
393
            try {
394
                $count = $queryBuilder->execute()->fetchColumn(0);
395
            } catch (DBALException $e) {
396
                throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074379, $e);
397
            }
398
            if ($query->getOffset()) {
399
                $count -= $query->getOffset();
400
            }
401
            if ($query->getLimit()) {
402
                $count = min($count, $query->getLimit());
403
            }
404
        }
405
        return (int)max(0, $count);
406
    }
407
408
    /**
409
     * Checks if a Value Object equal to the given Object exists in the database
410
     *
411
     * @param AbstractValueObject $object The Value Object
412
     * @return int|null The matching uid if an object was found, else FALSE
413
     * @throws SqlErrorException
414
     */
415
    public function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object): ?int
416
    {
417
        /** @var DataMapper $dataMapper */
418
        $dataMapper = $this->objectManager->get(DataMapper::class);
419
        $dataMap = $dataMapper->getDataMap(get_class($object));
420
        $tableName = $dataMap->getTableName();
421
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
422
        if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
423
            && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
424
        ) {
425
            $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
426
        }
427
        $whereClause = [];
428
        // loop over all properties of the object to exactly set the values of each database field
429
        $properties = $object->_getProperties();
430
        foreach ($properties as $propertyName => $propertyValue) {
431
            $propertyName = (string)$propertyName;
432
433
            // @todo We couple the Backend to the Entity implementation (uid, isClone); changes there breaks this method
434
            if ($dataMap->isPersistableProperty($propertyName) && $propertyName !== 'uid' && $propertyName !== 'pid' && $propertyName !== 'isClone') {
435
                $fieldName = $dataMap->getColumnMap($propertyName)->getColumnName();
436
                if ($propertyValue === null) {
437
                    $whereClause[] = $queryBuilder->expr()->isNull($fieldName);
438
                } else {
439
                    $whereClause[] = $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($dataMapper->getPlainValue($propertyValue)));
440
                }
441
            }
442
        }
443
        $queryBuilder
444
            ->select('uid')
445
            ->from($tableName)
446
            ->where(...$whereClause);
447
448
        try {
449
            $uid = (int)$queryBuilder
450
                ->execute()
451
                ->fetchColumn(0);
452
            if ($uid > 0) {
453
                return $uid;
454
            }
455
            return null;
456
        } catch (DBALException $e) {
457
            throw new SqlErrorException($e->getPrevious()->getMessage(), 1470231748, $e);
458
        }
459
    }
460
461
    /**
462
     * Performs workspace and language overlay on the given row array. The language and workspace id is automatically
463
     * detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
464
     *
465
     * @param Qom\SourceInterface $source The source (selector or join)
466
     * @param array $rows
467
     * @param QueryInterface $query
468
     * @param int|null $workspaceUid
469
     * @return array
470
     * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
471
     */
472
    protected function overlayLanguageAndWorkspace(SourceInterface $source, array $rows, QueryInterface $query, int $workspaceUid = null): array
473
    {
474
        $context = GeneralUtility::makeInstance(Context::class);
475
        if ($workspaceUid === null) {
476
            $workspaceUid = (int)$context->getPropertyFromAspect('workspace', 'id');
477
        } else {
478
            // A custom query is needed, so a custom context is cloned
479
            $context = clone $context;
480
            $context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $workspaceUid));
481
        }
482
483
        $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
484
        if ($source instanceof SelectorInterface) {
485
            $tableName = $source->getSelectorName();
486
            $rows = $this->resolveMovedRecordsInWorkspace($tableName, $rows, $workspaceUid);
487
            return $this->overlayLanguageAndWorkspaceForSelect($tableName, $rows, $pageRepository, $query);
488
        }
489
        if ($source instanceof JoinInterface) {
490
            $tableName = $source->getRight()->getSelectorName();
491
            // Special handling of joined select is only needed when doing workspace overlays, which does not happen
492
            // in live workspace
493
            if ($workspaceUid === 0) {
494
                return $this->overlayLanguageAndWorkspaceForSelect($tableName, $rows, $pageRepository, $query);
495
            }
496
            return $this->overlayLanguageAndWorkspaceForJoinedSelect($tableName, $rows, $pageRepository, $query);
497
        }
498
        // No proper source, so we do not have a table name here
499
        // we cannot do an overlay and return the original rows instead.
500
        return $rows;
501
    }
502
503
    /**
504
     * If the result is a plain SELECT (no JOIN) then the regular overlay process works for tables
505
     *  - overlay workspace
506
     *  - overlay language of versioned record again
507
     */
508
    protected function overlayLanguageAndWorkspaceForSelect(string $tableName, array $rows, PageRepository $pageRepository, QueryInterface $query): array
509
    {
510
        $overlaidRows = [];
511
        foreach ($rows as $row) {
512
            $row = $this->overlayLanguageAndWorkspaceForSingleRecord($tableName, $row, $pageRepository, $query);
513
            if (is_array($row)) {
514
                $overlaidRows[] = $row;
515
            }
516
        }
517
        return $overlaidRows;
518
    }
519
520
    /**
521
     * If the result consists of a JOIN (usually happens if a property is a relation with a MM table) then it is necessary
522
     * to only do overlays for the fields that are contained in the main database table, otherwise a SQL error is thrown.
523
     * In order to make this happen, a single SQL query is made to fetch all possible field names (= array keys) of
524
     * a record (TCA[$tableName][columns] does not contain all needed information), which is then used to compute
525
     * a separate subset of the row which can be overlaid properly.
526
     */
527
    protected function overlayLanguageAndWorkspaceForJoinedSelect(string $tableName, array $rows, PageRepository $pageRepository, QueryInterface $query): array
528
    {
529
        // No valid rows, so this is skipped
530
        if (!isset($rows[0]['uid'])) {
531
            return $rows;
532
        }
533
        // First, find out the fields that belong to the "main" selected table which is defined by TCA, and take the first
534
        // record to find out all possible fields in this database table
535
        $fieldsOfMainTable = $pageRepository->getRawRecord($tableName, $rows[0]['uid']);
536
        $overlaidRows = [];
537
        foreach ($rows as $row) {
538
            $mainRow = array_intersect_key($row, $fieldsOfMainTable);
0 ignored issues
show
Bug introduced by
It seems like $fieldsOfMainTable can also be of type integer; however, parameter $array2 of array_intersect_key() does only seem to accept array, 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

538
            $mainRow = array_intersect_key($row, /** @scrutinizer ignore-type */ $fieldsOfMainTable);
Loading history...
539
            $joinRow = array_diff_key($row, $mainRow);
540
            $mainRow = $this->overlayLanguageAndWorkspaceForSingleRecord($tableName, $mainRow, $pageRepository, $query);
541
            if (is_array($mainRow)) {
542
                $overlaidRows[] = array_replace($joinRow, $mainRow);
543
            }
544
        }
545
        return $overlaidRows;
546
    }
547
548
    /**
549
     * Takes one specific row, as defined in TCA and does all overlays.
550
     *
551
     * @param string $tableName
552
     * @param array $row
553
     * @param PageRepository $pageRepository
554
     * @param QueryInterface $query
555
     * @return array|int|mixed|null the overlaid row or false or null if overlay failed.
556
     */
557
    protected function overlayLanguageAndWorkspaceForSingleRecord(string $tableName, array $row, PageRepository $pageRepository, QueryInterface $query)
558
    {
559
        $querySettings = $query->getQuerySettings();
560
        // If current row is a translation select its parent
561
        $languageOfCurrentRecord = 0;
562
        if ($GLOBALS['TCA'][$tableName]['ctrl']['languageField'] ?? null
563
            && $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] ?? 0
564
        ) {
565
            $languageOfCurrentRecord = $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']];
566
        }
567
        if ($querySettings->getLanguageOverlayMode()
568
            && $languageOfCurrentRecord > 0
569
            && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
570
            && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
571
        ) {
572
            $row = $pageRepository->getRawRecord(
573
                $tableName,
574
                (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']]
575
            );
576
        }
577
        // Handle workspace overlays
578
        $pageRepository->versionOL($tableName, $row, true);
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type integer; however, parameter $row of TYPO3\CMS\Core\Domain\Re...Repository::versionOL() does only seem to accept array, 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

578
        $pageRepository->versionOL($tableName, /** @scrutinizer ignore-type */ $row, true);
Loading history...
579
        if (is_array($row) && $querySettings->getLanguageOverlayMode()) {
580
            if ($tableName === 'pages') {
581
                $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
582
            } else {
583
                // todo: remove type cast once getLanguageUid strictly returns an int
584
                $languageUid = (int)$querySettings->getLanguageUid();
585
                if (!$querySettings->getRespectSysLanguage()
586
                    && $languageOfCurrentRecord > 0
587
                    && (!$query instanceof Query || !$query->getParentQuery())
588
                ) {
589
                    // No parent query means we're processing the aggregate root.
590
                    // respectSysLanguage is false which means that records returned by the query
591
                    // might be from different languages (which is desired).
592
                    // So we must set the language used for overlay to the language of the current record
593
                    $languageUid = $languageOfCurrentRecord;
594
                }
595
                if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
596
                    && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
597
                    && $languageOfCurrentRecord > 0
598
                ) {
599
                    // Force overlay by faking default language record, as getRecordOverlay can only handle default language records
600
                    $row['uid'] = $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']];
601
                    $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = 0;
602
                }
603
                $row = $pageRepository->getRecordOverlay($tableName, $row, $languageUid, (string)$querySettings->getLanguageOverlayMode());
604
            }
605
        }
606
        return $row;
607
    }
608
609
    /**
610
     * Fetches the moved record in case it is supported
611
     * by the table and if there's only one row in the result set
612
     * (applying this to all rows does not work, since the sorting
613
     * order would be destroyed and possible limits are not met anymore)
614
     * The move pointers are later unset (see versionOL() last argument)
615
     */
616
    protected function resolveMovedRecordsInWorkspace(string $tableName, array $rows, int $workspaceUid): array
617
    {
618
        if ($workspaceUid === 0) {
619
            return $rows;
620
        }
621
        if (!BackendUtility::isTableWorkspaceEnabled($tableName)) {
622
            return $rows;
623
        }
624
        if (count($rows) !== 1) {
625
            return $rows;
626
        }
627
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
628
        $queryBuilder->getRestrictions()->removeAll();
629
        $movedRecords = $queryBuilder
630
            ->select('*')
631
            ->from($tableName)
632
            ->where(
633
                $queryBuilder->expr()->eq('t3ver_state', $queryBuilder->createNamedParameter(VersionState::MOVE_POINTER, \PDO::PARAM_INT)),
634
                $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter($workspaceUid, \PDO::PARAM_INT)),
635
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($rows[0]['uid'], \PDO::PARAM_INT))
636
            )
637
            ->setMaxResults(1)
638
            ->execute()
639
            ->fetchAll();
640
        if (!empty($movedRecords)) {
641
            $rows = $movedRecords;
642
        }
643
        return $rows;
644
    }
645
646
    /**
647
     * Clear the TYPO3 page cache for the given record.
648
     * If the record lies on a page, then we clear the cache of this page.
649
     * If the record has no PID column, we clear the cache of the current page as best-effort.
650
     *
651
     * Much of this functionality is taken from DataHandler::clear_cache() which unfortunately only works with logged-in BE user.
652
     *
653
     * @param string $tableName Table name of the record
654
     * @param int $uid UID of the record
655
     */
656
    protected function clearPageCache(string $tableName, int $uid): void
657
    {
658
        $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
659
        if (empty($frameworkConfiguration['persistence']['enableAutomaticCacheClearing'])) {
660
            return;
661
        }
662
        $pageIdsToClear = [];
663
        $storagePage = null;
664
665
        // As determining the table columns is a costly operation this is done only once per table during runtime and cached then
666
        if (!isset($this->hasPidColumn[$tableName])) {
667
            $columns = $this->connectionPool
668
                ->getConnectionForTable($tableName)
669
                ->getSchemaManager()
670
                ->listTableColumns($tableName);
671
            $this->hasPidColumn[$tableName] = array_key_exists('pid', $columns);
672
        }
673
674
        $tsfe = $this->getTSFE();
675
        if ($this->hasPidColumn[$tableName]) {
676
            $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
677
            $queryBuilder->getRestrictions()->removeAll();
678
            $result = $queryBuilder
679
                ->select('pid')
680
                ->from($tableName)
681
                ->where(
682
                    $queryBuilder->expr()->eq(
683
                        'uid',
684
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
685
                    )
686
                )
687
                ->execute();
688
            if ($row = $result->fetch()) {
689
                $storagePage = $row['pid'];
690
                $pageIdsToClear[] = $storagePage;
691
            }
692
        } elseif (isset($tsfe)) {
693
            // No PID column - we can do a best-effort to clear the cache of the current page if in FE
694
            $storagePage = $tsfe->id;
695
            $pageIdsToClear[] = $storagePage;
696
        }
697
        if ($storagePage === null) {
698
            return;
699
        }
700
701
        $pageTS = BackendUtility::getPagesTSconfig($storagePage);
702
        if (isset($pageTS['TCEMAIN.']['clearCacheCmd'])) {
703
            $clearCacheCommands = GeneralUtility::trimExplode(',', strtolower($pageTS['TCEMAIN.']['clearCacheCmd']), true);
704
            $clearCacheCommands = array_unique($clearCacheCommands);
705
            foreach ($clearCacheCommands as $clearCacheCommand) {
706
                if (MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) {
707
                    $pageIdsToClear[] = $clearCacheCommand;
708
                }
709
            }
710
        }
711
712
        foreach ($pageIdsToClear as $pageIdToClear) {
713
            $this->cacheService->getPageIdStack()->push($pageIdToClear);
714
        }
715
    }
716
717
    /**
718
     * @return TypoScriptFrontendController|null
719
     */
720
    protected function getTSFE(): ?TypoScriptFrontendController
721
    {
722
        return $GLOBALS['TSFE'] ?? null;
723
    }
724
}
725