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