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