[BUGFIX] Introduce exception for using offset without limit
[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 TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Extbase\Persistence\Generic\Qom;
19 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
20
21 /**
22 * A Storage backend
23 */
24 class Typo3DbBackend implements BackendInterface, \TYPO3\CMS\Core\SingletonInterface
25 {
26 /**
27 * The TYPO3 database object
28 *
29 * @var \TYPO3\CMS\Core\Database\DatabaseConnection
30 */
31 protected $databaseHandle;
32
33 /**
34 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
35 */
36 protected $dataMapper;
37
38 /**
39 * The TYPO3 page repository. Used for language and workspace overlay
40 *
41 * @var \TYPO3\CMS\Frontend\Page\PageRepository
42 */
43 protected $pageRepository;
44
45 /**
46 * A first-level TypoScript configuration cache
47 *
48 * @var array
49 */
50 protected $pageTSConfigCache = array();
51
52 /**
53 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
54 */
55 protected $configurationManager;
56
57 /**
58 * @var \TYPO3\CMS\Extbase\Service\CacheService
59 */
60 protected $cacheService;
61
62 /**
63 * @var \TYPO3\CMS\Core\Cache\CacheManager
64 */
65 protected $cacheManager;
66
67 /**
68 * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
69 */
70 protected $tableColumnCache;
71
72 /**
73 * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
74 */
75 protected $queryCache;
76
77 /**
78 * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
79 */
80 protected $environmentService;
81
82 /**
83 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser
84 */
85 protected $queryParser;
86
87 /**
88 * A first level cache for queries during runtime
89 *
90 * @var array
91 */
92 protected $queryRuntimeCache = array();
93
94 /**
95 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper
96 */
97 public function injectDataMapper(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper)
98 {
99 $this->dataMapper = $dataMapper;
100 }
101
102 /**
103 * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
104 */
105 public function injectConfigurationManager(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager)
106 {
107 $this->configurationManager = $configurationManager;
108 }
109
110 /**
111 * @param \TYPO3\CMS\Extbase\Service\CacheService $cacheService
112 */
113 public function injectCacheService(\TYPO3\CMS\Extbase\Service\CacheService $cacheService)
114 {
115 $this->cacheService = $cacheService;
116 }
117
118 /**
119 * @param \TYPO3\CMS\Core\Cache\CacheManager $cacheManager
120 */
121 public function injectCacheManager(\TYPO3\CMS\Core\Cache\CacheManager $cacheManager)
122 {
123 $this->cacheManager = $cacheManager;
124 }
125
126 /**
127 * @param \TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService
128 */
129 public function injectEnvironmentService(\TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService)
130 {
131 $this->environmentService = $environmentService;
132 }
133
134 /**
135 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser $queryParser
136 */
137 public function injectQueryParser(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser $queryParser)
138 {
139 $this->queryParser = $queryParser;
140 }
141
142 /**
143 * Constructor. takes the database handle from $GLOBALS['TYPO3_DB']
144 */
145 public function __construct()
146 {
147 $this->databaseHandle = $GLOBALS['TYPO3_DB'];
148 }
149
150 /**
151 * Lifecycle method
152 *
153 * @return void
154 */
155 public function initializeObject()
156 {
157 $this->tableColumnCache = $this->cacheManager->getCache('extbase_typo3dbbackend_tablecolumns');
158 $this->queryCache = $this->cacheManager->getCache('extbase_typo3dbbackend_queries');
159 }
160
161 /**
162 * Adds a row to the storage
163 *
164 * @param string $tableName The database table name
165 * @param array $fieldValues The row to be inserted
166 * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
167 * @return int The uid of the inserted row
168 */
169 public function addRow($tableName, array $fieldValues, $isRelation = false)
170 {
171 if (isset($fieldValues['uid'])) {
172 unset($fieldValues['uid']);
173 }
174
175 $this->databaseHandle->exec_INSERTquery($tableName, $fieldValues);
176 $this->checkSqlErrors();
177 $uid = $this->databaseHandle->sql_insert_id();
178
179 if (!$isRelation) {
180 $this->clearPageCache($tableName, $uid);
181 }
182 return (int)$uid;
183 }
184
185 /**
186 * Updates a row in the storage
187 *
188 * @param string $tableName The database table name
189 * @param array $fieldValues The row to be updated
190 * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
191 * @throws \InvalidArgumentException
192 * @return bool
193 */
194 public function updateRow($tableName, array $fieldValues, $isRelation = false)
195 {
196 if (!isset($fieldValues['uid'])) {
197 throw new \InvalidArgumentException('The given row must contain a value for "uid".');
198 }
199
200 $uid = (int)$fieldValues['uid'];
201 unset($fieldValues['uid']);
202
203 $updateSuccessful = $this->databaseHandle->exec_UPDATEquery($tableName, 'uid = ' . $uid, $fieldValues);
204 $this->checkSqlErrors();
205
206 if (!$isRelation) {
207 $this->clearPageCache($tableName, $uid);
208 }
209
210 return $updateSuccessful;
211 }
212
213 /**
214 * Updates a relation row in the storage.
215 *
216 * @param string $tableName The database relation table name
217 * @param array $fieldValues The row to be updated
218 * @throws \InvalidArgumentException
219 * @return bool
220 */
221 public function updateRelationTableRow($tableName, array $fieldValues)
222 {
223 if (!isset($fieldValues['uid_local']) && !isset($fieldValues['uid_foreign'])) {
224 throw new \InvalidArgumentException(
225 'The given fieldValues must contain a value for "uid_local" and "uid_foreign".', 1360500126
226 );
227 }
228
229 $where['uid_local'] = (int)$fieldValues['uid_local'];
230 $where['uid_foreign'] = (int)$fieldValues['uid_foreign'];
231 unset($fieldValues['uid_local']);
232 unset($fieldValues['uid_foreign']);
233
234 if (!empty($fieldValues['tablenames'])) {
235 $where['tablenames'] = $fieldValues['tablenames'];
236 unset($fieldValues['tablenames']);
237 }
238 if (!empty($fieldValues['fieldname'])) {
239 $where['fieldname'] = $fieldValues['fieldname'];
240 unset($fieldValues['fieldname']);
241 }
242
243 $updateSuccessful = $this->databaseHandle->exec_UPDATEquery(
244 $tableName,
245 $this->resolveWhereStatement($where, $tableName),
246 $fieldValues
247 );
248 $this->checkSqlErrors();
249
250 return $updateSuccessful;
251 }
252
253 /**
254 * Deletes a row in the storage
255 *
256 * @param string $tableName The database table name
257 * @param array $where An array of where array('fieldname' => value).
258 * @param bool $isRelation TRUE if we are currently manipulating a relation table, FALSE by default
259 * @return bool
260 */
261 public function removeRow($tableName, array $where, $isRelation = false)
262 {
263 $deleteSuccessful = $this->databaseHandle->exec_DELETEquery(
264 $tableName,
265 $this->resolveWhereStatement($where, $tableName)
266 );
267 $this->checkSqlErrors();
268
269 if (!$isRelation && isset($where['uid'])) {
270 $this->clearPageCache($tableName, $where['uid']);
271 }
272
273 return $deleteSuccessful;
274 }
275
276 /**
277 * Fetches maximal value for given table column from database.
278 *
279 * @param string $tableName The database table name
280 * @param array $where An array of where array('fieldname' => value).
281 * @param string $columnName column name to get the max value from
282 * @return mixed the max value
283 */
284 public function getMaxValueFromTable($tableName, array $where, $columnName)
285 {
286 $result = $this->databaseHandle->exec_SELECTgetSingleRow(
287 $columnName,
288 $tableName,
289 $this->resolveWhereStatement($where, $tableName),
290 '',
291 $columnName . ' DESC',
292 true
293 );
294 $this->checkSqlErrors();
295
296 return $result[0];
297 }
298
299 /**
300 * Fetches row data from the database
301 *
302 * @param string $tableName
303 * @param array $where An array of where array('fieldname' => value).
304 * @return array|bool
305 */
306 public function getRowByIdentifier($tableName, array $where)
307 {
308 $row = $this->databaseHandle->exec_SELECTgetSingleRow(
309 '*',
310 $tableName,
311 $this->resolveWhereStatement($where, $tableName)
312 );
313 $this->checkSqlErrors();
314
315 return $row ?: false;
316 }
317
318 /**
319 * Converts an array to an AND concatenated where statement
320 *
321 * @param array $where array('fieldName' => 'fieldValue')
322 * @param string $tableName table to use for escaping config
323 *
324 * @return string
325 */
326 protected function resolveWhereStatement(array $where, $tableName = 'foo')
327 {
328 $whereStatement = array();
329
330 foreach ($where as $fieldName => $fieldValue) {
331 $whereStatement[] = $fieldName . ' = ' . $this->databaseHandle->fullQuoteStr($fieldValue, $tableName);
332 }
333
334 return implode(' AND ', $whereStatement);
335 }
336
337 /**
338 * Returns the object data matching the $query.
339 *
340 * @param QueryInterface $query
341 * @return array
342 */
343 public function getObjectDataByQuery(QueryInterface $query)
344 {
345 $statement = $query->getStatement();
346 if ($statement instanceof Qom\Statement) {
347 $rows = $this->getObjectDataByRawQuery($statement);
348 } else {
349 $rows = $this->getRowsByStatementParts($query);
350 }
351
352 $rows = $this->doLanguageAndWorkspaceOverlay($query->getSource(), $rows, $query->getQuerySettings());
353 return $rows;
354 }
355
356 /**
357 * Creates the parameters for the query methods of the database methods in the TYPO3 core, from an array
358 * that came from a parsed query.
359 *
360 * @param array $statementParts
361 * @throws \InvalidArgumentException
362 * @return array
363 */
364 protected function createQueryCommandParametersFromStatementParts(array $statementParts)
365 {
366 if (isset($statementParts['offset']) && !isset($statementParts['limit'])) {
367 throw new \InvalidArgumentException(
368 'Trying to make query with offset and no limit, the offset would become a limit. You have to set a limit to use offset. To retrieve all rows from a certain offset up to the end of the result set, you can use some large number for the limit.',
369 1465223252
370 );
371 }
372 return array(
373 'selectFields' => implode(' ', $statementParts['keywords']) . ' ' . implode(',', $statementParts['fields']),
374 'fromTable' => implode(' ', $statementParts['tables']) . ' ' . implode(' ', $statementParts['unions']),
375 'whereClause' => (!empty($statementParts['where']) ? implode('', $statementParts['where']) : '1=1')
376 . (!empty($statementParts['additionalWhereClause'])
377 ? ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'])
378 : ''
379 ),
380 'orderBy' => (!empty($statementParts['orderings']) ? implode(', ', $statementParts['orderings']) : ''),
381 'limit' => ($statementParts['offset'] ? $statementParts['offset'] . ', ' : '')
382 . ($statementParts['limit'] ? $statementParts['limit'] : '')
383 );
384 }
385
386 /**
387 * Determines whether to use prepared statement or not and returns the rows from the corresponding method
388 *
389 * @param QueryInterface $query
390 * @return array
391 */
392 protected function getRowsByStatementParts(QueryInterface $query)
393 {
394 if ($query->getQuerySettings()->getUsePreparedStatement()) {
395 list($statementParts, $parameters) = $this->getStatementParts($query, false);
396 $rows = $this->getRowsFromPreparedDatabase($statementParts, $parameters);
397 } else {
398 list($statementParts) = $this->getStatementParts($query);
399 $rows = $this->getRowsFromDatabase($statementParts);
400 }
401
402 return $rows;
403 }
404
405 /**
406 * Fetches the rows directly from the database, not using prepared statement
407 *
408 * @param array $statementParts
409 * @return array the result
410 */
411 protected function getRowsFromDatabase(array $statementParts)
412 {
413 $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
414 $rows = $this->databaseHandle->exec_SELECTgetRows(
415 $queryCommandParameters['selectFields'],
416 $queryCommandParameters['fromTable'],
417 $queryCommandParameters['whereClause'],
418 '',
419 $queryCommandParameters['orderBy'],
420 $queryCommandParameters['limit']
421 );
422 $this->checkSqlErrors();
423
424 return $rows;
425 }
426
427 /**
428 * Fetches the rows from the database, using prepared statement
429 *
430 * @param array $statementParts
431 * @param array $parameters
432 * @return array the result
433 */
434 protected function getRowsFromPreparedDatabase(array $statementParts, array $parameters)
435 {
436 $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
437 $preparedStatement = $this->databaseHandle->prepare_SELECTquery(
438 $queryCommandParameters['selectFields'],
439 $queryCommandParameters['fromTable'],
440 $queryCommandParameters['whereClause'],
441 '',
442 $queryCommandParameters['orderBy'],
443 $queryCommandParameters['limit']
444 );
445
446 $preparedStatement->execute($parameters);
447 $rows = $preparedStatement->fetchAll();
448
449 $preparedStatement->free();
450 return $rows;
451 }
452
453 /**
454 * Returns the object data using a custom statement
455 *
456 * @param Qom\Statement $statement
457 * @return array
458 */
459 protected function getObjectDataByRawQuery(Qom\Statement $statement)
460 {
461 $realStatement = $statement->getStatement();
462 $parameters = $statement->getBoundVariables();
463
464 if ($realStatement instanceof \TYPO3\CMS\Core\Database\PreparedStatement) {
465 $realStatement->execute($parameters);
466 $rows = $realStatement->fetchAll();
467
468 $realStatement->free();
469 } else {
470 $result = $this->databaseHandle->sql_query($realStatement);
471 $this->checkSqlErrors();
472
473 $rows = array();
474 while ($row = $this->databaseHandle->sql_fetch_assoc($result)) {
475 if (is_array($row)) {
476 $rows[] = $row;
477 }
478 }
479 $this->databaseHandle->sql_free_result($result);
480 }
481
482 return $rows;
483 }
484
485 /**
486 * Returns the number of tuples matching the query.
487 *
488 * @param QueryInterface $query
489 * @throws Exception\BadConstraintException
490 * @return int The number of matching tuples
491 */
492 public function getObjectCountByQuery(QueryInterface $query)
493 {
494 if ($query->getConstraint() instanceof Qom\Statement) {
495 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);
496 }
497
498 list($statementParts) = $this->getStatementParts($query);
499
500 $fields = '*';
501 if (isset($statementParts['keywords']['distinct'])) {
502 $fields = 'DISTINCT ' . reset($statementParts['tables']) . '.uid';
503 }
504
505 $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
506 $count = $this->databaseHandle->exec_SELECTcountRows(
507 $fields,
508 $queryCommandParameters['fromTable'],
509 $queryCommandParameters['whereClause']
510 );
511 $this->checkSqlErrors();
512
513 if ($statementParts['offset']) {
514 $count -= $statementParts['offset'];
515 }
516
517 if ($statementParts['limit']) {
518 $count = min($count, $statementParts['limit']);
519 }
520
521 return (int)max(0, $count);
522 }
523
524 /**
525 * Looks for the query in cache or builds it up otherwise
526 *
527 * @param QueryInterface $query
528 * @param bool $resolveParameterPlaceholders whether to resolve the parameters or leave the placeholders
529 * @return array
530 * @throws \RuntimeException
531 */
532 protected function getStatementParts($query, $resolveParameterPlaceholders = true)
533 {
534 /**
535 * The queryParser will preparse the query to get the query's hash and parameters.
536 * If the hash is found in the cache and useQueryCaching is enabled, extbase will
537 * then take the string representation from cache and build a prepared query with
538 * the parameters found.
539 *
540 * Otherwise extbase will parse the complete query, build the string representation
541 * and run a usual query.
542 */
543 list($queryHash, $parameters) = $this->queryParser->preparseQuery($query);
544
545 if ($query->getQuerySettings()->getUseQueryCache()) {
546 $statementParts = $this->getQueryCacheEntry($queryHash);
547 if ($queryHash && !$statementParts) {
548 $statementParts = $this->queryParser->parseQuery($query);
549 $this->setQueryCacheEntry($queryHash, $statementParts);
550 }
551 } else {
552 $statementParts = $this->queryParser->parseQuery($query);
553 }
554
555 if (!$statementParts) {
556 throw new \RuntimeException('Your query could not be built.', 1394453197);
557 }
558
559 $this->queryParser->addDynamicQueryParts($query->getQuerySettings(), $statementParts);
560
561 // Limit and offset are not cached to allow caching of pagebrowser queries.
562 $statementParts['limit'] = ((int)$query->getLimit() ?: null);
563 $statementParts['offset'] = ((int)$query->getOffset() ?: null);
564
565 if ($resolveParameterPlaceholders === true) {
566 $statementParts = $this->resolveParameterPlaceholders($statementParts, $parameters);
567 }
568
569 return array($statementParts, $parameters);
570 }
571
572 /**
573 * Replaces the parameters in the queryStructure with given values
574 *
575 * @param array $statementParts
576 * @param array $parameters
577 * @return array
578 */
579 protected function resolveParameterPlaceholders(array $statementParts, array $parameters)
580 {
581 $tableName = reset($statementParts['tables']) ?: 'foo';
582
583 foreach ($parameters as $parameterPlaceholder => $parameter) {
584 $parameter = $this->dataMapper->getPlainValue($parameter, null, array($this, 'quoteTextValueCallback'), array('tablename' => $tableName));
585 $statementParts['where'] = str_replace($parameterPlaceholder, $parameter, $statementParts['where']);
586 }
587
588 return $statementParts;
589 }
590
591 /**
592 * Will be called by the data mapper to quote string values.
593 *
594 * @param string $value The value to be quoted.
595 * @param array $parameters Additional parameters array currently containing the "tablename" key.
596 * @return string The quoted string.
597 */
598 public function quoteTextValueCallback($value, $parameters)
599 {
600 return $this->databaseHandle->fullQuoteStr($value, $parameters['tablename']);
601 }
602
603 /**
604 * Checks if a Value Object equal to the given Object exists in the data base
605 *
606 * @param \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object The Value Object
607 * @return mixed The matching uid if an object was found, else FALSE
608 * @todo this is the last monster in this persistence series. refactor!
609 */
610 public function getUidOfAlreadyPersistedValueObject(\TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object)
611 {
612 $fields = array();
613 $parameters = array();
614 $dataMap = $this->dataMapper->getDataMap(get_class($object));
615 $properties = $object->_getProperties();
616 foreach ($properties as $propertyName => $propertyValue) {
617 // @todo We couple the Backend to the Entity implementation (uid, isClone); changes there breaks this method
618 if ($dataMap->isPersistableProperty($propertyName) && $propertyName !== 'uid' && $propertyName !== 'pid' && $propertyName !== 'isClone') {
619 if ($propertyValue === null) {
620 $fields[] = $dataMap->getColumnMap($propertyName)->getColumnName() . ' IS NULL';
621 } else {
622 $fields[] = $dataMap->getColumnMap($propertyName)->getColumnName() . '=?';
623 $parameters[] = $this->dataMapper->getPlainValue($propertyValue);
624 }
625 }
626 }
627 $sql = array();
628 $sql['additionalWhereClause'] = array();
629 $tableName = $dataMap->getTableName();
630 $this->addVisibilityConstraintStatement(new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings(), $tableName, $sql);
631 $statement = 'SELECT * FROM ' . $tableName;
632 $statement .= ' WHERE ' . implode(' AND ', $fields);
633 if (!empty($sql['additionalWhereClause'])) {
634 $statement .= ' AND ' . implode(' AND ', $sql['additionalWhereClause']);
635 }
636 $this->replacePlaceholders($statement, $parameters, $tableName);
637 // debug($statement,-2);
638 $res = $this->databaseHandle->sql_query($statement);
639 $this->checkSqlErrors($statement);
640 $row = $this->databaseHandle->sql_fetch_assoc($res);
641 if ($row !== false) {
642 return (int)$row['uid'];
643 } else {
644 return false;
645 }
646 }
647
648 /**
649 * Replace query placeholders in a query part by the given
650 * parameters.
651 *
652 * @param string &$sqlString The query part with placeholders
653 * @param array $parameters The parameters
654 * @param string $tableName
655 *
656 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
657 * @deprecated since 6.2, will be removed two versions later
658 * @todo add deprecation notice after getUidOfAlreadyPersistedValueObject is adjusted
659 */
660 protected function replacePlaceholders(&$sqlString, array $parameters, $tableName = 'foo')
661 {
662 // @todo profile this method again
663 if (substr_count($sqlString, '?') !== count($parameters)) {
664 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('The number of question marks to replace must be equal to the number of parameters.', 1460975513);
665 }
666 $offset = 0;
667 foreach ($parameters as $parameter) {
668 $markPosition = strpos($sqlString, '?', $offset);
669 if ($markPosition !== false) {
670 if ($parameter === null) {
671 $parameter = 'NULL';
672 } elseif (is_array($parameter) || $parameter instanceof \ArrayAccess || $parameter instanceof \Traversable) {
673 $items = array();
674 foreach ($parameter as $item) {
675 $items[] = $this->databaseHandle->fullQuoteStr($item, $tableName);
676 }
677 $parameter = '(' . implode(',', $items) . ')';
678 } else {
679 $parameter = $this->databaseHandle->fullQuoteStr($parameter, $tableName);
680 }
681 $sqlString = substr($sqlString, 0, $markPosition) . $parameter . substr($sqlString, ($markPosition + 1));
682 }
683 $offset = $markPosition + strlen($parameter);
684 }
685 }
686
687 /**
688 * Adds enableFields and deletedClause to the query if necessary
689 *
690 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings
691 * @param string $tableName The database table name
692 * @param array &$sql The query parts
693 * @return void
694 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
695 */
696 protected function addVisibilityConstraintStatement(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $tableName, array &$sql)
697 {
698 $statement = '';
699 if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
700 $ignoreEnableFields = $querySettings->getIgnoreEnableFields();
701 $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
702 $includeDeleted = $querySettings->getIncludeDeleted();
703 if ($this->environmentService->isEnvironmentInFrontendMode()) {
704 $statement .= $this->getFrontendConstraintStatement($tableName, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
705 } else {
706 // TYPO3_MODE === 'BE'
707 $statement .= $this->getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted);
708 }
709 if (!empty($statement)) {
710 $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
711 $sql['additionalWhereClause'][] = $statement;
712 }
713 }
714 }
715
716 /**
717 * Returns constraint statement for frontend context
718 *
719 * @param string $tableName
720 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
721 * @param array $enableFieldsToBeIgnored If $ignoreEnableFields is true, this array specifies enable fields to be ignored. If it is NULL or an empty array (default) all enable fields are ignored.
722 * @param bool $includeDeleted A flag indicating whether deleted records should be included
723 * @return string
724 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
725 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
726 */
727 protected function getFrontendConstraintStatement($tableName, $ignoreEnableFields, array $enableFieldsToBeIgnored = array(), $includeDeleted)
728 {
729 $statement = '';
730 if ($ignoreEnableFields && !$includeDeleted) {
731 if (!empty($enableFieldsToBeIgnored)) {
732 // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented
733 $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
734 } else {
735 $statement .= $this->getPageRepository()->deleteClause($tableName);
736 }
737 } elseif (!$ignoreEnableFields && !$includeDeleted) {
738 $statement .= $this->getPageRepository()->enableFields($tableName);
739 } elseif (!$ignoreEnableFields && $includeDeleted) {
740 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException('Query setting "ignoreEnableFields=FALSE" can not be used together with "includeDeleted=TRUE" in frontend context.', 1327678173);
741 }
742 return $statement;
743 }
744
745 /**
746 * Returns constraint statement for backend context
747 *
748 * @param string $tableName
749 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
750 * @param bool $includeDeleted A flag indicating whether deleted records should be included
751 * @return string
752 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
753 */
754 protected function getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted)
755 {
756 $statement = '';
757 if (!$ignoreEnableFields) {
758 $statement .= BackendUtility::BEenableFields($tableName);
759 }
760 if (!$includeDeleted) {
761 $statement .= BackendUtility::deleteClause($tableName);
762 }
763 return $statement;
764 }
765
766 /**
767 * Performs workspace and language overlay on the given row array. The language and workspace id is automatically
768 * detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
769 *
770 * @param Qom\SourceInterface $source The source (selector od join)
771 * @param array $rows
772 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
773 * @param null|int $workspaceUid
774 * @return array
775 */
776 protected function doLanguageAndWorkspaceOverlay(Qom\SourceInterface $source, array $rows, \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $workspaceUid = null)
777 {
778 if ($source instanceof Qom\SelectorInterface) {
779 $tableName = $source->getSelectorName();
780 } elseif ($source instanceof Qom\JoinInterface) {
781 $tableName = $source->getRight()->getSelectorName();
782 } else {
783 // No proper source, so we do not have a table name here
784 // we cannot do an overlay and return the original rows instead.
785 return $rows;
786 }
787
788 $pageRepository = $this->getPageRepository();
789 if (is_object($GLOBALS['TSFE'])) {
790 if ($workspaceUid !== null) {
791 $pageRepository->versioningWorkspaceId = $workspaceUid;
792 }
793 } else {
794 if ($workspaceUid === null) {
795 $workspaceUid = $GLOBALS['BE_USER']->workspace;
796 }
797 $pageRepository->versioningWorkspaceId = $workspaceUid;
798 }
799
800 // Fetches the move-placeholder in case it is supported
801 // by the table and if there's only one row in the result set
802 // (applying this to all rows does not work, since the sorting
803 // order would be destroyed and possible limits not met anymore)
804 if (!empty($pageRepository->versioningWorkspaceId)
805 && BackendUtility::isTableWorkspaceEnabled($tableName)
806 && count($rows) === 1
807 ) {
808 $movePlaceholder = $this->databaseHandle->exec_SELECTgetSingleRow(
809 $tableName . '.*',
810 $tableName,
811 't3ver_state=3 AND t3ver_wsid=' . $pageRepository->versioningWorkspaceId
812 . ' AND t3ver_move_id=' . $rows[0]['uid']
813 );
814 if (!empty($movePlaceholder)) {
815 $rows = array($movePlaceholder);
816 }
817 }
818
819 $overlaidRows = array();
820 foreach ($rows as $row) {
821 // If current row is a translation select its parent
822 if (isset($tableName) && isset($GLOBALS['TCA'][$tableName])
823 && isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
824 && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
825 && !isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerTable'])
826 ) {
827 if (isset($row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']])
828 && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
829 ) {
830 $row = $this->databaseHandle->exec_SELECTgetSingleRow(
831 $tableName . '.*',
832 $tableName,
833 $tableName . '.uid=' . (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] .
834 ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0'
835 );
836 }
837 }
838 $pageRepository->versionOL($tableName, $row, true);
839 if ($tableName == 'pages') {
840 $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
841 } elseif (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
842 && $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] !== ''
843 && !isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerTable'])
844 ) {
845 if (in_array($row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']], array(-1, 0))) {
846 $overlayMode = $querySettings->getLanguageMode() === 'strict' ? 'hideNonTranslated' : '';
847 $row = $pageRepository->getRecordOverlay($tableName, $row, $querySettings->getLanguageUid(), $overlayMode);
848 }
849 }
850 if ($row !== null && is_array($row)) {
851 $overlaidRows[] = $row;
852 }
853 }
854 return $overlaidRows;
855 }
856
857 /**
858 * @return \TYPO3\CMS\Frontend\Page\PageRepository
859 */
860 protected function getPageRepository()
861 {
862 if (!$this->pageRepository instanceof \TYPO3\CMS\Frontend\Page\PageRepository) {
863 if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
864 $this->pageRepository = $GLOBALS['TSFE']->sys_page;
865 } else {
866 $this->pageRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class);
867 }
868 }
869
870 return $this->pageRepository;
871 }
872
873 /**
874 * Checks if there are SQL errors in the last query, and if yes, throw an exception.
875 *
876 * @return void
877 * @param string $sql The SQL statement
878 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException
879 */
880 protected function checkSqlErrors($sql = '')
881 {
882 $error = $this->databaseHandle->sql_error();
883 if ($error !== '') {
884 $error .= $sql ? ': ' . $sql : '';
885 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException($error, 1247602160);
886 }
887 }
888
889 /**
890 * Clear the TYPO3 page cache for the given record.
891 * If the record lies on a page, then we clear the cache of this page.
892 * If the record has no PID column, we clear the cache of the current page as best-effort.
893 *
894 * Much of this functionality is taken from DataHandler::clear_cache() which unfortunately only works with logged-in BE user.
895 *
896 * @param string $tableName Table name of the record
897 * @param int $uid UID of the record
898 * @return void
899 */
900 protected function clearPageCache($tableName, $uid)
901 {
902 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
903 if (isset($frameworkConfiguration['persistence']['enableAutomaticCacheClearing']) && $frameworkConfiguration['persistence']['enableAutomaticCacheClearing'] === '1') {
904 } else {
905 // if disabled, return
906 return;
907 }
908 $pageIdsToClear = array();
909 $storagePage = null;
910 $columns = $this->databaseHandle->admin_get_fields($tableName);
911 if (array_key_exists('pid', $columns)) {
912 $result = $this->databaseHandle->exec_SELECTquery('pid', $tableName, 'uid=' . (int)$uid);
913 if ($row = $this->databaseHandle->sql_fetch_assoc($result)) {
914 $storagePage = $row['pid'];
915 $pageIdsToClear[] = $storagePage;
916 }
917 } elseif (isset($GLOBALS['TSFE'])) {
918 // No PID column - we can do a best-effort to clear the cache of the current page if in FE
919 $storagePage = $GLOBALS['TSFE']->id;
920 $pageIdsToClear[] = $storagePage;
921 }
922 if ($storagePage === null) {
923 return;
924 }
925 if (!isset($this->pageTSConfigCache[$storagePage])) {
926 $this->pageTSConfigCache[$storagePage] = BackendUtility::getPagesTSconfig($storagePage);
927 }
928 if (isset($this->pageTSConfigCache[$storagePage]['TCEMAIN.']['clearCacheCmd'])) {
929 $clearCacheCommands = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', strtolower($this->pageTSConfigCache[$storagePage]['TCEMAIN.']['clearCacheCmd']), true);
930 $clearCacheCommands = array_unique($clearCacheCommands);
931 foreach ($clearCacheCommands as $clearCacheCommand) {
932 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) {
933 $pageIdsToClear[] = $clearCacheCommand;
934 }
935 }
936 }
937
938 foreach ($pageIdsToClear as $pageIdToClear) {
939 $this->cacheService->getPageIdStack()->push($pageIdToClear);
940 }
941 }
942
943 /**
944 * Finds and returns a variable value from the query cache.
945 *
946 * @param string $entryIdentifier Identifier of the cache entry to fetch
947 * @return mixed The value
948 */
949 protected function getQueryCacheEntry($entryIdentifier)
950 {
951 if (!isset($this->queryRuntimeCache[$entryIdentifier])) {
952 $this->queryRuntimeCache[$entryIdentifier] = $this->queryCache->get($entryIdentifier);
953 }
954 return $this->queryRuntimeCache[$entryIdentifier];
955 }
956
957 /**
958 * Saves the value of a PHP variable in the query cache.
959 *
960 * @param string $entryIdentifier An identifier used for this cache entry
961 * @param mixed $variable The query to cache
962 * @return void
963 */
964 protected function setQueryCacheEntry($entryIdentifier, $variable)
965 {
966 $this->queryRuntimeCache[$entryIdentifier] = $variable;
967 $this->queryCache->set($entryIdentifier, $variable, array(), 0);
968 }
969 }