Passed
Push — master ( 5129e4...06ffb8 )
by William
12:21 queued 11s
created

ZoomSearchController   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 464
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 258
c 0
b 0
f 0
dl 0
loc 464
ccs 0
cts 320
cp 0
rs 8.96
wmc 43

8 Methods

Rating   Name   Duplication   Size   Complexity  
B loadTableInfo() 0 51 9
C index() 0 73 12
A getDataRowAction() 0 26 4
B getColumnProperties() 0 74 2
A __construct() 0 13 1
B zoomSubmitAction() 0 71 5
B displaySelectionFormAction() 0 40 6
A changeTableInfoAction() 0 24 4

How to fix   Complexity   

Complex Class

Complex classes like ZoomSearchController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ZoomSearchController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\Controllers\Table;
6
7
use PhpMyAdmin\Common;
8
use PhpMyAdmin\DatabaseInterface;
9
use PhpMyAdmin\Html\Generator;
10
use PhpMyAdmin\Relation;
11
use PhpMyAdmin\Response;
12
use PhpMyAdmin\Table\Search;
13
use PhpMyAdmin\Template;
14
use PhpMyAdmin\Util;
15
use function array_search;
16
use function count;
17
use function htmlspecialchars;
18
use function in_array;
19
use function intval;
20
use function is_numeric;
21
use function json_encode;
22
use function mb_strtolower;
23
use function md5;
24
use function preg_match;
25
use function preg_replace;
26
use function str_ireplace;
27
use function str_replace;
28
use function strncasecmp;
29
30
/**
31
 * Handles table zoom search tab.
32
 *
33
 * Display table zoom search form, create SQL queries from form data.
34
 */
