Passed
Push — master ( ca75ff...2f940f )
by
unknown
18:41
created

Typo3DbBackend::injectCacheService()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

492
            $mainRow = array_intersect_key($row, /** @scrutinizer ignore-type */ $fieldsOfMainTable);
Loading history...
493
            $joinRow = array_diff_key($row, $mainRow);
494
            $mainRow = $this->overlayLanguageAndWorkspaceForSingleRecord($tableName, $mainRow, $pageRepository, $query);
495
            if (is_array($mainRow)) {
496
                $overlaidRows[] = array_replace($joinRow, $mainRow);
497
            }
498
        }
499
        return $overlaidRows;
500
    }
501
502
    /**
503
     * Takes one specific row, as defined in TCA and does all overlays.
504
     *
505
     * @param string $tableName
506
     * @param array $row
507
     * @param PageRepository $pageRepository
508
     * @param QueryInterface $query
509
     * @return array|int|mixed|null the overlaid row or false or null if overlay failed.
510
     */
511
    protected function overlayLanguageAndWorkspaceForSingleRecord(string $tableName, array $row, PageRepository $pageRepository, QueryInterface $query)
512
    {
513
        $querySettings = $query->getQuerySettings();
514
        // If current row is a translation select its parent
515
        $languageOfCurrentRecord = 0;
516
        if ($GLOBALS['TCA'][$tableName]['ctrl']['languageField'] ?? null
517
            && $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] ?? 0
518
        ) {
519
            $languageOfCurrentRecord = $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']];
520
        }
521
        if ($querySettings->getLanguageOverlayMode()
522
            && $languageOfCurrentRecord > 0
523
            && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
524
            && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
525
        ) {
526
            $row = $pageRepository->getRawRecord(
527
                $tableName,
528
                (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']]
529
            );
530
        }
531
        // Handle workspace overlays
532
        $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

532
        $pageRepository->versionOL($tableName, /** @scrutinizer ignore-type */ $row, true);
Loading history...
533
        if (is_array($row) && $querySettings->getLanguageOverlayMode()) {
534
            if ($tableName === 'pages') {
535
                $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
536
            } else {
537
                // todo: remove type cast once getLanguageUid strictly returns an int
538
                $languageUid = (int)$querySettings->getLanguageUid();
539
                if (!$querySettings->getRespectSysLanguage()
540
                    && $languageOfCurrentRecord > 0
541
                    && (!$query instanceof Query || !$query->getParentQuery())
542
                ) {
543
                    // No parent query means we're processing the aggregate root.
544
                    // respectSysLanguage is false which means that records returned by the query
545
                    // might be from different languages (which is desired).
546
                    // So we must set the language used for overlay to the language of the current record
547
                    $languageUid = $languageOfCurrentRecord;
548
                }
549
                if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
550
                    && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
551
                    && $languageOfCurrentRecord > 0
552
                ) {
553
                    // Force overlay by faking default language record, as getRecordOverlay can only handle default language records
554
                    $row['uid'] = $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']];
555
                    $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = 0;
556
                }
557
                $row = $pageRepository->getRecordOverlay($tableName, $row, $languageUid, (string)$querySettings->getLanguageOverlayMode());
558
            }
559
        }
560
        return $row;
561
    }
562
563
    /**
564
     * Fetches the moved record in case it is supported
565
     * by the table and if there's only one row in the result set
566
     * (applying this to all rows does not work, since the sorting
567
     * order would be destroyed and possible limits are not met anymore)
568
     * The move pointers are later unset (see versionOL() last argument)
569
     */
570
    protected function resolveMovedRecordsInWorkspace(string $tableName, array $rows, int $workspaceUid): array
