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