1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace PhpMyAdmin\Controllers\Table; |
6
|
|
|
|
7
|
|
|
use PhpMyAdmin\Config; |
8
|
|
|
use PhpMyAdmin\ConfigStorage\Relation; |
9
|
|
|
use PhpMyAdmin\Controllers\InvocableController; |
10
|
|
|
use PhpMyAdmin\Current; |
11
|
|
|
use PhpMyAdmin\Dbal\DatabaseInterface; |
12
|
|
|
use PhpMyAdmin\DbTableExists; |
13
|
|
|
use PhpMyAdmin\Http\Response; |
14
|
|
|
use PhpMyAdmin\Http\ServerRequest; |
15
|
|
|
use PhpMyAdmin\Identifiers\DatabaseName; |
|
|
|
|
16
|
|
|
use PhpMyAdmin\Identifiers\TableName; |
|
|
|
|
17
|
|
|
use PhpMyAdmin\Message; |
18
|
|
|
use PhpMyAdmin\ResponseRenderer; |
19
|
|
|
use PhpMyAdmin\Sql; |
20
|
|
|
use PhpMyAdmin\Table\Search; |
21
|
|
|
use PhpMyAdmin\Template; |
22
|
|
|
use PhpMyAdmin\Url; |
23
|
|
|
use PhpMyAdmin\UrlParams; |
24
|
|
|
use PhpMyAdmin\Util; |
25
|
|
|
use PhpMyAdmin\Utils\Gis; |
26
|
|
|
|
27
|
|
|
use function __; |
28
|
|
|
use function array_keys; |
29
|
|
|
use function in_array; |
30
|
|
|
use function mb_strtolower; |
31
|
|
|
use function md5; |
32
|
|
|
use function preg_match; |
33
|
|
|
use function preg_replace; |
34
|
|
|
use function str_ireplace; |
35
|
|
|
use function str_replace; |
36
|
|
|
use function strncasecmp; |
37
|
|
|
use function strtoupper; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Handles table search tab. |
41
|
|
|
* |
42
|
|
|
* Display table search form, create SQL query from form data |
43
|
|
|
* and call Sql::executeQueryAndSendQueryResponse() to execute it. |
44
|
|
|
*/ |
45
|
|
|
final class SearchController implements InvocableController |
46
|
|
|
{ |
47
|
|
|
/** |
48
|
|
|
* Names of columns |
49
|
|
|
* |
50
|
|
|
* @var list<string> |
|
|
|
|
51
|
|
|
*/ |
52
|
|
|
private array $columnNames = []; |
53
|
|
|
/** |
54
|
|
|
* Types of columns |
55
|
|
|
* |
56
|
|
|
* @var list<string> |
57
|
|
|
*/ |
58
|
|
|
private array $columnTypes = []; |
59
|
|
|
/** |
60
|
|
|
* Types of columns without any replacement |
61
|
|
|
* |
62
|
|
|
* @var list<string> |
63
|
|
|
*/ |
64
|
|
|
private array $originalColumnTypes = []; |
65
|
|
|
/** |
66
|
|
|
* Collations of columns |
67
|
|
|
* |
68
|
|
|
* @var list<string> |
69
|
|
|
*/ |
70
|
|
|
private array $columnCollations = []; |
71
|
|
|
/** |
72
|
|
|
* Null Flags of columns |
73
|
|
|
* |
74
|
|
|
* @var list<bool> |
75
|
|
|
*/ |
76
|
|
|
private array $columnNullFlags = []; |
77
|
|
|
/** |
78
|
|
|
* Whether a geometry column is present |
79
|
|
|
*/ |
80
|
|
|
private bool $geomColumnFlag = false; |
81
|
|
|
/** |
82
|
|
|
* Foreign Keys |
83
|
|
|
* |
84
|
|
|
* @var mixed[] |
85
|
|
|
*/ |
86
|
|
|
private array $foreigners = []; |
87
|
|
|
|
88
|
4 |
|
public function __construct( |
89
|
|
|
private readonly ResponseRenderer $response, |
90
|
|
|
private readonly Template $template, |
91
|
|
|
private readonly Search $search, |
92
|
|
|
private readonly Relation $relation, |
93
|
|
|
private readonly DatabaseInterface $dbi, |
94
|
|
|
private readonly DbTableExists $dbTableExists, |
95
|
|
|
private readonly Config $config, |
96
|
|
|
private readonly Sql $sql, |
97
|
|
|
) { |
98
|
4 |
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Gets all the columns of a table along with their types, collations |
102
|
|
|
* and whether null or not. |
103
|
|
|
*/ |
104
|
4 |
|
private function loadTableInfo(): void |
105
|
|
|
{ |
106
|
|
|
// Gets the list and number of columns |
107
|
4 |
|
$columns = $this->dbi->getColumns(Current::$database, Current::$table); |
108
|
|
|
// Get details about the geometry functions |
109
|
4 |
|
$geomTypes = Gis::getDataTypes(); |
110
|
|
|
|
111
|
4 |
|
foreach ($columns as $row) { |
112
|
|
|
// set column name |
113
|
4 |
|
$this->columnNames[] = $row->field; |
114
|
|
|
|
115
|
4 |
|
$type = $row->type; |
116
|
|
|
// before any replacement |
117
|
4 |
|
$this->originalColumnTypes[] = mb_strtolower($type); |
118
|
|
|
// check whether table contains geometric columns |
119
|
4 |
|
if (in_array($type, $geomTypes, true)) { |
120
|
|
|
$this->geomColumnFlag = true; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
// reformat mysql query output |
124
|
4 |
|
if (strncasecmp($type, 'set', 3) == 0 || strncasecmp($type, 'enum', 4) == 0) { |
125
|
|
|
$type = str_replace(',', ', ', $type); |
126
|
|
|
} else { |
127
|
|
|
// strip the "BINARY" attribute, except if we find "BINARY(" because |
128
|
|
|
// this would be a BINARY or VARBINARY column type |
129
|
4 |
|
if (preg_match('@BINARY[\(]@i', $type) !== 1) { |
130
|
4 |
|
$type = str_ireplace('BINARY', '', $type); |
131
|
|
|
} |
132
|
|
|
|
133
|
4 |
|
$type = str_ireplace('ZEROFILL', '', $type); |
134
|
4 |
|
$type = str_ireplace('UNSIGNED', '', $type); |
135
|
4 |
|
$type = mb_strtolower($type); |
|
|
|
|
136
|
|
|
} |
137
|
|
|
|
138
|
4 |
|
if ($type === '') { |
139
|
|
|
$type = ' '; |
140
|
|
|
} |
141
|
|
|
|
142
|
4 |
|
$this->columnTypes[] = $type; |
143
|
4 |
|
$this->columnNullFlags[] = $row->isNull; |
144
|
4 |
|
$this->columnCollations[] = ! empty($row->collation) && $row->collation !== 'NULL' |
145
|
|
|
? $row->collation |
146
|
4 |
|
: ''; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
// Retrieve foreign keys |
150
|
4 |
|
$this->foreigners = $this->relation->getForeigners(Current::$database, Current::$table); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Index action |
155
|
|
|
*/ |
156
|
4 |
|
public function __invoke(ServerRequest $request): Response |
157
|
|
|
{ |
158
|
4 |
|
if (Current::$database === '') { |
159
|
|
|
return $this->response->missingParameterError('db'); |
160
|
|
|
} |
161
|
|
|
|
162
|
4 |
|
if (Current::$table === '') { |
163
|
|
|
return $this->response->missingParameterError('table'); |
164
|
|
|
} |
165
|
|
|
|
166
|
4 |
|
UrlParams::$params = ['db' => Current::$database, 'table' => Current::$table]; |
167
|
|
|
|
168
|
4 |
|
$databaseName = DatabaseName::tryFrom($request->getParam('db')); |
169
|
4 |
|
if ($databaseName === null || ! $this->dbTableExists->selectDatabase($databaseName)) { |
170
|
|
|
if ($request->isAjax()) { |
171
|
|
|
$this->response->setRequestStatus(false); |
172
|
|
|
$this->response->addJSON('message', Message::error(__('No databases selected.'))); |
173
|
|
|
|
174
|
|
|
return $this->response->response(); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
$this->response->redirectToRoute('/', ['reload' => true, 'message' => __('No databases selected.')]); |
178
|
|
|
|
179
|
|
|
return $this->response->response(); |
180
|
|
|
} |
181
|
|
|
|
182
|
4 |
|
$tableName = TableName::tryFrom($request->getParam('table')); |
183
|
4 |
|
if ($tableName === null || ! $this->dbTableExists->hasTable($databaseName, $tableName)) { |
184
|
|
|
if ($request->isAjax()) { |
185
|
|
|
$this->response->setRequestStatus(false); |
186
|
|
|
$this->response->addJSON('message', Message::error(__('No table selected.'))); |
187
|
|
|
|
188
|
|
|
return $this->response->response(); |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
$this->response->redirectToRoute('/', ['reload' => true, 'message' => __('No table selected.')]); |
192
|
|
|
|
193
|
|
|
return $this->response->response(); |
194
|
|
|
} |
195
|
|
|
|
196
|
4 |
|
$this->loadTableInfo(); |
197
|
|
|
|
198
|
4 |
|
$this->response->addScriptFiles([ |
199
|
4 |
|
'makegrid.js', |
200
|
4 |
|
'sql.js', |
201
|
4 |
|
'table/select.js', |
202
|
4 |
|
'table/change.js', |
203
|
4 |
|
'vendor/jquery/jquery.uitablefilter.js', |
204
|
4 |
|
'gis_data_editor.js', |
205
|
4 |
|
]); |
206
|
|
|
|
207
|
4 |
|
if (isset($_POST['range_search'])) { |
208
|
4 |
|
$this->rangeSearchAction(); |
209
|
|
|
|
210
|
4 |
|
return $this->response->response(); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* No selection criteria received -> display the selection form |
215
|
|
|
*/ |
216
|
|
|
if (! isset($_POST['columnsToDisplay']) && ! isset($_POST['displayAllColumns'])) { |
217
|
|
|
$this->displaySelectionFormAction(); |
218
|
|
|
} else { |
219
|
|
|
$this->doSelectionAction(); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
return $this->response->response(); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Do selection action |
227
|
|
|
*/ |
228
|
|
|
private function doSelectionAction(): void |
229
|
|
|
{ |
230
|
|
|
/** |
231
|
|
|
* Selection criteria have been submitted -> do the work |
232
|
|
|
*/ |
233
|
|
|
$sqlQuery = $this->search->buildSqlQuery(); |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Add this to ensure following procedures included running correctly. |
237
|
|
|
*/ |
238
|
|
|
$this->response->addHTML($this->sql->executeQueryAndSendQueryResponse( |
239
|
|
|
null, |
240
|
|
|
false, // is_gotofile |
241
|
|
|
Current::$database, // db |
242
|
|
|
Current::$table, // table |
243
|
|
|
'', // sql_query_for_bookmark |
244
|
|
|
'', // message_to_show |
245
|
|
|
UrlParams::$goto, // goto |
246
|
|
|
null, // disp_query |
247
|
|
|
'', // disp_message |
248
|
|
|
$sqlQuery, // sql_query |
249
|
|
|
$sqlQuery, // complete_query |
250
|
|
|
)); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Display selection form action |
255
|
|
|
*/ |
256
|
|
|
private function displaySelectionFormAction(): void |
257
|
|
|
{ |
258
|
|
|
if (UrlParams::$goto === '') { |
259
|
|
|
UrlParams::$goto = Url::getFromRoute($this->config->settings['DefaultTabTable']); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$properties = []; |
263
|
|
|
foreach (array_keys($this->columnNames) as $columnIndex) { |
264
|
|
|
$properties[$columnIndex] = $this->getColumnProperties($columnIndex, $columnIndex); |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
$this->response->render('table/search/index', [ |
268
|
|
|
'db' => Current::$database, |
269
|
|
|
'table' => Current::$table, |
270
|
|
|
'goto' => UrlParams::$goto, |
271
|
|
|
'properties' => $properties, |
272
|
|
|
'geom_column_flag' => $this->geomColumnFlag, |
273
|
|
|
'column_names' => $this->columnNames, |
274
|
|
|
'column_types' => $this->columnTypes, |
275
|
|
|
'column_collations' => $this->columnCollations, |
276
|
|
|
'default_sliders_state' => $this->config->settings['InitialSlidersState'], |
277
|
|
|
'max_rows' => (int) $this->config->settings['MaxRows'], |
278
|
|
|
]); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* Range search action |
283
|
|
|
*/ |
284
|
4 |
|
private function rangeSearchAction(): void |
285
|
|
|
{ |
286
|
4 |
|
$minMax = $this->getColumnMinMax($_POST['column']); |
287
|
4 |
|
$this->response->addJSON('column_data', $minMax); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Finds minimum and maximum value of a given column. |
292
|
|
|
* |
293
|
|
|
* @param string $column Column name |
294
|
|
|
* |
295
|
|
|
* @return array<string|null> |
296
|
|
|
*/ |
297
|
4 |
|
private function getColumnMinMax(string $column): array |
298
|
|
|
{ |
299
|
4 |
|
$sqlQuery = 'SELECT MIN(' . Util::backquote($column) . ') AS `min`, ' |
300
|
4 |
|
. 'MAX(' . Util::backquote($column) . ') AS `max` ' |
301
|
4 |
|
. 'FROM ' . Util::backquote(Current::$database) . '.' |
302
|
4 |
|
. Util::backquote(Current::$table); |
303
|
|
|
|
304
|
4 |
|
return $this->dbi->fetchSingleRow($sqlQuery); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Provides a column's type, collation, operators list, and criteria value |
309
|
|
|
* to display in table search form |
310
|
|
|
* |
311
|
|
|
* @param int $searchIndex Row number in table search form |
312
|
|
|
* @param int $columnIndex Column index in ColumnNames array |
313
|
|
|
* |
314
|
|
|
* @return mixed[] Array containing column's properties |
315
|
|
|
*/ |
316
|
|
|
private function getColumnProperties(int $searchIndex, int $columnIndex): array |
317
|
|
|
{ |
318
|
|
|
$selectedOperator = $_POST['criteriaColumnOperators'][$searchIndex] ?? ''; |
319
|
|
|
$enteredValue = $_POST['criteriaValues'] ?? ''; |
320
|
|
|
//Gets column's type and collation |
321
|
|
|
$type = $this->columnTypes[$columnIndex]; |
322
|
|
|
$collation = $this->columnCollations[$columnIndex]; |
323
|
|
|
$cleanType = preg_replace('@\(.*@s', '', $type); |
324
|
|
|
//Gets column's comparison operators depending on column type |
325
|
|
|
$typeOperators = $this->dbi->types->getTypeOperatorsHtml( |
326
|
|
|
$cleanType, |
327
|
|
|
$this->columnNullFlags[$columnIndex], |
328
|
|
|
$selectedOperator, |
329
|
|
|
); |
330
|
|
|
$func = $this->template->render('table/search/column_comparison_operators', [ |
331
|
|
|
'search_index' => $searchIndex, |
332
|
|
|
'type_operators' => $typeOperators, |
333
|
|
|
]); |
334
|
|
|
//Gets link to browse foreign data(if any) and criteria inputbox |
335
|
|
|
$foreignData = $this->relation->getForeignData( |
336
|
|
|
$this->foreigners, |
337
|
|
|
$this->columnNames[$columnIndex], |
338
|
|
|
false, |
339
|
|
|
'', |
340
|
|
|
'', |
341
|
|
|
); |
342
|
|
|
$htmlAttributes = ''; |
343
|
|
|
$isInteger = in_array($cleanType, $this->dbi->types->getIntegerTypes(), true); |
344
|
|
|
$isFloat = in_array($cleanType, $this->dbi->types->getFloatTypes(), true); |
345
|
|
|
if ($isInteger) { |
346
|
|
|
$extractedColumnspec = Util::extractColumnSpec($this->originalColumnTypes[$columnIndex]); |
347
|
|
|
$isUnsigned = $extractedColumnspec['unsigned']; |
348
|
|
|
$minMaxValues = $this->dbi->types->getIntegerRange($cleanType, ! $isUnsigned); |
349
|
|
|
$htmlAttributes = 'data-min="' . $minMaxValues[0] . '" ' |
350
|
|
|
. 'data-max="' . $minMaxValues[1] . '"'; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
$htmlAttributes .= ' onfocus="return ' |
354
|
|
|
. 'verifyAfterSearchFieldChange(' . $searchIndex . ', \'#tbl_search_form\')"'; |
355
|
|
|
|
356
|
|
|
$foreignDropdown = ''; |
357
|
|
|
|
358
|
|
|
$searchColumnInForeigners = $this->relation->searchColumnInForeigners( |
359
|
|
|
$this->foreigners, |
360
|
|
|
$this->columnNames[$columnIndex], |
361
|
|
|
); |
362
|
|
|
|
363
|
|
|
$hasForeigner = $searchColumnInForeigners !== false && $searchColumnInForeigners !== []; |
364
|
|
|
|
365
|
|
|
if ($hasForeigner && $foreignData->dispRow !== null) { |
366
|
|
|
$foreignDropdown = $this->relation->foreignDropdown( |
367
|
|
|
$foreignData->dispRow, |
368
|
|
|
$foreignData->foreignField, |
369
|
|
|
$foreignData->foreignDisplay, |
370
|
|
|
'', |
371
|
|
|
$this->config->settings['ForeignKeyMaxLimit'], |
372
|
|
|
); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
$value = $this->template->render('table/search/input_box', [ |
376
|
|
|
'str' => '', |
377
|
|
|
'column_type' => $type, |
378
|
|
|
'column_data_type' => $isInteger ? 'INT' : ($isFloat ? 'FLOAT' : strtoupper($cleanType)), |
379
|
|
|
'html_attributes' => $htmlAttributes, |
380
|
|
|
'column_id' => 'fieldID_', |
381
|
|
|
'in_zoom_search_edit' => false, |
382
|
|
|
'column_name' => $this->columnNames[$columnIndex], |
383
|
|
|
'column_name_hash' => md5($this->columnNames[$columnIndex]), |
384
|
|
|
'foreign_data' => $foreignData, |
385
|
|
|
'table' => Current::$table, |
386
|
|
|
'column_index' => $searchIndex, |
387
|
|
|
'criteria_values' => $enteredValue, |
388
|
|
|
'db' => Current::$database, |
389
|
|
|
'in_fbs' => true, |
390
|
|
|
'foreign_dropdown' => $foreignDropdown, |
391
|
|
|
'has_foreigner' => $hasForeigner, |
392
|
|
|
'is_integer' => $isInteger, |
393
|
|
|
'is_float' => $isFloat, |
394
|
|
|
]); |
395
|
|
|
|
396
|
|
|
return ['type' => $type, 'collation' => $collation, 'func' => $func, 'value' => $value]; |
397
|
|
|
} |
398
|
|
|
} |
399
|
|
|
|
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths