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