parseTableReplacements()   F
last analyzed

Complexity

Conditions 37
Paths 2

Size

Total Lines 191
Code Lines 105

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 105
c 1
b 0
f 0
dl 0
loc 191
rs 3.3333
cc 37
nc 2
nop 6

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @package     Redcore
4
 * @subpackage  Database
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
 * Sql Translation class enables table and fields replacement methods
14
 *
15
 * @package     Redcore
16
 * @subpackage  Database
17
 * @since       1.0
18
 */
19
class RDatabaseSqlparserSqltranslation extends RTranslationHelper
20
{
21
	/**
22
	 * The options.
23
	 *
24
	 * @var  array
25
	 */
26
	private static $options = array();
27
28
	/**
29
	 * The options.
30
	 *
31
	 * @var  array
32
	 */
33
	private static $parsedQueries = array();
34
35
	/**
36
	 * Checks if tables inside query have translatable tables and fields and fetch appropriate
37
	 * value from translations table
38
	 *
39
	 * @param   string $sql               SQL query
40
	 * @param   string $prefix            Table prefix
41
	 * @param   string $language          Language tag you want to fetch translation from
42
	 * @param   array  $translationTables List of translation tables
43
	 *
44
	 * @return  mixed  Parsed query with added table joins and fields if found
45
	 */
46
	public static function parseSelectQuery($sql = '', $prefix = '', $language = 'en-GB', $translationTables = array())
0 ignored issues
show
Unused Code introduced by
The parameter $prefix 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

46
	public static function parseSelectQuery($sql = '', /** @scrutinizer ignore-unused */ $prefix = '', $language = 'en-GB', $translationTables = array())

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...
47
	{
48
		if (empty($translationTables))
49
		{
50
			// We do not have any translation table to check
51
			return null;
52
		}
53
54
		try
55
		{
56
			$hashedQueryKey = md5($sql . $language);
57
58
			if (isset(self::$parsedQueries[$hashedQueryKey]))
59
			{
60
				return self::$parsedQueries[$hashedQueryKey];
61
			}
62
63
			$db                                   = JFactory::getDbo();
64
			self::$parsedQueries[$hashedQueryKey] = null;
65
			$sqlParser                            = new RDatabaseSqlparserSqlparser($sql);
0 ignored issues
show
Bug introduced by
$sql of type string is incompatible with the type boolean expected by parameter $sql of RDatabaseSqlparserSqlparser::__construct(). ( Ignorable by Annotation )

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

65
			$sqlParser                            = new RDatabaseSqlparserSqlparser(/** @scrutinizer ignore-type */ $sql);
Loading history...
66
			$parsedSql                            = $sqlParser->parsed;
67
68
			if (!empty($parsedSql))
69
			{
70
				$foundTables      = array();
71
				$originalTables   = array();
72
				$parsedSqlColumns = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $parsedSqlColumns is dead and can be removed.
Loading history...
73
				$subQueryFound    = false;
74
				$parsedSql        = self::parseTableReplacements($parsedSql, $translationTables, $foundTables, $originalTables, $language, $subQueryFound);
0 ignored issues
show
Bug introduced by
$subQueryFound of type false is incompatible with the type string expected by parameter $subQueryFound of RDatabaseSqlparserSqltra...arseTableReplacements(). ( Ignorable by Annotation )

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

74
				$parsedSql        = self::parseTableReplacements($parsedSql, $translationTables, $foundTables, $originalTables, $language, /** @scrutinizer ignore-type */ $subQueryFound);
Loading history...
75
76
				if (empty($foundTables) && !$subQueryFound)
77
				{
78
					// We did not find any table to translate
79
					return null;
80
				}
81
82
				// Prepare field replacement
83
				$columns          = array();
84
				$columnFound      = false;
85
				$parsedSqlColumns = $parsedSql;
86
87
				// Prepare column replacements
88
				foreach ($foundTables as $foundTable)
89
				{
90
					// Get all columns from that table
91
					$tableColumns         = (array) $translationTables[$foundTable['originalTableName']]->columns;
92
					$originalTableColumns = RTranslationTable::getTableColumns($foundTable['originalTableName']);
93
94
					if (!empty($tableColumns))
95
					{
96
						$selectAllOriginalColumn = $foundTable['alias']['originalName'] . '.*';
97
						$columnAll               = array();
98
						$columnAll['table']      = $foundTable;
99
						$columnAll['columnName'] = $selectAllOriginalColumn;
100
						$columnAll['base_expr']  = self::addBaseColumns($originalTableColumns, $tableColumns, $foundTable['alias']['originalName']);
101
102
						foreach ($tableColumns as $tableColumn)
103
						{
104
							$column        = array();
105
							$fallbackValue = $foundTable['alias']['originalName'] . '.' . $tableColumn;
106
107
							// Check to see if fallback option is turned on, if it is not then we set empty string as value
108
							if (!self::getOption('translationFallback', true))
109
							{
110
								// Additionally we check for columns that must have a fallback (ex. state, publish, etc.)
111
								if (empty($translationTables[$foundTable['originalTableName']]->fallbackColumns)
112
									|| !in_array($tableColumn, $translationTables[$foundTable['originalTableName']]->fallbackColumns))
113
								{
114
									$fallbackValue = $db->q('');
115
								}
116
							}
117
118
							// If column is primary key we do not need to translate it
119
							if (in_array($tableColumn, $translationTables[$foundTable['originalTableName']]->primaryKeys))
120
							{
121
								$column['base_expr'] = $foundTable['alias']['originalName'] . '.' . $tableColumn;
122
							}
123
							else
124
							{
125
								$column['base_expr'] = ''
126
									. 'COALESCE('
127
									. $foundTable['alias']['name'] . '.' . $tableColumn
128
									. ',' . $fallbackValue
129
									. ')';
130
							}
131
132
							$column['alias']      = $db->qn($tableColumn);
133
							$column['table']      = $foundTable;
134
							$column['columnName'] = $tableColumn;
135
136
							$columns[] = $column;
137
138
							if (!empty($columnAll['base_expr']))
139
							{
140
								$columnAll['base_expr'] .= ',';
141
							}
142
143
							$columnAll['base_expr'] .= $column['base_expr'] . ' AS ' . $db->qn($tableColumn);
144
						}
145
146
						$columns[] = $columnAll;
147
					}
148
				}
149
150
				if (!empty($foundTables))
151
				{
152
					$parsedSqlColumns = self::parseColumnReplacements($parsedSqlColumns, $columns, $translationTables, $columnFound);
153
				}
154
155
				// We are only returning parsed SQL if we found at least one column in translation table
156
				if ($columnFound || $subQueryFound)
157
				{
158
					$sqlCreator                           = new RDatabaseSqlparserSqlcreator($parsedSqlColumns);
159
					self::$parsedQueries[$hashedQueryKey] = $sqlCreator->created;
160
161
					return self::$parsedQueries[$hashedQueryKey];
162
				}
163
			}
164
		}
165
		catch (Exception $e)
166
		{
167
			return null;
168
		}
169
170
		return null;
171
	}
172
173
	/**
174
	 * Checks if this query is qualified for translation and parses query
175
	 *
176
	 * @param   string $sql    SQL query
177
	 * @param   string $prefix Table prefix
178
	 *
179
	 * @return  mixed  Parsed query with added table joins and fields if found
180
	 */
181
	public static function buildTranslationQuery($sql = '', $prefix = '')
182
	{
183
		$db = JFactory::getDbo();
184
185
		$selectedLanguage = !empty($db->forceLanguageTranslation) ? $db->forceLanguageTranslation : JFactory::getLanguage()->getTag();
0 ignored issues
show
Bug introduced by
The property forceLanguageTranslation does not seem to exist on JDatabaseDriver.
Loading history...
186
		$queryArray       = explode(',', (string) $sql);
187
		$hashValues       = array();
188
189
		// Replace long amount of numeric values if found. They do not have any affects for translation parser
190
		if (count($queryArray) > 50)
191
		{
192
			$index   = 0;
193
			$numbers = array($index => array());
194
195
			foreach ($queryArray as $key => $value)
196
			{
197
				if (is_numeric(trim($value)))
198
				{
199
					if (!array_key_exists($index, $numbers))
200
					{
201
						$numbers[$index] = array();
202
					}
203
204
					$numbers[$index][$key] = $value;
205
					unset($queryArray[$key]);
206
				}
207
				else
208
				{
209
					if (!empty($numbers[$index]))
210
					{
211
						$index++;
212
					}
213
				}
214
			}
215
216
			foreach ($numbers as $index => $values)
217
			{
218
				if (count($values) < 10)
219
				{
220
					foreach ($values as $key => $value)
221
					{
222
						$queryArray[$key] = $value;
223
					}
224
				}
225
				else
226
				{
227
					$firstKey               = key($values);
228
					$hashValue              = $db->q(md5(json_encode($values)));
229
					$queryArray[$firstKey]  = $hashValue;
230
					$hashValues[$hashValue] = $values;
231
				}
232
			}
233
234
			unset($numbers);
235
			ksort($queryArray);
236
			$sql = implode(',', $queryArray);
237
		}
238
239
		if (!empty($db->parseTablesBefore))
240
		{
241
			foreach ($db->parseTablesBefore as $tableGroup)
0 ignored issues
show
Bug introduced by
The property parseTablesBefore does not seem to exist on JDatabaseDriver.
Loading history...
242
			{
243
				$sql = self::parseSelectQuery($sql, $prefix, $tableGroup->language, $tableGroup->translationTables);
244
			}
245
		}
246
247
		// If we have a SELECT in the query then we check the reset of the params
248
		$validSelect = (!empty($sql) && stristr(mb_strtolower($sql), 'select'));
249
250
		// If the language is the default, there is no reason to translate
251
		$isDefaultLanguage = (RTranslationHelper::getSiteLanguage() == $selectedLanguage) && !self::getOption('forceTranslateDefault', false);
252
253
		// If this is the admin, but no an API request we shouldn't translate
254
		$isAdmin = RTranslationHelper::isAdmin();
255
256
		/**
257
		 * Basic check for translations, translation will not be inserted if:
258
		 * If we do not have SELECT anywhere in query
259
		 * If current language is site default language
260
		 * If we are in administration
261
		 */
262
		if (!$validSelect
263
			|| $isDefaultLanguage
264
			|| ($isAdmin && !self::getOption('translateInAdmin', false)))
265
		{
266
			if (empty($db->parseTablesBefore) && empty($db->parseTablesAfter))
267
			{
268
				return null;
269
			}
270
		}
271
272
		$translationTables = RTranslationTable::getInstalledTranslationTables();
273
		$translationTables = RTranslationHelper::removeFromEditForm($translationTables);
274
		$sql               = self::parseSelectQuery($sql, $prefix, $selectedLanguage, $translationTables);
275
276
		if (!empty($db->parseTablesAfter))
277
		{
278
			foreach ($db->parseTablesAfter as $tableGroup)
0 ignored issues
show
Bug introduced by
The property parseTablesAfter does not seem to exist on JDatabaseDriver.
Loading history...
279
			{
280
				$sql = self::parseSelectQuery($sql, $prefix, $tableGroup->language, $tableGroup->translationTables);
281
			}
282
		}
283
284
		// Turn back real long amount of numeric values
285
		foreach ($hashValues as $hash => $values)
286
		{
287
			$sql = str_replace($hash, implode(',', $values), $sql);
288
		}
289
290
		return $sql;
291
	}
292
293
	/**
294
	 * Recursive method which go through every array and joins table if we have found the match
295
	 *
296
	 * @param   array $parsedSqlColumns  Parsed SQL in array format
297
	 * @param   array $columns           Found replacement tables
298
	 * @param   array $translationTables List of translation tables
299
	 * @param   bool  &$columnFound      Found at least one column from original table
300
	 * @param   bool  $addAlias          Should we add alias after column name
301
	 *
302
	 * @return  array  Parsed query with added table joins if found
303
	 */
304
	public static function parseColumnReplacements($parsedSqlColumns, $columns, $translationTables, &$columnFound, $addAlias = true)
305
	{
306
		if (!empty($parsedSqlColumns) && is_array($parsedSqlColumns))
307
		{
308
			$db = JFactory::getDbo();
309
310
			// Replace all Tables and keys
311
			foreach ($parsedSqlColumns as $groupColumnsKey => $parsedColumnGroup)
312
			{
313
				if (!empty($parsedColumnGroup))
314
				{
315
					$filteredGroup = array();
316
317
					foreach ($parsedColumnGroup as $tagKey => $tagColumnsValue)
318
					{
319
						$column = null;
320
321
						if (!empty($tagColumnsValue['expr_type']) && $tagColumnsValue['expr_type'] == 'colref')
322
						{
323
							$column = self::getNameIfIncluded($tagColumnsValue['base_expr'], '', $columns, false, $groupColumnsKey);
324
325
							if (!empty($column))
326
							{
327
								$primaryKey = '';
328
329
								if (!empty($translationTables[$column['table']['originalTableName']]->primaryKeys))
330
								{
331
									foreach ($translationTables[$column['table']['originalTableName']]->primaryKeys as $primaryKeyValue)
332
									{
333
										$primaryKey = self::getNameIfIncluded(
334
											$primaryKeyValue,
335
											$column['table']['alias']['originalName'],
336
											array($tagColumnsValue['base_expr']),
337
											false,
338
											$groupColumnsKey
339
										);
340
341
										if (empty($primaryKey))
342
										{
343
											break;
344
										}
345
									}
346
								}
347
348
								// This is primary key so if only this is used in query then we do not need to parse it
349
								if (empty($primaryKey))
350
								{
351
									$columnFound = true;
352
								}
353
354
								if (in_array($groupColumnsKey, array('FROM')))
355
								{
356
									if (empty($primaryKey) && $groupColumnsKey != 'FROM')
357
									{
358
										$tagColumnsValue['base_expr'] = self::breakColumnAndReplace($tagColumnsValue['base_expr'], $column['table']['alias']['name']);
359
									}
360
									else
361
									{
362
										$tagColumnsValue['base_expr'] = self::breakColumnAndReplace($tagColumnsValue['base_expr'], $column['table']['alias']['originalName']);
363
									}
364
								}
365
								else
366
								{
367
									if (in_array($groupColumnsKey, array('ORDER', 'WHERE', 'GROUP')) && !empty($primaryKey))
368
									{
369
										$tagColumnsValue['base_expr'] = self::breakColumnAndReplace($tagColumnsValue['base_expr'], $column['table']['alias']['originalName']);
370
									}
371
									else
372
									{
373
										$tagColumnsValue['base_expr'] = $column['base_expr'];
374
375
										if ($addAlias
376
											&& !empty($column['alias'])
377
											&& !in_array($groupColumnsKey, array('ORDER', 'WHERE', 'GROUP')))
378
										{
379
											$alias = $column['alias'];
380
381
											if (!empty($tagColumnsValue['alias']['name']))
382
											{
383
												$alias = $tagColumnsValue['alias']['name'];
384
											}
385
386
											$alias                    = $db->qn(self::cleanEscaping($alias));
387
											$tagColumnsValue['alias'] = array(
388
												'as'        => true,
389
												'name'      => $alias,
390
												'base_expr' => 'as ' . $alias
391
											);
392
										}
393
									}
394
								}
395
							}
396
						}
397
						elseif (!empty($tagColumnsValue['sub_tree']))
398
						{
399
							if (!empty($tagColumnsValue['expr_type']) && $tagColumnsValue['expr_type'] == 'subquery')
400
							{
401
								// SubQuery is already parsed so we do not need to parse columns again
402
							}
403
							elseif (!empty($tagColumnsValue['expr_type']) && in_array($tagColumnsValue['expr_type'], array('expression')))
404
							{
405
								foreach ($tagColumnsValue['sub_tree'] as $subKey => $subTree)
406
								{
407
									if (!empty($tagColumnsValue['sub_tree'][$subKey]['sub_tree']))
408
									{
409
										$tagColumnsValue['sub_tree'][$subKey]['sub_tree'] = self::parseColumnReplacements(
410
											array($groupColumnsKey => $tagColumnsValue['sub_tree'][$subKey]['sub_tree']),
411
											$columns,
412
											$translationTables,
413
											$columnFound,
414
											false
415
										);
416
										$tagColumnsValue['sub_tree'][$subKey]['sub_tree'] = $tagColumnsValue['sub_tree'][$subKey]['sub_tree'][$groupColumnsKey];
417
									}
418
								}
419
							}
420
							elseif (is_array($tagColumnsValue['sub_tree']))
421
							{
422
								// We will not replace some aggregate functions columns
423
								if (!(in_array($tagColumnsValue['expr_type'], array('aggregate_function')) && strtolower($tagColumnsValue['base_expr']) == 'count'))
424
								{
425
									$keys = array_keys($tagColumnsValue['sub_tree']);
426
427
									if (!is_numeric($keys[0]))
428
									{
429
										$tagColumnsValue['sub_tree'] = self::parseColumnReplacements(
430
											$tagColumnsValue['sub_tree'],
431
											$columns,
432
											$translationTables,
433
											$columnFound,
434
											false
435
										);
436
									}
437
									else
438
									{
439
										$tagColumnsValue['sub_tree'] = self::parseColumnReplacements(
440
											array($groupColumnsKey => $tagColumnsValue['sub_tree']),
441
											$columns,
442
											$translationTables,
443
											$columnFound,
444
											false
445
										);
446
										$tagColumnsValue['sub_tree'] = $tagColumnsValue['sub_tree'][$groupColumnsKey];
447
									}
448
								}
449
							}
450
						}
451
						elseif (!empty($tagColumnsValue['ref_clause']))
452
						{
453
							if (is_array($tagColumnsValue['ref_clause']))
454
							{
455
								$keys = array_keys($tagColumnsValue['ref_clause']);
456
457
								if (!is_numeric($keys[0]))
458
								{
459
									$tagColumnsValue['ref_clause'] = self::parseColumnReplacements(
460
										$tagColumnsValue['ref_clause'],
461
										$columns,
462
										$translationTables,
463
										$columnFound,
464
										false
465
									);
466
								}
467
								else
468
								{
469
									$tagColumnsValue['ref_clause'] = self::parseColumnReplacements(
470
										array($groupColumnsKey => $tagColumnsValue['ref_clause']),
471
										$columns,
472
										$translationTables,
473
										$columnFound,
474
										false
475
									);
476
									$tagColumnsValue['ref_clause'] = $tagColumnsValue['ref_clause'][$groupColumnsKey];
477
								}
478
							}
479
						}
480
481
						if (!is_numeric($tagKey))
482
						{
483
							$filteredGroup[$tagKey] = $tagColumnsValue;
484
						}
485
						else
486
						{
487
							$filteredGroup[] = $tagColumnsValue;
488
						}
489
					}
490
491
					$parsedSqlColumns[$groupColumnsKey] = $filteredGroup;
492
				}
493
			}
494
		}
495
496
		return $parsedSqlColumns;
497
	}
498
499
	/**
500
	 * Recursive method which go through every array and joins table if we have found the match
501
	 *
502
	 * @param   array  $parsedSql         Parsed SQL in array format
503
	 * @param   array  $translationTables List of translation tables
504
	 * @param   array  &$foundTables      Found replacement tables
505
	 * @param   array  &$originalTables   Found original tables used for creating unique alias
506
	 * @param   string $language          Language tag you want to fetch translation from
507
	 * @param   string &$subQueryFound    If sub query is found then we must parse end sql
508
	 *
509
	 * @return  array  Parsed query with added table joins if found
510
	 */
511
	public static function parseTableReplacements($parsedSql, $translationTables, &$foundTables, &$originalTables, $language, &$subQueryFound)
512
	{
513
		if (!empty($parsedSql) && is_array($parsedSql))
514
		{
515
			// Replace all Tables and keys
516
			foreach ($parsedSql as $groupKey => $parsedGroup)
517
			{
518
				if (!empty($parsedGroup))
519
				{
520
					$filteredGroup            = array();
521
					$filteredGroupEndPosition = array();
522
523
					foreach ($parsedGroup as $tagKey => $tagValue)
524
					{
525
						$tableName   = null;
526
						$newTagValue = null;
527
528
						if (!empty($tagValue['expr_type']) && $tagValue['expr_type'] == 'table' && !empty($tagValue['table']))
529
						{
530
							$tableName = self::getNameIfIncluded(
531
								$tagValue['table'],
532
								!empty($tagValue['alias']['name']) ? $tagValue['alias']['name'] : '',
533
								$translationTables,
534
								true,
535
								$groupKey
536
							);
537
538
							if (!empty($tableName))
539
							{
540
								$newTagValue                      = $tagValue;
541
								$newTagValue['originalTableName'] = $tableName;
542
								$newTagValue['table']             = RTranslationTable::getTranslationsTableName($tableName, '');
543
								$newTagValue['join_type']         = 'LEFT';
544
								$newTagValue['ref_type']          = 'ON';
545
								$alias                            = self::getUniqueAlias($tableName, $originalTables);
546
547
								if (!empty($newTagValue['alias']['name']))
548
								{
549
									$alias = $newTagValue['alias']['name'];
550
								}
551
552
								$tagValue['alias'] = array(
553
									'as'        => true,
554
									'name'      => $alias,
555
									'base_expr' => ''
556
								);
557
558
								$newTagValue['alias'] = array(
559
									'as'           => true,
560
									'name'         => self::getUniqueAlias($newTagValue['table'], $foundTables),
561
									'originalName' => $alias,
562
									'base_expr'    => ''
563
								);
564
565
								$refClause                                             = self::createParserJoinOperand(
566
									$newTagValue['alias']['name'],
567
									'=',
568
									$newTagValue['alias']['originalName'],
569
									$translationTables[$tableName],
570
									$language
571
								);
572
								$newTagValue['ref_clause']                             = $refClause;
573
								$newTagValue['index_hints']                            = false;
574
								$foundTables[]                                         = $newTagValue;
575
								$originalTables[$newTagValue['alias']['originalName']] = isset($originalTables[$newTagValue['alias']['originalName']]) ?
576
									$originalTables[$newTagValue['alias']['originalName']]++ : 1;
577
							}
578
						}
579
						// There is an issue in sql parser for UNION and UNION ALL, this is a solution for it
580
						elseif (!empty($tagValue['union_tree']) && is_array($tagValue['union_tree']) && in_array('UNION', $tagValue['union_tree']))
581
						{
582
							$subQueryFound = true;
583
							$unionTree     = array();
584
585
							foreach ($tagValue['union_tree'] as $union)
586
							{
587
								$union = trim($union);
588
589
								if (!empty($union) && strtoupper($union) != 'UNION')
590
								{
591
									$parsedSubQuery = self::buildTranslationQuery(self::removeParenthesisFromStart($union));
592
									$unionTree[]    = !empty($parsedSubQuery) ? '(' . $parsedSubQuery . ')' : $union;
593
								}
594
							}
595
596
							$tagValue['base_expr'] = '(' . implode(' UNION ', $unionTree) . ')';
597
							$tagValue['expr_type'] = 'const';
598
599
							if (!empty($tagValue['sub_tree']))
600
							{
601
								unset($tagValue['sub_tree']);
602
							}
603
604
							if (!empty($tagValue['join_type']))
605
							{
606
								unset($tagValue['join_type']);
607
							}
608
						}
609
						// Other types of expressions
610
						elseif (!empty($tagValue['sub_tree']))
611
						{
612
							if (!empty($tagValue['expr_type']) && $tagValue['expr_type'] == 'subquery')
613
							{
614
								$parsedSubQuery = self::buildTranslationQuery($tagValue['base_expr']);
615
616
								if (!empty($parsedSubQuery))
617
								{
618
									$sqlParser             = new RDatabaseSqlparserSqlparser($parsedSubQuery);
619
									$tagValue['sub_tree']  = $sqlParser->parsed;
620
									$tagValue['base_expr'] = $parsedSubQuery;
621
									$subQueryFound         = true;
622
								}
623
							}
624
							elseif (!empty($tagValue['expr_type']) && in_array($tagValue['expr_type'], array('expression')))
625
							{
626
								foreach ($tagValue['sub_tree'] as $subKey => $subTree)
627
								{
628
									// In case we have a Subquery directly under the expression we handle it separately
629
									if (!empty($subTree['expr_type']) && $subTree['expr_type'] == 'subquery')
630
									{
631
										// We need to remove brackets from the query or else the query will not be parsed properly
632
										$sqlBracketLess = self::removeParenthesisFromStart($subTree['base_expr']);
633
										$parsedSubQuery = self::buildTranslationQuery($sqlBracketLess);
634
635
										if (!empty($parsedSubQuery))
636
										{
637
											$sqlParser                                  = new RDatabaseSqlparserSqlparser($parsedSubQuery);
638
											$tagValue['sub_tree'][$subKey]['sub_tree']  = $sqlParser->parsed;
639
											$tagValue['sub_tree'][$subKey]['base_expr'] = '(' . $parsedSubQuery . ')';
640
											$subQueryFound                              = true;
641
										}
642
									}
643
									elseif (!empty($tagValue['sub_tree'][$subKey]['sub_tree']))
644
									{
645
										$tagValue['sub_tree'][$subKey]['sub_tree'] = self::parseTableReplacements(
646
											$tagValue['sub_tree'][$subKey]['sub_tree'],
647
											$translationTables,
648
											$foundTables,
649
											$originalTables,
650
											$language,
651
											$subQueryFound
652
										);
653
									}
654
								}
655
							}
656
							else
657
							{
658
								$tagValue['sub_tree'] = self::parseTableReplacements(
659
									$tagValue['sub_tree'],
660
									$translationTables,
661
									$foundTables,
662
									$originalTables,
663
									$language,
664
									$subQueryFound
665
								);
666
							}
667
						}
668
669
						if (!is_numeric($tagKey))
670
						{
671
							$filteredGroup[$tagKey] = $tagValue;
672
						}
673
						else
674
						{
675
							$filteredGroup[] = $tagValue;
676
						}
677
678
						if (!empty($newTagValue))
679
						{
680
							if (!empty($translationTables[$tableName]->tableJoinEndPosition))
681
							{
682
								$filteredGroupEndPosition[] = $newTagValue;
683
							}
684
							else
685
							{
686
								$filteredGroup[] = $newTagValue;
687
							}
688
						}
689
					}
690
691
					foreach ($filteredGroupEndPosition as $table)
692
					{
693
						$filteredGroup[] = $table;
694
					}
695
696
					$parsedSql[$groupKey] = $filteredGroup;
697
				}
698
			}
699
		}
700
701
		return $parsedSql;
702
	}
703
704
	/**
705
	 * Creates unique Alias name not used in existing query
706
	 *
707
	 * @param   string $originalTableName Original table name which we use for creating alias
708
	 * @param   array  $foundTables       Currently used tables in the query
709
	 * @param   int    $counter           Auto increasing number if we already have alias with the same name
710
	 *
711
	 * @return  string  Parsed query with added table joins and fields if found
712
	 */
713
	public static function getUniqueAlias($originalTableName, $foundTables = array(), $counter = 0)
714
	{
715
		$string = str_replace('#__', '', $originalTableName);
716
		$string .= '_' . substr(RFilesystemFile::getUniqueName($counter), 0, 4);
0 ignored issues
show
Bug introduced by
RFilesystemFile::getUniqueName($counter) of type boolean is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

716
		$string .= '_' . substr(/** @scrutinizer ignore-type */ RFilesystemFile::getUniqueName($counter), 0, 4);
Loading history...
717
718
		foreach ($foundTables as $foundTable)
719
		{
720
			if (isset($foundTable['alias']['name']) && $foundTable['alias']['name'] == $string)
721
			{
722
				$counter++;
723
724
				return self::getUniqueAlias($originalTableName, $foundTables, $counter);
725
			}
726
		}
727
728
		return $string;
729
	}
730
731
	/**
732
	 * Breaks column name and replaces alias with the new one
733
	 *
734
	 * @param   string $column      Column Name with or without prefix
735
	 * @param   string $replaceWith Alias name to replace current one
736
	 *
737
	 * @return  string  Parsed query with added table joins and fields if found
738
	 */
739
	public static function breakColumnAndReplace($column, $replaceWith)
740
	{
741
		$column = explode('.', $column);
742
743
		if (!empty($column))
744
		{
745
			if (count($column) == 1)
746
			{
747
				$column[1] = $column[0];
748
			}
749
750
			if (empty($replaceWith))
751
			{
752
				return $column[1];
753
			}
754
755
			$column[0] = $replaceWith;
756
		}
757
758
		return implode('.', $column);
759
	}
760
761
	/**
762
	 * Creates array in sql Parser format, this function adds language filter as well
763
	 *
764
	 * @param   string $newTable    Table alias of new table
765
	 * @param   string $operator    Operator of joining tables
766
	 * @param   string $oldTable    Alias of original table
767
	 * @param   object $tableObject Alias of original table
768
	 * @param   string $language    Language tag you want to fetch translation from
769
	 *
770
	 * @return  string  Parsed query with added table joins and fields if found
771
	 */
772
	public static function createParserJoinOperand($newTable, $operator, $oldTable, $tableObject, $language)
773
	{
774
		$db        = JFactory::getDbo();
775
		$refClause = array();
776
777
		if (!empty($tableObject->primaryKeys))
778
		{
779
			foreach ($tableObject->primaryKeys as $primaryKey)
780
			{
781
				$refClause[] = self::createParserElement('colref', $db->qn($newTable) . '.' . $primaryKey);
782
				$refClause[] = self::createParserElement('operator', $operator);
783
				$refClause[] = self::createParserElement('colref', $db->qn(self::cleanEscaping($oldTable)) . '.' . $primaryKey);
784
785
				$refClause[] = self::createParserElement('operator', 'AND');
786
			}
787
		}
788
789
		$refClause[] = self::createParserElement('colref', $db->qn($newTable) . '.rctranslations_language');
790
		$refClause[] = self::createParserElement('operator', '=');
791
		$refClause[] = self::createParserElement('colref', $db->q($language));
792
793
		$refClause[] = self::createParserElement('operator', 'AND');
794
795
		$refClause[] = self::createParserElement('colref', $db->qn($newTable) . '.rctranslations_state');
796
		$refClause[] = self::createParserElement('operator', '=');
797
		$refClause[] = self::createParserElement('colref', $db->q('1'));
798
799
		if (!empty($tableObject->tableJoinParams))
800
		{
801
			foreach ($tableObject->tableJoinParams as $join)
802
			{
803
				$leftSide  = $join['left'];
804
				$rightSide = $join['right'];
805
806
				// Add alias if needed to the left side
807
				if (!empty($join['aliasLeft']))
808
				{
809
					$leftSide = ($join['aliasLeft'] == 'original' ? $db->qn(self::cleanEscaping($oldTable)) : $db->qn($newTable)) . '.' . $leftSide;
810
				}
811
812
				// Add alias if needed to the right side
813
				if (!empty($join['aliasRight']))
814
				{
815
					$rightSide = ($join['aliasRight'] == 'original' ? $db->qn(self::cleanEscaping($oldTable)) : $db->qn($newTable)) . '.' . $rightSide;
816
				}
817
818
				$refClause[] = self::createParserElement('operator', $join['expressionOperator']);
819
				$refClause[] = self::createParserElement('colref', $leftSide);
820
				$refClause[] = self::createParserElement('operator', $join['operator']);
821
				$refClause[] = self::createParserElement('colref', $rightSide);
822
			}
823
		}
824
825
		return $refClause;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $refClause returns the type array|array<mixed,array<string,false|string>> which is incompatible with the documented return type string.
Loading history...
826
	}
827
828
	/**
829
	 * Create Table Join Parameter
830
	 *
831
	 * @param   string $left               Table alias of new table
832
	 * @param   string $operator           Operator of joining tables
833
	 * @param   string $right              Alias of original table
834
	 * @param   object $aliasLeft          Alias of original table
835
	 * @param   string $aliasRight         Language tag you want to fetch translation from
836
	 * @param   string $expressionOperator Language tag you want to fetch translation from
837
	 *
838
	 * @return  array  table join param
839
	 */
840
	public static function createTableJoinParam($left, $operator = '=', $right = '', $aliasLeft = null, $aliasRight = null, $expressionOperator = 'AND')
841
	{
842
		return array(
843
			'left'               => $left,
844
			'operator'           => $operator,
845
			'right'              => $right,
846
			'aliasLeft'          => $aliasLeft,
847
			'aliasRight'         => $aliasRight,
848
			'expressionOperator' => $expressionOperator,
849
		);
850
	}
851
852
	/**
853
	 * Create Table Join Parameter
854
	 *
855
	 * @param   array  $originalTableColumns Table alias of new table
856
	 * @param   array  $tableColumns         Operator of joining tables
857
	 * @param   string $alias                Original table alias
858
	 *
859
	 * @return  array  table join param
860
	 */
861
	public static function addBaseColumns($originalTableColumns, $tableColumns, $alias)
862
	{
863
		$columns = array();
864
865
		foreach ($originalTableColumns as $key => $value)
866
		{
867
			if (!in_array($key, $tableColumns))
868
			{
869
				$columns[] = $alias . '.' . $key;
870
			}
871
		}
872
873
		return implode(',', $columns);
0 ignored issues
show
Bug Best Practice introduced by
The expression return implode(',', $columns) returns the type string which is incompatible with the documented return type array.
Loading history...
874
	}
875
876
	/**
877
	 * Creates array in sql Parser format, this function adds language filter as well
878
	 *
879
	 * @param   string $exprType Expression type
880
	 * @param   string $baseExpr Base expression
881
	 * @param   bool   $subTree  Sub Tree
882
	 *
883
	 * @return  array  Parser Element in array format
884
	 */
885
	public static function createParserElement($exprType, $baseExpr, $subTree = false)
886
	{
887
		$element = array(
888
			'expr_type' => $exprType,
889
			'base_expr' => $baseExpr,
890
			'sub_tree'  => $subTree
891
		);
892
893
		return $element;
894
	}
895
896
	/**
897
	 * Check for different types of field usage in field list and returns name with alias if present
898
	 *
899
	 * @param   string $field      Field name this can be with or without quotes
900
	 * @param   string $tableAlias Table alias | optional
901
	 * @param   array  $fieldList  List of fields to check against
902
	 * @param   bool   $isTable    If we are checking against table string
903
	 * @param   string $groupName  Group name
904
	 *
905
	 * @return  mixed  Returns List item if Field name is included in field list
906
	 */
907
	public static function getNameIfIncluded($field, $tableAlias = '', $fieldList = array(), $isTable = false, $groupName = 'SELECT')
908
	{
909
		// No fields to search for
910
		if (empty($fieldList) || empty($field) || self::skipTranslationColumn($groupName, $field))
911
		{
912
			return '';
913
		}
914
915
		$field      = self::cleanEscaping($field);
916
		$fieldParts = explode('.', $field);
917
		$alias      = '';
918
919
		if (count($fieldParts) > 1)
920
		{
921
			$alias = $fieldParts[0];
922
			$field = $fieldParts[1];
923
		}
924
925
		// Check for field inclusion with various cases
926
		foreach ($fieldList as $fieldFromListQuotes => $fieldFromList)
927
		{
928
			if ($isTable)
929
			{
930
				switch (self::cleanEscaping($fieldFromListQuotes))
931
				{
932
					case $field:
933
						if (!empty($fieldFromList->tableAliasesToParse)
934
							&& !empty($tableAlias)
935
							&& !in_array(self::cleanEscaping($tableAlias), $fieldFromList->tableAliasesToParse))
936
						{
937
							continue 2;
938
						}
939
940
						return $fieldFromListQuotes;
941
				}
942
			}
943
			elseif ($tableAlias == '')
944
			{
945
				switch (self::cleanEscaping($fieldFromList['columnName']))
946
				{
947
					case $field:
948
					case self::cleanEscaping($fieldFromList['table']['alias']['originalName'] . '.' . $field):
949
						// If this is different table we do not check columns
950
						if (!empty($alias) && $alias != self::cleanEscaping($fieldFromList['table']['alias']['originalName']))
951
						{
952
							continue 2;
953
						}
954
955
						return $fieldFromList;
956
				}
957
			}
958
			else
959
			{
960
				switch (self::cleanEscaping($fieldFromList))
961
				{
962
					case $field:
963
					case self::cleanEscaping($tableAlias . '.' . $field):
964
965
						return $fieldFromList;
966
				}
967
			}
968
		}
969
970
		return '';
971
	}
972
973
	/**
974
	 * Check for database escape and remove it
975
	 *
976
	 * @param   string $sql Sql to check against
977
	 *
978
	 * @return  string  Returns true if Field name is included in field list
979
	 */
980
	public static function cleanEscaping($sql)
981
	{
982
		return str_replace('`', '', trim($sql));
983
	}
984
985
	/**
986
	 * Check for enclosing brackets and remove it
987
	 *
988
	 * @param   string $sql Sql to check against
989
	 *
990
	 * @return  string  Returns sql query without enclosing brackets
991
	 */
992
	public static function removeParenthesisFromStart($sql)
993
	{
994
		$parenthesisRemoved = 0;
995
996
		$trim = trim($sql);
997
998
		if ($trim !== "" && $trim[0] === "(")
999
		{
1000
			// Remove only one parenthesis pair now!
1001
			$parenthesisRemoved++;
1002
			$trim[0] = " ";
1003
			$trim    = trim($trim);
1004
		}
1005
1006
		$parenthesis = $parenthesisRemoved;
1007
		$i           = 0;
1008
		$string      = 0;
1009
1010
		while ($i < strlen($trim))
1011
		{
1012
			if ($trim[$i] === "\\")
1013
			{
1014
				// An escape character, the next character is irrelevant
1015
				$i += 2;
1016
				continue;
1017
			}
1018
1019
			if ($trim[$i] === "'" || $trim[$i] === '"')
1020
			{
1021
				$string++;
1022
			}
1023
1024
			if (($string % 2 === 0) && ($trim[$i] === "("))
1025
			{
1026
				$parenthesis++;
1027
			}
1028
1029
			if (($string % 2 === 0) && ($trim[$i] === ")"))
1030
			{
1031
				if ($parenthesis == $parenthesisRemoved)
1032
				{
1033
					$trim[$i] = " ";
1034
					$parenthesisRemoved--;
1035
				}
1036
1037
				$parenthesis--;
1038
			}
1039
1040
			$i++;
1041
		}
1042
1043
		return trim($trim);
1044
	}
1045
1046
	/**
1047
	 * Check for database escape and remove it
1048
	 *
1049
	 * @param   string $groupName Group name
1050
	 * @param   string $field     Field name this can be with or without quotes
1051
	 *
1052
	 * @return  bool  Returns true if Field name is included in field list
1053
	 */
1054
	public static function skipTranslationColumn($groupName, $field)
1055
	{
1056
		$db = JFactory::getDbo();
1057
1058
		if (!empty($db->skipColumns))
1059
		{
1060
			foreach ($db->skipColumns as $skipGroupName => $skipColumns)
0 ignored issues
show
Bug introduced by
The property skipColumns does not seem to exist on JDatabaseDriver.
Loading history...
1061
			{
1062
				if (!empty($skipColumns))
1063
				{
1064
					foreach ($skipColumns as $column)
1065
					{
1066
						if ($groupName == $skipGroupName && self::cleanEscaping($field) == $column)
1067
						{
1068
							return true;
1069
						}
1070
					}
1071
				}
1072
			}
1073
		}
1074
1075
		return false;
1076
	}
1077
1078
	/**
1079
	 * Set a translation option value.
1080
	 *
1081
	 * @param   string $key The key
1082
	 * @param   mixed  $val The default value
1083
	 *
1084
	 * @return  null
1085
	 */
1086
	public static function setOption($key, $val)
1087
	{
1088
		self::$options[$key] = $val;
1089
	}
1090
1091
	/**
1092
	 * Get a translation option value.
1093
	 *
1094
	 * @param   string $key     The key
1095
	 * @param   mixed  $default The default value
1096
	 *
1097
	 * @return  mixed  The value or the default value
1098
	 */
1099
	public static function getOption($key, $default = null)
1100
	{
1101
		if (isset(self::$options[$key]))
1102
		{
1103
			return self::$options[$key];
1104
		}
1105
1106
		return $default;
1107
	}
1108
1109
	/**
1110
	 * Set a translation option fallback value.
1111
	 *
1112
	 * @param   bool $enable Enable or disable translation fallback feature
1113
	 *
1114
	 * @return  null
1115
	 */
1116
	public static function setTranslationFallback($enable = true)
1117
	{
1118
		self::setOption('translationFallback', $enable);
1119
	}
1120
1121
	/**
1122
	 * Set a translation option force translate default value.
1123
	 *
1124
	 * @param   bool $enable Enable or disable force translate default language feature
1125
	 *
1126
	 * @return  null
1127
	 */
1128
	public static function setForceTranslateDefaultLanguage($enable = false)
1129
	{
1130
		self::setOption('forceTranslateDefault', $enable);
1131
	}
1132
1133
	/**
1134
	 * Set a translate data in Admin value.
1135
	 *
1136
	 * @param   bool $enable Enable or disable translation fallback feature
1137
	 *
1138
	 * @return  null
1139
	 */
1140
	public static function setTranslationInAdmin($enable = false)
1141
	{
1142
		self::setOption('translateInAdmin', $enable);
1143
	}
1144
}
1145