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