SearchController::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 0
dl 0
loc 10
rs 10
c 0
b 0
f 0
ccs 1
cts 1
cp 1
cc 1
nc 1
nop 8
crap 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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