[BUGFIX] Remove duplicate PageTS cache in Typo3DbBackend
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Generic / Storage / Typo3DbBackend.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Doctrine\DBAL\DBALException;
18 use Doctrine\DBAL\Platforms\SQLServerPlatform;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
22 use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
23 use TYPO3\CMS\Core\SingletonInterface;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Core\Utility\MathUtility;
26 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
27 use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
28 use TYPO3\CMS\Extbase\Persistence\Generic\Qom;
29 use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException;
30 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
31
32 /**
33 * A Storage backend
34 */
35 class Typo3DbBackend implements BackendInterface, SingletonInterface
36 {
37 /**
38 * @var ConnectionPool
39 */
40 protected $connectionPool;
41
42 /**
43 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
44 */
45 protected $dataMapper;
46
47 /**
48 * The TYPO3 page repository. Used for language and workspace overlay
49 *
50 * @var \TYPO3\CMS\Frontend\Page\PageRepository
51 */
52 protected $pageRepository;
53
54 /**
55 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
56 */
57 protected $configurationManager;
58
59 /**
60 * @var \TYPO3\CMS\Extbase\Service\CacheService
61 */
62 protected $cacheService;
63
64 /**
65 * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
66 */
67 protected $environmentService;
68
69 /**
70 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
71 */
72 protected $objectManager;
73
74 /**
75 * As determining the table columns is a costly operation this is done only once per table during runtime and cached then
76 *
77 * @var array
78 * @see clearPageCache()
79 */
80 protected $hasPidColumn = [];
81
82 /**
83 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper
84 */
85 public function injectDataMapper(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper)
86 {
87 $this->dataMapper = $dataMapper;
88 }
89
90 /**
91 * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
92 */
93 public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
94 {
95 $this->configurationManager = $configurationManager;
96 }
97
98 /**
99 * @param \TYPO3\CMS\Extbase\Service\CacheService $cacheService
100 */
101 public function injectCacheService(\TYPO3\CMS\Extbase\Service\CacheService $cacheService)
102 {
103 $this->cacheService = $cacheService;
104 }
105
106 /**
107 * @param \TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService
108 */
109 public function injectEnvironmentService(\TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService)
110 {
111 $this->environmentService = $environmentService;
112 }
113
114 /**
115 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
116 */
117 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
118 {
119 $this->objectManager = $objectManager;
120 }
121
122 /**
123 * Constructor. takes the database handle from $GLOBALS['TYPO3_DB']
124 */
125 public function __construct()
126 {
127 $this->connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
128 }
129
130 /**
131 * Adds a row to the storage
132 *
133 * @param string $tableName The database table name
134 * @param array $fieldValues The row to be inserted
135 * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
136 * @return int The uid of the inserted row
137 * @throws SqlErrorException
138 */
139 public function addRow($tableName, array $fieldValues, $isRelation = false)
140 {
141 if (isset($fieldValues['uid'])) {
142 unset($fieldValues['uid']);
143 }
144 try {
145 $connection = $this->connectionPool->getConnectionForTable($tableName);
146
147 $types = [];
148 $platform = $connection->getDatabasePlatform();
149 if ($platform instanceof SQLServerPlatform) {
150 // mssql needs to set proper PARAM_LOB and others to update fields
151 $tableDetails = $connection->getSchemaManager()->listTableDetails($tableName);
152 foreach ($fieldValues as $columnName => $columnValue) {
153 $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
154 }
155 }
156
157 $connection->insert($tableName, $fieldValues, $types);
158 } catch (DBALException $e) {
159 throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230766);
160 }
161
162 $uid = 0;
163 if (!$isRelation) {
164 // Relation tables have no auto_increment column, so no retrieval must be tried.
165 $uid = $connection->lastInsertId($tableName);
166 $this->clearPageCache($tableName, $uid);
167 }
168 return (int)$uid;
169 }
170
171 /**
172 * Updates a row in the storage
173 *
174 * @param string $tableName The database table name
175 * @param array $fieldValues The row to be updated
176 * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
177 * @throws \InvalidArgumentException
178 * @throws SqlErrorException
179 * @return bool
180 */
181 public function updateRow($tableName, array $fieldValues, $isRelation = false)
182 {
183 if (!isset($fieldValues['uid'])) {
184 throw new \InvalidArgumentException('The given row must contain a value for "uid".', 1476045164);
185 }
186
187 $uid = (int)$fieldValues['uid'];
188 unset($fieldValues['uid']);
189
190 try {
191 $connection = $this->connectionPool->getConnectionForTable($tableName);
192
193 $types = [];
194 $platform = $connection->getDatabasePlatform();
195 if ($platform instanceof SQLServerPlatform) {
196 // mssql needs to set proper PARAM_LOB and others to update fields
197 $tableDetails = $connection->getSchemaManager()->listTableDetails($tableName);
198 foreach ($fieldValues as $columnName => $columnValue) {
199 $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
200 }
201 }
202
203 $connection->update($tableName, $fieldValues, ['uid' => $uid], $types);
204 } catch (DBALException $e) {
205 throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230767);
206 }
207
208 if (!$isRelation) {
209 $this->clearPageCache($tableName, $uid);
210 }
211
212 // always returns true
213 return true;
214 }
215
216 /**
217 * Updates a relation row in the storage.
218 *
219 * @param string $tableName The database relation table name
220 * @param array $fieldValues The row to be updated
221 * @throws \InvalidArgumentException
222 * @return bool
223 * @throws SqlErrorException
224 */
225 public function updateRelationTableRow($tableName, array $fieldValues)
226 {
227 if (!isset($fieldValues['uid_local']) && !isset($fieldValues['uid_foreign'])) {
228 throw new \InvalidArgumentException(
229 'The given fieldValues must contain a value for "uid_local" and "uid_foreign".',
230 1360500126
231 );
232 }
233
234 $where['uid_local'] = (int)$fieldValues['uid_local'];
235 $where['uid_foreign'] = (int)$fieldValues['uid_foreign'];
236 unset($fieldValues['uid_local']);
237 unset($fieldValues['uid_foreign']);
238
239 if (!empty($fieldValues['tablenames'])) {
240 $where['tablenames'] = $fieldValues['tablenames'];
241 unset($fieldValues['tablenames']);
242 }
243 if (!empty($fieldValues['fieldname'])) {
244 $where['fieldname'] = $fieldValues['fieldname'];
245 unset($fieldValues['fieldname']);
246 }
247
248 try {
249 $this->connectionPool->getConnectionForTable($tableName)
250 ->update($tableName, $fieldValues, $where);
251 } catch (DBALException $e) {
252 throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230768);
253 }
254
255 // always returns true
256 return true;
257 }
258
259 /**
260 * Deletes a row in the storage
261 *
262 * @param string $tableName The database table name
263 * @param array $where An array of where array('fieldname' => value).
264 * @param bool $isRelation TRUE if we are currently manipulating a relation table, FALSE by default
265 * @return bool
266 * @throws SqlErrorException
267 */
268 public function removeRow($tableName, array $where, $isRelation = false)
269 {
270 try {
271 $this->connectionPool->getConnectionForTable($tableName)->delete($tableName, $where);
272 } catch (DBALException $e) {
273 throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230769);
274 }
275
276 if (!$isRelation && isset($where['uid'])) {
277 $this->clearPageCache($tableName, $where['uid']);
278 }
279
280 // always returns true
281 return true;
282 }
283
284 /**
285 * Fetches maximal value for given table column from database.
286 *
287 * @param string $tableName The database table name
288 * @param array $where An array of where array('fieldname' => value).
289 * @param string $columnName column name to get the max value from
290 * @return mixed the max value
291 * @throws SqlErrorException
292 */
293 public function getMaxValueFromTable($tableName, array $where, $columnName)
294 {
295 try {
296 $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
297 $queryBuilder->getRestrictions()->removeAll();
298 $queryBuilder
299 ->select($columnName)
300 ->from($tableName)
301 ->orderBy($columnName, 'DESC')
302 ->setMaxResults(1);
303
304 foreach ($where as $fieldName => $value) {
305 $queryBuilder->andWhere(
306 $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
307 );
308 }
309
310 $result = $queryBuilder->execute()->fetchColumn(0);
311 } catch (DBALException $e) {
312 throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230770);
313 }
314 return $result;
315 }
316
317 /**
318 * Fetches row data from the database
319 *
320 * @param string $tableName
321 * @param array $where An array of where array('fieldname' => value).
322 * @return array|bool
323 * @throws SqlErrorException
324 */
325 public function getRowByIdentifier($tableName, array $where)
326 {
327 try {
328 $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
329 $queryBuilder->getRestrictions()->removeAll();
330 $queryBuilder
331 ->select('*')
332 ->from($tableName);
333
334 foreach ($where as $fieldName => $value) {
335 $queryBuilder->andWhere(
336 $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
337 );
338 }
339
340 $row = $queryBuilder->execute()->fetch();
341 } catch (DBALException $e) {
342 throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230771);
343 }
344 return $row ?: false;
345 }
346
347 /**
348 * Returns the object data matching the $query.
349 *
350 * @param QueryInterface $query
351 * @return array
352 * @throws SqlErrorException
353 */
354 public function getObjectDataByQuery(QueryInterface $query)
355 {
356 $statement = $query->getStatement();
357 if ($statement instanceof Qom\Statement
358 && !$statement->getStatement() instanceof QueryBuilder
359 ) {
360 $rows = $this->getObjectDataByRawQuery($statement);
361 } else {
362 $queryParser = $this->objectManager->get(Typo3DbQueryParser::class);
363 if ($statement instanceof Qom\Statement
364 && $statement->getStatement() instanceof QueryBuilder
365 ) {
366 $queryBuilder = $statement->getStatement();
367 } else {
368 $queryBuilder = $queryParser->convertQueryToDoctrineQueryBuilder($query);
369 }
370 $selectParts = $queryBuilder->getQueryPart('select');
371 if ($queryParser->isDistinctQuerySuggested() && !empty($selectParts)) {
372 $selectParts[0] = 'DISTINCT ' . $selectParts[0];
373 $queryBuilder->selectLiteral(...$selectParts);
374 }
375 if ($query->getOffset()) {
376 $queryBuilder->setFirstResult($query->getOffset());
377 }
378 if ($query->getLimit()) {
379 $queryBuilder->setMaxResults($query->getLimit());
380 }
381 try {
382 $rows = $queryBuilder->execute()->fetchAll();
383 } catch (DBALException $e) {
384 throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074485);
385 }
386 }
387
388 $rows = $this->doLanguageAndWorkspaceOverlay($query->getSource(), $rows, $query->getQuerySettings());
389 return $rows;
390 }
391
392 /**
393 * Returns the object data using a custom statement
394 *
395 * @param Qom\Statement $statement
396 * @return array
397 * @throws SqlErrorException when the raw SQL statement fails in the database
398 */
399 protected function getObjectDataByRawQuery(Qom\Statement $statement)
400 {
401 $realStatement = $statement->getStatement();
402 $parameters = $statement->getBoundVariables();
403
404 // The real statement is an instance of the Doctrine DBAL QueryBuilder, so fetching
405 // this directly is possible
406 if ($realStatement instanceof QueryBuilder) {
407 try {
408 $result = $realStatement->execute();
409 } catch (DBALException $e) {
410 throw new SqlErrorException($e->getPrevious()->getMessage(), 1472064721);
411 }
412 $rows = $result->fetchAll();
413 } elseif ($realStatement instanceof \Doctrine\DBAL\Statement) {
414 try {
415 $realStatement->execute($parameters);
416 } catch (DBALException $e) {
417 throw new SqlErrorException($e->getPrevious()->getMessage(), 1481281404);
418 }
419 $rows = $realStatement->fetchAll();
420 } elseif ($realStatement instanceof \TYPO3\CMS\Core\Database\PreparedStatement) {
421 GeneralUtility::deprecationLog('Extbase support for Prepared Statements has been deprecated in TYPO3 v8, and will be removed in TYPO3 v9. Use native Doctrine DBAL Statements or QueryBuilder objects.');
422 $realStatement->execute($parameters);
423 $rows = $realStatement->fetchAll();
424
425 $realStatement->free();
426 } else {
427 // Do a real raw query. This is very stupid, as it does not allow to use DBAL's real power if
428 // several tables are on different databases, so this is used with caution and could be removed
429 // in the future
430 try {
431 $connection = $this->connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
432 $statement = $connection->executeQuery($realStatement, $parameters);
433 } catch (DBALException $e) {
434 throw new SqlErrorException($e->getPrevious()->getMessage(), 1472064775);
435 }
436
437 $rows = $statement->fetchAll();
438 }
439
440 return $rows;
441 }
442
443 /**
444 * Returns the number of tuples matching the query.
445 *
446 * @param QueryInterface $query
447 * @throws Exception\BadConstraintException
448 * @return int The number of matching tuples
449 * @throws SqlErrorException
450 */
451 public function getObjectCountByQuery(QueryInterface $query)
452 {
453 if ($query->getConstraint() instanceof Qom\Statement) {
454 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException('Could not execute count on queries with a constraint of type TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Statement', 1256661045);
455 }
456
457 $queryParser = $this->objectManager->get(Typo3DbQueryParser::class);
458 $queryBuilder = $queryParser
459 ->convertQueryToDoctrineQueryBuilder($query)
460 ->resetQueryPart('orderBy');
461
462 if ($queryParser->isDistinctQuerySuggested()) {
463 $source = $queryBuilder->getQueryPart('from')[0];
464 // Tablename is already quoted for the DBMS, we need to treat table and field names separately
465 $tableName = $source['alias'] ?: $source['table'];
466 $fieldName = $queryBuilder->quoteIdentifier('uid');
467 $queryBuilder->resetQueryPart('groupBy')
468 ->selectLiteral(sprintf('COUNT(DISTINCT %s.%s)', $tableName, $fieldName));
469 } else {
470 $queryBuilder->count('*');
471 }
472
473 try {
474 $count = $queryBuilder->execute()->fetchColumn(0);
475 } catch (DBALException $e) {
476 throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074379);
477 }
478 if ($query->getOffset()) {
479 $count -= $query->getOffset();
480 }
481 if ($query->getLimit()) {
482 $count = min($count, $query->getLimit());
483 }
484 return (int)max(0, $count);
485 }
486
487 /**
488 * Checks if a Value Object equal to the given Object exists in the database
489 *
490 * @param AbstractValueObject $object The Value Object
491 * @return mixed The matching uid if an object was found, else FALSE
492 * @throws SqlErrorException
493 */
494 public function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object)
495 {
496 $dataMap = $this->dataMapper->getDataMap(get_class($object));
497 $tableName = $dataMap->getTableName();
498 $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
499 if ($this->environmentService->isEnvironmentInFrontendMode()) {
500 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
501 }
502 $whereClause = [];
503 // loop over all properties of the object to exactly set the values of each database field
504 $properties = $object->_getProperties();
505 foreach ($properties as $propertyName => $propertyValue) {
506 // @todo We couple the Backend to the Entity implementation (uid, isClone); changes there breaks this method
507 if ($dataMap->isPersistableProperty($propertyName) && $propertyName !== 'uid' && $propertyName !== 'pid' && $propertyName !== 'isClone') {
508 $fieldName = $dataMap->getColumnMap($propertyName)->getColumnName();
509 if ($propertyValue === null) {
510 $whereClause[] = $queryBuilder->expr()->isNull($fieldName);
511 } else {
512 $whereClause[] = $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($this->dataMapper->getPlainValue($propertyValue)));
513 }
514 }
515 }
516 $queryBuilder
517 ->select('uid')
518 ->from($tableName)
519 ->where(...$whereClause);
520
521 try {
522 $uid = (int)$queryBuilder
523 ->execute()
524 ->fetchColumn(0);
525 if ($uid > 0) {
526 return $uid;
527 }
528 return false;
529 } catch (DBALException $e) {
530 throw new SqlErrorException($e->getPrevious()->getMessage(), 1470231748);
531 }
532 }
533
534 /**
535 * Performs workspace and language overlay on the given row array. The language and workspace id is automatically
536 * detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
537 *
538 * @param Qom\SourceInterface $source The source (selector od join)
539 * @param array $rows
540 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
541 * @param null|int $workspaceUid
542 * @return array
543 */
544 protected function doLanguageAndWorkspaceOverlay(Qom\SourceInterface $source, array $rows, \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $workspaceUid = null)
545 {
546 if ($source instanceof Qom\SelectorInterface) {
547 $tableName = $source->getSelectorName();
548 } elseif ($source instanceof Qom\JoinInterface) {
549 $tableName = $source->getRight()->getSelectorName();
550 } else {
551 // No proper source, so we do not have a table name here
552 // we cannot do an overlay and return the original rows instead.
553 return $rows;
554 }
555
556 $pageRepository = $this->getPageRepository();
557 if (is_object($GLOBALS['TSFE'])) {
558 if ($workspaceUid !== null) {
559 $pageRepository->versioningWorkspaceId = $workspaceUid;
560 }
561 } else {
562 if ($workspaceUid === null) {
563 $workspaceUid = $GLOBALS['BE_USER']->workspace;
564 }
565 $pageRepository->versioningWorkspaceId = $workspaceUid;
566 }
567
568 // Fetches the move-placeholder in case it is supported
569 // by the table and if there's only one row in the result set
570 // (applying this to all rows does not work, since the sorting
571 // order would be destroyed and possible limits not met anymore)
572 if (!empty($pageRepository->versioningWorkspaceId)
573 && BackendUtility::isTableWorkspaceEnabled($tableName)
574 && count($rows) === 1
575 ) {
576 $versionId = $pageRepository->versioningWorkspaceId;
577 $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
578 $queryBuilder->getRestrictions()->removeAll();
579 $movePlaceholder = $queryBuilder->select($tableName . '.*')
580 ->from($tableName)
581 ->where(
582 $queryBuilder->expr()->eq('t3ver_state', $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT)),
583 $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter($versionId, \PDO::PARAM_INT)),
584 $queryBuilder->expr()->eq('t3ver_move_id', $queryBuilder->createNamedParameter($rows[0]['uid'], \PDO::PARAM_INT))
585 )
586 ->setMaxResults(1)
587 ->execute()
588 ->fetch();
589 if (!empty($movePlaceholder)) {
590 $rows = [$movePlaceholder];
591 }
592 }
593
594 $overlaidRows = [];
595 foreach ($rows as $row) {
596 // If current row is a translation select its parent
597 if (isset($tableName) && isset($GLOBALS['TCA'][$tableName])
598 && isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
599 && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
600 && $tableName !== 'pages_language_overlay'
601 ) {
602 if (isset($row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']])
603 && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
604 ) {
605 $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
606 $queryBuilder->getRestrictions()->removeAll();
607 $row = $queryBuilder->select($tableName . '.*')
608 ->from($tableName)
609 ->where(
610 $queryBuilder->expr()->eq(
611 $tableName . '.uid',
612 $queryBuilder->createNamedParameter(
613 $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']],
614 \PDO::PARAM_INT
615 )
616 ),
617 $queryBuilder->expr()->eq(
618 $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
619 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
620 )
621 )
622 ->setMaxResults(1)
623 ->execute()
624 ->fetch();
625 }
626 }
627 $pageRepository->versionOL($tableName, $row, true);
628 if ($tableName === 'pages') {
629 $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
630 } elseif (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
631 && $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] !== ''
632 && $tableName !== 'pages_language_overlay'
633 ) {
634 if (in_array($row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']], [-1, 0])) {
635 $overlayMode = $querySettings->getLanguageMode() === 'strict' ? 'hideNonTranslated' : '';
636 $row = $pageRepository->getRecordOverlay($tableName, $row, $querySettings->getLanguageUid(), $overlayMode);
637 }
638 }
639 if ($row !== null && is_array($row)) {
640 $overlaidRows[] = $row;
641 }
642 }
643 return $overlaidRows;
644 }
645
646 /**
647 * @return \TYPO3\CMS\Frontend\Page\PageRepository
648 */
649 protected function getPageRepository()
650 {
651 if (!$this->pageRepository instanceof \TYPO3\CMS\Frontend\Page\PageRepository) {
652 if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
653 $this->pageRepository = $GLOBALS['TSFE']->sys_page;
654 } else {
655 $this->pageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class);
656 }
657 }
658
659 return $this->pageRepository;
660 }
661
662 /**
663 * Clear the TYPO3 page cache for the given record.
664 * If the record lies on a page, then we clear the cache of this page.
665 * If the record has no PID column, we clear the cache of the current page as best-effort.
666 *
667 * Much of this functionality is taken from DataHandler::clear_cache() which unfortunately only works with logged-in BE user.
668 *
669 * @param string $tableName Table name of the record
670 * @param int $uid UID of the record
671 */
672 protected function clearPageCache($tableName, $uid)
673 {
674 $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
675 if (isset($frameworkConfiguration['persistence']['enableAutomaticCacheClearing']) && $frameworkConfiguration['persistence']['enableAutomaticCacheClearing'] === '1') {
676 } else {
677 // if disabled, return
678 return;
679 }
680 $pageIdsToClear = [];
681 $storagePage = null;
682
683 // As determining the table columns is a costly operation this is done only once per table during runtime and cached then
684 if (!isset($this->hasPidColumn[$tableName])) {
685 $columns = GeneralUtility::makeInstance(ConnectionPool::class)
686 ->getConnectionForTable($tableName)
687 ->getSchemaManager()
688 ->listTableColumns($tableName);
689 $this->hasPidColumn[$tableName] = array_key_exists('pid', $columns);
690 }
691
692 if ($this->hasPidColumn[$tableName]) {
693 $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
694 $queryBuilder->getRestrictions()->removeAll();
695 $result = $queryBuilder
696 ->select('pid')
697 ->from($tableName)
698 ->where(
699 $queryBuilder->expr()->eq(
700 'uid',
701 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
702 )
703 )
704 ->execute();
705 if ($row = $result->fetch()) {
706 $storagePage = $row['pid'];
707 $pageIdsToClear[] = $storagePage;
708 }
709 } elseif (isset($GLOBALS['TSFE'])) {
710 // No PID column - we can do a best-effort to clear the cache of the current page if in FE
711 $storagePage = $GLOBALS['TSFE']->id;
712 $pageIdsToClear[] = $storagePage;
713 }
714 if ($storagePage === null) {
715 return;
716 }
717
718 $pageTS = BackendUtility::getPagesTSconfig($storagePage);
719 if (isset($pageTS['TCEMAIN.']['clearCacheCmd'])) {
720 $clearCacheCommands = GeneralUtility::trimExplode(',', strtolower($pageTS['TCEMAIN.']['clearCacheCmd']), true);
721 $clearCacheCommands = array_unique($clearCacheCommands);
722 foreach ($clearCacheCommands as $clearCacheCommand) {
723 if (MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) {
724 $pageIdsToClear[] = $clearCacheCommand;
725 }
726 }
727 }
728
729 foreach ($pageIdsToClear as $pageIdToClear) {
730 $this->cacheService->getPageIdStack()->push($pageIdToClear);
731 }
732 }
733 }