35
class ZoomSearchController extends AbstractController
36
{
37
    /** @var Search */
38
    private $search;
39
40
    /** @var Relation */
41
    private $relation;
42
43
    /** @var array */
44
    private $_columnNames;
45
46
    /** @var array */
47
    private $_columnTypes;
48
49
    /** @var array */
50
    private $_originalColumnTypes;
51
52
    /** @var array */
53
    private $_columnCollations;
54
55
    /** @var array */
56
    private $_columnNullFlags;
57
58
    /** @var bool Whether a geometry column is present */
59
    private $_geomColumnFlag;
60
61
    /** @var array Foreign keys */
62
    private $_foreigners;
63
64
    /**
65
     * @param Response          $response A Response instance.
66
     * @param DatabaseInterface $dbi      A DatabaseInterface instance.
67
     * @param Template          $template A Template instance.
68
     * @param string            $db       Database name.
69
     * @param string            $table    Table name.
70
     * @param Search            $search   A Search instance.
71
     * @param Relation          $relation A Relation instance.
72
     */
73
    public function __construct($response, $dbi, Template $template, $db, $table, Search $search, Relation $relation)
74
    {
75
        parent::__construct($response, $dbi, $template, $db, $table);
76
        $this->search = $search;
77
        $this->relation = $relation;
78
        $this->_columnNames = [];
79
        $this->_columnTypes = [];
80
        $this->_originalColumnTypes = [];
81
        $this->_columnCollations = [];
82
        $this->_columnNullFlags = [];
83
        $this->_geomColumnFlag = false;
84
        $this->_foreigners = [];
85
        $this->loadTableInfo();
86
    }
87
88
    public function index(): void
89
    {
90
        global $goto;
91
92
        Common::table();
93
94
        $header = $this->response->getHeader();
95
        $scripts = $header->getScripts();
96
        $scripts->addFiles([
97
            'makegrid.js',
98
            'sql.js',
99
            'vendor/jqplot/jquery.jqplot.js',
100
            'vendor/jqplot/plugins/jqplot.canvasTextRenderer.js',
101
            'vendor/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js',
102
            'vendor/jqplot/plugins/jqplot.dateAxisRenderer.js',
103
            'vendor/jqplot/plugins/jqplot.highlighter.js',
104
            'vendor/jqplot/plugins/jqplot.cursor.js',
105
            'table/zoom_plot_jqplot.js',
106
            'table/change.js',
107
        ]);
108
109
        /**
110
         * Handle AJAX request for data row on point select
111
         */
112
        if (isset($_POST['get_data_row'])
113
            && $_POST['get_data_row'] == true
114
        ) {
115
            $this->getDataRowAction();
116
117
            return;
118
        }
119
120
        /**
121
         * Handle AJAX request for changing field information
122
         * (value,collation,operators,field values) in input form
123
         */
124
        if (isset($_POST['change_tbl_info'])
125
            && $_POST['change_tbl_info'] == true
126
        ) {
127
            $this->changeTableInfoAction();
128
129
            return;
130
        }
131
132
        //Set default datalabel if not selected
133
        if (! isset($_POST['zoom_submit']) || $_POST['dataLabel'] == '') {
134
            $dataLabel = $this->relation->getDisplayField($this->db, $this->table);
135
        } else {
136
            $dataLabel = $_POST['dataLabel'];
137
        }
138
139
        // Displays the zoom search form
140
        $this->displaySelectionFormAction($dataLabel);
141
142
        /**
143
         * Handle the input criteria and generate the query result
144
         * Form for displaying query results
145
         */
146
        if (! isset($_POST['zoom_submit'])
147
            || $_POST['criteriaColumnNames'][0] == 'pma_null'
148
            || $_POST['criteriaColumnNames'][1] == 'pma_null'
149
            || $_POST['criteriaColumnNames'][0] == $_POST['criteriaColumnNames'][1]
150
        ) {
151
            return;
152
        }
153
154
        if (! isset($goto)) {
155
            $goto = Util::getScriptNameForOption(
156
                $GLOBALS['cfg']['DefaultTabTable'],
157
                'table'
158
            );
159
        }
160
        $this->zoomSubmitAction($dataLabel, $goto);
161
    }
162
163
    /**
164
     * Gets all the columns of a table along with their types, collations
165
     * and whether null or not.
166
     */
167
    private function loadTableInfo(): void
168
    {
169
        // Gets the list and number of columns
170
        $columns = $this->dbi->getColumns(
171
            $this->db,
172
            $this->table,
173
            null,
174
            true
175
        );
176
        // Get details about the geometry functions
177
        $geom_types = Util::getGISDatatypes();
178
179
        foreach ($columns as $row) {
180
            // set column name
181
            $this->_columnNames[] = $row['Field'];
182
183
            $type = $row['Type'];
184
            // before any replacement
185
            $this->_originalColumnTypes[] = mb_strtolower($type);
186
            // check whether table contains geometric columns
187
            if (in_array($type, $geom_types)) {
188
                $this->_geomColumnFlag = true;
189
            }
190
            // reformat mysql query output
191
            if (strncasecmp($type, 'set', 3) == 0
192
                || strncasecmp($type, 'enum', 4) == 0
193
            ) {
194
                $type = str_replace(',', ', ', $type);
195
            } else {
196
                // strip the "BINARY" attribute, except if we find "BINARY(" because
197
                // this would be a BINARY or VARBINARY column type
198
                if (! preg_match('@BINARY[\(]@i', $type)) {
199
                    $type = str_ireplace('BINARY', '', $type);
200
                }
201
                $type = str_ireplace('ZEROFILL', '', $type);
202
                $type = str_ireplace('UNSIGNED', '', $type);
203
                $type = mb_strtolower($type);
204
            }
205
            if (empty($type)) {
206
                $type = '&nbsp;';
207
            }
208
            $this->_columnTypes[] = $type;
209
            $this->_columnNullFlags[] = $row['Null'];
210
            $this->_columnCollations[]
211
                = ! empty($row['Collation']) && $row['Collation'] != 'NULL'
212
                ? $row['Collation']
213
                : '';
214
        } // end for
215
216
        // Retrieve foreign keys
217
        $this->_foreigners = $this->relation->getForeigners($this->db, $this->table);
218
    }
219
220
    /**
221
     * Display selection form action
222
     *
223
     * @param string $dataLabel Data label
224
     *
225
     * @return void
226
     */
227
    public function displaySelectionFormAction($dataLabel = null)
228
    {
229
        global $goto;
230
231
        if (! isset($goto)) {
232
            $goto = Util::getScriptNameForOption(
233
                $GLOBALS['cfg']['DefaultTabTable'],
234
                'table'
235
            );
236
        }
237
238
        $column_names = $this->_columnNames;
239
        $criteria_column_names = $_POST['criteriaColumnNames'] ?? null;
240
        $keys = [];
241
        for ($i = 0; $i < 4; $i++) {
242
            if (! isset($criteria_column_names[$i])) {
243
                continue;
244
            }
245
246
            if ($criteria_column_names[$i] == 'pma_null') {
247
                continue;
248
            }
249
250
            $keys[$criteria_column_names[$i]] = array_search($criteria_column_names[$i], $column_names);
251
        }
252
253
        $this->render('table/zoom_search/index', [
254
            'db' => $this->db,
255
            'table' => $this->table,
256
            'goto' => $goto,
257
            'self' => $this,
258
            'geom_column_flag' => $this->_geomColumnFlag,
259
            'column_names' => $column_names,
260
            'data_label' => $dataLabel,
261
            'keys' => $keys,
262
            'criteria_column_names' => $criteria_column_names,
263
            'criteria_column_types' => $_POST['criteriaColumnTypes'] ?? null,
264
            'max_plot_limit' => ! empty($_POST['maxPlotLimit'])
265
                ? intval($_POST['maxPlotLimit'])
266
                : intval($GLOBALS['cfg']['maxRowPlotLimit']),
267
        ]);
268
    }
269
270
    /**
271
     * Get data row action
272
     *
273
     * @return void
274
     */
275
    public function getDataRowAction()
276
    {
277
        $extra_data = [];
278
        $row_info_query = 'SELECT * FROM ' . Util::backquote($_POST['db']) . '.'
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($_POST['db']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

278
        $row_info_query = 'SELECT * FROM ' . /** @scrutinizer ignore-type */ Util::backquote($_POST['db']) . '.'
Loading history...
279
            . Util::backquote($_POST['table']) . ' WHERE ' . $_POST['where_clause'];
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($_POST['table']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

279
            . /** @scrutinizer ignore-type */ Util::backquote($_POST['table']) . ' WHERE ' . $_POST['where_clause'];
Loading history...
280
        $result = $this->dbi->query(
281
            $row_info_query . ';',
282
            DatabaseInterface::CONNECT_USER,
283
            DatabaseInterface::QUERY_STORE
284
        );
285
        $fields_meta = $this->dbi->getFieldsMeta($result);
286
        while ($row = $this->dbi->fetchAssoc($result)) {
287
            // for bit fields we need to convert them to printable form
288
            $i = 0;
289
            foreach ($row as $col => $val) {
290
                if ($fields_meta[$i]->type == 'bit') {
291
                    $row[$col] = Util::printableBitValue(
292
                        (int) $val,
293
                        (int) $fields_meta[$i]->length
294
                    );
295
                }
296
                $i++;
297
            }
298
            $extra_data['row_info'] = $row;
299
        }
300
        $this->response->addJSON($extra_data);
301
    }
302
303
    /**
304
     * Change table info action
305
     *
306
     * @return void
307
     */
308
    public function changeTableInfoAction()
309
    {
310
        $field = $_POST['field'];
311
        if ($field == 'pma_null') {
312
            $this->response->addJSON('field_type', '');
313
            $this->response->addJSON('field_collation', '');
314
            $this->response->addJSON('field_operators', '');
315
            $this->response->addJSON('field_value', '');
316
317
            return;
318
        }
319
        $key = array_search($field, $this->_columnNames);
320
        $search_index
321
            = (isset($_POST['it']) && is_numeric($_POST['it'])
322
            ? intval($_POST['it']) : 0);
323
324
        $properties = $this->getColumnProperties($search_index, $key);
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type false and string; however, parameter $column_index of PhpMyAdmin\Controllers\T...::getColumnProperties() does only seem to accept integer, 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

324
        $properties = $this->getColumnProperties($search_index, /** @scrutinizer ignore-type */ $key);
Loading history...
325
        $this->response->addJSON(
326
            'field_type',
327
            htmlspecialchars($properties['type'])
328
        );
329
        $this->response->addJSON('field_collation', $properties['collation']);
330
        $this->response->addJSON('field_operators', $properties['func']);
331
        $this->response->addJSON('field_value', $properties['value']);
332
    }
333
334
    /**
335
     * Zoom submit action
336
     *
337
     * @param string $dataLabel Data label
338
     * @param string $goto      Goto
339
     *
340
     * @return void
341
     */
342
    public function zoomSubmitAction($dataLabel, $goto)
343
    {
344
        //Query generation part
345
        $sql_query = $this->search->buildSqlQuery();
346
        $sql_query .= ' LIMIT ' . $_POST['maxPlotLimit'];
347
348
        //Query execution part
349
        $result = $this->dbi->query(
350
            $sql_query . ';',
351
            DatabaseInterface::CONNECT_USER,
352
            DatabaseInterface::QUERY_STORE
353
        );
354
        $fields_meta = $this->dbi->getFieldsMeta($result);
355
        $data = [];
356
        while ($row = $this->dbi->fetchAssoc($result)) {
357
            //Need a row with indexes as 0,1,2 for the getUniqueCondition
358
            // hence using a temporary array
359
            $tmpRow = [];
360
            foreach ($row as $val) {
361
                $tmpRow[] = $val;
362
            }
363
            //Get unique condition on each row (will be needed for row update)
364
            $uniqueCondition = Util::getUniqueCondition(
365
                $result,
366
                count($this->_columnNames),
367
                $fields_meta,
368
                $tmpRow,
369
                true
370
            );
371
            //Append it to row array as where_clause
372
            $row['where_clause'] = $uniqueCondition[0];
373
374
            $tmpData = [
375
                $_POST['criteriaColumnNames'][0] =>
376
                    $row[$_POST['criteriaColumnNames'][0]],
377
                $_POST['criteriaColumnNames'][1] =>
378
                    $row[$_POST['criteriaColumnNames'][1]],
379
                'where_clause' => $uniqueCondition[0],
380
            ];
381
            $tmpData[$dataLabel] = $dataLabel ? $row[$dataLabel] : '';
382
            $data[] = $tmpData;
383
        }
384
        unset($tmpData);
385
386
        //Displays form for point data and scatter plot
387
        $titles = [
388
            'Browse' => Generator::getIcon(
389
                'b_browse',
390
                __('Browse foreign values')
391
            ),
392
        ];
393
        $column_names_hashes = [];
394
395
        foreach ($this->_columnNames as $columnName) {
396
            $column_names_hashes[$columnName] = md5($columnName);
397
        }
398
399
        $this->render('table/zoom_search/result_form', [
400
            'db' => $this->db,
401
            'table' => $this->table,
402
            'column_names' => $this->_columnNames,
403
            'column_names_hashes' => $column_names_hashes,
404
            'foreigners' => $this->_foreigners,
405
            'column_null_flags' => $this->_columnNullFlags,
406
            'column_types' => $this->_columnTypes,
407
            'titles' => $titles,
408
            'goto' => $goto,
409
            'data' => $data,
410
            'data_json' => json_encode($data),
411
            'zoom_submit' => isset($_POST['zoom_submit']),
412
            'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'],
413
        ]);
414
    }
415
416
    /**
417
     * Provides a column's type, collation, operators list, and criteria value
418
     * to display in table search form
419
     *
420
     * @param int $search_index Row number in table search form
421
     * @param int $column_index Column index in ColumnNames array
422
     *
423
     * @return array Array containing column's properties
424
     */
425
    public function getColumnProperties($search_index, $column_index)
426
    {
427
        $selected_operator = ($_POST['criteriaColumnOperators'][$search_index] ?? '');
428
        $entered_value = ($_POST['criteriaValues'] ?? '');
429
        $titles = [
430
            'Browse' => Generator::getIcon(
431
                'b_browse',
432
                __('Browse foreign values')
433
            ),
434
        ];
435
        //Gets column's type and collation
436
        $type = $this->_columnTypes[$column_index];
437
        $collation = $this->_columnCollations[$column_index];
438
        $cleanType = preg_replace('@\(.*@s', '', $type);
439
        //Gets column's comparison operators depending on column type
440
        $typeOperators = $this->dbi->types->getTypeOperatorsHtml(
441
            $cleanType,
442
            $this->_columnNullFlags[$column_index],
443
            $selected_operator
444
        );
445
        $func = $this->template->render('table/search/column_comparison_operators', [
446
            'search_index' => $search_index,
447
            'type_operators' => $typeOperators,
448
        ]);
449
        //Gets link to browse foreign data(if any) and criteria inputbox
450
        $foreignData = $this->relation->getForeignData(
451
            $this->_foreigners,
452
            $this->_columnNames[$column_index],
453
            false,
454
            '',
455
            ''
456
        );
457
        $htmlAttributes = '';
458
        if (in_array($cleanType, $this->dbi->types->getIntegerTypes())) {
459
            $extractedColumnspec = Util::extractColumnSpec(
460
                $this->_originalColumnTypes[$column_index]
461
            );
462
            $is_unsigned = $extractedColumnspec['unsigned'];
463
            $minMaxValues = $this->dbi->types->getIntegerRange(
464
                $cleanType,
465
                ! $is_unsigned
466
            );
467
            $htmlAttributes = 'data-min="' . $minMaxValues[0] . '" '
468
                            . 'data-max="' . $minMaxValues[1] . '"';
469
            $type = 'INT';
470
        }
471
472
        $htmlAttributes .= ' onchange="return '
473
                        . 'verifyAfterSearchFieldChange(' . $column_index . ', \'#zoom_search_form\')"';
474
475
        $value = $this->template->render('table/search/input_box', [
476
            'str' => '',
477
            'column_type' => (string) $type,
478
            'html_attributes' => $htmlAttributes,
479
            'column_id' => 'fieldID_',
480
            'in_zoom_search_edit' => false,
481
            'foreigners' => $this->_foreigners,
482
            'column_name' => $this->_columnNames[$column_index],
483
            'column_name_hash' => md5($this->_columnNames[$column_index]),
484
            'foreign_data' => $foreignData,
485
            'table' => $this->table,
486
            'column_index' => $search_index,
487
            'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'],
488
            'criteria_values' => $entered_value,
489
            'db' => $this->db,
490
            'titles' => $titles,
491
            'in_fbs' => true,
492
        ]);
493
494
        return [
495
            'type' => $type,
496
            'collation' => $collation,
497
            'func' => $func,
498
            'value' => $value,
499
        ];
500
    }
501
}
502