571
    {
572
        if ($workspaceUid === 0) {
573
            return $rows;
574
        }
575
        if (!BackendUtility::isTableWorkspaceEnabled($tableName)) {
576
            return $rows;
577
        }
578
        if (count($rows) !== 1) {
579
            return $rows;
580
        }
581
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
582
        $queryBuilder->getRestrictions()->removeAll();
583
        $movedRecords = $queryBuilder
584
            ->select('*')
585
            ->from($tableName)
586
            ->where(
587
                $queryBuilder->expr()->eq('t3ver_state', $queryBuilder->createNamedParameter(VersionState::MOVE_POINTER, \PDO::PARAM_INT)),
588
                $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter($workspaceUid, \PDO::PARAM_INT)),
589
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($rows[0]['uid'], \PDO::PARAM_INT))
590
            )
591
            ->setMaxResults(1)
592
            ->execute()
593
            ->fetchAll();
594
        if (!empty($movedRecords)) {
595
            $rows = $movedRecords;
596
        }
597
        return $rows;
598
    }
599
600
    /**
601
     * Clear the TYPO3 page cache for the given record.
602
     * If the record lies on a page, then we clear the cache of this page.
603
     * If the record has no PID column, we clear the cache of the current page as best-effort.
604
     *
605
     * Much of this functionality is taken from DataHandler::clear_cache() which unfortunately only works with logged-in BE user.
606
     *
607
     * @param string $tableName Table name of the record
608
     * @param int $uid UID of the record
609
     */
610
    protected function clearPageCache(string $tableName, int $uid): void
611
    {
612
        $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
613
        if (empty($frameworkConfiguration['persistence']['enableAutomaticCacheClearing'])) {
614
            return;
615
        }
616
        $pageIdsToClear = [];
617
        $storagePage = null;
618
619
        // As determining the table columns is a costly operation this is done only once per table during runtime and cached then
620
        if (!isset($this->hasPidColumn[$tableName])) {
621
            $columns = $this->connectionPool
622
                ->getConnectionForTable($tableName)
623
                ->getSchemaManager()
624
                ->listTableColumns($tableName);
625
            $this->hasPidColumn[$tableName] = array_key_exists('pid', $columns);
626
        }
627
628
        $tsfe = $this->getTSFE();
629
        if ($this->hasPidColumn[$tableName]) {
630
            $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
631
            $queryBuilder->getRestrictions()->removeAll();
632
            $result = $queryBuilder
633
                ->select('pid')
634
                ->from($tableName)
635
                ->where(
636
                    $queryBuilder->expr()->eq(
637
                        'uid',
638
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
639
                    )
640
                )
641
                ->execute();
642
            if ($row = $result->fetch()) {
643
                $storagePage = $row['pid'];
644
                $pageIdsToClear[] = $storagePage;
645
            }
646
        } elseif (isset($tsfe)) {
647
            // No PID column - we can do a best-effort to clear the cache of the current page if in FE
648
            $storagePage = $tsfe->id;
649
            $pageIdsToClear[] = $storagePage;
650
        }
651
        if ($storagePage === null) {
652
            return;
653
        }
654
655
        $pageTS = BackendUtility::getPagesTSconfig($storagePage);
656
        if (isset($pageTS['TCEMAIN.']['clearCacheCmd'])) {
657
            $clearCacheCommands = GeneralUtility::trimExplode(',', strtolower($pageTS['TCEMAIN.']['clearCacheCmd']), true);
658
            $clearCacheCommands = array_unique($clearCacheCommands);
659
            foreach ($clearCacheCommands as $clearCacheCommand) {
660
                if (MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) {
661
                    $pageIdsToClear[] = $clearCacheCommand;
662
                }
663
            }
664
        }
665
666
        foreach ($pageIdsToClear as $pageIdToClear) {
667
            $this->cacheService->getPageIdStack()->push($pageIdToClear);
668
        }
669
    }
670
671
    /**
672
     * @return TypoScriptFrontendController|null
673
     */
674
    protected function getTSFE(): ?TypoScriptFrontendController
675
    {
676
        return $GLOBALS['TSFE'] ?? null;
677
    }
678
}
679