SearchController::getColumnMinMax()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 8
ccs 6
cts 6
cp 1
crap 1
rs 10
c 0
b 0
f 0
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;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Identifiers\DatabaseName was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use PhpMyAdmin\Identifiers\TableName;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Identifiers\TableName was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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>
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Controllers\Table\list was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type array; however, parameter $string of mb_strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

135
                $type = mb_strtolower(/** @scrutinizer ignore-type */ $type);
Loading history...
136
            }
137
138 4
            if ($type === '') {
139
                $type = '&nbsp;';
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