RDatabaseSqlparserSqlparser   F
last analyzed

Complexity

Total Complexity 465

Size/Duplication

Total Lines 1847
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 465
eloc 923
c 1
b 0
f 0
dl 0
loc 1847
rs 1.677

41 Methods

Rating   Name   Duplication   Size   Complexity  
F process_select_expr() 0 107 20
F process_from() 0 161 42
A parse() 0 27 3
A isCommentToken() 0 4 6
A split_sql() 0 3 1
A process_group() 0 33 4
A initParseInfoForOrder() 0 3 1
A process_update() 0 2 1
A initParseInfoExprList() 0 24 3
B process_limit() 0 49 8
A getVariableType() 0 23 5
B process_insert() 0 38 7
A isUnion() 0 12 3
A isWhitespaceToken() 0 3 1
A isExpression() 0 3 2
A isSubQuery() 0 3 2
A process_into() 0 13 4
A isAggregateFunction() 0 3 2
F processSQLParts() 0 75 20
B process_order() 0 37 6
A isConstant() 0 3 2
C process_set_list() 0 56 12
B process_delete() 0 23 7
B processMySQLUnion() 0 36 7
B processUnion() 0 79 11
F process_expr_list() 0 301 96
A process_record() 0 15 3
C removeParenthesisFromStart() 0 48 12
A isColumnReference() 0 3 2
A __construct() 0 7 2
A getAssignment() 0 5 1
A isReserved() 0 3 2
A isFunction() 0 3 2
A process_values() 0 30 5
A revokeEscaping() 0 9 3
A process_select() 0 22 4
A initParseInfoForFrom() 0 13 2
A isBrackedExpression() 0 3 2
B processFromExpression() 0 58 7
F processSQL() 0 314 134
B processOrderExpression() 0 45 8

How to fix   Complexity   

Complex Class

Complex classes like RDatabaseSqlparserSqlparser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RDatabaseSqlparserSqlparser, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package     Redcore
4
 * @subpackage  Exception
5
 *
6
 * @copyright   Copyright (C) 2008 - 2021 redWEB.dk. All rights reserved.
7
 * @license     GNU General Public License version 2 or later, see LICENSE.
8
 */
9
10
defined('JPATH_REDCORE') or die;
11
12
/**
13
 * A pure PHP SQL (non validating) parser w/ focus on MySQL dialect of SQL
14
 *
15
 * Copyright (c) 2010-2012, Justin Swanhart
16
 * with contributions by André Rothe <[email protected], [email protected]>
17
 *
18
 * All rights reserved.
19
 *
20
 * Redistribution and use in source and binary forms, with or without modification,
21
 * are permitted provided that the following conditions are met:
22
 *
23
 *   * Redistributions of source code must retain the above copyright notice,
24
 *     this list of conditions and the following disclaimer.
25
 *   * Redistributions in binary form must reproduce the above copyright notice,
26
 *     this list of conditions and the following disclaimer in the documentation
27
 *     and/or other materials provided with the distribution.
28
 *
29
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
30
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
31
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
32
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
33
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
34
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
35
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
36
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
37
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
38
 * DAMAGE.
39
 */
40
41
/**
42
 * This class implements the parser functionality.
43
 * @author [email protected]
44
 * @author [email protected]
45
 */
