This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace Sofa\Eloquence; |
||
4 | |||
5 | use Illuminate\Database\Eloquent\Collection; |
||
6 | use Illuminate\Support\Str; |
||
7 | use Sofa\Eloquence\Contracts\Relations\Joiner; |
||
8 | use Sofa\Eloquence\Searchable\Column; |
||
9 | use Illuminate\Database\Query\Expression; |
||
10 | use Sofa\Hookable\Builder as HookableBuilder; |
||
11 | use Sofa\Eloquence\Searchable\ColumnCollection; |
||
12 | use Sofa\Eloquence\Contracts\Relations\JoinerFactory; |
||
13 | use Sofa\Eloquence\Contracts\Searchable\ParserFactory; |
||
14 | use Illuminate\Database\Query\Grammars\PostgresGrammar; |
||
15 | use Sofa\Eloquence\Searchable\Subquery as SearchableSubquery; |
||
16 | |||
17 | /** |
||
18 | * @method $this leftJoin($table, $one, $operator, $two) |
||
19 | */ |
||
20 | class Builder extends HookableBuilder |
||
21 | { |
||
22 | /** |
||
23 | * Parser factory instance. |
||
24 | * |
||
25 | * @var ParserFactory |
||
26 | */ |
||
27 | protected static $parser; |
||
28 | |||
29 | /** |
||
30 | * Joiner factory instance. |
||
31 | * |
||
32 | * @var JoinerFactory |
||
33 | */ |
||
34 | protected static $joinerFactory; |
||
35 | |||
36 | /** |
||
37 | * Relations joiner instance. |
||
38 | * |
||
39 | * @var Joiner |
||
40 | */ |
||
41 | protected $joiner; |
||
42 | |||
43 | /* |
||
44 | |-------------------------------------------------------------------------- |
||
45 | | Additional features |
||
46 | |-------------------------------------------------------------------------- |
||
47 | */ |
||
48 | |||
49 | /** |
||
50 | * Execute the query as a "select" statement. |
||
51 | * |
||
52 | * @param array $columns |
||
53 | * @return Collection |
||
54 | */ |
||
55 | public function get($columns = ['*']) |
||
56 | { |
||
57 | if ($this->query->from instanceof Subquery) { |
||
58 | $this->wheresToSubquery($this->query->from); |
||
59 | } |
||
60 | |||
61 | return parent::get($columns); |
||
62 | } |
||
63 | |||
64 | /** |
||
65 | * Search through any columns on current table or any defined relations |
||
66 | * and return results ordered by search relevance. |
||
67 | * |
||
68 | * @param array|string $query |
||
69 | * @param array $columns |
||
70 | * @param bool $fulltext |
||
71 | * @param float $threshold |
||
72 | * @return $this |
||
73 | */ |
||
74 | public function search($query, $columns = null, $fulltext = true, $threshold = null) |
||
75 | { |
||
76 | if (is_bool($columns)) { |
||
77 | list($fulltext, $columns) = [$columns, []]; |
||
78 | } |
||
79 | |||
80 | $parser = static::$parser->make(); |
||
81 | |||
82 | $words = is_array($query) ? $query : $parser->parseQuery($query, $fulltext); |
||
83 | |||
84 | $columns = $parser->parseWeights($columns ?: $this->model->getSearchableColumns()); |
||
85 | |||
86 | if (count($words) && count($columns)) { |
||
87 | $this->query->from($this->buildSubquery($words, $columns, $threshold)); |
||
88 | } |
||
89 | |||
90 | return $this; |
||
91 | } |
||
92 | |||
93 | /** |
||
94 | * Build the search subquery. |
||
95 | * |
||
96 | * @param array $words |
||
97 | * @param array $mappings |
||
98 | * @param float $threshold |
||
99 | * @return SearchableSubquery |
||
100 | */ |
||
101 | protected function buildSubquery(array $words, array $mappings, $threshold) |
||
102 | { |
||
103 | $subquery = new SearchableSubquery($this->query->newQuery(), $this->model->getTable()); |
||
104 | |||
105 | $columns = $this->joinForSearch($mappings, $subquery); |
||
106 | |||
107 | $threshold = (is_null($threshold)) |
||
108 | ? array_sum($columns->getWeights()) / 4 |
||
109 | : (float) $threshold; |
||
110 | |||
111 | $subquery->select($this->model->getTable() . '.*') |
||
112 | ->from($this->model->getTable()) |
||
113 | ->groupBy($this->model->getQualifiedKeyName()); |
||
114 | |||
115 | $this->addSearchClauses($subquery, $columns, $words, $threshold); |
||
116 | |||
117 | return $subquery; |
||
118 | } |
||
119 | |||
120 | /** |
||
121 | * Add select and where clauses on the subquery. |
||
122 | * |
||
123 | * @param SearchableSubquery $subquery |
||
124 | * @param ColumnCollection $columns |
||
125 | * @param array $words |
||
126 | * @param float $threshold |
||
127 | * @return void |
||
128 | */ |
||
129 | protected function addSearchClauses( |
||
130 | SearchableSubquery $subquery, |
||
131 | ColumnCollection $columns, |
||
132 | array $words, |
||
133 | $threshold |
||
134 | ) { |
||
135 | $whereBindings = $this->searchSelect($subquery, $columns, $words, $threshold); |
||
0 ignored issues
–
show
|
|||
136 | |||
137 | // For morphOne/morphMany support we need to port the bindings from JoinClauses. |
||
138 | $joinBindings = collect($subquery->getQuery()->joins)->flatMap(function ($join) { |
||
139 | return $join->getBindings(); |
||
140 | })->all(); |
||
141 | |||
142 | $this->addBinding($joinBindings, 'select'); |
||
143 | |||
144 | // Developer may want to skip the score threshold filtering by passing zero |
||
145 | // value as threshold in order to simply order full result by relevance. |
||
146 | // Otherwise we are going to add where clauses for speed improvement. |
||
147 | if ($threshold > 0) { |
||
148 | $this->searchWhere($subquery, $columns, $words, $whereBindings); |
||
149 | } |
||
150 | |||
151 | $this->query->where('relevance', '>=', new Expression(number_format($threshold, 2))); |
||
152 | |||
153 | $this->query->orders = array_merge( |
||
154 | [['column' => 'relevance', 'direction' => 'desc']], |
||
155 | (array) $this->query->orders |
||
156 | ); |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * Apply relevance select on the subquery. |
||
161 | * |
||
162 | * @param SearchableSubquery $subquery |
||
163 | * @param ColumnCollection $columns |
||
164 | * @param array $words |
||
165 | * @return array |
||
166 | */ |
||
167 | protected function searchSelect(SearchableSubquery $subquery, ColumnCollection $columns, array $words) |
||
168 | { |
||
169 | $cases = $bindings = []; |
||
170 | |||
171 | foreach ($columns as $column) { |
||
172 | list($cases[], $binding) = $this->buildCase($column, $words); |
||
173 | |||
174 | $bindings = array_merge_recursive($bindings, $binding); |
||
175 | } |
||
176 | |||
177 | $select = implode(' + ', $cases); |
||
178 | |||
179 | $subquery->selectRaw("max({$select}) as relevance"); |
||
180 | |||
181 | $this->addBinding($bindings['select'], 'select'); |
||
182 | |||
183 | return $bindings['where']; |
||
184 | } |
||
185 | |||
186 | /** |
||
187 | * Apply where clauses on the subquery. |
||
188 | * |
||
189 | * @param SearchableSubquery $subquery |
||
190 | * @param ColumnCollection $columns |
||
191 | * @param array $words |
||
192 | * @return void |
||
193 | */ |
||
194 | protected function searchWhere( |
||
195 | SearchableSubquery $subquery, |
||
196 | ColumnCollection $columns, |
||
197 | array $words, |
||
198 | array $bindings |
||
199 | ) { |
||
200 | $operator = $this->getLikeOperator(); |
||
201 | |||
202 | $wheres = []; |
||
203 | |||
204 | foreach ($columns as $column) { |
||
205 | $wheres[] = implode( |
||
206 | ' or ', |
||
207 | array_fill(0, count($words), sprintf('%s %s ?', $column->getWrapped(), $operator)) |
||
208 | ); |
||
209 | } |
||
210 | |||
211 | $where = implode(' or ', $wheres); |
||
212 | |||
213 | $subquery->whereRaw("({$where})"); |
||
214 | |||
215 | $this->addBinding($bindings, 'select'); |
||
216 | } |
||
217 | |||
218 | /** |
||
219 | * Move where clauses to subquery to improve performance. |
||
220 | * |
||
221 | * @param SearchableSubquery $subquery |
||
222 | * @return void |
||
223 | */ |
||
224 | protected function wheresToSubquery(SearchableSubquery $subquery) |
||
225 | { |
||
226 | $bindingKey = 0; |
||
227 | |||
228 | $typesToMove = [ |
||
229 | 'basic', 'in', 'notin', 'between', 'null', |
||
230 | 'notnull', 'date', 'day', 'month', 'year', |
||
231 | ]; |
||
232 | |||
233 | // Here we are going to move all the where clauses that we might apply |
||
234 | // on the subquery in order to improve performance, since this way |
||
235 | // we can drastically reduce number of joined rows on subquery. |
||
236 | foreach ((array) $this->query->wheres as $key => $where) { |
||
237 | $type = strtolower($where['type']); |
||
238 | |||
239 | $bindingsCount = $this->countBindings($where, $type); |
||
240 | |||
241 | if (in_array($type, $typesToMove) && $this->model->hasColumn($where['column'])) { |
||
242 | unset($this->query->wheres[$key]); |
||
243 | |||
244 | $where['column'] = $this->model->getTable() . '.' . $where['column']; |
||
245 | |||
246 | $subquery->getQuery()->wheres[] = $where; |
||
247 | |||
248 | $whereBindings = $this->query->getRawBindings()['where']; |
||
249 | |||
250 | $bindings = array_splice($whereBindings, $bindingKey, $bindingsCount); |
||
251 | |||
252 | $this->query->setBindings($whereBindings, 'where'); |
||
253 | |||
254 | $this->query->addBinding($bindings, 'select'); |
||
255 | |||
256 | // if where is not to be moved onto the subquery, let's increment |
||
257 | // binding key appropriately, so we can reliably move binding |
||
258 | // for the next where clauses in the loop that is running. |
||
259 | } else { |
||
260 | $bindingKey += $bindingsCount; |
||
261 | } |
||
262 | } |
||
263 | } |
||
264 | |||
265 | /** |
||
266 | * Get number of bindings provided for a where clause. |
||
267 | * |
||
268 | * @param array $where |
||
269 | * @param string $type |
||
270 | * @return int |
||
271 | */ |
||
272 | protected function countBindings(array $where, $type) |
||
273 | { |
||
274 | if ($this->isHasWhere($where, $type)) { |
||
275 | return substr_count($where['column'] . $where['value'], '?'); |
||
276 | } elseif ($type === 'basic') { |
||
277 | return (int) !$where['value'] instanceof Expression; |
||
278 | } elseif (in_array($type, ['basic', 'date', 'year', 'month', 'day'])) { |
||
279 | return (int) !$where['value'] instanceof Expression; |
||
280 | } elseif (in_array($type, ['null', 'notnull'])) { |
||
281 | return 0; |
||
282 | } elseif ($type === 'between') { |
||
283 | return 2; |
||
284 | } elseif (in_array($type, ['in', 'notin'])) { |
||
285 | return count($where['values']); |
||
286 | } elseif ($type === 'raw') { |
||
287 | return substr_count($where['sql'], '?'); |
||
288 | } elseif (in_array($type, ['nested', 'sub', 'exists', 'notexists', 'insub', 'notinsub'])) { |
||
289 | return count($where['query']->getBindings()); |
||
290 | } |
||
291 | } |
||
292 | |||
293 | /** |
||
294 | * Determine whether where clause is eloquent has subquery. |
||
295 | * |
||
296 | * @param array $where |
||
297 | * @param string $type |
||
298 | * @return bool |
||
299 | */ |
||
300 | protected function isHasWhere($where, $type) |
||
301 | { |
||
302 | return $type === 'basic' |
||
303 | && $where['column'] instanceof Expression |
||
304 | && $where['value'] instanceof Expression; |
||
305 | } |
||
306 | |||
307 | /** |
||
308 | * Build case clause from all words for a single column. |
||
309 | * |
||
310 | * @param Column $column |
||
311 | * @param array $words |
||
312 | * @return array |
||
313 | */ |
||
314 | protected function buildCase(Column $column, array $words) |
||
315 | { |
||
316 | // THIS IS BAD |
||
317 | // @todo refactor |
||
318 | |||
319 | $operator = $this->getLikeOperator(); |
||
320 | |||
321 | $bindings['select'] = $bindings['where'] = array_map(function ($word) { |
||
322 | return $this->caseBinding($word); |
||
323 | }, $words); |
||
324 | |||
325 | $case = $this->buildEqualsCase($column, $words); |
||
326 | |||
327 | if (strpos(implode('', $words), '*') !== false) { |
||
328 | $leftMatching = []; |
||
329 | |||
330 | View Code Duplication | foreach ($words as $key => $word) { |
|
331 | if ($this->isLeftMatching($word)) { |
||
332 | $leftMatching[] = sprintf('%s %s ?', $column->getWrapped(), $operator); |
||
333 | $bindings['select'][] = $bindings['where'][$key] = $this->caseBinding($word) . '%'; |
||
334 | } |
||
335 | } |
||
336 | |||
337 | View Code Duplication | if (count($leftMatching)) { |
|
338 | $leftMatching = implode(' or ', $leftMatching); |
||
339 | $score = 5 * $column->getWeight(); |
||
340 | $case .= " + case when {$leftMatching} then {$score} else 0 end"; |
||
341 | } |
||
342 | |||
343 | $wildcards = []; |
||
344 | |||
345 | View Code Duplication | foreach ($words as $key => $word) { |
|
346 | if ($this->isWildcard($word)) { |
||
347 | $wildcards[] = sprintf('%s %s ?', $column->getWrapped(), $operator); |
||
348 | $bindings['select'][] = $bindings['where'][$key] = '%' . $this->caseBinding($word) . '%'; |
||
349 | } |
||
350 | } |
||
351 | |||
352 | View Code Duplication | if (count($wildcards)) { |
|
353 | $wildcards = implode(' or ', $wildcards); |
||
354 | $score = 1 * $column->getWeight(); |
||
355 | $case .= " + case when {$wildcards} then {$score} else 0 end"; |
||
356 | } |
||
357 | } |
||
358 | |||
359 | return [$case, $bindings]; |
||
360 | } |
||
361 | |||
362 | /** |
||
363 | * Replace '?' with single character SQL wildcards. |
||
364 | * |
||
365 | * @param string $word |
||
366 | * @return string |
||
367 | */ |
||
368 | protected function caseBinding($word) |
||
369 | { |
||
370 | $parser = static::$parser->make(); |
||
371 | |||
372 | return str_replace('?', '_', $parser->stripWildcards($word)); |
||
373 | } |
||
374 | |||
375 | /** |
||
376 | * Build basic search case for 'equals' comparison. |
||
377 | * |
||
378 | * @param Column $column |
||
379 | * @param array $words |
||
380 | * @return string |
||
381 | */ |
||
382 | protected function buildEqualsCase(Column $column, array $words) |
||
383 | { |
||
384 | $equals = implode(' or ', array_fill(0, count($words), sprintf('%s = ?', $column->getWrapped()))); |
||
385 | |||
386 | $score = 15 * $column->getWeight(); |
||
387 | |||
388 | return "case when {$equals} then {$score} else 0 end"; |
||
389 | } |
||
390 | |||
391 | /** |
||
392 | * Determine whether word ends with wildcard. |
||
393 | * |
||
394 | * @param string $word |
||
395 | * @return bool |
||
396 | */ |
||
397 | protected function isLeftMatching($word) |
||
398 | { |
||
399 | return Str::endsWith($word, '*'); |
||
400 | } |
||
401 | |||
402 | /** |
||
403 | * Determine whether word starts and ends with wildcards. |
||
404 | * |
||
405 | * @param string $word |
||
406 | * @return bool |
||
407 | */ |
||
408 | protected function isWildcard($word) |
||
409 | { |
||
410 | return Str::endsWith($word, '*') && Str::startsWith($word, '*'); |
||
411 | } |
||
412 | |||
413 | /** |
||
414 | * Get driver-specific case insensitive like operator. |
||
415 | * |
||
416 | * @return string |
||
417 | */ |
||
418 | public function getLikeOperator() |
||
419 | { |
||
420 | $grammar = $this->query->getGrammar(); |
||
421 | |||
422 | if ($grammar instanceof PostgresGrammar) { |
||
423 | return 'ilike'; |
||
424 | } |
||
425 | |||
426 | return 'like'; |
||
427 | } |
||
428 | |||
429 | /** |
||
430 | * Join related tables on the search subquery. |
||
431 | * |
||
432 | * @param array $mappings |
||
433 | * @param SearchableSubquery $subquery |
||
434 | * @return ColumnCollection |
||
435 | */ |
||
436 | protected function joinForSearch($mappings, $subquery) |
||
437 | { |
||
438 | $mappings = is_array($mappings) ? $mappings : (array) $mappings; |
||
439 | |||
440 | $columns = new ColumnCollection; |
||
441 | |||
442 | $grammar = $this->query->getGrammar(); |
||
443 | |||
444 | $joiner = static::$joinerFactory->make($subquery->getQuery(), $this->model); |
||
445 | |||
446 | // Here we loop through the search mappings in order to join related tables |
||
447 | // appropriately and build a searchable column collection, which we will |
||
448 | // use to build select and where clauses with correct table prefixes. |
||
449 | foreach ($mappings as $mapping => $weight) { |
||
450 | if (strpos($mapping, '.') !== false) { |
||
451 | list($relation, $column) = $this->model->parseMappedColumn($mapping); |
||
452 | |||
453 | $related = $joiner->leftJoin($relation); |
||
454 | |||
455 | $columns->add( |
||
456 | new Column($grammar, $related->getTable(), $column, $mapping, $weight) |
||
457 | ); |
||
458 | } else { |
||
459 | $columns->add( |
||
460 | new Column($grammar, $this->model->getTable(), $mapping, $mapping, $weight) |
||
461 | ); |
||
462 | } |
||
463 | } |
||
464 | |||
465 | return $columns; |
||
466 | } |
||
467 | |||
468 | /** |
||
469 | * Prefix selected columns with table name in order to avoid collisions. |
||
470 | * |
||
471 | * @return $this |
||
472 | */ |
||
473 | public function prefixColumnsForJoin() |
||
474 | { |
||
475 | if (!$columns = $this->query->columns) { |
||
476 | return $this->select($this->model->getTable() . '.*'); |
||
477 | } |
||
478 | |||
479 | foreach ($columns as $key => $column) { |
||
480 | if ($this->model->hasColumn($column)) { |
||
481 | $columns[$key] = $this->model->getTable() . '.' . $column; |
||
482 | } |
||
483 | } |
||
484 | |||
485 | $this->query->columns = $columns; |
||
486 | |||
487 | return $this; |
||
488 | } |
||
489 | |||
490 | /** |
||
491 | * Join related tables. |
||
492 | * |
||
493 | * @param array|string $relations |
||
494 | * @param string $type |
||
495 | * @return $this |
||
496 | */ |
||
497 | public function joinRelations($relations, $type = 'inner') |
||
498 | { |
||
499 | if (is_null($this->joiner)) { |
||
500 | $this->joiner = static::$joinerFactory->make($this); |
||
501 | } |
||
502 | |||
503 | if (!is_array($relations)) { |
||
504 | list($relations, $type) = [func_get_args(), 'inner']; |
||
505 | } |
||
506 | |||
507 | foreach ($relations as $relation) { |
||
508 | $this->joiner->join($relation, $type); |
||
509 | } |
||
510 | |||
511 | return $this; |
||
512 | } |
||
513 | |||
514 | /** |
||
515 | * Left join related tables. |
||
516 | * |
||
517 | * @param array|string $relations |
||
518 | * @return $this |
||
519 | */ |
||
520 | public function leftJoinRelations($relations) |
||
521 | { |
||
522 | $relations = is_array($relations) ? $relations : func_get_args(); |
||
523 | |||
524 | return $this->joinRelations($relations, 'left'); |
||
525 | } |
||
526 | |||
527 | /** |
||
528 | * Right join related tables. |
||
529 | * |
||
530 | * @param array|string $relations |
||
531 | * @return $this |
||
532 | */ |
||
533 | public function rightJoinRelations($relations) |
||
534 | { |
||
535 | $relations = is_array($relations) ? $relations : func_get_args(); |
||
536 | |||
537 | return $this->joinRelations($relations, 'right'); |
||
538 | } |
||
539 | |||
540 | /** |
||
541 | * Set search query parser factory instance. |
||
542 | * |
||
543 | * @param ParserFactory $factory |
||
544 | */ |
||
545 | public static function setParserFactory(ParserFactory $factory) |
||
546 | { |
||
547 | static::$parser = $factory; |
||
548 | } |
||
549 | |||
550 | /** |
||
551 | * Set the relations joiner factory instance. |
||
552 | * |
||
553 | * @param JoinerFactory $factory |
||
554 | */ |
||
555 | public static function setJoinerFactory(JoinerFactory $factory) |
||
556 | { |
||
557 | static::$joinerFactory = $factory; |
||
558 | } |
||
559 | } |
||
560 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.