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 Soupmix; |
||
4 | |||
5 | class SQLQueryBuilder extends AbstractQueryBuilder |
||
6 | { |
||
7 | |||
8 | private $queryBuilder; |
||
9 | |||
10 | 2 | public function run() |
|
11 | { |
||
12 | 2 | $this->queryBuilder = $this->getQueryBuilder(); |
|
13 | 2 | $this->setJoins(); |
|
14 | 2 | $numberOfRows = $this->getCount(); |
|
15 | 2 | if ($numberOfRows === 0) { |
|
16 | return ['total' => 0, 'data' => null]; |
||
17 | } |
||
18 | 2 | $this->setSortOrders(); |
|
19 | 2 | $this->setOffsetAndLimit(); |
|
20 | 2 | $this->setReturnFields(); |
|
21 | 2 | $stmt = $this->conn->executeQuery( |
|
22 | 2 | $this->queryBuilder->getSql(), |
|
23 | 2 | $this->queryBuilder->getParameters() |
|
24 | ); |
||
25 | 2 | $result = $stmt->fetchAll(\PDO::FETCH_ASSOC); |
|
26 | 2 | if ($this->distinctFieldName !== null) { |
|
27 | 1 | $numberOfRows = count($result); |
|
28 | } |
||
29 | 2 | return ['total' => $numberOfRows, 'data' => $result]; |
|
30 | } |
||
31 | |||
32 | 2 | private function getQueryBuilder() |
|
33 | { |
||
34 | 2 | if ($this->orFilters !== null) { |
|
35 | 2 | $this->andFilters[] = $this->orFilters; |
|
36 | } |
||
37 | 2 | $this->filters = $this->andFilters; |
|
38 | 2 | return $this->buildQuery($this->collection, $this->filters); |
|
39 | } |
||
40 | |||
41 | 2 | private function getCount() |
|
42 | { |
||
43 | 2 | $queryBuilderCount = clone $this->queryBuilder; |
|
44 | 2 | $queryBuilderCount->select(' COUNT(*) AS total '); |
|
45 | 2 | $stmt = $this->conn->executeQuery($queryBuilderCount->getSql(), $queryBuilderCount->getParameters()); |
|
46 | 2 | return (int) $stmt->fetch(\PDO::FETCH_ASSOC)['total']; |
|
47 | } |
||
48 | |||
49 | 2 | private function setSortOrders() |
|
50 | { |
||
51 | 2 | if ($this->sortFields !== null) { |
|
52 | foreach ($this->addAlias($this->sortFields) as $sortKey => $sortDir) { |
||
0 ignored issues
–
show
|
|||
53 | $this->queryBuilder->addOrderBy($sortKey, $sortDir); |
||
54 | } |
||
55 | } |
||
56 | 2 | } |
|
57 | |||
58 | 2 | private function setJoins() |
|
59 | { |
||
60 | 2 | $this->setJoinsForType('innerJoin'); |
|
61 | 2 | $this->setJoinsForType('leftJoin'); |
|
62 | 2 | $this->setJoinsForType('rightJoin'); |
|
63 | 2 | $this->setJoinsForType('outerJoin'); |
|
64 | 2 | } |
|
65 | |||
66 | 2 | private function setJoinsForType($joinType) |
|
67 | { |
||
68 | 2 | if ($this->{$joinType} === null) { |
|
69 | 2 | return; |
|
70 | } |
||
71 | 1 | foreach ($this->{$joinType} as $collectionName => $collection) { |
|
72 | 1 | $fieldNames = $this->addAlias($collection['returnFields'], $collectionName); |
|
73 | 1 | $this->returnFieldsForJoin($fieldNames); |
|
0 ignored issues
–
show
It seems like
$fieldNames defined by $this->addAlias($collect...lds'], $collectionName) on line 72 can also be of type string ; however, Soupmix\SQLQueryBuilder::returnFieldsForJoin() does only seem to accept null|array , maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. ![]() |
|||
74 | 1 | $joinCondition = ''; |
|
75 | 1 | foreach ($collection['relations'] as $relation) { |
|
76 | 1 | $joinCondition .= empty($joinCondition) ? '':' AND '; |
|
77 | 1 | $relationType = array_keys($relation)[0]; |
|
78 | 1 | $source = array_keys($relation[$relationType])[0]; |
|
79 | 1 | $condition = $collectionName . '.' . $source |
|
80 | 1 | . ' = ' . $this->collection . '.' . $relation[$relationType][$source]; |
|
81 | 1 | if ($relationType != 'field') { |
|
82 | 1 | $condition = $collectionName . '.' . $source . ' ' |
|
83 | 1 | . $relationType . ' '. $relation[$relationType][$source]; |
|
84 | } |
||
85 | 1 | $joinCondition .= $condition; |
|
86 | } |
||
87 | 1 | $this->queryBuilder->{$joinType}($this->collection, $collectionName, $collectionName, $joinCondition); |
|
88 | } |
||
89 | 1 | return $this; |
|
90 | } |
||
91 | 1 | public function returnFieldsForJoin(array $fieldNames = null) |
|
92 | { |
||
93 | 1 | if ($fieldNames !== null) { |
|
94 | 1 | foreach ($fieldNames as $fieldName) { |
|
95 | 1 | $this->fieldNames[] = $fieldName; |
|
96 | } |
||
97 | } |
||
98 | 1 | } |
|
99 | |||
100 | 2 | private function setReturnFields() |
|
101 | { |
||
102 | 2 | if ($this->distinctFieldName === null) { |
|
103 | 2 | $fieldNames = ($this->fieldNames === null) |
|
104 | 1 | ? $this->addAlias('*') |
|
105 | 2 | : $this->addAlias($this->fieldNames); |
|
106 | 2 | $this->queryBuilder->select($fieldNames); |
|
107 | 2 | return; |
|
108 | } |
||
109 | 1 | $this->queryBuilder->select('DISTINCT (' . $this->collection . '.' . $this->distinctFieldName . ')'); |
|
110 | 1 | } |
|
111 | |||
112 | 2 | private function setOffsetAndLimit() |
|
113 | { |
||
114 | 2 | $this->queryBuilder->setFirstResult($this->offset) |
|
115 | 2 | ->setMaxResults($this->limit); |
|
116 | 2 | } |
|
117 | |||
118 | 2 | private function addAlias($fields, $collection = null) |
|
119 | { |
||
120 | 2 | $collection = (null !== $collection) ? $collection : $this->collection; |
|
121 | 2 | if (!is_array($fields)) { |
|
122 | 1 | return $collection . '.' . $fields; |
|
123 | } |
||
124 | |||
125 | 1 | $newFields = []; |
|
126 | 1 | foreach ($fields as $field => $value) { |
|
127 | |||
128 | 1 | if (is_int($field)) { |
|
129 | 1 | if (strpos($value, '.') > 0) { |
|
130 | 1 | $newFields[] = $value; |
|
131 | 1 | continue; |
|
132 | } |
||
133 | 1 | $newFields[] = $collection . '.' . $value; |
|
134 | 1 | continue; |
|
135 | } |
||
136 | $newFields[$collection.'.'.$field] = $value; |
||
137 | } |
||
138 | 1 | return $newFields; |
|
139 | } |
||
140 | |||
141 | 2 | protected function buildQuery($collection, $filters) |
|
142 | { |
||
143 | 2 | $queryBuilder = $this->conn->createQueryBuilder(); |
|
144 | 2 | $queryBuilder->from($collection, $collection); |
|
145 | 2 | if ($filters === null) { |
|
146 | return $queryBuilder; |
||
147 | } |
||
148 | 2 | return $this->buildQueryFilters($queryBuilder, $filters); |
|
149 | } |
||
150 | |||
151 | 2 | protected function buildQueryFilters($queryBuilder, $filters) |
|
152 | { |
||
153 | 2 | foreach ($filters as $key => $value) { |
|
154 | 2 | if (is_array($value) && strpos($key, '__') === false) { |
|
155 | 2 | $queryBuilder = $this->buildQueryForOr($queryBuilder, $value); |
|
156 | 2 | continue; |
|
157 | } |
||
158 | 2 | $queryBuilder = $this->buildQueryForAnd($queryBuilder, $key, $value); |
|
159 | } |
||
160 | 2 | return $queryBuilder; |
|
161 | } |
||
162 | |||
163 | 2 | protected function buildQueryForAnd($queryBuilder, $key, $value) |
|
164 | { |
||
165 | 2 | $sqlOptions = self::buildFilter([$key => $value]); |
|
166 | 2 | View Code Duplication | if (in_array($sqlOptions['method'], ['in', 'notIn'], true)) { |
167 | $queryBuilder->andWhere( |
||
168 | $queryBuilder->expr()->{$sqlOptions['method']}( |
||
169 | $this->collection . '.' . $sqlOptions['key'], $sqlOptions['value'] |
||
170 | ) |
||
171 | ); |
||
172 | return $queryBuilder; |
||
173 | } |
||
174 | 2 | $queryBuilder->andWhere( |
|
175 | 2 | '' . $this->collection . '.' . $sqlOptions['key'].'' |
|
176 | 2 | . ' ' . $sqlOptions['operand'] |
|
177 | 2 | . ' ' . $queryBuilder->createNamedParameter($sqlOptions['value']) |
|
178 | ); |
||
179 | 2 | return $queryBuilder; |
|
180 | } |
||
181 | 2 | protected function buildQueryForOr($queryBuilder, $value) |
|
182 | { |
||
183 | 2 | $orQuery =[]; |
|
184 | 2 | foreach ($value as $orValue) { |
|
185 | 2 | $subKey = array_keys($orValue)[0]; |
|
186 | 2 | $subValue = $orValue[$subKey]; |
|
187 | 2 | $sqlOptions = self::buildFilter([$subKey => $subValue]); |
|
188 | 2 | View Code Duplication | if (in_array($sqlOptions['method'], ['in', 'notIn'], true)) { |
189 | 1 | $orQuery[] = $queryBuilder->expr()->{$sqlOptions['method']}( |
|
190 | 1 | $this->collection . '.' . $sqlOptions['key'], $sqlOptions['value'] |
|
191 | ); |
||
192 | 1 | continue; |
|
193 | } |
||
194 | 2 | $orQuery[] = |
|
195 | 2 | '' . $this->collection . '.' . $sqlOptions['key'].'' |
|
196 | 2 | . ' ' . $sqlOptions['operand'] |
|
197 | 2 | . ' ' . $queryBuilder->createNamedParameter($sqlOptions['value']); |
|
198 | } |
||
199 | 2 | $queryBuilder->andWhere( |
|
200 | 2 | '(' . implode(' OR ', $orQuery) . ')' |
|
201 | ); |
||
202 | 2 | return $queryBuilder; |
|
203 | } |
||
204 | |||
205 | |||
206 | 2 | public static function buildFilter($filter) |
|
207 | { |
||
208 | 2 | $key = array_keys($filter)[0]; |
|
209 | 2 | $value = $filter[$key]; |
|
210 | 2 | $operator = ' = '; |
|
211 | 2 | $method = 'eq'; |
|
212 | $options =[ |
||
213 | 2 | 'gte' => ['method' => 'gte', 'operand' => ' >= '], |
|
214 | 'gt' => ['method' => 'gt', 'operand' => ' > '], |
||
215 | 'lte' => ['method' => 'lte', 'operand' => ' <= '], |
||
216 | 'lt' => ['method' => 'lt', 'operand' => ' < '], |
||
217 | 'in' => ['method' => 'in', 'operand' => ' IN '], |
||
218 | '!in' => ['method' => 'notIn', 'operand' => ' NOT IN '], |
||
219 | 'not' => ['method' => 'not', 'operand' => ' != '], |
||
220 | 'wildcard' => ['method' => 'like', 'operand' => ' LIKE '], |
||
221 | 'prefix' => ['method' => 'like', 'operand' => ' LIKE '], |
||
222 | ]; |
||
223 | 2 | if (strpos($key, '__') !== false) { |
|
224 | 2 | preg_match('/__(.*?)$/', $key, $matches); |
|
225 | 2 | $key = str_replace($matches[0], '', $key); |
|
226 | 2 | $queryOperator = $matches[1]; |
|
227 | 2 | $method = $options[$queryOperator]['method']; |
|
228 | 2 | $operator = $options[$queryOperator]['operand']; |
|
229 | switch ($queryOperator) { |
||
230 | 2 | case 'wildcard': |
|
231 | 1 | $value = '%'.str_replace(array('?', '*'), array('_', '%'), $value).'%'; |
|
232 | 1 | break; |
|
233 | 2 | case 'prefix': |
|
234 | 1 | $value .= '%'; |
|
235 | 1 | break; |
|
236 | } |
||
237 | } |
||
238 | return [ |
||
239 | 2 | 'key' => $key, |
|
240 | 2 | 'operand' => $operator, |
|
241 | 2 | 'method' => $method, |
|
242 | 2 | 'value' => $value |
|
243 | ]; |
||
244 | } |
||
245 | } |
||
246 |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.