46
class RDatabaseSqlparserSqlparser extends RDatabaseSqlparserSqlparserutils
47
{
48
	/**
49
	 * @var RDatabaseSqlparserSqllexer
50
	 */
51
	private $lexer;
52
53
	/**
54
	 * @var
55
	 */
56
	public $parsed;
57
58
	/**
59
	 * RDatabaseSqlparserSqlparser constructor.
60
	 *
61
	 * @param   boolean  $sql
62
	 * @param   boolean  $calcPositions
63
	 */
64
	public function __construct($sql = false, $calcPositions = false)
65
	{
66
		$this->lexer = new RDatabaseSqlparserSqllexer;
67
68
		if ($sql)
69
		{
70
			$this->parse($sql, $calcPositions);
71
		}
72
	}
73
74
	/**
75
	 * @param      $sql
76
	 * @param bool $calcPositions
77
	 *
78
	 * @return mixed
79
	 */
80
	public function parse($sql, $calcPositions = false)
81
	{
82
		// Lex the SQL statement
83
		$inputArray = $this->split_sql($sql);
84
85
		// This is the highest level lexical analysis.  This is the part of the
86
		// code which finds UNION and UNION ALL query parts
87
		$queries = $this->processUnion($inputArray);
88
89
		//  If there was no UNION or UNION ALL in the query, then the query is
90
		//  stored at $queries[0].
91
		if (!$this->isUnion($queries))
92
		{
93
			$queries = $this->processSQL($queries[0]);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $queries is correct as $this->processSQL($queries[0]) targeting RDatabaseSqlparserSqlparser::processSQL() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
94
		}
95
96
		// Calc the positions of some important tokens
97
		if ($calcPositions)
98
		{
99
			$calculator = new RDatabaseSqlparserPositioncalculator;
100
			$queries    = $calculator->setPositionsWithinSQL($sql, $queries);
101
		}
102
103
		// store the parsed queries
104
		$this->parsed = $queries;
105
106
		return $this->parsed;
107
	}
108
109
	private function processUnion($inputArray)
110
	{
111
		$outputArray = array();
112
113
		// sometimes the parser needs to skip ahead until a particular
114
		// token is found
115
		$skipUntilToken = false;
116
117
		// This is the last type of union used (UNION or UNION ALL)
118
		// indicates a) presence of at least one union in this query
119
		//           b) the type of union if this is the first or last query
120
		$unionType = false;
121
122
		// Sometimes a "query" consists of more than one query (like a UNION query)
123
		// this array holds all the queries
124
		$queries = array();
125
126
		foreach ($inputArray as $key => $token)
127
		{
128
			$trim = trim($token);
129
130
			// overread all tokens till that given token
131
			if ($skipUntilToken)
132
			{
133
				if ($trim === "")
134
				{
135
					continue; //  read the next token
136
				}
137
				if (strtoupper($trim) === $skipUntilToken)
138
				{
139
					$skipUntilToken = false;
140
					continue; // read the next token
141
				}
142
			}
143
144
			if (strtoupper($trim) !== "UNION")
145
			{
146
				$outputArray[] = $token; // here we get empty tokens, if we remove these, we get problems in parse_sql()
147
				continue;
148
			}
149
150
			$unionType = "UNION";
151
152
			// we are looking for an ALL token right after UNION
153
			for ($i = $key + 1; $i < count($inputArray); ++$i)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
154
			{
155
				if (trim($inputArray[$i]) === "")
156
				{
157
					continue;
158
				}
159
				if (strtoupper($inputArray[$i]) !== "ALL")
160
				{
161
					break;
162
				}
163
				// the other for-loop should overread till "ALL"
164
				$skipUntilToken = "ALL";
165
				$unionType      = "UNION ALL";
166
			}
167
168
			// store the tokens related to the unionType
169
			$queries[$unionType][] = $outputArray;
170
			$outputArray           = array();
171
		}
172
173
		// the query tokens after the last UNION or UNION ALL
174
		// or we don't have an UNION/UNION ALL
175
		if (!empty($outputArray))
176
		{
177
			if ($unionType)
178
			{
179
				$queries[$unionType][] = $outputArray;
180
			}
181
			else
182
			{
183
				$queries[] = $outputArray;
184
			}
185
		}
186
187
		return $this->processMySQLUnion($queries);
188
	}
189
190
	/** MySQL supports a special form of UNION:
191
	 * (select ...)
192
	 * union
193
	 * (select ...)
194
	 *
195
	 * This function handles this query syntax.  Only one such subquery
196
	 * is supported in each UNION block.  (select)(select)union(select) is not legal.
197
	 * The extra queries will be silently ignored.
198
	 */
199
	private function processMySQLUnion($queries)
200
	{
201
		$unionTypes = array('UNION', 'UNION ALL');
202
		foreach ($unionTypes as $unionType)
203
		{
204
205
			if (empty($queries[$unionType]))
206
			{
207
				continue;
208
			}
209
210
			foreach ($queries[$unionType] as $key => $tokenList)
211
			{
212
				foreach ($tokenList as $token)
213
				{
214
					$token = trim($token);
215
					if ($token === "")
216
					{
217
						continue;
218
					}
219
220
					// starts with "(select"
221
					if (preg_match("/^\\(\\s*select\\s*/i", $token))
222
					{
223
						$queries[$unionType][$key] = $this->parse($this->removeParenthesisFromStart($token));
224
						break;
225
					}
226
227
					$queries[$unionType][$key] = $this->processSQL($queries[$unionType][$key]);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $queries[$unionType][$key] is correct as $this->processSQL($queries[$unionType][$key]) targeting RDatabaseSqlparserSqlparser::processSQL() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
228
					break;
229
				}
230
			}
231
		}
232
233
		// it can be parsed or not
234
		return $queries;
235
	}
236
237
	private function isUnion($queries)
238
	{
239
		$unionTypes = array('UNION', 'UNION ALL');
240
		foreach ($unionTypes as $unionType)
241
		{
242
			if (!empty($queries[$unionType]))
243
			{
244
				return true;
245
			}
246
		}
247
248
		return false;
249
	}
250
251
	// this function splits up a SQL statement into easy to "parse"
252
	// tokens for the SQL processor
253
	private function split_sql($sql)
254
	{
255
		return $this->lexer->split($sql);
256
	}
257
258
	/**
259
	 * This function breaks up the SQL statement into logical sections.
260
	 * Some sections are then further handled by specialized functions.
261
	 *
262
	 * @param   array  $tokens  Token
263
	 *
264
	 * @return  void
265
	 */
266
	private function processSQL(&$tokens)
267
	{
268
		$prev_category  = "";
269
		$token_category = "";
270
		$skip_next      = false;
271
		$out            = false;
272
273
		$tokenCount = count($tokens);
274
275
		for ($tokenNumber = 0; $tokenNumber < $tokenCount; ++$tokenNumber)
276
		{
277
			$token = $tokens[$tokenNumber];
278
			$trim  = trim($token); // this removes also \n and \t!
279
280
			// if it starts with an "(", it should follow a SELECT
281
			if ($trim !== "" && $trim[0] == "(" && $token_category == "")
282
			{
283
				$token_category = 'SELECT';
284
			}
285
286
			/* If it isn't obvious, when $skip_next is set, then we ignore the next real
287
			 token, that is we ignore whitespace.
288
			 */
289
			if ($skip_next)
290
			{
291
				if ($trim === "")
292
				{
293
					if ($token_category !== "")
294
					{ // is this correct??
295
						$out[$token_category][] = $token;
296
					}
297
					continue;
298
				}
299
				// to skip the token we replace it with whitespace
300
				$trim      = "";
301
				$token     = "";
302
				$skip_next = false;
303
			}
304
305
			$upper = strtoupper($trim);
306
			switch ($upper)
307
			{
308
309
				/* Tokens that get their own sections. These keywords have subclauses. */
310
				case 'SELECT':
311
				case 'ORDER':
312
				case 'LIMIT':
313
				case 'SET':
314
				case 'DUPLICATE':
315
				case 'VALUES':
316
				case 'GROUP':
317
				case 'HAVING':
318
				case 'WHERE':
319
				case 'RENAME':
320
				case 'CALL':
321
				case 'PROCEDURE':
322
				case 'FUNCTION':
323
				case 'DATABASE':
324
				case 'SERVER':
325
				case 'LOGFILE':
326
				case 'DEFINER':
327
				case 'RETURNS':
328
				case 'TABLESPACE':
329
				case 'TRIGGER':
330
				case 'DO':
331
				case 'PLUGIN':
332
				case 'FROM':
333
				case 'FLUSH':
334
				case 'KILL':
335
				case 'RESET':
336
				case 'START':
337
				case 'STOP':
338
				case 'PURGE':
339
				case 'EXECUTE':
340
				case 'PREPARE':
341
				case 'DEALLOCATE':
342
					if ($trim === 'DEALLOCATE')
343
					{
344
						$skip_next = true;
345
					}
346
					/* this FROM is different from FROM in other DML (not join related) */
347
					if ($token_category === 'PREPARE' && $upper === 'FROM')
348
					{
349
						continue 2;
350
					}
351
352
					$token_category = $upper;
353
					break;
354
355
				case 'EVENT':
356
					// issue 71
357
					if ($prev_category === 'DROP' || $prev_category === 'ALTER' || $prev_category === 'CREATE')
358
					{
359
						$token_category = $upper;
360
					}
361
					break;
362
363
				case 'DATA':
364
					// prevent wrong handling of DATA as keyword
365
					if ($prev_category === 'LOAD')
366
					{
367
						$token_category = $upper;
368
					}
369
					break;
370
371
				case 'PASSWORD':
372
					// prevent wrong handling of PASSWORD as keyword
373
					if ($prev_category === 'SET')
374
					{
375
						$token_category = $upper;
376
					}
377
					break;
378
379
				case 'INTO':
380
					// prevent wrong handling of CACHE within LOAD INDEX INTO CACHE...
381
					if ($prev_category === 'LOAD')
382
					{
383
						$out[$prev_category][] = $upper;
384
						continue 2;
385
					}
386
					$token_category = $upper;
387
					break;
388
389
				case 'USER':
390
					// prevent wrong processing as keyword
391
					if ($prev_category === 'CREATE' || $prev_category === 'RENAME' || $prev_category === 'DROP')
392
					{
393
						$token_category = $upper;
394
					}
395
					break;
396
397
				case 'VIEW':
398
					// prevent wrong processing as keyword
399
					if ($prev_category === 'CREATE' || $prev_category === 'ALTER' || $prev_category === 'DROP')
400
					{
401
						$token_category = $upper;
402
					}
403
					break;
404
405
				/* These tokens get their own section, but have no subclauses.
406
				 These tokens identify the statement but have no specific subclauses of their own. */
407
				case 'DELETE':
408
				case 'ALTER':
409
				case 'INSERT':
410
				case 'REPLACE':
411
				case 'TRUNCATE':
412
				case 'CREATE':
413
				case 'OPTIMIZE':
414
				case 'GRANT':
415
				case 'REVOKE':
416
				case 'SHOW':
417
				case 'HANDLER':
418
				case 'LOAD':
419
				case 'ROLLBACK':
420
				case 'SAVEPOINT':
421
				case 'UNLOCK':
422
				case 'INSTALL':
423
				case 'UNINSTALL':
424
				case 'ANALZYE':
425
				case 'BACKUP':
426
				case 'CHECK':
427
				case 'CHECKSUM':
428
				case 'REPAIR':
429
				case 'RESTORE':
430
				case 'DESCRIBE':
431
				case 'EXPLAIN':
432
				case 'USE':
433
				case 'HELP':
434
					// We are using USE from FROM statement, not separate token category
435
					if ($prev_category == 'FROM')
436
					{
437
						break;
438
					}
439
					$token_category = $upper; /* set the category in case these get subclauses
440
										  in a future version of MySQL */
441
					$out[$upper][0] = $upper;
442
					continue 2;
443
444
				case 'CACHE':
445
					if ($prev_category === "" || $prev_category === 'RESET' || $prev_category === 'FLUSH'
446
						|| $prev_category === 'LOAD')
447
					{
448
						$token_category = $upper;
449
						continue 2;
450
					}
451
					break;
452
453
				/* This is either LOCK TABLES or SELECT ... LOCK IN SHARE MODE*/
454
				case 'LOCK':
455
					if ($token_category === "")
456
					{
457
						$token_category = $upper;
458
						$out[$upper][0] = $upper;
459
					}
460
					else
461
					{
462
						$trim             = 'LOCK IN SHARE MODE';
463
						$skip_next        = true;
464
						$out['OPTIONS'][] = $trim;
465
					}
466
					continue 2;
467
468
				case 'USING': /* USING in FROM clause is different from USING w/ prepared statement*/
469
					if ($token_category === 'EXECUTE')
470
					{
471
						$token_category = $upper;
472
						continue 2;
473
					}
474
					if ($token_category === 'FROM' && !empty($out['DELETE']))
475
					{
476
						$token_category = $upper;
477
						continue 2;
478
					}
479
					break;
480
481
				/* DROP TABLE is different from ALTER TABLE DROP ... */
482
				case 'DROP':
483
					if ($token_category !== 'ALTER')
484
					{
485
						$token_category = $upper;
486
						$out[$upper][0] = $upper;
487
						continue 2;
488
					}
489
					break;
490
491
				case 'FOR':
492
					$skip_next        = true;
493
					$out['OPTIONS'][] = 'FOR UPDATE';
494
					continue 2;
495
496
				case 'UPDATE':
497
					if ($token_category === "")
498
					{
499
						$token_category = $upper;
500
						continue 2;
501
502
					}
503
					if ($token_category === 'DUPLICATE')
504
					{
505
						continue 2;
506
					}
507
					break;
508
509
				/*case 'START':
510
					$trim = "BEGIN";
511
					$out[$upper][0] = $upper;
512
					$skip_next = true;
513
					break;*/
514
515
				/* These tokens are ignored. */
516
				case 'BY':
517
				case 'ALL':
518
				case 'SHARE':
519
				case 'MODE':
520
				case 'TO':
521
				case ';':
522
					continue 2;
523
524
				case 'KEY':
525
					if ($token_category === 'DUPLICATE')
526
					{
527
						continue 2;
528
					}
529
					break;
530
531
				/* These tokens set particular options for the statement.  They never stand alone.*/
532
				case 'HIGH_PRIORITY':
533
				case 'LOW_PRIORITY':
534
				case 'DELAYED':
535
				case 'IGNORE':
536
				case 'FORCE':
537
				case 'STRAIGHT_JOIN':
538
				case 'SQL_SMALL_RESULT':
539
				case 'SQL_BIG_RESULT':
540
				case 'QUICK':
541
				case 'SQL_BUFFER_RESULT':
542
				case 'SQL_CACHE':
543
				case 'SQL_NO_CACHE':
544
				case 'SQL_CALC_FOUND_ROWS':
545
					$out['OPTIONS'][] = $upper;
546
					continue 2;
547
548
				case 'WITH':
549
					if ($token_category === 'GROUP')
550
					{
551
						$skip_next        = true;
552
						$out['OPTIONS'][] = 'WITH ROLLUP';
553
						continue 2;
554
					}
555
					break;
556
557
				case 'AS':
558
					break;
559
560
				case '':
561
				case ',':
562
					//case ';':
563
					break;
564
565
				default:
566
					break;
567
			}
568
569
			// remove obsolete category after union (empty category because of
570
			// empty token before select)
571
			if ($token_category !== "" && ($prev_category === $token_category))
572
			{
573
				$out[$token_category][] = $token;
574
			}
575
576
			$prev_category = $token_category;
577
		}
578
579
		return $this->processSQLParts($out);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->processSQLParts($out) returns the type false which is incompatible with the documented return type void.
Loading history...
580
	}
581
582
	private function processSQLParts($out)
583
	{
584
		if (!$out)
585
		{
586
			return false;
587
		}
588
		if (!empty($out['SELECT']))
589
		{
590
			$out['SELECT'] = $this->process_select($out['SELECT']);
591
		}
592
		if (!empty($out['FROM']))
593
		{
594
			$out['FROM'] = $this->process_from($out['FROM']);
595
		}
596
		if (!empty($out['USING']))
597
		{
598
			$out['USING'] = $this->process_from($out['USING']);
599
		}
600
		if (!empty($out['UPDATE']))
601
		{
602
			$out['UPDATE'] = $this->process_from($out['UPDATE']);
603
		}
604
		if (!empty($out['GROUP']))
605
		{
606
			// set empty array if we have partial SQL statement
607
			$out['GROUP'] = $this->process_group($out['GROUP'], isset($out['SELECT']) ? $out['SELECT'] : array());
608
		}
609
		if (!empty($out['ORDER']))
610
		{
611
			// set empty array if we have partial SQL statement
612
			$out['ORDER'] = $this->process_order($out['ORDER'], isset($out['SELECT']) ? $out['SELECT'] : array());
613
		}
614
		if (!empty($out['LIMIT']))
615
		{
616
			$out['LIMIT'] = $this->process_limit($out['LIMIT']);
617
		}
618
		if (!empty($out['WHERE']))
619
		{
620
			$out['WHERE'] = $this->process_expr_list($out['WHERE']);
621
		}
622
		if (!empty($out['HAVING']))
623
		{
624
			$out['HAVING'] = $this->process_expr_list($out['HAVING']);
625
		}
626
		if (!empty($out['SET']))
627
		{
628
			$out['SET'] = $this->process_set_list($out['SET'], isset($out['UPDATE']));
629
		}
630
		if (!empty($out['DUPLICATE']))
631
		{
632
			$out['ON DUPLICATE KEY UPDATE'] = $this->process_set_list($out['DUPLICATE'], isset($out['UPDATE']));
633
			unset($out['DUPLICATE']);
634
		}
635
		if (!empty($out['INSERT']))
636
		{
637
			$out = $this->process_insert($out);
638
		}
639
		if (!empty($out['REPLACE']))
640
		{
641
			$out = $this->process_insert($out, 'REPLACE');
642
		}
643
		if (!empty($out['DELETE']))
644
		{
645
			$out = $this->process_delete($out);
646
		}
647
		if (!empty($out['VALUES']))
648
		{
649
			$out = $this->process_values($out);
650
		}
651
		if (!empty($out['INTO']))
652
		{
653
			$out = $this->process_into($out);
654
		}
655
656
		return $out;
657
	}
658
659
	/* A SET list is simply a list of key = value expressions separated by comma (,).
660
	 This function produces a list of the key/value expressions.
661
	 */
662
	private function getAssignment($base_expr)
663
	{
664
		$assignment = $this->process_expr_list($this->split_sql($base_expr));
665
666
		return array('expr_type' => 'expression', 'base_expr' => trim($base_expr), 'sub_tree' => $assignment);
667
	}
668
669
	private function getVariableType($expression)
670
	{
671
		// $expression must contain only upper-case characters
672
		if ($expression[1] !== "@")
673
		{
674
			return 'user_variable';
675
		}
676
677
		$type = substr($expression, 2, strpos($expression, ".", 2));
678
679
		switch ($type)
680
		{
681
			case 'GLOBAL':
682
			case 'LOCAL':
683
			case 'SESSION':
684
				$type = strtolower($type) . '_variable';
685
				break;
686
			default:
687
				$type = 'session_variable';
688
				break;
689
		}
690
691
		return $type;
692
	}
693
694
	private function process_set_list($tokens, $isUpdate)
695
	{
696
		$result   = array();
697
		$baseExpr = "";
698
		$varType  = false;
699
700
		foreach ($tokens as $token)
701
		{
702
			$upper = strtoupper(trim($token));
703
704
			switch ($upper)
705
			{
706
				case 'LOCAL':
707
				case 'SESSION':
708
				case 'GLOBAL':
709
					if (!$isUpdate)
710
					{
711
						$varType  = strtolower($upper) . '_variable';
712
						$baseExpr = "";
713
						continue 2;
714
					}
715
					break;
716
717
				case ',':
718
					$assignment = $this->getAssignment($baseExpr);
719
					if (!$isUpdate)
720
					{
721
						if ($varType !== false)
722
						{
723
							$assignment['sub_tree'][0]['expr_type'] = $varType;
724
						}
725
					}
726
					$result[] = $assignment;
727
					$baseExpr = "";
728
					$varType  = false;
729
					continue 2;
730
731
				default:
732
			}
733
			$baseExpr .= $token;
734
		}
735
736
		if (trim($baseExpr) !== "")
737
		{
738
			$assignment = $this->getAssignment($baseExpr);
739
			if (!$isUpdate)
740
			{
741
				if ($varType !== false)
742
				{
743
					$assignment['sub_tree'][0]['expr_type'] = $varType;
744
				}
745
			}
746
			$result[] = $assignment;
747
		}
748
749
		return $result;
750
	}
751
752
	/* This function processes the LIMIT section.
753
	 start,end are set.  If only end is provided in the query
754
	 then start is set to 0.
755
	 */
756
	private function process_limit($tokens)
757
	{
758
		$rowcount = "";
759
		$offset   = "";
760
761
		$comma    = -1;
762
		$exchange = false;
763
764
		for ($i = 0; $i < count($tokens); ++$i)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
765
		{
766
			$trim = trim($tokens[$i]);
767
			if ($trim === ",")
768
			{
769
				$comma = $i;
770
				break;
771
			}
772
			if ($trim === "OFFSET")
773
			{
774
				$comma    = $i;
775
				$exchange = true;
776
				break;
777
			}
778
		}
779
780
		for ($i = 0; $i < $comma; ++$i)
781
		{
782
			if ($exchange)
783
			{
784
				$rowcount .= $tokens[$i];
785
			}
786
			else
787
			{
788
				$offset .= $tokens[$i];
789
			}
790
		}
791
792
		for ($i = $comma + 1; $i < count($tokens); ++$i)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
793
		{
794
			if ($exchange)
795
			{
796
				$offset .= $tokens[$i];
797
			}
798
			else
799
			{
800
				$rowcount .= $tokens[$i];
801
			}
802
		}
803
804
		return array('offset' => trim($offset), 'rowcount' => trim($rowcount));
805
	}
806
807
	/* This function processes the SELECT section.  It splits the clauses at the commas.
808
	 Each clause is then processed by process_select_expr() and the results are added to
809
	 the expression list.
810
	
811
	 Finally, at the end, the epxression list is returned.
812
	 */
813
	private function process_select(&$tokens)
814
	{
815
		$expression = "";
816
		$expr       = array();
817
		foreach ($tokens as $token)
818
		{
819
			if (trim($token) === ',')
820
			{
821
				$expr[]     = $this->process_select_expr(trim($expression));
822
				$expression = "";
823
			}
824
			else
825
			{
826
				$expression .= $token;
827
			}
828
		}
829
		if ($expression)
830
		{
831
			$expr[] = $this->process_select_expr(trim($expression));
832
		}
833
834
		return $expr;
835
	}
836
837
	private function revokeEscaping($sql)
838
	{
839
		$sql = trim($sql);
840
		if (($sql[0] === '`') && ($sql[strlen($sql) - 1] === '`'))
841
		{
842
			$sql = substr($sql, 1, -1);
843
		}
844
845
		return str_replace('`', '', $sql);
846
	}
847
848
	private function isWhitespaceToken($token)
849
	{
850
		return (trim($token) === "");
851
	}
852
853
	private function isCommentToken($token)
854
	{
855
		return isset($token[0]) && isset($token[1])
856
			&& (($token[0] === '-' && $token[1] === '-') || ($token[0] === '/' && $token[1] === '*'));
857
	}
858
859
	private function isColumnReference($out)
860
	{
861
		return (isset($out['expr_type']) && $out['expr_type'] === 'colref');
862
	}
863
864
	private function isReserved($out)
865
	{
866
		return (isset($out['expr_type']) && $out['expr_type'] === 'reserved');
867
	}
868
869
	private function isConstant($out)
870
	{
871
		return (isset($out['expr_type']) && $out['expr_type'] === 'const');
872
	}
873
874
	private function isAggregateFunction($out)
875
	{
876
		return (isset($out['expr_type']) && $out['expr_type'] === 'aggregate_function');
877
	}
878
879
	private function isFunction($out)
880
	{
881
		return (isset($out['expr_type']) && $out['expr_type'] === 'function');
882
	}
883
884
	private function isExpression($out)
885
	{
886
		return (isset($out['expr_type']) && $out['expr_type'] === 'expression');
887
	}
888
889
	private function isBrackedExpression($out)
890
	{
891
		return (isset($out['expr_type']) && $out['expr_type'] === 'bracked_expression');
892
	}
893
894
	private function isSubQuery($out)
895
	{
896
		return (isset($out['expr_type']) && $out['expr_type'] === 'subquery');
897
	}
898
899
	/* This fuction processes each SELECT clause.  We determine what (if any) alias
900
	 is provided, and we set the type of expression.
901
	 */
902
	private function process_select_expr($expression)
903
	{
904
905
		$tokens      = $this->split_sql($expression);
906
		$token_count = count($tokens);
907
908
		/* Determine if there is an explicit alias after the AS clause.
909
		 If AS is found, then the next non-whitespace token is captured as the alias.
910
		 The tokens after (and including) the AS are removed.
911
		 */
912
		$base_expr = "";
913
		$stripped  = array();
914
		$capture   = false;
915
		$alias     = false;
916
917
		for ($i = 0; $i < $token_count; ++$i)
918
		{
919
			$token = $tokens[$i];
920
			$upper = strtoupper($token);
921
922
			if ($upper === 'AS')
923
			{
924
				$alias      = array('as' => true, "name" => "", "base_expr" => $token);
925
				$tokens[$i] = "";
926
				$capture    = true;
927
				continue;
928
			}
929
930
			if (!$this->isWhitespaceToken($upper))
931
			{
932
				$stripped[] = $token;
933
			}
934
935
			// we have an explicit AS, next one can be the alias
936
			// but also a comment!
937
			if ($capture)
938
			{
939
				if (!$this->isWhitespaceToken($upper) && !$this->isCommentToken($upper))
940
				{
941
					$alias['name'] .= $token;
942
					array_pop($stripped);
943
				}
944
				$alias['base_expr'] .= $token;
945
				$tokens[$i]         = "";
946
				continue;
947
			}
948
949
			$base_expr .= $token;
950
		}
951
952
		$stripped = $this->process_expr_list($stripped);
953
954
		// TODO: the last part can also be a comment, don't use array_pop
955
956
		// we remove the last token, if it is a colref,
957
		// it can be an alias without an AS
958
		$last = array_pop($stripped);
0 ignored issues
show
Bug introduced by
It seems like $stripped can also be of type false; however, parameter $array of array_pop() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

958
		$last = array_pop(/** @scrutinizer ignore-type */ $stripped);
Loading history...
959
		if (!$alias && $this->isColumnReference($last))
960
		{
961
962
			// TODO: it can be a comment, don't use array_pop
963
964
			// check the token before the colref
965
			$prev = array_pop($stripped);
966
967
			if ($this->isReserved($prev) || $this->isConstant($prev) || $this->isAggregateFunction($prev)
968
				|| $this->isFunction($prev) || $this->isExpression($prev) || $this->isSubQuery($prev)
969
				|| $this->isColumnReference($prev) || $this->isBrackedExpression($prev))
970
			{
971
972
				$alias = array('as'        => false, 'name' => trim($last['base_expr']),
973
				               'base_expr' => trim($last['base_expr']));
974
				// remove the last token
975
				array_pop($tokens);
976
				$base_expr = join("", $tokens);
977
			}
978
		}
979
980
		if (!$alias)
981
		{
982
			$base_expr = join("", $tokens);
983
		}
984
		else
985
		{
986
			/* remove escape from the alias */
987
			$alias['name']      = trim($alias['name']);
988
			$alias['base_expr'] = trim($alias['base_expr']);
989
		}
990
991
		// this is always done with $stripped, how we do it twice?
992
		$processed = $this->process_expr_list($tokens);
993
994
		// if there is only one part, we copy the expr_type
995
		// in all other cases we use "expression" as global type
996
		$type = 'expression';
997
		if (count($processed) == 1)
0 ignored issues
show
Bug introduced by
It seems like $processed can also be of type false; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

997
		if (count(/** @scrutinizer ignore-type */ $processed) == 1)
Loading history...
998
		{
999
			if (!$this->isSubQuery($processed[0]))
1000
			{
1001
				$type      = $processed[0]['expr_type'];
1002
				$base_expr = $processed[0]['base_expr'];
1003
				$processed = $processed[0]['sub_tree']; // it can be FALSE
1004
			}
1005
		}
1006
1007
		return array('expr_type' => $type, 'alias' => $alias, 'base_expr' => trim($base_expr),
1008
		             'sub_tree'  => $processed);
1009
	}
1010
1011
	private function process_from(&$tokens)
1012
	{
1013
1014
		$parseInfo = $this->initParseInfoForFrom();
1015
		$expr      = array();
1016
1017
		$skip_next = false;
1018
		$i         = 0;
1019
1020
		foreach ($tokens as $token)
1021
		{
1022
			$upper = strtoupper(trim($token));
1023
1024
			if ($skip_next && $token !== "")
1025
			{
1026
				$parseInfo['token_count']++;
1027
				$skip_next = false;
1028
				continue;
1029
			}
1030
			else
1031
			{
1032
				if ($skip_next)
1033
				{
1034
					continue;
1035
				}
1036
			}
1037
1038
			switch ($upper)
1039
			{
1040
				case 'OUTER':
1041
				case 'LEFT':
1042
				case 'RIGHT':
1043
				case 'NATURAL':
1044
				case 'CROSS':
1045
				case ',':
1046
				case 'JOIN':
1047
				case 'INNER':
1048
					break;
1049
1050
				default:
1051
					$parseInfo['expression'] .= $token;
1052
					if ($parseInfo['ref_type'] !== false)
1053
					{ // all after ON / USING
1054
						$parseInfo['ref_expr'] .= $token;
1055
					}
1056
					break;
1057
			}
1058
1059
			switch ($upper)
1060
			{
1061
				case 'AS':
1062
					$parseInfo['alias'] = array('as' => true, 'name' => "", 'base_expr' => $token);
1063
					$parseInfo['token_count']++;
1064
					$n   = 1;
1065
					$str = "";
1066
					while ($str == "")
1067
					{
1068
						$parseInfo['alias']['base_expr'] .= ($tokens[$i + $n] === "" ? " " : $tokens[$i + $n]);
1069
						$str                             = trim($tokens[$i + $n]);
1070
						++$n;
1071
					}
1072
					$parseInfo['alias']['name']      = $str;
1073
					$parseInfo['alias']['base_expr'] = trim($parseInfo['alias']['base_expr']);
1074
					break;
1075
1076
				case 'INDEX':
1077
					if (!empty($token_category) && $token_category == 'CREATE')
1078
					{
1079
						$token_category = $upper;
1080
						continue 2;
1081
					}
1082
1083
					break;
1084
1085
				case 'USING':
1086
				case 'ON':
1087
					$parseInfo['ref_type'] = $upper;
1088
					$parseInfo['ref_expr'] = "";
1089
					$parseInfo['token_count']++;
1090
					break;
1091
1092
				case 'USE':
1093
				case 'FORCE':
1094
				case 'IGNORE':
1095
					$tableOptions = array('option' => $upper, 'name' => $upper, 'expr_type' => 'index_hints', 'base_expr' => $token);
1096
					$parseInfo['token_count']++;
1097
					$n          = 1;
1098
					$tokenCount = count($token);
1099
1100
					while ($tokenCount < ($i + $n))
1101
					{
1102
						$tableOptions['base_expr'] .= ($tokens[$i + $n] === "" ? " " : $tokens[$i + $n]);
1103
1104
						if (strpos($tokens[$i + $n], ')') !== false)
1105
						{
1106
							break;
1107
						}
1108
1109
						++$n;
1110
					}
1111
1112
					$parseInfo['index_hints'] = $tableOptions;
1113
					break;
1114
1115
				case 'CROSS':
1116
				case 'INNER':
1117
				case 'OUTER':
1118
					$parseInfo['token_count']++;
1119
					break;
1120
1121
				case 'FOR':
1122
					$parseInfo['token_count']++;
1123
					$skip_next = true;
1124
					break;
1125
1126
				case 'LEFT':
1127
				case 'RIGHT':
1128
				case 'STRAIGHT_JOIN':
1129
					$parseInfo['next_join_type'] = $upper;
1130
					break;
1131
1132
				case ',':
1133
					$parseInfo['next_join_type'] = 'CROSS';
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
1134
1135
				case 'JOIN':
1136
					if ($parseInfo['subquery'])
1137
					{
1138
						$parseInfo['sub_tree']   = $this->parse($this->removeParenthesisFromStart($parseInfo['subquery']));
1139
						$parseInfo['expression'] = $parseInfo['subquery'];
1140
					}
1141
1142
					$expr[]    = $this->processFromExpression($parseInfo);
1143
					$parseInfo = $this->initParseInfoForFrom($parseInfo);
1144
					break;
1145
1146
				default:
1147
					if ($upper === "")
1148
					{
1149
						break; // ends the switch statement!
1150
					}
1151
1152
					if ($parseInfo['token_count'] === 0)
1153
					{
1154
						if ($parseInfo['table'] === "")
1155
						{
1156
							$parseInfo['table'] = $token;
1157
						}
1158
					}
1159
					else if ($parseInfo['token_count'] === 1)
1160
					{
1161
						$parseInfo['alias'] = array('as' => false, 'name' => trim($token), 'base_expr' => trim($token));
1162
					}
1163
					$parseInfo['token_count']++;
1164
					break;
1165
			}
1166
			++$i;
1167
		}
1168
1169
		$expr[] = $this->processFromExpression($parseInfo);
1170
1171
		return $expr;
1172
	}
1173
1174
	private function initParseInfoForFrom($parseInfo = false)
1175
	{
1176
		// first init
1177
		if ($parseInfo === false)
1178
		{
1179
			$parseInfo = array('join_type' => "", 'saved_join_type' => "JOIN");
1180
		}
1181
1182
		// loop init
1183
		return array('expression'     => "", 'token_count' => 0, 'table' => "", 'alias' => false, 'join_type' => "",
1184
		             'next_join_type' => "", 'saved_join_type' => $parseInfo['saved_join_type'],
1185
		             'ref_type'       => false, 'ref_expr' => false, 'base_expr' => false, 'sub_tree' => false, 'index_hints' => false,
1186
		             'subquery'       => "");
1187
	}
1188
1189
	private function processFromExpression(&$parseInfo)
1190
	{
1191
1192
		$res = array();
1193
1194
		// exchange the join types (join_type is save now, saved_join_type holds the next one)
1195
		$parseInfo['join_type']       = $parseInfo['saved_join_type']; // initialized with JOIN
1196
		$parseInfo['saved_join_type'] = ($parseInfo['next_join_type'] ? $parseInfo['next_join_type'] : 'JOIN');
1197
1198
		// we have a reg_expr, so we have to parse it
1199
		if ($parseInfo['ref_expr'] !== false)
1200
		{
1201
			$unparsed = $this->split_sql($this->removeParenthesisFromStart($parseInfo['ref_expr']));
1202
1203
			// here we can get a comma separated list
1204
			foreach ($unparsed as $k => $v)
1205
			{
1206
				if (trim($v) === ',')
1207
				{
1208
					$unparsed[$k] = "";
1209
				}
1210
			}
1211
			$parseInfo['ref_expr'] = $this->process_expr_list($unparsed);
1212
		}
1213
1214
		// there is an expression, we have to parse it
1215
		if (substr(trim($parseInfo['table']), 0, 1) == '(')
1216
		{
1217
			$parseInfo['expression'] = $this->removeParenthesisFromStart($parseInfo['table']);
1218
1219
			if (preg_match("/^\\s*select/i", $parseInfo['expression']))
1220
			{
1221
				$parseInfo['sub_tree'] = $this->parse($parseInfo['expression']);
1222
				$res['expr_type']      = 'subquery';
1223
			}
1224
			else
1225
			{
1226
				$tmp                   = $this->split_sql($parseInfo['expression']);
1227
				$parseInfo['sub_tree'] = $this->process_from($tmp);
1228
				$res['expr_type']      = 'table_expression';
1229
				$res['union_tree']     = $tmp;
1230
			}
1231
		}
1232
		else
1233
		{
1234
			$res['expr_type'] = 'table';
1235
			$res['table']     = $parseInfo['table'];
1236
		}
1237
1238
		$res['alias']       = $parseInfo['alias'];
1239
		$res['join_type']   = $parseInfo['join_type'];
1240
		$res['ref_type']    = $parseInfo['ref_type'];
1241
		$res['ref_clause']  = $parseInfo['ref_expr'];
1242
		$res['base_expr']   = trim($parseInfo['expression']);
1243
		$res['sub_tree']    = $parseInfo['sub_tree'];
1244
		$res['index_hints'] = $parseInfo['index_hints'];
1245
1246
		return $res;
1247
	}
1248
1249
	private function processOrderExpression(&$parseInfo, $select, $groupby = false)
1250
	{
1251
		$parseInfo['expr'] = trim($parseInfo['expr']);
1252
1253
		if ($parseInfo['expr'] === "")
1254
		{
1255
			return false;
1256
		}
1257
1258
		if ($groupby)
1259
			$parseInfo['expr'] = trim($parseInfo['expr']);
1260
		else
1261
			$parseInfo['expr'] = trim($this->revokeEscaping($parseInfo['expr']));
1262
1263
		if (is_numeric($parseInfo['expr']))
1264
		{
1265
			$parseInfo['type'] = 'pos';
1266
		}
1267
		else
1268
		{
1269
			// search to see if the expression matches an alias
1270
			foreach ($select as $clause)
1271
			{
1272
				if (!$clause['alias'])
1273
				{
1274
					continue;
1275
				}
1276
				if ($clause['alias']['name'] === $parseInfo['expr'])
1277
				{
1278
					$parseInfo['type'] = 'alias';
1279
				}
1280
			}
1281
		}
1282
1283
		if ($parseInfo['type'] === "expression")
1284
		{
1285
			$expr              = $this->process_select_expr($parseInfo['expr']);
1286
			$expr['direction'] = $parseInfo['dir'];
1287
			unset($expr['alias']);
1288
1289
			return $expr;
1290
		}
1291
1292
		return array('expr_type' => $parseInfo['type'], 'base_expr' => $parseInfo['expr'],
1293
		             'direction' => $parseInfo['dir']);
1294
	}
1295
1296
	private function initParseInfoForOrder()
1297
	{
1298
		return array('base_expr' => "", 'expr' => "", 'dir' => "ASC", 'type' => 'expression');
1299
	}
1300
1301
	private function process_order($tokens, $select)
1302
	{
1303
		$out       = array();
1304
		$parseInfo = $this->initParseInfoForOrder();
1305
1306
		if (!$tokens)
1307
		{
1308
			return false;
1309
		}
1310
1311
		foreach ($tokens as $token)
1312
		{
1313
			$upper = strtoupper(trim($token));
1314
			switch ($upper)
1315
			{
1316
				case ',':
1317
					$out[]     = $this->processOrderExpression($parseInfo, $select);
1318
					$parseInfo = $this->initParseInfoForOrder();
1319
					break;
1320
1321
				case 'DESC':
1322
					$parseInfo['dir'] = "DESC";
1323
					break;
1324
1325
				case 'ASC':
1326
					$parseInfo['dir'] = "ASC";
1327
					break;
1328
1329
				default:
1330
					$parseInfo['expr'] .= $token;
1331
1332
			}
1333
		}
1334
1335
		$out[] = $this->processOrderExpression($parseInfo, $select);
1336
1337
		return $out;
1338
	}
1339
1340
	private function process_group($tokens, $select)
1341
	{
1342
		$out       = array();
1343
		$parseInfo = $this->initParseInfoForOrder();
1344
1345
		if (!$tokens)
1346
		{
1347
			return false;
1348
		}
1349
1350
		foreach ($tokens as $token)
1351
		{
1352
			$trim = strtoupper(trim($token));
1353
			switch ($trim)
1354
			{
1355
				case ',':
1356
					$parsed = $this->processOrderExpression($parseInfo, $select, true);
1357
					unset($parsed['direction']);
1358
1359
					$out[]     = $parsed;
1360
					$parseInfo = $this->initParseInfoForOrder();
1361
					break;
1362
				default:
1363
					$parseInfo['base_expr'] .= $token;
1364
					$parseInfo['expr']      .= $token;
1365
			}
1366
		}
1367
1368
		$parsed = $this->processOrderExpression($parseInfo, $select, true);
1369
		unset($parsed['direction']);
1370
		$out[] = $parsed;
1371
1372
		return $out;
1373
	}
1374
1375
	private function removeParenthesisFromStart($token)
1376
	{
1377
1378
		$parenthesisRemoved = 0;
1379
1380
		$trim = trim($token);
1381
		if ($trim !== "" && $trim[0] === "(")
1382
		{ // remove only one parenthesis pair now!
1383
			$parenthesisRemoved++;
1384
			$trim[0] = " ";
1385
			$trim    = trim($trim);
1386
		}
1387
1388
		$parenthesis = $parenthesisRemoved;
1389
		$i           = 0;
1390
		$string      = 0;
1391
		while ($i < strlen($trim))
1392
		{
1393
1394
			if ($trim[$i] === "\\")
1395
			{
1396
				$i += 2; // an escape character, the next character is irrelevant
1397
				continue;
1398
			}
1399
1400
			if ($trim[$i] === "'" || $trim[$i] === '"')
1401
			{
1402
				$string++;
1403
			}
1404
1405
			if (($string % 2 === 0) && ($trim[$i] === "("))
1406
			{
1407
				$parenthesis++;
1408
			}
1409
1410
			if (($string % 2 === 0) && ($trim[$i] === ")"))
1411
			{
1412
				if ($parenthesis == $parenthesisRemoved)
1413
				{
1414
					$trim[$i] = " ";
1415
					$parenthesisRemoved--;
1416
				}
1417
				$parenthesis--;
1418
			}
1419
			$i++;
1420
		}
1421
1422
		return trim($trim);
1423
	}
1424
1425
	private function initParseInfoExprList($parseInfo = false)
1426
	{
1427
		if ($parseInfo === false)
1428
		{
1429
			return array(
1430
				'processed' => false,
1431
				'expr' => "",
1432
				'key' => false,
1433
				'token' => false,
1434
				'tokenType' => "",
1435
				'prevToken' => "",
1436
				'prevTokenType' => "",
1437
				'trim' => false,
1438
				'upper' => false
1439
			);
1440
		}
1441
1442
		$expr   = empty($parseInfo['expr']) ? array() : $parseInfo['expr'];
1443
		$expr[] = array('expr_type' => $parseInfo['tokenType'], 'base_expr' => $parseInfo['token'],
1444
		                'sub_tree'  => $parseInfo['processed']);
1445
1446
		return array('processed' => false, 'expr' => $expr, 'key' => false, 'token' => false, 'tokenType' => "",
1447
		             'prevToken' => $parseInfo['upper'], 'prevTokenType' => $parseInfo['tokenType'],
1448
		             'trim'      => false, 'upper' => false);
1449
	}
1450
1451
	/* Some sections are just lists of expressions, like the WHERE and HAVING clauses.  This function
1452
	 processes these sections.  Recursive.
1453
	 */
1454
	private function process_expr_list($tokens)
1455
	{
1456
1457
		$parseInfo = $this->initParseInfoExprList();
1458
		$skip_next = false;
1459
1460
		foreach ($tokens as $parseInfo['key'] => $parseInfo['token'])
1461
		{
1462
1463
			$parseInfo['trim'] = trim($parseInfo['token']);
0 ignored issues
show
Bug introduced by
$parseInfo['token'] of type false is incompatible with the type string expected by parameter $string of trim(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1463
			$parseInfo['trim'] = trim(/** @scrutinizer ignore-type */ $parseInfo['token']);
Loading history...
1464
1465
			if ($parseInfo['trim'] === "")
1466
			{
1467
				continue;
1468
			}
1469
1470
			if ($skip_next)
1471
			{
1472
				// skip the next non-whitespace token
1473
				$skip_next = false;
1474
				continue;
1475
			}
1476
1477
			$parseInfo['upper'] = strtoupper($parseInfo['trim']);
1478
1479
			/* is it a subquery?*/
1480
			if (preg_match("/^\\(\\s*SELECT/i", $parseInfo['trim']))
1481
			{
1482
				// tokenize and parse the subquery.
1483
				// we remove the enclosing parenthesis for the tokenizer
1484
				$parseInfo['processed'] = $this->parse($this->removeParenthesisFromStart($parseInfo['trim']));
1485
				$parseInfo['tokenType'] = 'subquery';
1486
			}
1487
			elseif ($parseInfo['upper'][0] === '(' && substr($parseInfo['upper'], -1) === ')')
1488
			{
1489
				/* is it an inlist (upper is derived from trim!) */
1490
1491
				// if we have a colref followed by a parenthesis pair,
1492
				// it isn't a colref, it is a user-function
1493
				if ($parseInfo['prevTokenType'] === 'colref' || $parseInfo['prevTokenType'] === 'function'
1494
					|| $parseInfo['prevTokenType'] === 'aggregate_function')
1495
				{
1496
1497
					$tmptokens = $this->split_sql($this->removeParenthesisFromStart($parseInfo['trim']));
1498
					foreach ($tmptokens as $k => $v)
1499
					{
1500
						if (trim($v) == ',')
1501
						{
1502
							unset($tmptokens[$k]);
1503
						}
1504
					}
1505
1506
					$tmptokens              = array_values($tmptokens);
1507
					$parseInfo['processed'] = $this->process_expr_list($tmptokens);
1508
1509
					$last                       = array_pop($parseInfo['expr']);
0 ignored issues
show
Bug introduced by
$parseInfo['expr'] of type string is incompatible with the type array expected by parameter $array of array_pop(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1509
					$last                       = array_pop(/** @scrutinizer ignore-type */ $parseInfo['expr']);
Loading history...
1510
					$parseInfo['token']         = $last['base_expr'];
1511
					$parseInfo['tokenType']     = ($parseInfo['prevTokenType'] === 'colref' ? 'function'
1512
						: $parseInfo['prevTokenType']);
1513
					$parseInfo['prevTokenType'] = $parseInfo['prevToken'] = "";
1514
				}
1515
1516
				if ($parseInfo['prevToken'] == 'IN')
1517
				{
1518
1519
					$tmptokens = $this->split_sql($this->removeParenthesisFromStart($parseInfo['trim']));
1520
					foreach ($tmptokens as $k => $v)
1521
					{
1522
						if (trim($v) == ',')
1523
						{
1524
							unset($tmptokens[$k]);
1525
						}
1526
					}
1527
1528
					$tmptokens                  = array_values($tmptokens);
1529
					$parseInfo['processed']     = $this->process_expr_list($tmptokens);
1530
					$parseInfo['prevTokenType'] = $parseInfo['prevToken'] = "";
1531
					$parseInfo['tokenType']     = "in-list";
1532
				}
1533
1534
				if ($parseInfo['prevToken'] == 'AGAINST')
1535
				{
1536
1537
					$tmptokens = $this->split_sql($this->removeParenthesisFromStart($parseInfo['trim']));
1538
					// Todo: rewiew later this was not set
1539
					$list[0] = null;
1540
					if (count($tmptokens) > 1)
1541
					{
1542
						$match_mode             = implode('', array_slice($tmptokens, 1));
1543
						$parseInfo['processed'] = array($list[0], $match_mode);
1544
					}
1545
					else
1546
					{
1547
						$parseInfo['processed'] = $list[0];
1548
					}
1549
1550
					$parseInfo['prevTokenType'] = $parseInfo['prevToken'] = "";
1551
					$parseInfo['tokenType']     = "match-arguments";
1552
				}
1553
1554
			}
1555
			elseif ($parseInfo['upper'][0] === '@')
1556
			{
1557
				// a variable
1558
				$parseInfo['tokenType'] = $this->getVariableType($parseInfo['upper']);
1559
				$parseInfo['processed'] = false;
1560
			}
1561
			else
1562
			{
1563
				/* it is either an operator, a colref or a constant */
1564
				switch ($parseInfo['upper'])
1565
				{
1566
1567
					case '*':
1568
						$parseInfo['processed'] = false; // no subtree
1569
1570
						// last token is colref, const or expression
1571
						// it is an operator, in all other cases it is an all-columns-alias
1572
						// if the previous colref ends with a dot, the * is the all-columns-alias
1573
						if (!is_array($parseInfo['expr']))
1574
						{
1575
							$parseInfo['tokenType'] = "colref"; // single or first element of select -> *
1576
							break;
1577
						}
1578
1579
						$last = array_pop($parseInfo['expr']);
1580
						if ($last['expr_type'] !== 'colref' && $last['expr_type'] !== 'const'
1581
							&& $last['expr_type'] !== 'expression')
1582
						{
1583
							$parseInfo['expr'][]    = $last;
1584
							$parseInfo['tokenType'] = "colref";
1585
							break;
1586
						}
1587
1588
						if ($last['expr_type'] === 'colref' && substr($last['base_expr'], -1, 1) === ".")
1589
						{
1590
							$last['base_expr']   .= '*'; // tablealias dot *
1591
							$parseInfo['expr'][] = $last;
1592
							continue 2;
1593
						}
1594
1595
						$parseInfo['expr'][]    = $last;
1596
						$parseInfo['tokenType'] = "operator";
1597
						break;
1598
1599
					case 'AND':
1600
					case '&&':
1601
					case 'BETWEEN':
1602
					case 'BINARY':
1603
					case '&':
1604
					case '~':
1605
					case '|':
1606
					case '^':
1607
					case 'DIV':
1608
					case '/':
1609
					case '<=>':
1610
					case '=':
1611
					case '>=':
1612
					case '>':
1613
					case 'IS':
1614
					case 'NOT':
1615
					case '<<':
1616
					case '<=':
1617
					case '<':
1618
					case 'LIKE':
1619
					case '%':
1620
					case '!=':
1621
					case '<>':
1622
					case 'REGEXP':
1623
					case '!':
1624
					case '||':
1625
					case 'OR':
1626
					case '>>':
1627
					case 'RLIKE':
1628
					case 'SOUNDS':
1629
					case 'XOR':
1630
					case 'IN':
1631
						$parseInfo['processed'] = false;
1632
						$parseInfo['tokenType'] = "operator";
1633
						break;
1634
1635
					case 'NULL':
1636
						$parseInfo['processed'] = false;
1637
						$parseInfo['tokenType'] = 'const';
1638
						break;
1639
1640
					case '-':
1641
					case '+':
1642
						// differ between preceding sign and operator
1643
						$parseInfo['processed'] = false;
1644
1645
						if ($parseInfo['prevTokenType'] === 'colref' || $parseInfo['prevTokenType'] === 'function'
1646
							|| $parseInfo['prevTokenType'] === 'aggregate_function'
1647
							|| $parseInfo['prevTokenType'] === 'const'
1648
							|| $parseInfo['prevTokenType'] === 'subquery')
1649
						{
1650
							$parseInfo['tokenType'] = "operator";
1651
						}
1652
						else
1653
						{
1654
							$parseInfo['tokenType'] = "sign";
1655
						}
1656
						break;
1657
1658
					default:
1659
						switch ($parseInfo['token'][0])
1660
						{
1661
							case "'":
1662
							case '"':
1663
								$parseInfo['tokenType'] = 'const';
1664
								break;
1665
							case '`':
1666
								$parseInfo['tokenType'] = 'colref';
1667
								break;
1668
1669
							default:
1670
								if (is_numeric($parseInfo['token']))
1671
								{
1672
									$parseInfo['tokenType'] = 'const';
1673
1674
									if ($parseInfo['prevTokenType'] === 'sign')
1675
									{
1676
										array_pop($parseInfo['expr']);
1677
										$parseInfo['token'] = $parseInfo['prevToken'] . $parseInfo['token'];
1678
									}
1679
1680
								}
1681
								else
1682
								{
1683
									$parseInfo['tokenType'] = 'colref';
1684
								}
1685
								break;
1686
1687
						}
1688
						$parseInfo['processed'] = false;
1689
				}
1690
			}
1691
1692
			/* is a reserved word? */
1693
			if ($parseInfo['tokenType'] !== 'operator' && $parseInfo['tokenType'] !== 'in-list'
1694
				&& $parseInfo['tokenType'] !== 'function' && $parseInfo['tokenType'] !== 'aggregate_function'
1695
				&& in_array($parseInfo['upper'], parent::$reserved))
1696
			{
1697
1698
				switch ($parseInfo['upper'])
1699
				{
1700
					case 'AVG':
1701
					case 'SUM':
1702
					case 'COUNT':
1703
					case 'MIN':
1704
					case 'MAX':
1705
					case 'STDDEV':
1706
					case 'STDDEV_SAMP':
1707
					case 'STDDEV_POP':
1708
					case 'VARIANCE':
1709
					case 'VAR_SAMP':
1710
					case 'VAR_POP':
1711
					case 'GROUP_CONCAT':
1712
					case 'BIT_AND':
1713
					case 'BIT_OR':
1714
					case 'BIT_XOR':
1715
						$parseInfo['tokenType'] = 'aggregate_function';
1716
						break;
1717
1718
					case 'NULL':
1719
						// it is a reserved word, but we would like to have set it as constant
1720
						$parseInfo['tokenType'] = 'const';
1721
						break;
1722
1723
					default:
1724
						if (in_array($parseInfo['upper'], parent::$functions))
1725
						{
1726
							$parseInfo['tokenType'] = 'function';
1727
						}
1728
						else
1729
						{
1730
							$parseInfo['tokenType'] = 'reserved';
1731
						}
1732
						break;
1733
				}
1734
			}
1735
1736
			if (!$parseInfo['tokenType'])
1737
			{
1738
				if ($parseInfo['upper'][0] === '(')
1739
				{
1740
					$local_expr             = $this->removeParenthesisFromStart($parseInfo['trim']);
1741
					$parseInfo['tokenType'] = 'bracket_expression';
1742
				}
1743
				else
1744
				{
1745
					$local_expr             = $parseInfo['trim'];
1746
					$parseInfo['tokenType'] = 'expression';
1747
				}
1748
				$parseInfo['processed'] = $this->process_expr_list($this->split_sql($local_expr));
1749
			}
1750
1751
			$parseInfo = $this->initParseInfoExprList($parseInfo);
1752
		} // end of for-loop
1753
1754
		return (is_array($parseInfo['expr']) ? $parseInfo['expr'] : false);
1755
	}
1756
1757
	private function process_update($tokens)
0 ignored issues
show
Unused Code introduced by
The parameter $tokens is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1757
	private function process_update(/** @scrutinizer ignore-unused */ $tokens)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The method process_update() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
1758
	{
1759
1760
	}
1761
1762
	private function process_delete($tokens)
1763
	{
1764
		$tables = array();
1765
1766
		foreach ($tokens['DELETE'] as $expression)
1767
		{
1768
			if ($expression != 'DELETE' && trim($expression, ' .*') != "" && $expression != ',')
1769
			{
1770
				$tables[] = trim($expression, '.* ');
1771
			}
1772
		}
1773
1774
		if (empty($tables))
1775
		{
1776
			foreach ($tokens['FROM'] as $table)
1777
			{
1778
				$tables[] = $table['table'];
1779
			}
1780
		}
1781
1782
		$tokens['DELETE'] = array('TABLES' => $tables);
1783
1784
		return $tokens;
1785
	}
1786
1787
	private function process_insert($tokens, $token_category = 'INSERT')
1788
	{
1789
		$table = "";
1790
		$cols  = array();
1791
1792
		$into = $tokens['INTO'];
1793
		foreach ($into as $token)
1794
		{
1795
			if (trim($token) === "")
1796
				continue;
1797
			if ($table === "")
1798
			{
1799
				$table = $token;
1800
			}
1801
			elseif (empty($cols))
1802
			{
1803
				$cols[] = $token;
1804
			}
1805
		}
1806
1807
		if (empty($cols))
1808
		{
1809
			$cols = false;
1810
		}
1811
		else
1812
		{
1813
			$columns = explode(",", $this->removeParenthesisFromStart($cols[0]));
1814
			$cols    = array();
1815
			foreach ($columns as $k => $v)
1816
			{
1817
				$cols[] = array('expr_type' => 'colref', 'base_expr' => trim($v));
1818
			}
1819
		}
1820
1821
		unset($tokens['INTO']);
1822
		$tokens[$token_category] = array('table' => $table, 'columns' => $cols, 'base_expr' => $table);
1823
1824
		return $tokens;
1825
	}
1826
1827
	private function process_record($unparsed)
1828
	{
1829
1830
		$unparsed = $this->removeParenthesisFromStart($unparsed);
1831
		$values   = $this->split_sql($unparsed);
1832
1833
		foreach ($values as $k => $v)
1834
		{
1835
			if (trim($v) === ",")
1836
			{
1837
				$values[$k] = "";
1838
			}
1839
		}
1840
1841
		return $this->process_expr_list($values);
1842
	}
1843
1844
	private function process_values($tokens)
1845
	{
1846
1847
		$unparsed = "";
1848
		foreach ($tokens['VALUES'] as $k => $v)
1849
		{
1850
			if (trim($v) === "")
1851
			{
1852
				continue;
1853
			}
1854
			$unparsed .= $v;
1855
		}
1856
1857
		$values = $this->split_sql($unparsed);
1858
1859
		foreach ($values as $k => $v)
1860
		{
1861
			if (trim($v) === ",")
1862
			{
1863
				unset($values[$k]);
1864
			}
1865
			else
1866
			{
1867
				$values[$k] = array('expr_type' => 'record', 'base_expr' => $v, 'data' => $this->process_record($v));
1868
			}
1869
		}
1870
1871
		$tokens['VALUES'] = array_values($values);
1872
1873
		return $tokens;
1874
	}
1875
1876
	/**
1877
	 * TODO: This is a dummy function, we cannot parse INTO as part of SELECT
1878
	 * at the moment
1879
	 */
1880
	private function process_into($tokens)
1881
	{
1882
		$unparsed = $tokens['INTO'];
1883
		foreach ($unparsed as $k => $token)
1884
		{
1885
			if ((trim($token) === "") || (trim($token) === ","))
1886
			{
1887
				unset($unparsed[$k]);
1888
			}
1889
		}
1890
		$tokens['INTO'] = array_values($unparsed);
1891
1892
		return $tokens;
1893
	}
1894
}
1895