[BUGFIX] Respect tablenames, fieldname when updating mm record
[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\Statement;
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 integer 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 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')
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 Statement $statement
394 * @return array
395 */
396 protected function getObjectDataByRawQuery(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 /**
407 * @deprecated since 6.2, this block will be removed in two versions
408 * the deprecation log is in Qom\Statement
409 */
410 if (!empty($parameters)) {
411 $this->replacePlaceholders($realStatement, $parameters);
412 }
413
414 $result = $this->databaseHandle->sql_query($realStatement);
415 $this->checkSqlErrors();
416
417 $rows = array();
418 while ($row = $this->databaseHandle->sql_fetch_assoc($result)) {
419 if (is_array($row)) {
420 $rows[] = $row;
421 }
422 }
423 $this->databaseHandle->sql_free_result($result);
424 }
425
426 return $rows;
427 }
428
429 /**
430 * Returns the number of tuples matching the query.
431 *
432 * @param QueryInterface $query
433 * @throws Exception\BadConstraintException
434 * @return integer The number of matching tuples
435 */
436 public function getObjectCountByQuery(QueryInterface $query) {
437 if ($query->getConstraint() instanceof Statement) {
438 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);
439 }
440
441 list($statementParts) = $this->getStatementParts($query);
442
443 $fields = '*';
444 if (isset($statementParts['keywords']['distinct'])) {
445 $fields = 'DISTINCT ' . reset($statementParts['tables']) . '.uid';
446 }
447
448 $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
449 $count = $this->databaseHandle->exec_SELECTcountRows(
450 $fields,
451 $queryCommandParameters['fromTable'],
452 $queryCommandParameters['whereClause']
453 );
454 $this->checkSqlErrors();
455
456 if ($statementParts['offset']) {
457 $count -= $statementParts['offset'];
458 }
459
460 if ($statementParts['limit']) {
461 $count = min($count, $statementParts['limit']);
462 }
463
464 return (int)max(0, $count);
465 }
466
467 /**
468 * Looks for the query in cache or builds it up otherwise
469 *
470 * @param QueryInterface $query
471 * @param bool $resolveParameterPlaceholders whether to resolve the parameters or leave the placeholders
472 * @return array
473 * @throws \RuntimeException
474 */
475 protected function getStatementParts($query, $resolveParameterPlaceholders = TRUE) {
476 /**
477 * The queryParser will preparse the query to get the query's hash and parameters.
478 * If the hash is found in the cache and useQueryCaching is enabled, extbase will
479 * then take the string representation from cache and build a prepared query with
480 * the parameters found.
481 *
482 * Otherwise extbase will parse the complete query, build the string representation
483 * and run a usual query.
484 */
485 list($queryHash, $parameters) = $this->queryParser->preparseQuery($query);
486
487 if ($query->getQuerySettings()->getUseQueryCache()) {
488 $statementParts = $this->getQueryCacheEntry($queryHash);
489 if ($queryHash && !$statementParts) {
490 $statementParts = $this->queryParser->parseQuery($query);
491 $this->setQueryCacheEntry($queryHash, $statementParts);
492 }
493 } else {
494 $statementParts = $this->queryParser->parseQuery($query);
495 }
496
497 if (!$statementParts) {
498 throw new \RuntimeException('Your query could not be built.', 1394453197);
499 }
500
501 $this->queryParser->addDynamicQueryParts($query->getQuerySettings(), $statementParts);
502
503 // Limit and offset are not cached to allow caching of pagebrowser queries.
504 $statementParts['limit'] = ((int)$query->getLimit() ?: NULL);
505 $statementParts['offset'] = ((int)$query->getOffset() ?: NULL);
506
507 if ($resolveParameterPlaceholders === TRUE) {
508 $statementParts = $this->resolveParameterPlaceholders($statementParts, $parameters);
509 }
510
511 return array($statementParts, $parameters);
512 }
513
514 /**
515 * Replaces the parameters in the queryStructure with given values
516 *
517 * @param array $statementParts
518 * @param array $parameters
519 * @return array
520 */
521 protected function resolveParameterPlaceholders(array $statementParts, array $parameters) {
522 $tableNameForEscape = (reset($statementParts['tables']) ?: 'foo');
523
524 foreach ($parameters as $parameterPlaceholder => $parameter) {
525 if ($parameter instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
526 $parameter = $parameter->_loadRealInstance();
527 }
528
529 if ($parameter instanceof \DateTime) {
530 $parameter = $parameter->format('U');
531 } elseif ($parameter instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
532 $parameter = (int)$parameter->getUid();
533 } elseif (is_array($parameter)) {
534 $subParameters = array();
535 foreach ($parameter as $subParameter) {
536 $subParameters[] = $this->databaseHandle->fullQuoteStr($subParameter, $tableNameForEscape);
537 }
538 $parameter = implode(',', $subParameters);
539 } elseif ($parameter === NULL) {
540 $parameter = 'NULL';
541 } elseif (is_bool($parameter)) {
542 $parameter = (int)$parameter;
543 } else {
544 $parameter = $this->databaseHandle->fullQuoteStr((string)$parameter, $tableNameForEscape);
545 }
546
547 $statementParts['where'] = str_replace($parameterPlaceholder, $parameter, $statementParts['where']);
548 }
549
550 return $statementParts;
551 }
552
553 /**
554 * Checks if a Value Object equal to the given Object exists in the data base
555 *
556 * @param \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object The Value Object
557 * @return mixed The matching uid if an object was found, else FALSE
558 * @todo this is the last monster in this persistence series. refactor!
559 */
560 public function getUidOfAlreadyPersistedValueObject(\TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object) {
561 $fields = array();
562 $parameters = array();
563 $dataMap = $this->dataMapper->getDataMap(get_class($object));
564 $properties = $object->_getProperties();
565 foreach ($properties as $propertyName => $propertyValue) {
566 // FIXME We couple the Backend to the Entity implementation (uid, isClone); changes there breaks this method
567 if ($dataMap->isPersistableProperty($propertyName) && $propertyName !== 'uid' && $propertyName !== 'pid' && $propertyName !== 'isClone') {
568 if ($propertyValue === NULL) {
569 $fields[] = $dataMap->getColumnMap($propertyName)->getColumnName() . ' IS NULL';
570 } else {
571 $fields[] = $dataMap->getColumnMap($propertyName)->getColumnName() . '=?';
572 $parameters[] = $this->getPlainValue($propertyValue);
573 }
574 }
575 }
576 $sql = array();
577 $sql['additionalWhereClause'] = array();
578 $tableName = $dataMap->getTableName();
579 $this->addVisibilityConstraintStatement(new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings(), $tableName, $sql);
580 $statement = 'SELECT * FROM ' . $tableName;
581 $statement .= ' WHERE ' . implode(' AND ', $fields);
582 if (!empty($sql['additionalWhereClause'])) {
583 $statement .= ' AND ' . implode(' AND ', $sql['additionalWhereClause']);
584 }
585 $this->replacePlaceholders($statement, $parameters, $tableName);
586 // debug($statement,-2);
587 $res = $this->databaseHandle->sql_query($statement);
588 $this->checkSqlErrors($statement);
589 $row = $this->databaseHandle->sql_fetch_assoc($res);
590 if ($row !== FALSE) {
591 return (int)$row['uid'];
592 } else {
593 return FALSE;
594 }
595 }
596
597 /**
598 * Returns a plain value, i.e. objects are flattened out if possible.
599 *
600 * @param mixed $input
601 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
602 * @return mixed
603 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
604 */
605 protected function getPlainValue($input) {
606 if (is_array($input)) {
607 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException('An array could not be converted to a plain value.', 1274799932);
608 }
609 if ($input instanceof \DateTime) {
610 return $input->format('U');
611 } elseif ($input instanceof \TYPO3\CMS\Core\Type\TypeInterface) {
612 return (string) $input;
613 } elseif (is_object($input)) {
614 if ($input instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
615 $realInput = $input->_loadRealInstance();
616 } else {
617 $realInput = $input;
618 }
619 if ($realInput instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
620 return $realInput->getUid();
621 } else {
622 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException('An object of class "' . get_class($realInput) . '" could not be converted to a plain value.', 1274799934);
623 }
624 } elseif (is_bool($input)) {
625 return (int)$input;
626 } else {
627 return $input;
628 }
629 }
630
631 /**
632 * Replace query placeholders in a query part by the given
633 * parameters.
634 *
635 * @param string &$sqlString The query part with placeholders
636 * @param array $parameters The parameters
637 * @param string $tableName
638 *
639 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
640 * @deprecated since 6.2, will be removed two versions later
641 * @todo add deprecation notice after getUidOfAlreadyPersistedValueObject is adjusted
642 */
643 protected function replacePlaceholders(&$sqlString, array $parameters, $tableName = 'foo') {
644 // TODO profile this method again
645 if (substr_count($sqlString, '?') !== count($parameters)) {
646 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('The number of question marks to replace must be equal to the number of parameters.', 1242816074);
647 }
648 $offset = 0;
649 foreach ($parameters as $parameter) {
650 $markPosition = strpos($sqlString, '?', $offset);
651 if ($markPosition !== FALSE) {
652 if ($parameter === NULL) {
653 $parameter = 'NULL';
654 } elseif (is_array($parameter) || $parameter instanceof \ArrayAccess || $parameter instanceof \Traversable) {
655 $items = array();
656 foreach ($parameter as $item) {
657 $items[] = $this->databaseHandle->fullQuoteStr($item, $tableName);
658 }
659 $parameter = '(' . implode(',', $items) . ')';
660 } else {
661 $parameter = $this->databaseHandle->fullQuoteStr($parameter, $tableName);
662 }
663 $sqlString = substr($sqlString, 0, $markPosition) . $parameter . substr($sqlString, ($markPosition + 1));
664 }
665 $offset = $markPosition + strlen($parameter);
666 }
667 }
668
669 /**
670 * Adds enableFields and deletedClause to the query if necessary
671 *
672 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings
673 * @param string $tableName The database table name
674 * @param array &$sql The query parts
675 * @return void
676 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
677 */
678 protected function addVisibilityConstraintStatement(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $tableName, array &$sql) {
679 $statement = '';
680 if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
681 $ignoreEnableFields = $querySettings->getIgnoreEnableFields();
682 $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
683 $includeDeleted = $querySettings->getIncludeDeleted();
684 if ($this->environmentService->isEnvironmentInFrontendMode()) {
685 $statement .= $this->getFrontendConstraintStatement($tableName, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
686 } else {
687 // TYPO3_MODE === 'BE'
688 $statement .= $this->getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted);
689 }
690 if (!empty($statement)) {
691 $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
692 $sql['additionalWhereClause'][] = $statement;
693 }
694 }
695 }
696
697 /**
698 * Returns constraint statement for frontend context
699 *
700 * @param string $tableName
701 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
702 * @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.
703 * @param bool $includeDeleted A flag indicating whether deleted records should be included
704 * @return string
705 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
706 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
707 */
708 protected function getFrontendConstraintStatement($tableName, $ignoreEnableFields, array $enableFieldsToBeIgnored = array(), $includeDeleted) {
709 $statement = '';
710 if ($ignoreEnableFields && !$includeDeleted) {
711 if (count($enableFieldsToBeIgnored)) {
712 // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented
713 $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
714 } else {
715 $statement .= $this->getPageRepository()->deleteClause($tableName);
716 }
717 } elseif (!$ignoreEnableFields && !$includeDeleted) {
718 $statement .= $this->getPageRepository()->enableFields($tableName);
719 } elseif (!$ignoreEnableFields && $includeDeleted) {
720 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);
721 }
722 return $statement;
723 }
724
725 /**
726 * Returns constraint statement for backend context
727 *
728 * @param string $tableName
729 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
730 * @param bool $includeDeleted A flag indicating whether deleted records should be included
731 * @return string
732 * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
733 */
734 protected function getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted) {
735 $statement = '';
736 if (!$ignoreEnableFields) {
737 $statement .= BackendUtility::BEenableFields($tableName);
738 }
739 if (!$includeDeleted) {
740 $statement .= BackendUtility::deleteClause($tableName);
741 }
742 return $statement;
743 }
744
745 /**
746 * Performs workspace and language overlay on the given row array. The language and workspace id is automatically
747 * detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
748 *
749 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source (selector od join)
750 * @param array $rows
751 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
752 * @param null|integer $workspaceUid
753 * @return array
754 */
755 protected function doLanguageAndWorkspaceOverlay(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array $rows, \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $workspaceUid = NULL) {
756 if ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface) {
757 $tableName = $source->getSelectorName();
758 } elseif ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
759 $tableName = $source->getRight()->getSelectorName();
760 } else {
761 // No proper source, so we do not have a table name here
762 // we cannot do an overlay and return the original rows instead.
763 return $rows;
764 }
765
766 $pageRepository = $this->getPageRepository();
767 if (is_object($GLOBALS['TSFE'])) {
768 if ($workspaceUid !== NULL) {
769 $pageRepository->versioningWorkspaceId = $workspaceUid;
770 }
771 } else {
772 if ($workspaceUid === NULL) {
773 $workspaceUid = $GLOBALS['BE_USER']->workspace;
774 }
775 $pageRepository->versioningWorkspaceId = $workspaceUid;
776 }
777
778 $overlaidRows = array();
779 foreach ($rows as $row) {
780 // If current row is a translation select its parent
781 if (isset($tableName) && isset($GLOBALS['TCA'][$tableName])
782 && isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
783 && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
784 && !isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerTable'])
785 ) {
786 if (isset($row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']])
787 && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
788 ) {
789 $row = $this->databaseHandle->exec_SELECTgetSingleRow(
790 $tableName . '.*',
791 $tableName,
792 $tableName . '.uid=' . (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] .
793 ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0'
794 );
795 }
796 }
797 $pageRepository->versionOL($tableName, $row, TRUE);
798 if ($pageRepository->versioningPreview && isset($row['_ORIG_uid'])) {
799 $row['uid'] = $row['_ORIG_uid'];
800 }
801 if ($tableName == 'pages') {
802 $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
803 } elseif (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
804 && $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] !== ''
805 && !isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerTable'])
806 ) {
807 if (in_array($row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']], array(-1, 0))) {
808 $overlayMode = $querySettings->getLanguageMode() === 'strict' ? 'hideNonTranslated' : '';
809 $row = $pageRepository->getRecordOverlay($tableName, $row, $querySettings->getLanguageUid(), $overlayMode);
810 }
811 }
812 if ($row !== NULL && is_array($row)) {
813 $overlaidRows[] = $row;
814 }
815 }
816 return $overlaidRows;
817 }
818
819 /**
820 * @return \TYPO3\CMS\Frontend\Page\PageRepository
821 */
822 protected function getPageRepository() {
823 if (!$this->pageRepository instanceof \TYPO3\CMS\Frontend\Page\PageRepository) {
824 if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
825 $this->pageRepository = $GLOBALS['TSFE']->sys_page;
826 } else {
827 $this->pageRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Frontend\\Page\\PageRepository');
828 }
829 }
830
831 return $this->pageRepository;
832 }
833
834 /**
835 * Checks if there are SQL errors in the last query, and if yes, throw an exception.
836 *
837 * @return void
838 * @param string $sql The SQL statement
839 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException
840 */
841 protected function checkSqlErrors($sql = '') {
842 $error = $this->databaseHandle->sql_error();
843 if ($error !== '') {
844 $error .= $sql ? ': ' . $sql : '';
845 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException($error, 1247602160);
846 }
847 }
848
849 /**
850 * Clear the TYPO3 page cache for the given record.
851 * If the record lies on a page, then we clear the cache of this page.
852 * If the record has no PID column, we clear the cache of the current page as best-effort.
853 *
854 * Much of this functionality is taken from t3lib_tcemain::clear_cache() which unfortunately only works with logged-in BE user.
855 *
856 * @param string $tableName Table name of the record
857 * @param integer $uid UID of the record
858 * @return void
859 */
860 protected function clearPageCache($tableName, $uid) {
861 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
862 if (isset($frameworkConfiguration['persistence']['enableAutomaticCacheClearing']) && $frameworkConfiguration['persistence']['enableAutomaticCacheClearing'] === '1') {
863 } else {
864 // if disabled, return
865 return;
866 }
867 $pageIdsToClear = array();
868 $storagePage = NULL;
869 $columns = $this->databaseHandle->admin_get_fields($tableName);
870 if (array_key_exists('pid', $columns)) {
871 $result = $this->databaseHandle->exec_SELECTquery('pid', $tableName, 'uid=' . (int)$uid);
872 if ($row = $this->databaseHandle->sql_fetch_assoc($result)) {
873 $storagePage = $row['pid'];
874 $pageIdsToClear[] = $storagePage;
875 }
876 } elseif (isset($GLOBALS['TSFE'])) {
877 // No PID column - we can do a best-effort to clear the cache of the current page if in FE
878 $storagePage = $GLOBALS['TSFE']->id;
879 $pageIdsToClear[] = $storagePage;
880 }
881 if ($storagePage === NULL) {
882 return;
883 }
884 if (!isset($this->pageTSConfigCache[$storagePage])) {
885 $this->pageTSConfigCache[$storagePage] = BackendUtility::getPagesTSconfig($storagePage);
886 }
887 if (isset($this->pageTSConfigCache[$storagePage]['TCEMAIN.']['clearCacheCmd'])) {
888 $clearCacheCommands = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', strtolower($this->pageTSConfigCache[$storagePage]['TCEMAIN.']['clearCacheCmd']), TRUE);
889 $clearCacheCommands = array_unique($clearCacheCommands);
890 foreach ($clearCacheCommands as $clearCacheCommand) {
891 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) {
892 $pageIdsToClear[] = $clearCacheCommand;
893 }
894 }
895 }
896
897 foreach ($pageIdsToClear as $pageIdToClear) {
898 $this->cacheService->getPageIdStack()->push($pageIdToClear);
899 }
900 }
901
902 /**
903 * Finds and returns a variable value from the query cache.
904 *
905 * @param string $entryIdentifier Identifier of the cache entry to fetch
906 * @return mixed The value
907 */
908 protected function getQueryCacheEntry($entryIdentifier) {
909 if (!isset($this->queryRuntimeCache[$entryIdentifier])) {
910 $this->queryRuntimeCache[$entryIdentifier] = $this->queryCache->get($entryIdentifier);
911 }
912 return $this->queryRuntimeCache[$entryIdentifier];
913 }
914
915 /**
916 * Saves the value of a PHP variable in the query cache.
917 *
918 * @param string $entryIdentifier An identifier used for this cache entry
919 * @param mixed $variable The query to cache
920 * @return void
921 */
922 protected function setQueryCacheEntry($entryIdentifier, $variable) {
923 $this->queryRuntimeCache[$entryIdentifier] = $variable;
924 $this->queryCache->set($entryIdentifier, $variable, array(), 0);
925 }
926 }