1 | <?php |
||
2 | |||
3 | namespace Lagdo\DbAdmin\Db\Facades; |
||
4 | |||
5 | use Exception; |
||
6 | use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity; |
||
7 | |||
8 | use function count; |
||
9 | use function str_replace; |
||
10 | use function compact; |
||
11 | use function preg_match; |
||
12 | use function microtime; |
||
13 | use function trim; |
||
14 | use function md5; |
||
15 | use function strlen; |
||
16 | use function strpos; |
||
17 | |||
18 | /** |
||
19 | * Facade to table select functions |
||
20 | */ |
||
21 | class SelectFacade extends AbstractFacade |
||
22 | { |
||
23 | use Traits\SelectTrait; |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
24 | |||
25 | /** |
||
26 | * @param array $select |
||
27 | * @param array $columns |
||
28 | * @param array $indexes |
||
29 | * @param int $limit |
||
30 | * @param int $textLength |
||
31 | * @param array $queryOptions |
||
32 | * |
||
33 | * @return array |
||
34 | */ |
||
35 | private function getAllOptions(array $select, array $columns, array $indexes, |
||
36 | int $limit, int $textLength, array $queryOptions): array |
||
37 | { |
||
38 | return [ |
||
39 | 'columns' => $this->getColumnsOptions($select, $columns, $queryOptions), |
||
40 | 'filters' => $this->getFiltersOptions($columns, $indexes, $queryOptions), |
||
41 | 'sorting' => $this->getSortingOptions($columns, $queryOptions), |
||
42 | 'limit' => $this->getLimitOptions($limit), |
||
43 | 'length' => $this->getLengthOptions($textLength), |
||
44 | // 'action' => $this->getActionOptions($indexes), |
||
45 | ]; |
||
46 | } |
||
47 | |||
48 | /** |
||
49 | * Find out foreign keys for each column |
||
50 | * |
||
51 | * @param string $table |
||
52 | * |
||
53 | * @return array |
||
54 | */ |
||
55 | private function foreignKeys(string $table): array |
||
56 | { |
||
57 | $keys = []; |
||
58 | foreach ($this->driver->foreignKeys($table) as $foreignKey) { |
||
59 | foreach ($foreignKey->source as $val) { |
||
60 | $keys[$val][] = $foreignKey; |
||
61 | } |
||
62 | } |
||
63 | return $keys; |
||
64 | } |
||
65 | |||
66 | /** |
||
67 | * Get required data for create/update on tables |
||
68 | * |
||
69 | * @param string $table The table name |
||
70 | * @param array $queryOptions The query options |
||
71 | * |
||
72 | * @return array |
||
73 | * @throws Exception |
||
74 | */ |
||
75 | private function prepareSelect(string $table, array &$queryOptions = []): array |
||
76 | { |
||
77 | $page = $this->setDefaultOptions($queryOptions); |
||
78 | $this->utils->input->setValues($queryOptions); |
||
79 | |||
80 | // From select.inc.php |
||
81 | $fields = $this->driver->fields($table); |
||
82 | [, $columns, $textLength] = $this->getFieldsOptions($fields); |
||
83 | if (!$columns && $this->driver->support("table")) { |
||
84 | throw new Exception($this->utils->trans->lang('Unable to select the table') . |
||
85 | ($fields ? "." : ": " . $this->driver->error())); |
||
86 | } |
||
87 | |||
88 | $indexes = $this->driver->indexes($table); |
||
89 | $foreignKeys = $this->foreignKeys($table); |
||
90 | [$select, $group] = $this->admin->processSelectColumns(); |
||
91 | $where = $this->admin->processSelectWhere($fields, $indexes); |
||
92 | $order = $this->admin->processSelectOrder(); |
||
93 | $limit = $this->admin->processSelectLimit(); |
||
94 | $tableStatus = $this->driver->tableStatusOrName($table); |
||
95 | $unselected = $this->setPrimaryKey($indexes, $select, $tableStatus); |
||
96 | $tableName = $this->admin->tableName($tableStatus); |
||
97 | |||
98 | // $set = null; |
||
99 | // if(isset($rights["insert"]) || !this->driver->support("table")) { |
||
100 | // $set = ""; |
||
101 | // foreach((array) $queryOptions["where"] as $val) { |
||
102 | // if($foreignKeys[$val["col"]] && count($foreignKeys[$val["col"]]) == 1 && ($val["op"] == "=" |
||
103 | // || (!$val["op"] && !preg_match('~[_%]~', $val["val"])) // LIKE in Editor |
||
104 | // )) { |
||
105 | // $set .= "&set" . urlencode("[" . $this->driver->bracketEscape($val["col"]) . "]") . "=" . urlencode($val["val"]); |
||
106 | // } |
||
107 | // } |
||
108 | // } |
||
109 | // $this->admin->selectLinks($tableStatus, $set); |
||
110 | |||
111 | // if($page == "last") |
||
112 | // { |
||
113 | // $isGroup = count($group) < count($select); |
||
114 | // $found_rows = $this->driver->result($this->driver->getRowCountQuery($table, $where, $isGroup, $group)); |
||
115 | // $page = \floor(\max(0, $found_rows - 1) / $limit); |
||
116 | // } |
||
117 | |||
118 | $options = $this->getAllOptions($select, $columns, |
||
119 | $indexes, $limit, $textLength, $queryOptions); |
||
120 | $entity = $this->getSelectEntity($table, $columns, $fields, |
||
121 | $select, $group, $where, $order, $unselected, $limit, $page); |
||
122 | $query = $this->driver->buildSelectQuery($entity); |
||
123 | // From adminer.inc.php |
||
124 | $query = str_replace("\n", " ", $query); |
||
125 | |||
126 | return [$options, $query, $select, $fields, $foreignKeys, $columns, $indexes, |
||
127 | $where, $group, $order, $limit, $page, $textLength, $tableName, $unselected]; |
||
128 | } |
||
129 | |||
130 | /** |
||
131 | * Get required data for create/update on tables |
||
132 | * |
||
133 | * @param string $table The table name |
||
134 | * @param array $queryOptions The query options |
||
135 | * |
||
136 | * @return array |
||
137 | * @throws Exception |
||
138 | */ |
||
139 | public function getSelectData(string $table, array $queryOptions = []): array |
||
140 | { |
||
141 | [$options, $query, , , , , , , , , $limit, $page] = $this->prepareSelect($table, $queryOptions); |
||
142 | $query = $this->utils->str->html($query); |
||
143 | |||
144 | return compact('options', 'query', 'limit', 'page'); |
||
145 | } |
||
146 | |||
147 | /** |
||
148 | * @param string $query |
||
149 | * @param int $page |
||
150 | * |
||
151 | * @return array |
||
152 | */ |
||
153 | private function executeSelect(string $query, int $page): array |
||
154 | { |
||
155 | // From driver.inc.php |
||
156 | $statement = $this->driver->execute($query); |
||
157 | // From adminer.inc.php |
||
158 | |||
159 | if (!$statement) { |
||
160 | return ['error' => $this->driver->error()]; |
||
161 | } |
||
162 | // From select.inc.php |
||
163 | $rows = []; |
||
164 | while (($row = $statement->fetchAssoc())) { |
||
165 | if ($page && $this->driver->jush() == "oracle") { |
||
166 | unset($row["RNUM"]); |
||
167 | } |
||
168 | $rows[] = $row; |
||
169 | } |
||
170 | |||
171 | return [$rows, 0]; |
||
172 | } |
||
173 | |||
174 | /** |
||
175 | * @param array $rows |
||
176 | * @param array $select |
||
177 | * @param array $fields |
||
178 | * @param array $unselected |
||
179 | * @param array $queryOptions |
||
180 | * |
||
181 | * @return array |
||
182 | */ |
||
183 | private function getResultHeaders(array $rows, array $select, array $fields, array $unselected, array $queryOptions): array |
||
184 | { |
||
185 | // Results headers |
||
186 | $headers = [ |
||
187 | '', // !$group && $select ? '' : lang('Modify'); |
||
188 | ]; |
||
189 | $names = []; |
||
190 | // $functions = []; |
||
191 | reset($select); |
||
192 | $rank = 1; |
||
193 | foreach ($rows[0] as $key => $value) { |
||
194 | $header = []; |
||
195 | if (!isset($unselected[$key])) { |
||
196 | $value = $queryOptions["columns"][key($select)] ?? []; |
||
197 | $fun = $value["fun"] ?? ''; |
||
198 | $field = $fields[$select ? ($value ? $value["col"] : current($select)) : $key]; |
||
199 | $name = ($field ? $this->admin->fieldName($field, $rank) : ($fun ? "*" : $key)); |
||
200 | $header = compact('value', 'field', 'name'); |
||
201 | if ($name != "") { |
||
202 | $rank++; |
||
203 | $names[$key] = $name; |
||
204 | $column = $this->driver->escapeId($key); |
||
205 | // $href = remove_from_uri('(order|desc)[^=]*|page') . '&order%5B0%5D=' . urlencode($key); |
||
206 | // $desc = "&desc%5B0%5D=1"; |
||
207 | $header['column'] = $column; |
||
208 | $header['key'] = $this->utils->str->html($this->driver->bracketEscape($key)); |
||
209 | $header['sql'] = $this->admin->applySqlFunction($fun, $name); //! columns looking like functions |
||
210 | } |
||
211 | // $functions[$key] = $fun; |
||
212 | next($select); |
||
213 | } |
||
214 | $headers[] = $header; |
||
215 | } |
||
216 | return [$headers, $names]; |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * @param array $rows |
||
221 | * @param array $queryOptions |
||
222 | * |
||
223 | * @return array |
||
224 | */ |
||
225 | /*private function getValuesLengths(array $rows, array $queryOptions): array |
||
226 | { |
||
227 | $lengths = []; |
||
228 | if($queryOptions["modify"]) |
||
229 | { |
||
230 | foreach($rows as $row) |
||
231 | { |
||
232 | foreach($row as $key => $value) |
||
233 | { |
||
234 | $lengths[$key] = \max($lengths[$key], \min(40, strlen(\utf8_decode($value)))); |
||
235 | } |
||
236 | } |
||
237 | } |
||
238 | return $lengths; |
||
239 | }*/ |
||
240 | |||
241 | /** |
||
242 | * @param array $row |
||
243 | * @param array $indexes |
||
244 | * |
||
245 | * @return array |
||
246 | */ |
||
247 | private function getUniqueIds(array $row, array $indexes): array |
||
248 | { |
||
249 | $uniqueIds = $this->admin->uniqueIds($row, $indexes); |
||
250 | if (empty($uniqueIds)) { |
||
251 | $pattern = '~^(COUNT\((\*|(DISTINCT )?`(?:[^`]|``)+`)\)|(AVG|GROUP_CONCAT|MAX|MIN|SUM)\(`(?:[^`]|``)+`\))$~'; |
||
252 | foreach ($row as $key => $value) { |
||
253 | if (!preg_match($pattern, $key)) { |
||
254 | //! columns looking like functions |
||
255 | $uniqueIds[$key] = $value; |
||
256 | } |
||
257 | } |
||
258 | } |
||
259 | return $uniqueIds; |
||
260 | } |
||
261 | |||
262 | /** |
||
263 | * @param array $row |
||
264 | * @param array $fields |
||
265 | * @param array $indexes |
||
266 | * |
||
267 | * @return array |
||
268 | */ |
||
269 | private function getRowIds(array $row, array $fields, array $indexes): array |
||
270 | { |
||
271 | $uniqueIds = $this->getUniqueIds($row, $indexes); |
||
272 | // Unique identifier to edit returned data. |
||
273 | // $unique_idf = ""; |
||
274 | $rowIds = ['where' => [], 'null' => []]; |
||
275 | foreach ($uniqueIds as $key => $value) { |
||
276 | $key = trim($key); |
||
277 | $type = ''; |
||
278 | $collation = ''; |
||
279 | if (isset($fields[$key])) { |
||
280 | $type = $fields[$key]->type; |
||
281 | $collation = $fields[$key]->collation; |
||
282 | } |
||
283 | if (($this->driver->jush() == "sql" || $this->driver->jush() == "pgsql") && |
||
284 | preg_match('~char|text|enum|set~', $type) && strlen($value) > 64) { |
||
285 | $key = (strpos($key, '(') ? $key : $this->driver->escapeId($key)); //! columns looking like functions |
||
286 | $key = "MD5(" . ($this->driver->jush() != 'sql' || preg_match("~^utf8~", $collation) ? |
||
287 | $key : "CONVERT($key USING " . $this->driver->charset() . ")") . ")"; |
||
288 | $value = md5($value); |
||
289 | } |
||
290 | if ($value !== null) { |
||
291 | $rowIds['where'][$this->driver->bracketEscape($key)] = $value; |
||
292 | } else { |
||
293 | $rowIds['null'][] = $this->driver->bracketEscape($key); |
||
294 | } |
||
295 | // $unique_idf .= "&" . ($value !== null ? \urlencode("where[" . $this->driver->bracketEscape($key) . "]") . |
||
296 | // "=" . \urlencode($value) : \urlencode("null[]") . "=" . \urlencode($key)); |
||
297 | } |
||
298 | return $rowIds; |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | * @param array $row |
||
303 | * @param array $fields |
||
304 | * @param array $names |
||
305 | * @param int $textLength |
||
306 | * |
||
307 | * @return array |
||
308 | */ |
||
309 | private function getRowColumns(array $row, array $fields, array $names, int $textLength): array |
||
310 | { |
||
311 | $cols = []; |
||
312 | foreach ($row as $key => $value) { |
||
313 | if (isset($names[$key])) { |
||
314 | $field = $fields[$key] ?? new TableFieldEntity(); |
||
315 | $value = $this->driver->value($value, $field); |
||
316 | /*if ($value != "" && (!isset($email_fields[$key]) || $email_fields[$key] != "")) { |
||
317 | //! filled e-mails can be contained on other pages |
||
318 | $email_fields[$key] = ($this->admin->isMail($value) ? $names[$key] : ""); |
||
319 | }*/ |
||
320 | $cols[] = [ |
||
321 | // 'id', |
||
322 | 'text' => preg_match('~text|lob~', $field->type), |
||
323 | 'value' => $this->admin->selectValue($field, $value, $textLength), |
||
324 | // 'editable' => false, |
||
325 | ]; |
||
326 | } |
||
327 | } |
||
328 | return $cols; |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * Get required data for create/update on tables |
||
333 | * |
||
334 | * @param string $table The table name |
||
335 | * @param array $queryOptions The query options |
||
336 | * |
||
337 | * @return int |
||
338 | */ |
||
339 | public function countSelect(string $table, array $queryOptions): int |
||
340 | { |
||
341 | [, , $select, , , , , $where, $group] = $this->prepareSelect($table, $queryOptions); |
||
342 | |||
343 | try { |
||
344 | $isGroup = count($group) < count($select); |
||
345 | $query = $this->driver->getRowCountQuery($table, $where, $isGroup, $group); |
||
346 | return (int)$this->driver->result($query); |
||
347 | } catch(Exception $_) { |
||
348 | return -1; |
||
349 | } |
||
350 | } |
||
351 | |||
352 | /** |
||
353 | * Get required data for create/update on tables |
||
354 | * |
||
355 | * @param string $table The table name |
||
356 | * @param array $queryOptions The query options |
||
357 | * |
||
358 | * @return array |
||
359 | * @throws Exception |
||
360 | */ |
||
361 | public function execSelect(string $table, array $queryOptions): array |
||
362 | { |
||
363 | [, $query, $select, $fields, , , $indexes, $where, $group, , $limit, $page, |
||
364 | $textLength, , $unselected] = $this->prepareSelect($table, $queryOptions); |
||
365 | |||
366 | [$rows, $duration] = $this->executeSelect($query, $page); |
||
367 | if (!$rows) { |
||
368 | return ['message' => $this->utils->trans->lang('No rows.')]; |
||
369 | } |
||
370 | // $backward_keys = $this->driver->backwardKeys($table, $tableName); |
||
371 | // lengths = $this->getValuesLengths($rows, $queryOptions); |
||
372 | |||
373 | [$headers, $names] = $this->getResultHeaders($rows, $select, $fields, $unselected, $queryOptions); |
||
374 | |||
375 | $results = []; |
||
376 | foreach ($rows as $row) { |
||
377 | // Unique identifier to edit returned data. |
||
378 | $rowIds = $this->getRowIds($row, $fields, $indexes); |
||
379 | $cols = $this->getRowColumns($row, $fields, $names, $textLength); |
||
380 | $results[] = ['ids' => $rowIds, 'cols' => $cols]; |
||
381 | } |
||
382 | |||
383 | $rows = $results; |
||
384 | $message = null; |
||
385 | return compact('duration', 'headers', 'query', 'rows', 'limit', 'message'); |
||
386 | } |
||
387 | } |
||
388 |