[SECURITY] Extbase must not cache dynamic parts of queries
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Generic / Storage / Typo3DbQueryParser.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2014 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\Generic\Mapper\ColumnMap;
33 use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;
34 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
35 use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
36 use TYPO3\CMS\Extbase\Persistence\Generic\Qom;
37
38 /**
39 * QueryParser, converting the qom to string representation
40 */
41 class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface {
42
43 /**
44 * The TYPO3 database object
45 *
46 * @var \TYPO3\CMS\Core\Database\DatabaseConnection
47 */
48 protected $databaseHandle;
49
50 /**
51 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
52 * @inject
53 */
54 protected $dataMapper;
55
56 /**
57 * The TYPO3 page repository. Used for language and workspace overlay
58 *
59 * @var \TYPO3\CMS\Frontend\Page\PageRepository
60 */
61 protected $pageRepository;
62
63 /**
64 * @var \TYPO3\CMS\Core\Cache\CacheManager
65 * @inject
66 */
67 protected $cacheManager;
68
69 /**
70 * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
71 */
72 protected $tableColumnCache;
73
74 /**
75 * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
76 * @inject
77 */
78 protected $environmentService;
79
80 /**
81 * Constructor. takes the database handle from $GLOBALS['TYPO3_DB']
82 */
83 public function __construct() {
84 $this->databaseHandle = $GLOBALS['TYPO3_DB'];
85 }
86
87 /**
88 * Lifecycle method
89 *
90 * @return void
91 */
92 public function initializeObject() {
93 $this->tableColumnCache = $this->cacheManager->getCache('extbase_typo3dbbackend_tablecolumns');
94 }
95
96 /**
97 * Preparses the query and returns the query's hash and the parameters
98 *
99 * @param QueryInterface $query The query
100 * @return array the hash and the parameters
101 */
102 public function preparseQuery(QueryInterface $query) {
103 list($parameters, $operators) = $this->preparseComparison($query->getConstraint());
104 $hashPartials = array(
105 $query->getQuerySettings(),
106 $query->getSource(),
107 array_keys($parameters),
108 $operators,
109 $query->getOrderings(),
110 );
111 $hash = md5(serialize($hashPartials));
112
113 return array($hash, $parameters);
114 }
115
116 /**
117 * Walks through the qom's constraints and extracts the properties and values.
118 *
119 * In the qom the query structure and values are glued together. This walks through the
120 * qom and only extracts the parts necessary for generating the hash and filling the
121 * statement. It leaves out the actual statement generation, as it is the most time
122 * consuming.
123 *
124 * @param Qom\ConstraintInterface $comparison The constraint. Could be And-, Or-, Not- or ComparisonInterface
125 * @param string $qomPath current position of the child in the qom
126 * @return array Array of parameters and operators
127 * @throws \Exception
128 */
129 protected function preparseComparison($comparison, $qomPath = '') {
130 $parameters = array();
131 $operators = array();
132 $objectsToParse = array();
133
134 $delimiter = '';
135 if ($comparison instanceof Qom\AndInterface) {
136 $delimiter = 'AND';
137 $objectsToParse = array($comparison->getConstraint1(), $comparison->getConstraint2());
138 } elseif ($comparison instanceof Qom\OrInterface) {
139 $delimiter = 'OR';
140 $objectsToParse = array($comparison->getConstraint1(), $comparison->getConstraint2());
141 } elseif ($comparison instanceof Qom\NotInterface) {
142 $delimiter = 'NOT';
143 $objectsToParse = array($comparison->getConstraint());
144 } elseif ($comparison instanceof Qom\ComparisonInterface) {
145 $operand1 = $comparison->getOperand1();
146 $parameterIdentifier = $this->normalizeParameterIdentifier($qomPath . $operand1->getPropertyName());
147 $comparison->setParameterIdentifier($parameterIdentifier);
148 $operator = $comparison->getOperator();
149 $operand2 = $comparison->getOperand2();
150 if ($operator === QueryInterface::OPERATOR_IN) {
151 $items = array();
152 foreach ($operand2 as $value) {
153 $value = $this->getPlainValue($value);
154 if ($value !== NULL) {
155 $items[] = $value;
156 }
157 }
158 $parameters[$parameterIdentifier] = $items;
159 } else {
160 $parameters[$parameterIdentifier] = $operand2;
161 }
162 $operators[] = $operator;
163 } elseif (!is_object($comparison)) {
164 $parameters = array(array(), $comparison);
165 return array($parameters, $operators);
166 } else {
167 throw new \Exception('Can not hash Query Component "' . get_class($comparison) . '".', 1392840462);
168 }
169
170 $childObjectIterator = 0;
171 foreach ($objectsToParse as $objectToParse) {
172 list($preparsedParameters, $preparsedOperators) = $this->preparseComparison($objectToParse, $qomPath . $delimiter . $childObjectIterator++);
173 if (!empty($preparsedParameters)) {
174 $parameters = array_merge($parameters, $preparsedParameters);
175 }
176 if (!empty($preparsedOperators)) {
177 $operators = array_merge($operators, $preparsedOperators);
178 }
179 }
180
181 return array($parameters, $operators);
182 }
183
184 /**
185 * normalizes the parameter's identifier
186 *
187 * @param string $identifier
188 * @return string
189 * @todo come on, clean up that method!
190 */
191 public function normalizeParameterIdentifier($identifier) {
192 return ':' . preg_replace('/[^A-Za-z0-9]/', '', $identifier);
193 }
194
195 /**
196 * Parses the query and returns the SQL statement parts.
197 *
198 * @param QueryInterface $query The query
199 * @return array The SQL statement parts
200 */
201 public function parseQuery(QueryInterface $query) {
202 $sql = array();
203 $sql['keywords'] = array();
204 $sql['tables'] = array();
205 $sql['unions'] = array();
206 $sql['fields'] = array();
207 $sql['where'] = array();
208 $sql['additionalWhereClause'] = array();
209 $sql['orderings'] = array();
210 $sql['limit'] = ((int)$query->getLimit() ?: NULL);
211 $sql['offset'] = ((int)$query->getOffset() ?: NULL);
212 $source = $query->getSource();
213 $this->parseSource($source, $sql);
214 $this->parseConstraint($query->getConstraint(), $source, $sql);
215 $this->parseOrderings($query->getOrderings(), $source, $sql);
216
217 $tableNames = array_unique(array_keys($sql['tables'] + $sql['unions']));
218 foreach ($tableNames as $tableName) {
219 if (is_string($tableName) && !empty($tableName)) {
220 $this->addAdditionalWhereClause($query->getQuerySettings(), $tableName, $sql);
221 }
222 }
223
224 return $sql;
225 }
226
227 /**
228 * Add query parts that MUST NOT be cached.
229 * Call this function for any query
230 *
231 * @param QuerySettingsInterface $querySettings
232 * @param array $sql
233 * @throws \InvalidArgumentException
234 * @return void
235 */
236 public function addDynamicQueryParts(QuerySettingsInterface $querySettings, array &$sql) {
237 if (!isset($sql['additionalWhereClause'])) {
238 throw new \InvalidArgumentException('Invalid statement given.', 1399512421);
239 }
240 $tableNames = array_unique(array_keys($sql['tables'] + $sql['unions']));
241 foreach ($tableNames as $tableName) {
242 if (is_string($tableName) && !empty($tableName)) {
243 $this->addVisibilityConstraintStatement($querySettings, $tableName, $sql);
244 }
245 }
246 }
247
248 /**
249 * Transforms a Query Source into SQL and parameter arrays
250 *
251 * @param Qom\SourceInterface $source The source
252 * @param array &$sql
253 * @return void
254 */
255 protected function parseSource(Qom\SourceInterface $source, array &$sql) {
256 if ($source instanceof Qom\SelectorInterface) {
257 $className = $source->getNodeTypeName();
258 $tableName = $this->dataMapper->getDataMap($className)->getTableName();
259 $this->addRecordTypeConstraint($className, $sql);
260 $sql['fields'][$tableName] = $tableName . '.*';
261 $sql['tables'][$tableName] = $tableName;
262 } elseif ($source instanceof Qom\JoinInterface) {
263 $this->parseJoin($source, $sql);
264 }
265 }
266
267 /**
268 * Transforms a constraint into SQL and parameter arrays
269 *
270 * @param Qom\ConstraintInterface $constraint The constraint
271 * @param Qom\SourceInterface $source The source
272 * @param array &$sql The query parts
273 * @return void
274 */
275 protected function parseConstraint(Qom\ConstraintInterface $constraint = NULL, Qom\SourceInterface $source, array &$sql) {
276 if ($constraint instanceof Qom\AndInterface) {
277 $sql['where'][] = '(';
278 $this->parseConstraint($constraint->getConstraint1(), $source, $sql);
279 $sql['where'][] = ' AND ';
280 $this->parseConstraint($constraint->getConstraint2(), $source, $sql);
281 $sql['where'][] = ')';
282 } elseif ($constraint instanceof Qom\OrInterface) {
283 $sql['where'][] = '(';
284 $this->parseConstraint($constraint->getConstraint1(), $source, $sql);
285 $sql['where'][] = ' OR ';
286 $this->parseConstraint($constraint->getConstraint2(), $source, $sql);
287 $sql['where'][] = ')';
288 } elseif ($constraint instanceof Qom\NotInterface) {
289 $sql['where'][] = 'NOT (';
290 $this->parseConstraint($constraint->getConstraint(), $source, $sql);
291 $sql['where'][] = ')';
292 } elseif ($constraint instanceof Qom\ComparisonInterface) {
293 $this->parseComparison($constraint, $source, $sql);
294 }
295 }
296
297 /**
298 * Transforms orderings into SQL.
299 *
300 * @param array $orderings An array of orderings (Qom\Ordering)
301 * @param Qom\SourceInterface $source The source
302 * @param array &$sql The query parts
303 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException
304 * @return void
305 */
306 protected function parseOrderings(array $orderings, Qom\SourceInterface $source, array &$sql) {
307 foreach ($orderings as $propertyName => $order) {
308 switch ($order) {
309 case Qom\QueryObjectModelConstantsInterface::JCR_ORDER_ASCENDING:
310
311 case QueryInterface::ORDER_ASCENDING:
312 $order = 'ASC';
313 break;
314 case Qom\QueryObjectModelConstantsInterface::JCR_ORDER_DESCENDING:
315
316 case QueryInterface::ORDER_DESCENDING:
317 $order = 'DESC';
318 break;
319 default:
320 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException('Unsupported order encountered.', 1242816074);
321 }
322 $className = '';
323 $tableName = '';
324 if ($source instanceof Qom\SelectorInterface) {
325 $className = $source->getNodeTypeName();
326 $tableName = $this->dataMapper->convertClassNameToTableName($className);
327 while (strpos($propertyName, '.') !== FALSE) {
328 $this->addUnionStatement($className, $tableName, $propertyName, $sql);
329 }
330 } elseif ($source instanceof Qom\JoinInterface) {
331 $tableName = $source->getLeft()->getSelectorName();
332 }
333 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
334 if ($tableName !== '') {
335 $sql['orderings'][] = $tableName . '.' . $columnName . ' ' . $order;
336 } else {
337 $sql['orderings'][] = $columnName . ' ' . $order;
338 }
339 }
340 }
341
342 /**
343 * Parse a Comparison into SQL and parameter arrays.
344 *
345 * @param Qom\ComparisonInterface $comparison The comparison to parse
346 * @param Qom\SourceInterface $source The source
347 * @param array &$sql SQL query parts to add to
348 * @throws \RuntimeException
349 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException
350 * @return void
351 */
352 protected function parseComparison(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source, array &$sql) {
353 $parameterIdentifier = $this->normalizeParameterIdentifier($comparison->getParameterIdentifier());
354
355 $operator = $comparison->getOperator();
356 $operand2 = $comparison->getOperand2();
357 if ($operator === QueryInterface::OPERATOR_IN) {
358 $hasValue = FALSE;
359 foreach ($operand2 as $value) {
360 $value = $this->getPlainValue($value);
361 if ($value !== NULL) {
362 $parameters[] = $value;
363 $hasValue = TRUE;
364 }
365 }
366 if ($hasValue === FALSE) {
367 $sql['where'][] = '1<>1';
368 } else {
369 $this->parseDynamicOperand($comparison, $source, $sql);
370 }
371 } elseif ($operator === QueryInterface::OPERATOR_CONTAINS) {
372 if ($operand2 === NULL) {
373 $sql['where'][] = '1<>1';
374 } else {
375 if (!$source instanceof Qom\SelectorInterface) {
376 throw new \RuntimeException('Source is not of type "SelectorInterface"', 1395362539);
377 }
378 $className = $source->getNodeTypeName();
379 $tableName = $this->dataMapper->convertClassNameToTableName($className);
380 $operand1 = $comparison->getOperand1();
381 $propertyName = $operand1->getPropertyName();
382 while (strpos($propertyName, '.') !== FALSE) {
383 $this->addUnionStatement($className, $tableName, $propertyName, $sql);
384 }
385 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
386 $dataMap = $this->dataMapper->getDataMap($className);
387 $columnMap = $dataMap->getColumnMap($propertyName);
388 $typeOfRelation = $columnMap instanceof ColumnMap ? $columnMap->getTypeOfRelation() : NULL;
389 if ($typeOfRelation === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
390 $relationTableName = $columnMap->getRelationTableName();
391 $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
392 if (is_array($relationTableMatchFields)) {
393 $additionalWhere = array();
394 foreach ($relationTableMatchFields as $fieldName => $value) {
395 $additionalWhere[] = $fieldName . ' = ' . $this->databaseHandle->fullQuoteStr($value, $relationTableName);
396 }
397 $additionalWhereForMatchFields = ' AND ' . implode(' AND ', $additionalWhere);
398 } else {
399 $additionalWhereForMatchFields = '';
400 }
401 $sql['where'][] = $tableName . '.uid IN (SELECT ' . $columnMap->getParentKeyFieldName() . ' FROM ' . $relationTableName . ' WHERE ' . $columnMap->getChildKeyFieldName() . '=' . $parameterIdentifier . $additionalWhereForMatchFields . ')';
402 } elseif ($typeOfRelation === ColumnMap::RELATION_HAS_MANY) {
403 $parentKeyFieldName = $columnMap->getParentKeyFieldName();
404 if (isset($parentKeyFieldName)) {
405 $childTableName = $columnMap->getChildTableName();
406 $sql['where'][] = $tableName . '.uid=(SELECT ' . $childTableName . '.' . $parentKeyFieldName . ' FROM ' . $childTableName . ' WHERE ' . $childTableName . '.uid=' . $parameterIdentifier . ')';
407 } else {
408 $sql['where'][] = 'FIND_IN_SET(' . $parameterIdentifier . ', ' . $tableName . '.' . $columnName . ')';
409 }
410 } else {
411 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745);
412 }
413 }
414 } else {
415 $this->parseDynamicOperand($comparison, $source, $sql);
416 }
417 }
418
419 /**
420 * Parse a DynamicOperand into SQL and parameter arrays.
421 *
422 * @param Qom\ComparisonInterface $comparison
423 * @param Qom\SourceInterface $source The source
424 * @param array &$sql The query parts
425 * @return void
426 */
427 protected function parseDynamicOperand(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source, array &$sql) {
428 $operator = $this->resolveOperator($comparison->getOperator());
429 $operand = $comparison->getOperand1();
430
431 $constraintSQL = $this->parseOperand($operand, $source, $sql) . ' ' . $operator . ' ';
432
433 $parameterIdentifier = $this->normalizeParameterIdentifier($comparison->getParameterIdentifier());
434 if ($operator === 'IN') {
435 $parameterIdentifier = '(' . $parameterIdentifier . ')';
436 }
437 $constraintSQL .= $parameterIdentifier;
438
439 $sql['where'][] = $constraintSQL;
440 }
441
442 /**
443 * @param Qom\DynamicOperandInterface $operand
444 * @param Qom\SourceInterface $source The source
445 * @param array &$sql The query parts
446 * @return string
447 * @throws \InvalidArgumentException
448 */
449 protected function parseOperand(Qom\DynamicOperandInterface $operand, Qom\SourceInterface $source, array &$sql) {
450 if ($operand instanceof Qom\LowerCaseInterface) {
451 $constraintSQL = 'LOWER(' . $this->parseOperand($operand->getOperand(), $source, $sql) . ')';
452 } elseif ($operand instanceof Qom\UpperCaseInterface) {
453 $constraintSQL = 'UPPER(' . $this->parseOperand($operand->getOperand(), $source, $sql) . ')';
454 } elseif ($operand instanceof Qom\PropertyValueInterface) {
455 $propertyName = $operand->getPropertyName();
456 $className = '';
457 if ($source instanceof Qom\SelectorInterface) {
458 // FIXME Only necessary to differ from Join
459 $className = $source->getNodeTypeName();
460 $tableName = $this->dataMapper->convertClassNameToTableName($className);
461 while (strpos($propertyName, '.') !== FALSE) {
462 $this->addUnionStatement($className, $tableName, $propertyName, $sql);
463 }
464 } elseif ($source instanceof Qom\JoinInterface) {
465 $tableName = $source->getJoinCondition()->getSelector1Name();
466 }
467 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
468 $constraintSQL = (!empty($tableName) ? $tableName . '.' : '') . $columnName;
469 } else {
470 throw new \InvalidArgumentException('Given operand has invalid type "' . get_class($operand) . '".', 1395710211);
471 }
472 return $constraintSQL;
473 }
474
475 /**
476 * Add a constraint to ensure that the record type of the returned tuples is matching the data type of the repository.
477 *
478 * @param string $className The class name
479 * @param array &$sql The query parts
480 * @return void
481 */
482 protected function addRecordTypeConstraint($className, &$sql) {
483 if ($className !== NULL) {
484 $dataMap = $this->dataMapper->getDataMap($className);
485 if ($dataMap->getRecordTypeColumnName() !== NULL) {
486 $recordTypes = array();
487 if ($dataMap->getRecordType() !== NULL) {
488 $recordTypes[] = $dataMap->getRecordType();
489 }
490 foreach ($dataMap->getSubclasses() as $subclassName) {
491 $subclassDataMap = $this->dataMapper->getDataMap($subclassName);
492 if ($subclassDataMap->getRecordType() !== NULL) {
493 $recordTypes[] = $subclassDataMap->getRecordType();
494 }
495 }
496 if (!empty($recordTypes)) {
497 $recordTypeStatements = array();
498 foreach ($recordTypes as $recordType) {
499 $tableName = $dataMap->getTableName();
500 $recordTypeStatements[] = $tableName . '.' . $dataMap->getRecordTypeColumnName() . '=' . $this->databaseHandle->fullQuoteStr($recordType, $tableName);
501 }
502 $sql['additionalWhereClause'][] = '(' . implode(' OR ', $recordTypeStatements) . ')';
503 }
504 }
505 }
506 }
507
508 /**
509 * Adds additional WHERE statements according to the query settings.
510 *
511 * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
512 * @param string $tableName The table name to add the additional where clause for
513 * @param string &$sql
514 * @return void
515 */
516 protected function addAdditionalWhereClause(QuerySettingsInterface $querySettings, $tableName, &$sql) {
517 if ($querySettings->getRespectSysLanguage()) {
518 $this->addSysLanguageStatement($tableName, $sql, $querySettings);
519 }
520 if ($querySettings->getRespectStoragePage()) {
521 $this->addPageIdStatement($tableName, $sql, $querySettings->getStoragePageIds());
522 }
523 }
524
525 /**
526 * Adds enableFields and deletedClause to the query if necessary
527 *
528 * @param QuerySettingsInterface $querySettings
529 * @param string $tableName The database table name
530 * @param array &$sql The query parts
531 * @return void
532 */
533 protected function addVisibilityConstraintStatement(QuerySettingsInterface $querySettings, $tableName, array &$sql) {
534 $statement = '';
535 if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
536 $ignoreEnableFields = $querySettings->getIgnoreEnableFields();
537 $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
538 $includeDeleted = $querySettings->getIncludeDeleted();
539 if ($this->environmentService->isEnvironmentInFrontendMode()) {
540 $statement .= $this->getFrontendConstraintStatement($tableName, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
541 } else {
542 // TYPO3_MODE === 'BE'
543 $statement .= $this->getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted);
544 }
545 if (!empty($statement)) {
546 $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
547 $sql['additionalWhereClause'][] = $statement;
548 }
549 }
550 }
551
552 /**
553 * Returns constraint statement for frontend context
554 *
555 * @param string $tableName
556 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
557 * @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.
558 * @param bool $includeDeleted A flag indicating whether deleted records should be included
559 * @return string
560 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
561 */
562 protected function getFrontendConstraintStatement($tableName, $ignoreEnableFields, array $enableFieldsToBeIgnored = array(), $includeDeleted) {
563 $statement = '';
564 if ($ignoreEnableFields && !$includeDeleted) {
565 if (count($enableFieldsToBeIgnored)) {
566 // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented
567 $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
568 } else {
569 $statement .= $this->getPageRepository()->deleteClause($tableName);
570 }
571 } elseif (!$ignoreEnableFields && !$includeDeleted) {
572 $statement .= $this->getPageRepository()->enableFields($tableName);
573 } elseif (!$ignoreEnableFields && $includeDeleted) {
574 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);
575 }
576 return $statement;
577 }
578
579 /**
580 * Returns constraint statement for backend context
581 *
582 * @param string $tableName
583 * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
584 * @param bool $includeDeleted A flag indicating whether deleted records should be included
585 * @return string
586 */
587 protected function getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted) {
588 $statement = '';
589 if (!$ignoreEnableFields) {
590 $statement .= BackendUtility::BEenableFields($tableName);
591 }
592 if (!$includeDeleted) {
593 $statement .= BackendUtility::deleteClause($tableName);
594 }
595 return $statement;
596 }
597
598 /**
599 * Builds the language field statement
600 *
601 * @param string $tableName The database table name
602 * @param array &$sql The query parts
603 * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
604 * @return void
605 */
606 protected function addSysLanguageStatement($tableName, array &$sql, $querySettings) {
607 if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
608 if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
609 // Select all entries for the current language
610 $additionalWhereClause = $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' IN (' . (int)$querySettings->getLanguageUid() . ',-1)';
611 // If any language is set -> get those entries which are not translated yet
612 // They will be removed by t3lib_page::getRecordOverlay if not matching overlay mode
613 if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
614 && $querySettings->getLanguageUid() > 0
615 ) {
616 $additionalWhereClause .= ' OR (' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' .
617 ' AND ' . $tableName . '.uid NOT IN (SELECT ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] .
618 ' FROM ' . $tableName .
619 ' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' .
620 ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '>0';
621
622 // Add delete clause to ensure all entries are loaded
623 if (isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
624 $additionalWhereClause .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
625 }
626 $additionalWhereClause .= '))';
627 }
628 $sql['additionalWhereClause'][] = '(' . $additionalWhereClause . ')';
629 }
630 }
631 }
632
633 /**
634 * Builds the page ID checking statement
635 *
636 * @param string $tableName The database table name
637 * @param array &$sql The query parts
638 * @param array $storagePageIds list of storage page ids
639 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
640 * @return void
641 */
642 protected function addPageIdStatement($tableName, array &$sql, array $storagePageIds) {
643 $tableColumns = $this->tableColumnCache->get($tableName);
644 if ($tableColumns === FALSE) {
645 $tableColumns = $this->databaseHandle->admin_get_fields($tableName);
646 $this->tableColumnCache->set($tableName, $tableColumns);
647 }
648 if (is_array($GLOBALS['TCA'][$tableName]['ctrl']) && array_key_exists('pid', $tableColumns)) {
649 $rootLevel = (int)$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'];
650 if ($rootLevel) {
651 if ($rootLevel === 1) {
652 $sql['additionalWhereClause'][] = $tableName . '.pid = 0';
653 }
654 } else {
655 if (empty($storagePageIds)) {
656 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException('Missing storage page ids.', 1365779762);
657 }
658 $sql['additionalWhereClause'][] = $tableName . '.pid IN (' . implode(', ', $storagePageIds) . ')';
659 }
660 }
661 }
662
663 /**
664 * Transforms a Join into SQL and parameter arrays
665 *
666 * @param Qom\JoinInterface $join The join
667 * @param array &$sql The query parts
668 * @return void
669 */
670 protected function parseJoin(Qom\JoinInterface $join, array &$sql) {
671 $leftSource = $join->getLeft();
672 $leftClassName = $leftSource->getNodeTypeName();
673 $leftTableName = $leftSource->getSelectorName();
674 $this->addRecordTypeConstraint($leftClassName, $sql);
675 $rightSource = $join->getRight();
676 if ($rightSource instanceof Qom\JoinInterface) {
677 $left = $rightSource->getLeft();
678 $rightClassName = $left->getNodeTypeName();
679 $rightTableName = $left->getSelectorName();
680 } else {
681 $rightClassName = $rightSource->getNodeTypeName();
682 $rightTableName = $rightSource->getSelectorName();
683 $sql['fields'][$leftTableName] = $rightTableName . '.*';
684 }
685 $this->addRecordTypeConstraint($rightClassName, $sql);
686 $sql['tables'][$leftTableName] = $leftTableName;
687 $sql['unions'][$rightTableName] = 'LEFT JOIN ' . $rightTableName;
688 $joinCondition = $join->getJoinCondition();
689 if ($joinCondition instanceof Qom\EquiJoinCondition) {
690 $column1Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty1Name(), $leftClassName);
691 $column2Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty2Name(), $rightClassName);
692 $sql['unions'][$rightTableName] .= ' ON ' . $joinCondition->getSelector1Name() . '.' . $column1Name . ' = ' . $joinCondition->getSelector2Name() . '.' . $column2Name;
693 }
694 if ($rightSource instanceof Qom\JoinInterface) {
695 $this->parseJoin($rightSource, $sql);
696 }
697 }
698
699 /**
700 * Returns a plain value, i.e. objects are flattened out if possible.
701 *
702 * @param mixed $input
703 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
704 * @return mixed
705 */
706 protected function getPlainValue($input) {
707 if (is_array($input)) {
708 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException('An array could not be converted to a plain value.', 1274799932);
709 }
710 if ($input instanceof \DateTime) {
711 return $input->format('U');
712 } elseif (TypeHandlingUtility::isCoreType($input)) {
713 return (string) $input;
714 } elseif (is_object($input)) {
715 if ($input instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
716 $realInput = $input->_loadRealInstance();
717 } else {
718 $realInput = $input;
719 }
720 if ($realInput instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
721 return $realInput->getUid();
722 } else {
723 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);
724 }
725 } elseif (is_bool($input)) {
726 return $input === TRUE ? 1 : 0;
727 } else {
728 return $input;
729 }
730 }
731
732
733 /**
734 * adds a union statement to the query, mostly for tables referenced in the where condition.
735 *
736 * @param string &$className
737 * @param string &$tableName
738 * @param array &$propertyPath
739 * @param array &$sql
740 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
741 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException
742 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException
743 */
744 protected function addUnionStatement(&$className, &$tableName, &$propertyPath, array &$sql) {
745 $explodedPropertyPath = explode('.', $propertyPath, 2);
746 $propertyName = $explodedPropertyPath[0];
747 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
748 $tableName = $this->dataMapper->convertClassNameToTableName($className);
749 $columnMap = $this->dataMapper->getDataMap($className)->getColumnMap($propertyName);
750
751 if ($columnMap === NULL) {
752 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException('The ColumnMap for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1355142232);
753 }
754
755 $parentKeyFieldName = $columnMap->getParentKeyFieldName();
756 $childTableName = $columnMap->getChildTableName();
757
758 if ($childTableName === NULL) {
759 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException('The relation information for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1353170925);
760 }
761
762 if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_ONE) {
763 if (isset($parentKeyFieldName)) {
764 $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.uid=' . $childTableName . '.' . $parentKeyFieldName;
765 } else {
766 $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.' . $columnName . '=' . $childTableName . '.uid';
767 }
768 $className = $this->dataMapper->getType($className, $propertyName);
769 } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
770 if (isset($parentKeyFieldName)) {
771 $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.uid=' . $childTableName . '.' . $parentKeyFieldName;
772 } else {
773 $onStatement = '(FIND_IN_SET(' . $childTableName . '.uid, ' . $tableName . '.' . $columnName . '))';
774 $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $onStatement;
775 }
776 $className = $this->dataMapper->getType($className, $propertyName);
777 } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
778 $relationTableName = $columnMap->getRelationTableName();
779 $sql['unions'][$relationTableName] = 'LEFT JOIN ' . $relationTableName . ' ON ' . $tableName . '.uid=' . $relationTableName . '.' . $columnMap->getParentKeyFieldName();
780 $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $relationTableName . '.' . $columnMap->getChildKeyFieldName() . '=' . $childTableName . '.uid';
781 $className = $this->dataMapper->getType($className, $propertyName);
782 } else {
783 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Could not determine type of relation.', 1252502725);
784 }
785 // TODO check if there is another solution for this
786 $sql['keywords']['distinct'] = 'DISTINCT';
787 $propertyPath = $explodedPropertyPath[1];
788 $tableName = $childTableName;
789 }
790
791 /**
792 * Returns the SQL operator for the given JCR operator type.
793 *
794 * @param string $operator One of the JCR_OPERATOR_* constants
795 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
796 * @return string an SQL operator
797 */
798 protected function resolveOperator($operator) {
799 switch ($operator) {
800 case QueryInterface::OPERATOR_IN:
801 $operator = 'IN';
802 break;
803 case QueryInterface::OPERATOR_EQUAL_TO:
804 $operator = '=';
805 break;
806 case QueryInterface::OPERATOR_EQUAL_TO_NULL:
807 $operator = 'IS';
808 break;
809 case QueryInterface::OPERATOR_NOT_EQUAL_TO:
810 $operator = '!=';
811 break;
812 case QueryInterface::OPERATOR_NOT_EQUAL_TO_NULL:
813 $operator = 'IS NOT';
814 break;
815 case QueryInterface::OPERATOR_LESS_THAN:
816 $operator = '<';
817 break;
818 case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
819 $operator = '<=';
820 break;
821 case QueryInterface::OPERATOR_GREATER_THAN:
822 $operator = '>';
823 break;
824 case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
825 $operator = '>=';
826 break;
827 case QueryInterface::OPERATOR_LIKE:
828 $operator = 'LIKE';
829 break;
830 default:
831 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Unsupported operator encountered.', 1242816073);
832 }
833 return $operator;
834 }
835
836 /**
837 * @return \TYPO3\CMS\Frontend\Page\PageRepository
838 */
839 protected function getPageRepository() {
840 if (!$this->pageRepository instanceof \TYPO3\CMS\Frontend\Page\PageRepository) {
841 if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
842 $this->pageRepository = $GLOBALS['TSFE']->sys_page;
843 } else {
844 $this->pageRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Frontend\\Page\\PageRepository');
845 }
846 }
847
848 return $this->pageRepository;
849 }
850 }