[TASK] Replace GeneralUtility::inList() with comparison
[Packages/TYPO3.CMS.git] / typo3 / sysext / dbal / Classes / Database / SqlParser.php
1 <?php
2 namespace TYPO3\CMS\Dbal\Database;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18
19 /**
20 * PHP SQL engine / server
21 */
22 class SqlParser
23 {
24 /**
25 * Parsing error string
26 *
27 * @var string
28 */
29 public $parse_error = '';
30
31 /**
32 * Last stop keyword used.
33 *
34 * @var string
35 */
36 public $lastStopKeyWord = '';
37
38 /**
39 * Find "comparator"
40 *
41 * @var array
42 */
43 protected static $comparatorPatterns = array(
44 '<=',
45 '>=',
46 '<>',
47 '<',
48 '>',
49 '=',
50 '!=',
51 'NOT[[:space:]]+IN',
52 'IN',
53 'NOT[[:space:]]+LIKE[[:space:]]+BINARY',
54 'LIKE[[:space:]]+BINARY',
55 'NOT[[:space:]]+LIKE',
56 'LIKE',
57 'IS[[:space:]]+NOT',
58 'IS',
59 'BETWEEN',
60 'NOT[[:space]]+BETWEEN'
61 );
62
63 /**
64 * Whitespaces in a query
65 *
66 * @var array
67 */
68 protected static $interQueryWhitespaces = array(' ', TAB, CR, LF);
69
70 /**
71 * @var DatabaseConnection
72 */
73 protected $databaseConnection;
74
75 /**
76 * @var SqlCompilers\Mysql
77 */
78 protected $nativeSqlCompiler;
79
80 /**
81 * @var SqlCompilers\Adodb
82 */
83 protected $sqlCompiler;
84
85 /**
86 * @param DatabaseConnection $databaseConnection
87 */
88 public function __construct(DatabaseConnection $databaseConnection = null)
89 {
90 $this->databaseConnection = $databaseConnection ?: $GLOBALS['TYPO3_DB'];
91 $this->sqlCompiler = GeneralUtility::makeInstance(SqlCompilers\Adodb::class, $this->databaseConnection);
92 $this->nativeSqlCompiler = GeneralUtility::makeInstance(SqlCompilers\Mysql::class, $this->databaseConnection);
93 }
94
95 /**
96 * Gets value in quotes from $parseString.
97 *
98 * @param string $parseString String from which to find value in quotes. Notice that $parseString is passed by reference and is shortened by the output of this function.
99 * @param string $quote The quote used; input either " or '
100 * @return string The value, passed through parseStripslashes()!
101 */
102 protected function getValueInQuotes(&$parseString, $quote)
103 {
104 switch ((string)$this->databaseConnection->handlerCfg[$this->databaseConnection->lastHandlerKey]['type']) {
105 case 'adodb':
106 if ($this->databaseConnection->runningADOdbDriver('mssql')) {
107 $value = $this->getValueInQuotesMssql($parseString, $quote);
108 } else {
109 $value = $this->getValueInQuotesGeneric($parseString, $quote);
110 }
111 break;
112 default:
113 $value = $this->getValueInQuotesGeneric($parseString, $quote);
114 }
115 return $value;
116 }
117
118 /**
119 * Get value in quotes from $parseString.
120 * NOTICE: If a query being parsed was prepared for another database than MySQL this function should probably be changed
121 *
122 * @param string $parseString String from which to find value in quotes. Notice that $parseString is passed by reference and is shortend by the output of this function.
123 * @param string $quote The quote used; input either " or '
124 * @return string The value, passed through stripslashes() !
125 */
126 protected function getValueInQuotesGeneric(&$parseString, $quote)
127 {
128 $parts = explode($quote, substr($parseString, 1));
129 $buffer = '';
130 foreach ($parts as $k => $v) {
131 $buffer .= $v;
132 $reg = array();
133 preg_match('/\\\\$/', $v, $reg);
134 if ($reg && strlen($reg[0]) % 2) {
135 $buffer .= $quote;
136 } else {
137 $parseString = ltrim(substr($parseString, strlen($buffer) + 2));
138 return $this->parseStripslashes($buffer);
139 }
140 }
141 }
142
143 /**
144 * Gets value in quotes from $parseString. This method targets MSSQL exclusively.
145 *
146 * @param string $parseString String from which to find value in quotes. Notice that $parseString is passed by reference and is shortened by the output of this function.
147 * @param string $quote The quote used; input either " or '
148 * @return string
149 */
150 protected function getValueInQuotesMssql(&$parseString, $quote)
151 {
152 $previousIsQuote = false;
153 $inQuote = false;
154 // Go through the whole string
155 for ($c = 0; $c < strlen($parseString); $c++) {
156 // If the parsed string character is the quote string
157 if ($parseString[$c] === $quote) {
158 // If we are already in a quote
159 if ($inQuote) {
160 // Was the previous a quote?
161 if ($previousIsQuote) {
162 // If yes, replace it by a \
163 $parseString[$c - 1] = '\\';
164 }
165 // Invert the state
166 $previousIsQuote = !$previousIsQuote;
167 } else {
168 // So we are in a quote since now
169 $inQuote = true;
170 }
171 } elseif ($inQuote && $previousIsQuote) {
172 $inQuote = false;
173 $previousIsQuote = false;
174 } else {
175 $previousIsQuote = false;
176 }
177 }
178 $parts = explode($quote, substr($parseString, 1));
179 $buffer = '';
180 foreach ($parts as $v) {
181 $buffer .= $v;
182 $reg = array();
183 preg_match('/\\\\$/', $v, $reg);
184 if ($reg && strlen($reg[0]) % 2) {
185 $buffer .= $quote;
186 } else {
187 $parseString = ltrim(substr($parseString, strlen($buffer) + 2));
188 return $this->parseStripslashes($buffer);
189 }
190 }
191 return '';
192 }
193
194 /*************************************
195 *
196 * SQL Parsing, full queries
197 *
198 **************************************/
199 /**
200 * Parses any single SQL query
201 *
202 * @param string $parseString SQL query
203 * @return array Result array with all the parts in - or error message string
204 * @see compileSQL(), debug_testSQL()
205 */
206 public function parseSQL($parseString)
207 {
208 // Prepare variables:
209 $parseString = $this->trimSQL($parseString);
210 $this->parse_error = '';
211 $result = array();
212 // Finding starting keyword of string:
213 $_parseString = $parseString;
214 // Protecting original string...
215 $keyword = $this->nextPart($_parseString, '^(SELECT|UPDATE|INSERT[[:space:]]+INTO|DELETE[[:space:]]+FROM|EXPLAIN|(DROP|CREATE|ALTER|TRUNCATE)[[:space:]]+TABLE|CREATE[[:space:]]+DATABASE)[[:space:]]+');
216 $keyword = $this->normalizeKeyword($keyword);
217 switch ($keyword) {
218 case 'SELECT':
219 // Parsing SELECT query:
220 $result = $this->parseSELECT($parseString);
221 break;
222 case 'UPDATE':
223 // Parsing UPDATE query:
224 $result = $this->parseUPDATE($parseString);
225 break;
226 case 'INSERTINTO':
227 // Parsing INSERT query:
228 $result = $this->parseINSERT($parseString);
229 break;
230 case 'DELETEFROM':
231 // Parsing DELETE query:
232 $result = $this->parseDELETE($parseString);
233 break;
234 case 'EXPLAIN':
235 // Parsing EXPLAIN SELECT query:
236 $result = $this->parseEXPLAIN($parseString);
237 break;
238 case 'DROPTABLE':
239 // Parsing DROP TABLE query:
240 $result = $this->parseDROPTABLE($parseString);
241 break;
242 case 'ALTERTABLE':
243 // Parsing ALTER TABLE query:
244 $result = $this->parseALTERTABLE($parseString);
245 break;
246 case 'CREATETABLE':
247 // Parsing CREATE TABLE query:
248 $result = $this->parseCREATETABLE($parseString);
249 break;
250 case 'CREATEDATABASE':
251 // Parsing CREATE DATABASE query:
252 $result = $this->parseCREATEDATABASE($parseString);
253 break;
254 case 'TRUNCATETABLE':
255 // Parsing TRUNCATE TABLE query:
256 $result = $this->parseTRUNCATETABLE($parseString);
257 break;
258 default:
259 $result = $this->parseError('"' . $keyword . '" is not a keyword', $parseString);
260 }
261 return $result;
262 }
263
264 /**
265 * Parsing SELECT query
266 *
267 * @param string $parseString SQL string with SELECT query to parse
268 * @param array $parameterReferences Array holding references to either named (:name) or question mark (?) parameters found
269 * @return mixed Returns array with components of SELECT query on success, otherwise an error message string.
270 * @see compileSELECT()
271 */
272 protected function parseSELECT($parseString, &$parameterReferences = null)
273 {
274 // Removing SELECT:
275 $parseString = $this->trimSQL($parseString);
276 $parseString = ltrim(substr($parseString, 6));
277 // Init output variable:
278 $result = array();
279 if ($parameterReferences === null) {
280 $result['parameters'] = array();
281 $parameterReferences = &$result['parameters'];
282 }
283 $result['type'] = 'SELECT';
284 // Looking for STRAIGHT_JOIN keyword:
285 $result['STRAIGHT_JOIN'] = $this->nextPart($parseString, '^(STRAIGHT_JOIN)[[:space:]]+');
286 // Select fields:
287 $result['SELECT'] = $this->parseFieldList($parseString, '^(FROM)[[:space:]]+');
288 if ($this->parse_error) {
289 return $this->parse_error;
290 }
291 // Continue if string is not ended:
292 if ($parseString) {
293 // Get table list:
294 $result['FROM'] = $this->parseFromTables($parseString, '^(WHERE)[[:space:]]+');
295 if ($this->parse_error) {
296 return $this->parse_error;
297 }
298 // If there are more than just the tables (a WHERE clause that would be...)
299 if ($parseString) {
300 // Get WHERE clause:
301 $result['WHERE'] = $this->parseWhereClause($parseString, '^((GROUP|ORDER)[[:space:]]+BY|LIMIT)[[:space:]]+', $parameterReferences);
302 if ($this->parse_error) {
303 return $this->parse_error;
304 }
305 // If the WHERE clause parsing was stopped by GROUP BY, ORDER BY or LIMIT, then proceed with parsing:
306 if ($this->lastStopKeyWord) {
307 // GROUP BY parsing:
308 if ($this->lastStopKeyWord === 'GROUPBY') {
309 $result['GROUPBY'] = $this->parseFieldList($parseString, '^(ORDER[[:space:]]+BY|LIMIT)[[:space:]]+');
310 if ($this->parse_error) {
311 return $this->parse_error;
312 }
313 }
314 // ORDER BY parsing:
315 if ($this->lastStopKeyWord === 'ORDERBY') {
316 $result['ORDERBY'] = $this->parseFieldList($parseString, '^(LIMIT)[[:space:]]+');
317 if ($this->parse_error) {
318 return $this->parse_error;
319 }
320 }
321 // LIMIT parsing:
322 if ($this->lastStopKeyWord === 'LIMIT') {
323 if (preg_match('/^([0-9]+|[0-9]+[[:space:]]*,[[:space:]]*[0-9]+)$/', trim($parseString))) {
324 $result['LIMIT'] = $parseString;
325 } else {
326 return $this->parseError('No value for limit!', $parseString);
327 }
328 }
329 }
330 }
331 } else {
332 return $this->parseError('No table to select from!', $parseString);
333 }
334 // Store current parseString in the result array for possible further processing (e.g., subquery support by DBAL)
335 $result['parseString'] = $parseString;
336 // Return result:
337 return $result;
338 }
339
340 /**
341 * Parsing UPDATE query
342 *
343 * @param string $parseString SQL string with UPDATE query to parse
344 * @return mixed Returns array with components of UPDATE query on success, otherwise an error message string.
345 * @see compileUPDATE()
346 */
347 protected function parseUPDATE($parseString)
348 {
349 // Removing UPDATE
350 $parseString = $this->trimSQL($parseString);
351 $parseString = ltrim(substr($parseString, 6));
352 // Init output variable:
353 $result = array();
354 $result['type'] = 'UPDATE';
355 // Get table:
356 $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+');
357 // Continue if string is not ended:
358 if ($result['TABLE']) {
359 if ($parseString && $this->nextPart($parseString, '^(SET)[[:space:]]+')) {
360 $comma = true;
361 // Get field/value pairs:
362 while ($comma) {
363 if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]*=')) {
364 // Strip off "=" sign.
365 $this->nextPart($parseString, '^(=)');
366 $value = $this->getValue($parseString);
367 $result['FIELDS'][$fieldName] = $value;
368 } else {
369 return $this->parseError('No fieldname found', $parseString);
370 }
371 $comma = $this->nextPart($parseString, '^(,)');
372 }
373 // WHERE
374 if ($this->nextPart($parseString, '^(WHERE)')) {
375 $result['WHERE'] = $this->parseWhereClause($parseString);
376 if ($this->parse_error) {
377 return $this->parse_error;
378 }
379 }
380 } else {
381 return $this->parseError('Query missing SET...', $parseString);
382 }
383 } else {
384 return $this->parseError('No table found!', $parseString);
385 }
386 // Should be no more content now:
387 if ($parseString) {
388 return $this->parseError('Still content in clause after parsing!', $parseString);
389 }
390 // Return result:
391 return $result;
392 }
393
394 /**
395 * Parsing INSERT query
396 *
397 * @param string $parseString SQL string with INSERT query to parse
398 * @return mixed Returns array with components of INSERT query on success, otherwise an error message string.
399 * @see compileINSERT()
400 */
401 protected function parseINSERT($parseString)
402 {
403 // Removing INSERT
404 $parseString = $this->trimSQL($parseString);
405 $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 4));
406 // Init output variable:
407 $result = array();
408 $result['type'] = 'INSERT';
409 // Get table:
410 $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)([[:space:]]+|\\()');
411 if ($result['TABLE']) {
412 // In this case there are no field names mentioned in the SQL!
413 if ($this->nextPart($parseString, '^(VALUES)([[:space:]]+|\\()')) {
414 // Get values/fieldnames (depending...)
415 $result['VALUES_ONLY'] = $this->getValue($parseString, 'IN');
416 if ($this->parse_error) {
417 return $this->parse_error;
418 }
419 if (preg_match('/^,/', $parseString)) {
420 $result['VALUES_ONLY'] = array($result['VALUES_ONLY']);
421 $result['EXTENDED'] = '1';
422 while ($this->nextPart($parseString, '^(,)') === ',') {
423 $result['VALUES_ONLY'][] = $this->getValue($parseString, 'IN');
424 if ($this->parse_error) {
425 return $this->parse_error;
426 }
427 }
428 }
429 } else {
430 // There are apparently fieldnames listed:
431 $fieldNames = $this->getValue($parseString, '_LIST');
432 if ($this->parse_error) {
433 return $this->parse_error;
434 }
435 // "VALUES" keyword binds the fieldnames to values:
436 if ($this->nextPart($parseString, '^(VALUES)([[:space:]]+|\\()')) {
437 $result['FIELDS'] = array();
438 do {
439 // Using the "getValue" function to get the field list...
440 $values = $this->getValue($parseString, 'IN');
441 if ($this->parse_error) {
442 return $this->parse_error;
443 }
444 $insertValues = array();
445 foreach ($fieldNames as $k => $fN) {
446 if (preg_match('/^[[:alnum:]_]+$/', $fN)) {
447 if (isset($values[$k])) {
448 if (!isset($insertValues[$fN])) {
449 $insertValues[$fN] = $values[$k];
450 } else {
451 return $this->parseError('Fieldname ("' . $fN . '") already found in list!', $parseString);
452 }
453 } else {
454 return $this->parseError('No value set!', $parseString);
455 }
456 } else {
457 return $this->parseError('Invalid fieldname ("' . $fN . '")', $parseString);
458 }
459 }
460 if (isset($values[$k + 1])) {
461 return $this->parseError('Too many values in list!', $parseString);
462 }
463 $result['FIELDS'][] = $insertValues;
464 } while ($this->nextPart($parseString, '^(,)') === ',');
465 if (count($result['FIELDS']) === 1) {
466 $result['FIELDS'] = $result['FIELDS'][0];
467 } else {
468 $result['EXTENDED'] = '1';
469 }
470 } else {
471 return $this->parseError('VALUES keyword expected', $parseString);
472 }
473 }
474 } else {
475 return $this->parseError('No table found!', $parseString);
476 }
477 // Should be no more content now:
478 if ($parseString) {
479 return $this->parseError('Still content after parsing!', $parseString);
480 }
481 // Return result
482 return $result;
483 }
484
485 /**
486 * Parsing DELETE query
487 *
488 * @param string $parseString SQL string with DELETE query to parse
489 * @return mixed Returns array with components of DELETE query on success, otherwise an error message string.
490 * @see compileDELETE()
491 */
492 protected function parseDELETE($parseString)
493 {
494 // Removing DELETE
495 $parseString = $this->trimSQL($parseString);
496 $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 4));
497 // Init output variable:
498 $result = array();
499 $result['type'] = 'DELETE';
500 // Get table:
501 $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+');
502 if ($result['TABLE']) {
503 // WHERE
504 if ($this->nextPart($parseString, '^(WHERE)')) {
505 $result['WHERE'] = $this->parseWhereClause($parseString);
506 if ($this->parse_error) {
507 return $this->parse_error;
508 }
509 }
510 } else {
511 return $this->parseError('No table found!', $parseString);
512 }
513 // Should be no more content now:
514 if ($parseString) {
515 return $this->parseError('Still content in clause after parsing!', $parseString);
516 }
517 // Return result:
518 return $result;
519 }
520
521 /**
522 * Parsing EXPLAIN query
523 *
524 * @param string $parseString SQL string with EXPLAIN query to parse
525 * @return mixed Returns array with components of EXPLAIN query on success, otherwise an error message string.
526 * @see parseSELECT()
527 */
528 protected function parseEXPLAIN($parseString)
529 {
530 // Removing EXPLAIN
531 $parseString = $this->trimSQL($parseString);
532 $parseString = ltrim(substr($parseString, 6));
533 // Init output variable:
534 $result = $this->parseSELECT($parseString);
535 if (is_array($result)) {
536 $result['type'] = 'EXPLAIN';
537 }
538 return $result;
539 }
540
541 /**
542 * Parsing CREATE TABLE query
543 *
544 * @param string $parseString SQL string starting with CREATE TABLE
545 * @return mixed Returns array with components of CREATE TABLE query on success, otherwise an error message string.
546 * @see compileCREATETABLE()
547 */
548 protected function parseCREATETABLE($parseString)
549 {
550 // Removing CREATE TABLE
551 $parseString = $this->trimSQL($parseString);
552 $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 5));
553 // Init output variable:
554 $result = array();
555 $result['type'] = 'CREATETABLE';
556 // Get table:
557 $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]*\\(', true);
558 if ($result['TABLE']) {
559 // While the parseString is not yet empty:
560 while ($parseString !== '') {
561 // Getting key
562 if ($key = $this->nextPart($parseString, '^(KEY|PRIMARY KEY|UNIQUE KEY|UNIQUE)([[:space:]]+|\\()')) {
563 $key = $this->normalizeKeyword($key);
564 switch ($key) {
565 case 'PRIMARYKEY':
566 $result['KEYS']['PRIMARYKEY'] = $this->getValue($parseString, '_LIST');
567 if ($this->parse_error) {
568 return $this->parse_error;
569 }
570 break;
571 case 'UNIQUE':
572
573 case 'UNIQUEKEY':
574 if ($keyName = $this->nextPart($parseString, '^([[:alnum:]_]+)([[:space:]]+|\\()')) {
575 $result['KEYS']['UNIQUE'] = array($keyName => $this->getValue($parseString, '_LIST'));
576 if ($this->parse_error) {
577 return $this->parse_error;
578 }
579 } else {
580 return $this->parseError('No keyname found', $parseString);
581 }
582 break;
583 case 'KEY':
584 if ($keyName = $this->nextPart($parseString, '^([[:alnum:]_]+)([[:space:]]+|\\()')) {
585 $result['KEYS'][$keyName] = $this->getValue($parseString, '_LIST', 'INDEX');
586 if ($this->parse_error) {
587 return $this->parse_error;
588 }
589 } else {
590 return $this->parseError('No keyname found', $parseString);
591 }
592 break;
593 }
594 } elseif ($fieldName = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+')) {
595 // Getting field:
596 $result['FIELDS'][$fieldName]['definition'] = $this->parseFieldDef($parseString);
597 if ($this->parse_error) {
598 return $this->parse_error;
599 }
600 }
601 // Finding delimiter:
602 $delim = $this->nextPart($parseString, '^(,|\\))');
603 if (!$delim) {
604 return $this->parseError('No delimiter found', $parseString);
605 } elseif ($delim === ')') {
606 break;
607 }
608 }
609 // Finding what is after the table definition - table type in MySQL
610 if ($delim === ')') {
611 if ($this->nextPart($parseString, '^((ENGINE|TYPE)[[:space:]]*=)')) {
612 $result['engine'] = $parseString;
613 $parseString = '';
614 }
615 } else {
616 return $this->parseError('No fieldname found!', $parseString);
617 }
618 } else {
619 return $this->parseError('No table found!', $parseString);
620 }
621 // Should be no more content now:
622 if ($parseString) {
623 return $this->parseError('Still content in clause after parsing!', $parseString);
624 }
625 return $result;
626 }
627
628 /**
629 * Parsing ALTER TABLE query
630 *
631 * @param string $parseString SQL string starting with ALTER TABLE
632 * @return mixed Returns array with components of ALTER TABLE query on success, otherwise an error message string.
633 * @see compileALTERTABLE()
634 */
635 protected function parseALTERTABLE($parseString)
636 {
637 // Removing ALTER TABLE
638 $parseString = $this->trimSQL($parseString);
639 $parseString = ltrim(substr(ltrim(substr($parseString, 5)), 5));
640 // Init output variable:
641 $result = array();
642 $result['type'] = 'ALTERTABLE';
643 // Get table:
644 $hasBackquote = $this->nextPart($parseString, '^(`)') === '`';
645 $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)' . ($hasBackquote ? '`' : '') . '[[:space:]]+');
646 if ($hasBackquote && $this->nextPart($parseString, '^(`)') !== '`') {
647 return $this->parseError('No end backquote found!', $parseString);
648 }
649 if ($result['TABLE']) {
650 if ($result['action'] = $this->nextPart($parseString, '^(CHANGE|DROP[[:space:]]+KEY|DROP[[:space:]]+PRIMARY[[:space:]]+KEY|ADD[[:space:]]+KEY|ADD[[:space:]]+PRIMARY[[:space:]]+KEY|ADD[[:space:]]+UNIQUE|DROP|ADD|RENAME|DEFAULT[[:space:]]+CHARACTER[[:space:]]+SET|ENGINE)([[:space:]]+|\\(|=)')) {
651 $actionKey = $this->normalizeKeyword($result['action']);
652 // Getting field:
653 if ($actionKey === 'ADDPRIMARYKEY' || $actionKey === 'DROPPRIMARYKEY' || $actionKey === 'ENGINE' || ($fieldKey = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'))) {
654 switch ($actionKey) {
655 case 'ADD':
656 $result['FIELD'] = $fieldKey;
657 $result['definition'] = $this->parseFieldDef($parseString);
658 if ($this->parse_error) {
659 return $this->parse_error;
660 }
661 break;
662 case 'DROP':
663 case 'RENAME':
664 $result['FIELD'] = $fieldKey;
665 break;
666 case 'CHANGE':
667 $result['FIELD'] = $fieldKey;
668 if ($result['newField'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+')) {
669 $result['definition'] = $this->parseFieldDef($parseString);
670 if ($this->parse_error) {
671 return $this->parse_error;
672 }
673 } else {
674 return $this->parseError('No NEW field name found', $parseString);
675 }
676 break;
677 case 'ADDKEY':
678 case 'ADDPRIMARYKEY':
679 case 'ADDUNIQUE':
680 $result['KEY'] = $fieldKey;
681 $result['fields'] = $this->getValue($parseString, '_LIST', 'INDEX');
682 if ($this->parse_error) {
683 return $this->parse_error;
684 }
685 break;
686 case 'DROPKEY':
687 $result['KEY'] = $fieldKey;
688 break;
689 case 'DROPPRIMARYKEY':
690 // @todo ???
691 break;
692 case 'DEFAULTCHARACTERSET':
693 $result['charset'] = $fieldKey;
694 break;
695 case 'ENGINE':
696 $result['engine'] = $this->nextPart($parseString, '^=[[:space:]]*([[:alnum:]]+)[[:space:]]+', true);
697 break;
698 }
699 } else {
700 return $this->parseError('No field name found', $parseString);
701 }
702 } else {
703 return $this->parseError('No action CHANGE, DROP or ADD found!', $parseString);
704 }
705 } else {
706 return $this->parseError('No table found!', $parseString);
707 }
708 // Should be no more content now:
709 if ($parseString) {
710 return $this->parseError('Still content in clause after parsing!', $parseString);
711 }
712 return $result;
713 }
714
715 /**
716 * Parsing DROP TABLE query
717 *
718 * @param string $parseString SQL string starting with DROP TABLE
719 * @return mixed Returns array with components of DROP TABLE query on success, otherwise an error message string.
720 */
721 protected function parseDROPTABLE($parseString)
722 {
723 // Removing DROP TABLE
724 $parseString = $this->trimSQL($parseString);
725 $parseString = ltrim(substr(ltrim(substr($parseString, 4)), 5));
726 // Init output variable:
727 $result = array();
728 $result['type'] = 'DROPTABLE';
729 // IF EXISTS
730 $result['ifExists'] = $this->nextPart($parseString, '^(IF[[:space:]]+EXISTS[[:space:]]+)');
731 // Get table:
732 $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+');
733 if ($result['TABLE']) {
734 // Should be no more content now:
735 if ($parseString) {
736 return $this->parseError('Still content in clause after parsing!', $parseString);
737 }
738 return $result;
739 } else {
740 return $this->parseError('No table found!', $parseString);
741 }
742 }
743
744 /**
745 * Parsing CREATE DATABASE query
746 *
747 * @param string $parseString SQL string starting with CREATE DATABASE
748 * @return mixed Returns array with components of CREATE DATABASE query on success, otherwise an error message string.
749 */
750 protected function parseCREATEDATABASE($parseString)
751 {
752 // Removing CREATE DATABASE
753 $parseString = $this->trimSQL($parseString);
754 $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 8));
755 // Init output variable:
756 $result = array();
757 $result['type'] = 'CREATEDATABASE';
758 // Get table:
759 $result['DATABASE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+');
760 if ($result['DATABASE']) {
761 // Should be no more content now:
762 if ($parseString) {
763 return $this->parseError('Still content in clause after parsing!', $parseString);
764 }
765 return $result;
766 } else {
767 return $this->parseError('No database found!', $parseString);
768 }
769 }
770
771 /**
772 * Parsing TRUNCATE TABLE query
773 *
774 * @param string $parseString SQL string starting with TRUNCATE TABLE
775 * @return mixed Returns array with components of TRUNCATE TABLE query on success, otherwise an error message string.
776 */
777 protected function parseTRUNCATETABLE($parseString)
778 {
779 // Removing TRUNCATE TABLE
780 $parseString = $this->trimSQL($parseString);
781 $parseString = ltrim(substr(ltrim(substr($parseString, 8)), 5));
782 // Init output variable:
783 $result = array();
784 $result['type'] = 'TRUNCATETABLE';
785 // Get table:
786 $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+');
787 if ($result['TABLE']) {
788 // Should be no more content now:
789 if ($parseString) {
790 return $this->parseError('Still content in clause after parsing!', $parseString);
791 }
792 return $result;
793 } else {
794 return $this->parseError('No table found!', $parseString);
795 }
796 }
797
798 /**************************************
799 *
800 * SQL Parsing, helper functions for parts of queries
801 *
802 **************************************/
803 /**
804 * Parsing the fields in the "SELECT [$selectFields] FROM" part of a query into an array.
805 * The output from this function can be compiled back into a field list with ->compileFieldList()
806 * Will detect the keywords "DESC" and "ASC" after the table name; thus is can be used for parsing the more simply ORDER BY and GROUP BY field lists as well!
807 *
808 * @param string $parseString The string with fieldnames, eg. "title, uid AS myUid, max(tstamp), count(*)" etc. NOTICE: passed by reference!
809 * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(FROM)([[:space:]]*)'
810 * @return array If successful parsing, returns an array, otherwise an error string.
811 * @see compileFieldList()
812 */
813 public function parseFieldList(&$parseString, $stopRegex = '')
814 {
815 $stack = array();
816 // Contains the parsed content
817 if ($parseString === '') {
818 return $stack;
819 }
820 // @todo - should never happen, why does it?
821 // Pointer to positions in $stack
822 $pnt = 0;
823 // Indicates the parenthesis level we are at.
824 $level = 0;
825 // Recursivity brake.
826 $loopExit = 0;
827 // Prepare variables:
828 $parseString = $this->trimSQL($parseString);
829 $this->lastStopKeyWord = '';
830 $this->parse_error = '';
831 // Parse any SQL hint / comments
832 $stack[$pnt]['comments'] = $this->nextPart($parseString, '^(\\/\\*.*\\*\\/)');
833 // $parseString is continuously shortened by the process and we keep parsing it till it is zero:
834 while ($parseString !== '') {
835 // Checking if we are inside / outside parenthesis (in case of a function like count(), max(), min() etc...):
836 // Inside parenthesis here (does NOT detect if values in quotes are used, the only token is ")" or "("):
837 if ($level > 0) {
838 // Accumulate function content until next () parenthesis:
839 $funcContent = $this->nextPart($parseString, '^([^()]*.)');
840 $stack[$pnt]['func_content.'][] = array(
841 'level' => $level,
842 'func_content' => substr($funcContent, 0, -1)
843 );
844 $stack[$pnt]['func_content'] .= $funcContent;
845 // Detecting ( or )
846 switch (substr($stack[$pnt]['func_content'], -1)) {
847 case '(':
848 $level++;
849 break;
850 case ')':
851 $level--;
852 // If this was the last parenthesis:
853 if (!$level) {
854 $stack[$pnt]['func_content'] = substr($stack[$pnt]['func_content'], 0, -1);
855 // Remove any whitespace after the parenthesis.
856 $parseString = ltrim($parseString);
857 }
858 break;
859 }
860 } else {
861 // Outside parenthesis, looking for next field:
862 // Looking for a flow-control construct (only known constructs supported)
863 if (preg_match('/^case([[:space:]][[:alnum:]\\*._]+)?[[:space:]]when/i', $parseString)) {
864 $stack[$pnt]['type'] = 'flow-control';
865 $stack[$pnt]['flow-control'] = $this->parseCaseStatement($parseString);
866 // Looking for "AS" alias:
867 if ($as = $this->nextPart($parseString, '^(AS)[[:space:]]+')) {
868 $stack[$pnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)(,|[[:space:]]+)');
869 $stack[$pnt]['as_keyword'] = $as;
870 }
871 } else {
872 // Looking for a known function (only known functions supported)
873 $func = $this->nextPart($parseString, '^(count|max|min|floor|sum|avg)[[:space:]]*\\(');
874 if ($func) {
875 // Strip off "("
876 $parseString = trim(substr($parseString, 1));
877 $stack[$pnt]['type'] = 'function';
878 $stack[$pnt]['function'] = $func;
879 // increse parenthesis level counter.
880 $level++;
881 } else {
882 $stack[$pnt]['distinct'] = $this->nextPart($parseString, '^(distinct[[:space:]]+)');
883 // Otherwise, look for regular fieldname:
884 if (($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)(,|[[:space:]]+)')) !== '') {
885 $stack[$pnt]['type'] = 'field';
886 // Explode fieldname into field and table:
887 $tableField = explode('.', $fieldName, 2);
888 if (count($tableField) === 2) {
889 $stack[$pnt]['table'] = $tableField[0];
890 $stack[$pnt]['field'] = $tableField[1];
891 } else {
892 $stack[$pnt]['table'] = '';
893 $stack[$pnt]['field'] = $tableField[0];
894 }
895 } else {
896 return $this->parseError('No field name found as expected in parseFieldList()', $parseString);
897 }
898 }
899 }
900 }
901 // After a function or field we look for "AS" alias and a comma to separate to the next field in the list:
902 if (!$level) {
903 // Looking for "AS" alias:
904 if ($as = $this->nextPart($parseString, '^(AS)[[:space:]]+')) {
905 $stack[$pnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)(,|[[:space:]]+)');
906 $stack[$pnt]['as_keyword'] = $as;
907 }
908 // Looking for "ASC" or "DESC" keywords (for ORDER BY)
909 if ($sDir = $this->nextPart($parseString, '^(ASC|DESC)([[:space:]]+|,)')) {
910 $stack[$pnt]['sortDir'] = $sDir;
911 }
912 // Looking for stop-keywords:
913 if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) {
914 $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord);
915 return $stack;
916 }
917 // Looking for comma (since the stop-keyword did not trigger a return...)
918 if ($parseString !== '' && !$this->nextPart($parseString, '^(,)')) {
919 return $this->parseError('No comma found as expected in parseFieldList()', $parseString);
920 }
921 // Increasing pointer:
922 $pnt++;
923 }
924 // Check recursivity brake:
925 $loopExit++;
926 if ($loopExit > 500) {
927 return $this->parseError('More than 500 loops, exiting prematurely in parseFieldList()...', $parseString);
928 }
929 }
930 // Return result array:
931 return $stack;
932 }
933
934 /**
935 * Parsing a CASE ... WHEN flow-control construct.
936 * The output from this function can be compiled back with ->compileCaseStatement()
937 *
938 * @param string $parseString The string with the CASE ... WHEN construct, eg. "CASE field WHEN 1 THEN 0 ELSE ..." etc. NOTICE: passed by reference!
939 * @return array If successful parsing, returns an array, otherwise an error string.
940 * @see compileCaseConstruct()
941 */
942 protected function parseCaseStatement(&$parseString)
943 {
944 $result = array();
945 $result['type'] = $this->nextPart($parseString, '^(case)[[:space:]]+');
946 if (!preg_match('/^when[[:space:]]+/i', $parseString)) {
947 $value = $this->getValue($parseString);
948 if (!(isset($value[1]) || is_numeric($value[0]))) {
949 $result['case_field'] = $value[0];
950 } else {
951 $result['case_value'] = $value;
952 }
953 }
954 $result['when'] = array();
955 while ($this->nextPart($parseString, '^(when)[[:space:]]')) {
956 $when = array();
957 $when['when_value'] = $this->parseWhereClause($parseString, '^(then)[[:space:]]+');
958 $when['then_value'] = $this->getValue($parseString);
959 $result['when'][] = $when;
960 }
961 if ($this->nextPart($parseString, '^(else)[[:space:]]+')) {
962 $result['else'] = $this->getValue($parseString);
963 }
964 if (!$this->nextPart($parseString, '^(end)[[:space:]]+')) {
965 return $this->parseError('No "end" keyword found as expected in parseCaseStatement()', $parseString);
966 }
967 return $result;
968 }
969
970 /**
971 * Parsing a CAST definition in the "JOIN [$parseString] ..." part of a query into an array.
972 * The success of this parsing determines if that part of the query is supported by TYPO3.
973 *
974 * @param string $parseString JOIN clause to parse. NOTICE: passed by reference!
975 * @return mixed If successful parsing, returns an array, otherwise an error string.
976 */
977 protected function parseCastStatement(&$parseString)
978 {
979 $this->nextPart($parseString, '^(CAST)[[:space:]]*');
980 $parseString = trim(substr($parseString, 1));
981 $castDefinition = array('type' => 'cast');
982 // Strip off "("
983 if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) {
984 // Parse field name into field and table:
985 $tableField = explode('.', $fieldName, 2);
986 if (count($tableField) === 2) {
987 $castDefinition['table'] = $tableField[0];
988 $castDefinition['field'] = $tableField[1];
989 } else {
990 $castDefinition['table'] = '';
991 $castDefinition['field'] = $tableField[0];
992 }
993 } else {
994 return $this->parseError('No casted join field found in parseCastStatement()!', $parseString);
995 }
996 if ($this->nextPart($parseString, '^([[:space:]]*AS[[:space:]]*)')) {
997 $castDefinition['datatype'] = $this->getValue($parseString);
998 }
999 if (!$this->nextPart($parseString, '^([)])')) {
1000 return $this->parseError('No end parenthesis at end of CAST function', $parseString);
1001 }
1002 return $castDefinition;
1003 }
1004
1005 /**
1006 * Parsing the tablenames in the "FROM [$parseString] WHERE" part of a query into an array.
1007 * The success of this parsing determines if that part of the query is supported by TYPO3.
1008 *
1009 * @param string $parseString List of tables, eg. "pages, tt_content" or "pages A, pages B". NOTICE: passed by reference!
1010 * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(WHERE)([[:space:]]*)'
1011 * @return array If successful parsing, returns an array, otherwise an error string.
1012 * @see compileFromTables()
1013 */
1014 public function parseFromTables(&$parseString, $stopRegex = '')
1015 {
1016 // Prepare variables:
1017 $parseString = $this->trimSQL($parseString);
1018 $this->lastStopKeyWord = '';
1019 $this->parse_error = '';
1020 // Contains the parsed content
1021 $stack = array();
1022 // Pointer to positions in $stack
1023 $pnt = 0;
1024 // Recursivity brake.
1025 $loopExit = 0;
1026 // $parseString is continously shortend by the process and we keep parsing it till it is zero:
1027 while ($parseString !== '') {
1028 // Looking for the table:
1029 if ($stack[$pnt]['table'] = $this->nextPart($parseString, '^([[:alnum:]_]+)(,|[[:space:]]+)')) {
1030 // Looking for stop-keywords before fetching potential table alias:
1031 if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) {
1032 $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord);
1033 return $stack;
1034 }
1035 if (!preg_match('/^(LEFT|RIGHT|JOIN|INNER)[[:space:]]+/i', $parseString)) {
1036 $stack[$pnt]['as_keyword'] = $this->nextPart($parseString, '^(AS[[:space:]]+)');
1037 $stack[$pnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]*');
1038 }
1039 } else {
1040 return $this->parseError('No table name found as expected in parseFromTables()!', $parseString);
1041 }
1042 // Looking for JOIN
1043 $joinCnt = 0;
1044 while ($join = $this->nextPart($parseString, '^(((INNER|(LEFT|RIGHT)([[:space:]]+OUTER)?)[[:space:]]+)?JOIN)[[:space:]]+')) {
1045 $stack[$pnt]['JOIN'][$joinCnt]['type'] = $join;
1046 if ($stack[$pnt]['JOIN'][$joinCnt]['withTable'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+', 1)) {
1047 if (!preg_match('/^ON[[:space:]]+/i', $parseString)) {
1048 $stack[$pnt]['JOIN'][$joinCnt]['as_keyword'] = $this->nextPart($parseString, '^(AS[[:space:]]+)');
1049 $stack[$pnt]['JOIN'][$joinCnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+');
1050 }
1051 if (!$this->nextPart($parseString, '^(ON[[:space:]]+)')) {
1052 return $this->parseError('No join condition found in parseFromTables()!', $parseString);
1053 }
1054 $stack[$pnt]['JOIN'][$joinCnt]['ON'] = array();
1055 $condition = array('operator' => '');
1056 $parseCondition = true;
1057 while ($parseCondition) {
1058 if (($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)[[:space:]]*(<=|>=|<|>|=|!=)')) !== '') {
1059 // Parse field name into field and table:
1060 $tableField = explode('.', $fieldName, 2);
1061 $condition['left'] = array();
1062 if (count($tableField) === 2) {
1063 $condition['left']['table'] = $tableField[0];
1064 $condition['left']['field'] = $tableField[1];
1065 } else {
1066 $condition['left']['table'] = '';
1067 $condition['left']['field'] = $tableField[0];
1068 }
1069 } elseif (preg_match('/^CAST[[:space:]]*[(]/i', $parseString)) {
1070 $condition['left'] = $this->parseCastStatement($parseString);
1071 // Return the parse error
1072 if (!is_array($condition['left'])) {
1073 return $condition['left'];
1074 }
1075 } else {
1076 return $this->parseError('No join field found in parseFromTables()!', $parseString);
1077 }
1078 // Find "comparator":
1079 $condition['comparator'] = $this->nextPart($parseString, '^(<=|>=|<|>|=|!=)');
1080 if (preg_match('/^CAST[[:space:]]*[(]/i', $parseString)) {
1081 $condition['right'] = $this->parseCastStatement($parseString);
1082 // Return the parse error
1083 if (!is_array($condition['right'])) {
1084 return $condition['right'];
1085 }
1086 } elseif (($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)')) !== '') {
1087 // Parse field name into field and table:
1088 $tableField = explode('.', $fieldName, 2);
1089 $condition['right'] = array();
1090 if (count($tableField) === 2) {
1091 $condition['right']['table'] = $tableField[0];
1092 $condition['right']['field'] = $tableField[1];
1093 } else {
1094 $condition['right']['table'] = '';
1095 $condition['right']['field'] = $tableField[0];
1096 }
1097 } elseif ($value = $this->getValue($parseString)) {
1098 $condition['right']['value'] = $value;
1099 } else {
1100 return $this->parseError('No join field found in parseFromTables()!', $parseString);
1101 }
1102 $stack[$pnt]['JOIN'][$joinCnt]['ON'][] = $condition;
1103 if (($operator = $this->nextPart($parseString, '^(AND|OR)')) !== '') {
1104 $condition = array('operator' => $operator);
1105 } else {
1106 $parseCondition = false;
1107 }
1108 }
1109 $joinCnt++;
1110 } else {
1111 return $this->parseError('No join table found in parseFromTables()!', $parseString);
1112 }
1113 }
1114 // Looking for stop-keywords:
1115 if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) {
1116 $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord);
1117 return $stack;
1118 }
1119 // Looking for comma:
1120 if ($parseString !== '' && !$this->nextPart($parseString, '^(,)')) {
1121 return $this->parseError('No comma found as expected in parseFromTables()', $parseString);
1122 }
1123 // Increasing pointer:
1124 $pnt++;
1125 // Check recursivity brake:
1126 $loopExit++;
1127 if ($loopExit > 500) {
1128 return $this->parseError('More than 500 loops, exiting prematurely in parseFromTables()...', $parseString);
1129 }
1130 }
1131 // Return result array:
1132 return $stack;
1133 }
1134
1135 /**
1136 * Parsing the WHERE clause fields in the "WHERE [$parseString] ..." part of a query into a multidimensional array.
1137 * The success of this parsing determines if that part of the query is supported by TYPO3.
1138 *
1139 * @param string $parseString WHERE clause to parse. NOTICE: passed by reference!
1140 * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(GROUP BY|ORDER BY|LIMIT)([[:space:]]*)'
1141 * @param array $parameterReferences Array holding references to either named (:name) or question mark (?) parameters found
1142 * @return mixed If successful parsing, returns an array, otherwise an error string.
1143 */
1144 public function parseWhereClause(&$parseString, $stopRegex = '', array &$parameterReferences = array())
1145 {
1146 // Prepare variables:
1147 $parseString = $this->trimSQL($parseString);
1148 $this->lastStopKeyWord = '';
1149 $this->parse_error = '';
1150 // Contains the parsed content
1151 $stack = array(0 => array());
1152 // Pointer to positions in $stack
1153 $pnt = array(0 => 0);
1154 // Determines parenthesis level
1155 $level = 0;
1156 // Recursivity brake.
1157 $loopExit = 0;
1158 // $parseString is continuously shortened by the process and we keep parsing it till it is zero:
1159 while ($parseString !== '') {
1160 // Look for next parenthesis level:
1161 $newLevel = $this->nextPart($parseString, '^([(])');
1162 // If new level is started, manage stack/pointers:
1163 if ($newLevel === '(') {
1164 // Increase level
1165 $level++;
1166 // Reset pointer for this level
1167 $pnt[$level] = 0;
1168 // Reset stack for this level
1169 $stack[$level] = array();
1170 } else {
1171 // If no new level is started, just parse the current level:
1172 // Find "modifier", eg. "NOT or !"
1173 $stack[$level][$pnt[$level]]['modifier'] = trim($this->nextPart($parseString, '^(!|NOT[[:space:]]+)'));
1174 // See if condition is EXISTS with a subquery
1175 if (preg_match('/^EXISTS[[:space:]]*[(]/i', $parseString)) {
1176 $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(EXISTS)[[:space:]]*');
1177 // Strip off "("
1178 $parseString = trim(substr($parseString, 1));
1179 $stack[$level][$pnt[$level]]['func']['subquery'] = $this->parseSELECT($parseString, $parameterReferences);
1180 // Seek to new position in parseString after parsing of the subquery
1181 $parseString = $stack[$level][$pnt[$level]]['func']['subquery']['parseString'];
1182 unset($stack[$level][$pnt[$level]]['func']['subquery']['parseString']);
1183 if (!$this->nextPart($parseString, '^([)])')) {
1184 return 'No ) parenthesis at end of subquery';
1185 }
1186 } else {
1187 // See if LOCATE function is found
1188 if (preg_match('/^LOCATE[[:space:]]*[(]/i', $parseString)) {
1189 $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(LOCATE)[[:space:]]*');
1190 // Strip off "("
1191 $parseString = trim(substr($parseString, 1));
1192 $stack[$level][$pnt[$level]]['func']['substr'] = $this->getValue($parseString);
1193 if (!$this->nextPart($parseString, '^(,)')) {
1194 return $this->parseError('No comma found as expected in parseWhereClause()', $parseString);
1195 }
1196 if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) {
1197 // Parse field name into field and table:
1198 $tableField = explode('.', $fieldName, 2);
1199 if (count($tableField) === 2) {
1200 $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0];
1201 $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1];
1202 } else {
1203 $stack[$level][$pnt[$level]]['func']['table'] = '';
1204 $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0];
1205 }
1206 } else {
1207 return $this->parseError('No field name found as expected in parseWhereClause()', $parseString);
1208 }
1209 if ($this->nextPart($parseString, '^(,)')) {
1210 $stack[$level][$pnt[$level]]['func']['pos'] = $this->getValue($parseString);
1211 }
1212 if (!$this->nextPart($parseString, '^([)])')) {
1213 return $this->parseError('No ) parenthesis at end of function', $parseString);
1214 }
1215 } elseif (preg_match('/^IFNULL[[:space:]]*[(]/i', $parseString)) {
1216 $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(IFNULL)[[:space:]]*');
1217 $parseString = trim(substr($parseString, 1));
1218 // Strip off "("
1219 if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) {
1220 // Parse field name into field and table:
1221 $tableField = explode('.', $fieldName, 2);
1222 if (count($tableField) === 2) {
1223 $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0];
1224 $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1];
1225 } else {
1226 $stack[$level][$pnt[$level]]['func']['table'] = '';
1227 $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0];
1228 }
1229 } else {
1230 return $this->parseError('No field name found as expected in parseWhereClause()', $parseString);
1231 }
1232 if ($this->nextPart($parseString, '^(,)')) {
1233 $stack[$level][$pnt[$level]]['func']['default'] = $this->getValue($parseString);
1234 }
1235 if (!$this->nextPart($parseString, '^([)])')) {
1236 return $this->parseError('No ) parenthesis at end of function', $parseString);
1237 }
1238 } elseif (preg_match('/^CAST[[:space:]]*[(]/i', $parseString)) {
1239 $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(CAST)[[:space:]]*');
1240 $parseString = trim(substr($parseString, 1));
1241 // Strip off "("
1242 if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) {
1243 // Parse field name into field and table:
1244 $tableField = explode('.', $fieldName, 2);
1245 if (count($tableField) === 2) {
1246 $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0];
1247 $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1];
1248 } else {
1249 $stack[$level][$pnt[$level]]['func']['table'] = '';
1250 $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0];
1251 }
1252 } else {
1253 return $this->parseError('No field name found as expected in parseWhereClause()', $parseString);
1254 }
1255 if ($this->nextPart($parseString, '^([[:space:]]*AS[[:space:]]*)')) {
1256 $stack[$level][$pnt[$level]]['func']['datatype'] = $this->getValue($parseString);
1257 }
1258 if (!$this->nextPart($parseString, '^([)])')) {
1259 return $this->parseError('No ) parenthesis at end of function', $parseString);
1260 }
1261 } elseif (preg_match('/^FIND_IN_SET[[:space:]]*[(]/i', $parseString)) {
1262 $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(FIND_IN_SET)[[:space:]]*');
1263 // Strip off "("
1264 $parseString = trim(substr($parseString, 1));
1265 if ($str = $this->getValue($parseString)) {
1266 $stack[$level][$pnt[$level]]['func']['str'] = $str;
1267 if ($fieldName = $this->nextPart($parseString, '^,[[:space:]]*([[:alnum:]._]+)[[:space:]]*', true)) {
1268 // Parse field name into field and table:
1269 $tableField = explode('.', $fieldName, 2);
1270 if (count($tableField) === 2) {
1271 $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0];
1272 $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1];
1273 } else {
1274 $stack[$level][$pnt[$level]]['func']['table'] = '';
1275 $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0];
1276 }
1277 } else {
1278 return $this->parseError('No field name found as expected in parseWhereClause()', $parseString);
1279 }
1280 if (!$this->nextPart($parseString, '^([)])')) {
1281 return $this->parseError('No ) parenthesis at end of function', $parseString);
1282 }
1283 } else {
1284 return $this->parseError('No item to look for found as expected in parseWhereClause()', $parseString);
1285 }
1286 } else {
1287 // Support calculated value only for:
1288 // - "&" (boolean AND)
1289 // - "+" (addition)
1290 // - "-" (substraction)
1291 // - "*" (multiplication)
1292 // - "/" (division)
1293 // - "%" (modulo)
1294 $calcOperators = '&|\\+|-|\\*|\\/|%';
1295 // Fieldname:
1296 if (($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)([[:space:]]+|' . $calcOperators . '|<=|>=|<|>|=|!=|IS)')) !== '') {
1297 // Parse field name into field and table:
1298 $tableField = explode('.', $fieldName, 2);
1299 if (count($tableField) === 2) {
1300 $stack[$level][$pnt[$level]]['table'] = $tableField[0];
1301 $stack[$level][$pnt[$level]]['field'] = $tableField[1];
1302 } else {
1303 $stack[$level][$pnt[$level]]['table'] = '';
1304 $stack[$level][$pnt[$level]]['field'] = $tableField[0];
1305 }
1306 } else {
1307 return $this->parseError('No field name found as expected in parseWhereClause()', $parseString);
1308 }
1309 // See if the value is calculated:
1310 $stack[$level][$pnt[$level]]['calc'] = $this->nextPart($parseString, '^(' . $calcOperators . ')');
1311 if ((string)$stack[$level][$pnt[$level]]['calc'] !== '') {
1312 // Finding value for calculation:
1313 $calc_value = $this->getValue($parseString);
1314 $stack[$level][$pnt[$level]]['calc_value'] = $calc_value;
1315 if (count($calc_value) === 1 && is_string($calc_value[0])) {
1316 // Value is a field, store it to allow DBAL to post-process it (quoting, remapping)
1317 $tableField = explode('.', $calc_value[0], 2);
1318 if (count($tableField) === 2) {
1319 $stack[$level][$pnt[$level]]['calc_table'] = $tableField[0];
1320 $stack[$level][$pnt[$level]]['calc_field'] = $tableField[1];
1321 } else {
1322 $stack[$level][$pnt[$level]]['calc_table'] = '';
1323 $stack[$level][$pnt[$level]]['calc_field'] = $tableField[0];
1324 }
1325 }
1326 }
1327 }
1328 $stack[$level][$pnt[$level]]['comparator'] = $this->nextPart($parseString, '^(' . implode('|', self::$comparatorPatterns) . ')');
1329 if ($stack[$level][$pnt[$level]]['comparator'] !== '') {
1330 if (preg_match('/^CONCAT[[:space:]]*\\(/', $parseString)) {
1331 $this->nextPart($parseString, '^(CONCAT[[:space:]]?[(])');
1332 $values = array(
1333 'operator' => 'CONCAT',
1334 'args' => array()
1335 );
1336 $cnt = 0;
1337 while ($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)')) {
1338 // Parse field name into field and table:
1339 $tableField = explode('.', $fieldName, 2);
1340 if (count($tableField) === 2) {
1341 $values['args'][$cnt]['table'] = $tableField[0];
1342 $values['args'][$cnt]['field'] = $tableField[1];
1343 } else {
1344 $values['args'][$cnt]['table'] = '';
1345 $values['args'][$cnt]['field'] = $tableField[0];
1346 }
1347 // Looking for comma:
1348 $this->nextPart($parseString, '^(,)');
1349 $cnt++;
1350 }
1351 // Look for ending parenthesis:
1352 $this->nextPart($parseString, '([)])');
1353 $stack[$level][$pnt[$level]]['value'] = $values;
1354 } else {
1355 $comparator = $this->normalizeKeyword($stack[$level][$pnt[$level]]['comparator']);
1356 if (($comparator === 'IN' || $comparator == 'NOT IN') && preg_match('/^[(][[:space:]]*SELECT[[:space:]]+/', $parseString)) {
1357 $this->nextPart($parseString, '^([(])');
1358 $stack[$level][$pnt[$level]]['subquery'] = $this->parseSELECT($parseString, $parameterReferences);
1359 // Seek to new position in parseString after parsing of the subquery
1360 if (!empty($stack[$level][$pnt[$level]]['subquery']['parseString'])) {
1361 $parseString = $stack[$level][$pnt[$level]]['subquery']['parseString'];
1362 unset($stack[$level][$pnt[$level]]['subquery']['parseString']);
1363 }
1364 if (!$this->nextPart($parseString, '^([)])')) {
1365 return 'No ) parenthesis at end of subquery';
1366 }
1367 } elseif ($comparator === 'BETWEEN' || $comparator === 'NOT BETWEEN') {
1368 $stack[$level][$pnt[$level]]['values'] = array();
1369 $stack[$level][$pnt[$level]]['values'][0] = $this->getValue($parseString);
1370 if (!$this->nextPart($parseString, '^(AND)')) {
1371 return $this->parseError('No AND operator found as expected in parseWhereClause()', $parseString);
1372 }
1373 $stack[$level][$pnt[$level]]['values'][1] = $this->getValue($parseString);
1374 } else {
1375 // Finding value for comparator:
1376 $stack[$level][$pnt[$level]]['value'] = &$this->getValueOrParameter($parseString, $stack[$level][$pnt[$level]]['comparator'], '', $parameterReferences);
1377 if ($this->parse_error) {
1378 return $this->parse_error;
1379 }
1380 }
1381 }
1382 }
1383 }
1384 // Finished, increase pointer:
1385 $pnt[$level]++;
1386 // Checking if we are back to level 0 and we should still decrease level,
1387 // meaning we were probably parsing as subquery and should return here:
1388 if ($level === 0 && preg_match('/^[)]/', $parseString)) {
1389 // Return the stacks lowest level:
1390 return $stack[0];
1391 }
1392 // Checking if we are back to level 0 and we should still decrease level,
1393 // meaning we were probably parsing a subquery and should return here:
1394 if ($level === 0 && preg_match('/^[)]/', $parseString)) {
1395 // Return the stacks lowest level:
1396 return $stack[0];
1397 }
1398 // Checking if the current level is ended, in that case do stack management:
1399 while ($this->nextPart($parseString, '^([)])')) {
1400 $level--;
1401 // Decrease level:
1402 // Copy stack
1403 $stack[$level][$pnt[$level]]['sub'] = $stack[$level + 1];
1404 // Increase pointer of the new level
1405 $pnt[$level]++;
1406 // Make recursivity check:
1407 $loopExit++;
1408 if ($loopExit > 500) {
1409 return $this->parseError('More than 500 loops (in search for exit parenthesis), exiting prematurely in parseWhereClause()...', $parseString);
1410 }
1411 }
1412 // Detecting the operator for the next level:
1413 $op = $this->nextPart($parseString, '^(AND[[:space:]]+NOT|&&[[:space:]]+NOT|OR[[:space:]]+NOT|OR[[:space:]]+NOT|\\|\\|[[:space:]]+NOT|AND|&&|OR|\\|\\|)(\\(|[[:space:]]+)');
1414 if ($op) {
1415 // Normalize boolean operator
1416 $op = str_replace(array('&&', '||'), array('AND', 'OR'), $op);
1417 $stack[$level][$pnt[$level]]['operator'] = $op;
1418 } elseif ($parseString !== '') {
1419 // Looking for stop-keywords:
1420 if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) {
1421 $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord);
1422 return $stack[0];
1423 } else {
1424 return $this->parseError('No operator, but parsing not finished in parseWhereClause().', $parseString);
1425 }
1426 }
1427 }
1428 // Make recursivity check:
1429 $loopExit++;
1430 if ($loopExit > 500) {
1431 return $this->parseError('More than 500 loops, exiting prematurely in parseWhereClause()...', $parseString);
1432 }
1433 }
1434 // Return the stacks lowest level:
1435 return $stack[0];
1436 }
1437
1438 /**
1439 * Parsing the WHERE clause fields in the "WHERE [$parseString] ..." part of a query into a multidimensional array.
1440 * The success of this parsing determines if that part of the query is supported by TYPO3.
1441 *
1442 * @param string $parseString WHERE clause to parse. NOTICE: passed by reference!
1443 * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(GROUP BY|ORDER BY|LIMIT)([[:space:]]*)'
1444 * @return mixed If successful parsing, returns an array, otherwise an error string.
1445 */
1446 public function parseFieldDef(&$parseString, $stopRegex = '')
1447 {
1448 // Prepare variables:
1449 $parseString = $this->trimSQL($parseString);
1450 $this->lastStopKeyWord = '';
1451 $this->parse_error = '';
1452 $result = array();
1453 // Field type:
1454 if ($result['fieldType'] = $this->nextPart($parseString, '^(int|smallint|tinyint|mediumint|bigint|double|numeric|decimal|float|varchar|char|text|tinytext|mediumtext|longtext|blob|tinyblob|mediumblob|longblob|date|datetime|time|year|timestamp)([[:space:],]+|\\()')) {
1455 // Looking for value:
1456 if ($parseString[0] === '(') {
1457 $parseString = substr($parseString, 1);
1458 if ($result['value'] = $this->nextPart($parseString, '^([^)]*)')) {
1459 $parseString = ltrim(substr($parseString, 1));
1460 } else {
1461 return $this->parseError('No end-parenthesis for value found in parseFieldDef()!', $parseString);
1462 }
1463 }
1464 // Looking for keywords
1465 while ($keyword = $this->nextPart($parseString, '^(DEFAULT|NOT[[:space:]]+NULL|AUTO_INCREMENT|UNSIGNED)([[:space:]]+|,|\\))')) {
1466 $keywordCmp = $this->normalizeKeyword($keyword);
1467 $result['featureIndex'][$keywordCmp]['keyword'] = $keyword;
1468 switch ($keywordCmp) {
1469 case 'DEFAULT':
1470 $result['featureIndex'][$keywordCmp]['value'] = $this->getValue($parseString);
1471 break;
1472 }
1473 }
1474 } else {
1475 return $this->parseError('Field type unknown in parseFieldDef()!', $parseString);
1476 }
1477 return $result;
1478 }
1479
1480 /**
1481 * Checks if the submitted feature index contains a default value definition and the default value
1482 *
1483 * @param array $featureIndex A feature index as produced by parseFieldDef()
1484 * @return bool
1485 * @see \TYPO3\CMS\Core\Database\SqlParser::parseFieldDef()
1486 */
1487 public function checkEmptyDefaultValue($featureIndex)
1488 {
1489 if (!is_array($featureIndex['DEFAULT']['value'])) {
1490 return true;
1491 }
1492 return !is_numeric($featureIndex['DEFAULT']['value'][0]) && empty($featureIndex['DEFAULT']['value'][0]);
1493 }
1494
1495 /************************************
1496 *
1497 * Parsing: Helper functions
1498 *
1499 ************************************/
1500 /**
1501 * Strips off a part of the parseString and returns the matching part.
1502 * Helper function for the parsing methods.
1503 *
1504 * @param string $parseString Parse string; if $regex finds anything the value of the first () level will be stripped of the string in the beginning. Further $parseString is left-trimmed (on success). Notice; parsestring is passed by reference.
1505 * @param string $regex Regex to find a matching part in the beginning of the string. Rules: You MUST start the regex with "^" (finding stuff in the beginning of string) and the result of the first parenthesis is what will be returned to you (and stripped of the string). Eg. '^(AND|OR|&&)[[:space:]]+' will return AND, OR or && if found and having one of more whitespaces after it, plus shorten $parseString with that match and any space after (by ltrim())
1506 * @param bool $trimAll If set the full match of the regex is stripped of the beginning of the string!
1507 * @return string The value of the first parenthesis level of the REGEX.
1508 */
1509 protected function nextPart(&$parseString, $regex, $trimAll = false)
1510 {
1511 $reg = array();
1512 // Adding space char because [[:space:]]+ is often a requirement in regex's
1513 if (preg_match('/' . $regex . '/i', $parseString . ' ', $reg)) {
1514 $parseString = ltrim(substr($parseString, strlen($reg[$trimAll ? 0 : 1])));
1515 return $reg[1];
1516 }
1517 // No match found
1518 return '';
1519 }
1520
1521 /**
1522 * Normalizes the keyword by removing any separator and changing to uppercase
1523 *
1524 * @param string $keyword The keyword being normalized
1525 * @return string
1526 */
1527 public static function normalizeKeyword($keyword)
1528 {
1529 return strtoupper(str_replace(self::$interQueryWhitespaces, '', $keyword));
1530 }
1531
1532 /**
1533 * Finds value or either named (:name) or question mark (?) parameter markers at the beginning
1534 * of $parseString, returns result and strips it of parseString.
1535 * This method returns a pointer to the parameter or value that was found. In case of a parameter
1536 * the pointer is a reference to the corresponding item in array $parameterReferences.
1537 *
1538 * @param string $parseString The parseString
1539 * @param string $comparator The comparator used before.
1540 * @param string $mode The mode, e.g., "INDEX
1541 * @param mixed $parameterReferences The value (string/integer) or parameter (:name/?). Otherwise an array with error message in first key (0)
1542 * @return mixed
1543 */
1544 protected function &getValueOrParameter(&$parseString, $comparator = '', $mode = '', array &$parameterReferences = array())
1545 {
1546 $parameter = $this->nextPart($parseString, '^(\\:[[:alnum:]_]+|\\?)');
1547 if ($parameter === '?') {
1548 if (!isset($parameterReferences['?'])) {
1549 $parameterReferences['?'] = array();
1550 }
1551 $value = array('?');
1552 $parameterReferences['?'][] = &$value;
1553 } elseif ($parameter !== '') {
1554 // named parameter
1555 if (isset($parameterReferences[$parameter])) {
1556 // Use the same reference as last time we encountered this parameter
1557 $value = &$parameterReferences[$parameter];
1558 } else {
1559 $value = array($parameter);
1560 $parameterReferences[$parameter] = &$value;
1561 }
1562 } else {
1563 $value = $this->getValue($parseString, $comparator, $mode);
1564 }
1565 return $value;
1566 }
1567
1568 /**
1569 * Finds value in beginning of $parseString, returns result and strips it of parseString
1570 *
1571 * @param string $parseString The parseString, eg. "(0,1,2,3) ..." or "('asdf','qwer') ..." or "1234 ..." or "'My string value here' ...
1572 * @param string $comparator The comparator used before. If "NOT IN" or "IN" then the value is expected to be a list of values. Otherwise just an integer (un-quoted) or string (quoted)
1573 * @param string $mode The mode, eg. "INDEX
1574 * @return mixed The value (string/integer). Otherwise an array with error message in first key (0)
1575 */
1576 protected function getValue(&$parseString, $comparator = '', $mode = '')
1577 {
1578 $value = '';
1579 $comparator = $this->normalizeKeyword($comparator);
1580 if ($comparator === 'NOTIN' || $comparator === 'IN' || $comparator === '_LIST') {
1581 // List of values:
1582 if ($this->nextPart($parseString, '^([(])')) {
1583 $listValues = array();
1584 $comma = ',';
1585 while ($comma === ',') {
1586 $listValues[] = $this->getValue($parseString);
1587 if ($mode === 'INDEX') {
1588 // Remove any length restriction on INDEX definition
1589 $this->nextPart($parseString, '^([(]\\d+[)])');
1590 }
1591 $comma = $this->nextPart($parseString, '^([,])');
1592 }
1593 $out = $this->nextPart($parseString, '^([)])');
1594 if ($out) {
1595 if ($comparator === '_LIST') {
1596 $kVals = array();
1597 foreach ($listValues as $vArr) {
1598 $kVals[] = $vArr[0];
1599 }
1600 return $kVals;
1601 } else {
1602 return $listValues;
1603 }
1604 } else {
1605 return array($this->parseError('No ) parenthesis in list', $parseString));
1606 }
1607 } else {
1608 return array($this->parseError('No ( parenthesis starting the list', $parseString));
1609 }
1610 } else {
1611 // Just plain string value, in quotes or not:
1612 // Quote?
1613 $firstChar = $parseString[0];
1614 switch ($firstChar) {
1615 case '"':
1616 $value = array($this->getValueInQuotes($parseString, '"'), '"');
1617 break;
1618 case '\'':
1619 $value = array($this->getValueInQuotes($parseString, '\''), '\'');
1620 break;
1621 default:
1622 $reg = array();
1623 if (preg_match('/^([[:alnum:]._-]+(?:\\([0-9]+\\))?)/i', $parseString, $reg)) {
1624 $parseString = ltrim(substr($parseString, strlen($reg[0])));
1625 $value = array($reg[1]);
1626 }
1627 }
1628 }
1629 return $value;
1630 }
1631
1632 /**
1633 * Strip slashes function used for parsing
1634 * NOTICE: If a query being parsed was prepared for another database than MySQL this function should probably be changed
1635 *
1636 * @param string $str Input string
1637 * @return string Output string
1638 */
1639 protected function parseStripslashes($str)
1640 {
1641 $search = array('\\\\', '\\\'', '\\"', '\0', '\n', '\r', '\Z');
1642 $replace = array('\\', '\'', '"', "\x00", "\x0a", "\x0d", "\x1a");
1643
1644 return str_replace($search, $replace, $str);
1645 }
1646
1647 /**
1648 * Setting the internal error message value, $this->parse_error and returns that value.
1649 *
1650 * @param string $msg Input error message
1651 * @param string $restQuery Remaining query to parse.
1652 * @return string Error message.
1653 */
1654 protected function parseError($msg, $restQuery)
1655 {
1656 $this->parse_error = 'SQL engine parse ERROR: ' . $msg . ': near "' . substr($restQuery, 0, 50) . '"';
1657 return $this->parse_error;
1658 }
1659
1660 /**
1661 * Trimming SQL as preparation for parsing.
1662 * ";" in the end is stripped off.
1663 * White space is trimmed away around the value
1664 * A single space-char is added in the end
1665 *
1666 * @param string $str Input string
1667 * @return string Output string
1668 */
1669 protected function trimSQL($str)
1670 {
1671 return rtrim(rtrim(trim($str), ';')) . ' ';
1672 }
1673
1674 /*************************
1675 *
1676 * Compiling queries
1677 *
1678 *************************/
1679
1680 /**
1681 * Compiles an SQL query from components
1682 *
1683 * @param array $components Array of SQL query components
1684 * @return string SQL query
1685 * @see parseSQL()
1686 */
1687 public function compileSQL($components)
1688 {
1689 return $this->getSqlCompiler()->compileSQL($components);
1690 }
1691
1692 /**
1693 * Compiles a "SELECT [output] FROM..:" field list based on input array (made with ->parseFieldList())
1694 * Can also compile field lists for ORDER BY and GROUP BY.
1695 *
1696 * @param array $selectFields Array of select fields, (made with ->parseFieldList())
1697 * @param bool $compileComments Whether comments should be compiled
1698 * @return string Select field string
1699 * @see parseFieldList()
1700 */
1701 public function compileFieldList($selectFields, $compileComments = true)
1702 {
1703 return $this->getSqlCompiler()->compileFieldList($selectFields, $compileComments);
1704 }
1705
1706 /**
1707 * Compiles a "FROM [output] WHERE..:" table list based on input array (made with ->parseFromTables())
1708 *
1709 * @param array $tablesArray Array of table names, (made with ->parseFromTables())
1710 * @return string Table name string
1711 * @see parseFromTables()
1712 */
1713 public function compileFromTables($tablesArray)
1714 {
1715 return $this->getSqlCompiler()->compileFromTables($tablesArray);
1716 }
1717
1718 /**
1719 * Compile field definition
1720 *
1721 * @param array $fieldCfg Field definition parts
1722 * @return string Field definition string
1723 */
1724 public function compileFieldCfg($fieldCfg)
1725 {
1726 return $this->getSqlCompiler()->compileFieldCfg($fieldCfg);
1727 }
1728
1729 /**
1730 * Implodes an array of WHERE clause configuration into a WHERE clause.
1731 *
1732 * DBAL-specific: The only(!) handled "calc" operators supported by parseWhereClause() are:
1733 * - the bitwise logical and (&)
1734 * - the addition (+)
1735 * - the substraction (-)
1736 * - the multiplication (*)
1737 * - the division (/)
1738 * - the modulo (%)
1739 *
1740 * @param array $clauseArray
1741 * @param bool $functionMapping
1742 * @return string WHERE clause as string.
1743 * @see \TYPO3\CMS\Core\Database\SqlParser::parseWhereClause()
1744 */
1745 public function compileWhereClause($clauseArray, $functionMapping = true)
1746 {
1747 return $this->getSqlCompiler()->compileWhereClause($clauseArray, $functionMapping);
1748 }
1749
1750 /**
1751 * @return \TYPO3\CMS\Dbal\Database\SqlCompilers\Adodb|\TYPO3\CMS\Dbal\Database\SqlCompilers\Mysql
1752 */
1753 protected function getSqlCompiler()
1754 {
1755 if ((string)$this->databaseConnection->handlerCfg[$this->databaseConnection->lastHandlerKey]['type'] === 'native') {
1756 return $this->nativeSqlCompiler;
1757 } else {
1758 return $this->sqlCompiler;
1759 }
1760 }
1761
1762 /*************************
1763 *
1764 * Debugging
1765 *
1766 *************************/
1767 /**
1768 * Performs the ultimate test of the parser: Direct a SQL query in; You will get it back (through the parsed and re-compiled) if no problems, otherwise the script will print the error and exit
1769 *
1770 * @param string $SQLquery SQL query
1771 * @return string Query if all is well, otherwise exit.
1772 */
1773 public function debug_testSQL($SQLquery)
1774 {
1775 // Getting result array:
1776 $parseResult = $this->parseSQL($SQLquery);
1777 // If result array was returned, proceed. Otherwise show error and exit.
1778 if (is_array($parseResult)) {
1779 // Re-compile query:
1780 $newQuery = $this->compileSQL($parseResult);
1781 // TEST the new query:
1782 $testResult = $this->debug_parseSQLpartCompare($SQLquery, $newQuery);
1783 // Return new query if OK, otherwise show error and exit:
1784 if (!is_array($testResult)) {
1785 return $newQuery;
1786 } else {
1787 debug(array('ERROR MESSAGE' => 'Input query did not match the parsed and recompiled query exactly (not observing whitespace)', 'TEST result' => $testResult), 'SQL parsing failed:');
1788 die;
1789 }
1790 } else {
1791 debug(array('query' => $SQLquery, 'ERROR MESSAGE' => $parseResult), 'SQL parsing failed:');
1792 die;
1793 }
1794 }
1795
1796 /**
1797 * Check parsability of input SQL part string; Will parse and re-compile after which it is compared
1798 *
1799 * @param string $part Part definition of string; "SELECT" = fieldlist (also ORDER BY and GROUP BY), "FROM" = table list, "WHERE" = Where clause.
1800 * @param string $str SQL string to verify parsability of
1801 * @return mixed Returns array with string 1 and 2 if error, otherwise FALSE
1802 */
1803 public function debug_parseSQLpart($part, $str)
1804 {
1805 $retVal = false;
1806 switch ($part) {
1807 case 'SELECT':
1808 $retVal = $this->debug_parseSQLpartCompare($str, $this->compileFieldList($this->parseFieldList($str)));
1809 break;
1810 case 'FROM':
1811 $retVal = $this->debug_parseSQLpartCompare($str, $this->getSqlCompiler()->compileFromTables($this->parseFromTables($str)));
1812 break;
1813 case 'WHERE':
1814 $retVal = $this->debug_parseSQLpartCompare($str, $this->getSqlCompiler()->compileWhereClause($this->parseWhereClause($str)));
1815 break;
1816 }
1817 return $retVal;
1818 }
1819
1820 /**
1821 * Compare two query strings by stripping away whitespace.
1822 *
1823 * @param string $str SQL String 1
1824 * @param string $newStr SQL string 2
1825 * @param bool $caseInsensitive If TRUE, the strings are compared insensitive to case
1826 * @return mixed Returns array with string 1 and 2 if error, otherwise FALSE
1827 */
1828 public function debug_parseSQLpartCompare($str, $newStr, $caseInsensitive = false)
1829 {
1830 if ($caseInsensitive) {
1831 $str1 = strtoupper($str);
1832 $str2 = strtoupper($newStr);
1833 } else {
1834 $str1 = $str;
1835 $str2 = $newStr;
1836 }
1837
1838 // Fixing escaped chars:
1839 $search = array(NUL, LF, CR, SUB);
1840 $replace = array("\x00", "\x0a", "\x0d", "\x1a");
1841 $str1 = str_replace($search, $replace, $str1);
1842 $str2 = str_replace($search, $replace, $str2);
1843
1844 $search = self::$interQueryWhitespaces;
1845 if (str_replace($search, '', $this->trimSQL($str1)) !== str_replace($search, '', $this->trimSQL($str2))) {
1846 return array(
1847 str_replace($search, ' ', $str),
1848 str_replace($search, ' ', $newStr),
1849 );
1850 }
1851 }
1852 }