CentralColumns   F
last analyzed

Complexity

Total Complexity 103

Size/Duplication

Total Lines 877
Duplicated Lines 0 %

Test Coverage

Coverage 58.98%

Importance

Changes 0
Metric Value
wmc 103
eloc 430
c 0
b 0
f 0
dl 0
loc 877
rs 2
ccs 266
cts 451
cp 0.5898

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getColumnsCount() 0 20 4
A getWhereInColumns() 0 5 1
A getListRaw() 0 27 4
A __construct() 0 10 1
C syncUniqueColumns() 0 86 12
A findExistingColumns() 0 20 2
A getStorageNotReadyMessage() 0 4 1
A updateOneColumn() 0 51 5
A getInsertQuery() 0 26 2
B handleColumnExtra() 0 18 7
B getHtmlForEditTableRow() 0 41 10
B deleteColumnsFromList() 0 70 11
A getHtmlForEditingPage() 0 12 2
C getTemplateVariablesForMain() 0 86 14
A getColumnsList() 0 23 3
A getCount() 0 18 3
B makeConsistentWithList() 0 60 11
A getColumnsNotInCentralList() 0 7 1
A updateMultipleColumn() 0 34 5
A getParams() 0 11 2
A findExistingColNames() 0 18 2

How to fix   Complexity   

Complex Class

Complex classes like CentralColumns 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 CentralColumns, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\Database;
6
7
use PhpMyAdmin\Charsets;
8
use PhpMyAdmin\ColumnFull;
9
use PhpMyAdmin\Config;
10
use PhpMyAdmin\ConfigStorage\Relation;
11
use PhpMyAdmin\DatabaseInterface;
12
use PhpMyAdmin\Dbal\ConnectionType;
13
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...
14
use PhpMyAdmin\Message;
15
use PhpMyAdmin\Template;
16
use PhpMyAdmin\Util;
17
18
use function __;
19
use function array_column;
20
use function array_diff;
21
use function array_map;
22
use function array_merge;
23
use function array_unique;
24
use function array_values;
25
use function bin2hex;
26
use function ceil;
27
use function count;
28
use function explode;
29
use function htmlspecialchars;
30
use function implode;
31
use function in_array;
32
use function is_array;
33
use function is_bool;
34
use function mb_strtoupper;
35
use function sprintf;
36
use function trim;
37
38
class CentralColumns
39
{
40
    /**
41
     * Current user
42
     */
43
    private string $user;
44
45
    /**
46
     * Number of rows displayed when browsing a result set
47
     */
48
    private int $maxRows;
49
50
    /**
51
     * Which editor should be used for CHAR/VARCHAR fields
52
     */
53
    private string $charEditing;
54
55
    /**
56
     * Disable use of INFORMATION_SCHEMA
57
     */
58
    private bool $disableIs;
59
60
    private Relation $relation;
61
62
    public Template $template;
63
64 48
    public function __construct(private DatabaseInterface $dbi)
65
    {
66 48
        $config = Config::getInstance();
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Config::getInstance() has been deprecated: Use dependency injection instead. ( Ignorable by Annotation )

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

66
        $config = /** @scrutinizer ignore-deprecated */ Config::getInstance();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
67 48
        $this->user = $config->selectedServer['user'];
68 48
        $this->maxRows = $config->settings['MaxRows'];
69 48
        $this->charEditing = $config->settings['CharEditing'];
70 48
        $this->disableIs = $config->selectedServer['DisableIS'];
71
72 48
        $this->relation = new Relation($this->dbi);
73 48
        $this->template = new Template();
74
    }
75
76
    /**
77
     * Returns the configuration storage settings for central columns or false if no storage is available
78
     *
79
     * @return array{user:string, db: string, table:string}|false
80
     */
81 48
    public function getParams(): array|false
82
    {
83 48
        $centralColumnsFeature = $this->relation->getRelationParameters()->centralColumnsFeature;
84 48
        if ($centralColumnsFeature === null) {
85
            return false;
86
        }
87
88 48
        return [
89 48
            'user' => $this->user,
90 48
            'db' => $centralColumnsFeature->database->getName(),
91 48
            'table' => $centralColumnsFeature->centralColumns->getName(),
92 48
        ];
93
    }
94
95
    /**
96
     * get $num columns of given database from central columns list
97
     * starting at offset $from
98
     *
99
     * @param string $db   selected database
100
     * @param int    $from starting offset of first result
101
     * @param int    $num  maximum number of results to return
102
     *
103
     * @return mixed[] list of $num columns present in central columns list
104
     * starting at offset $from for the given database
105
     */
106 4
    public function getColumnsList(string $db, int $from = 0, int $num = 25): array
107
    {
108 4
        $cfgCentralColumns = $this->getParams();
109 4
        if (! is_array($cfgCentralColumns)) {
110
            return [];
111
        }
112
113 4
        $pmadb = $cfgCentralColumns['db'];
114 4
        $centralListTable = $cfgCentralColumns['table'];
115
        //get current values of $db from central column list
116 4
        if ($num === 0) {
117
            $query = 'SELECT * FROM ' . Util::backquote($pmadb) . '.' . Util::backquote($centralListTable) . ' '
118
                . 'WHERE db_name = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser) . ';';
119
        } else {
120 4
            $query = 'SELECT * FROM ' . Util::backquote($pmadb) . '.' . Util::backquote($centralListTable) . ' '
121 4
                . 'WHERE db_name = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser) . ' '
122 4
                . 'LIMIT ' . $from . ', ' . $num . ';';
123
        }
124
125 4
        $hasList = $this->dbi->fetchResult($query, null, null, ConnectionType::ControlUser);
126 4
        $this->handleColumnExtra($hasList);
127
128 4
        return $hasList;
129
    }
130
131
    /**
132
     * Get the number of columns present in central list for given db
133
     *
134
     * @param string $db current database
135
     *
136
     * @return int number of columns in central list of columns for $db
137
     */
138 4
    public function getCount(string $db): int
139
    {
140 4
        $cfgCentralColumns = $this->getParams();
141 4
        if (! is_array($cfgCentralColumns)) {
142
            return 0;
143
        }
144
145 4
        $pmadb = $cfgCentralColumns['db'];
146 4
        $centralListTable = $cfgCentralColumns['table'];
147 4
        $query = 'SELECT count(db_name) FROM '
148 4
            . Util::backquote($pmadb) . '.' . Util::backquote($centralListTable) . ' '
149 4
            . 'WHERE db_name = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser) . ';';
150 4
        $res = $this->dbi->fetchResult($query, null, null, ConnectionType::ControlUser);
151 4
        if (isset($res[0])) {
152 4
            return (int) $res[0];
153
        }
154
155
        return 0;
156
    }
157
158
    /**
159
     * return the existing columns in central list among the given list of columns
160
     *
161
     * @param string   $db   the selected database
162
     * @param string[] $cols list of given columns
163
     *
164
     * @return string[] list of columns in central columns among given set of columns
165
     */
166 8
    public function findExistingColNames(
167
        string $db,
168
        array $cols,
169
    ): array {
170 8
        $cfgCentralColumns = $this->getParams();
171 8
        if (! is_array($cfgCentralColumns)) {
172
            return [];
173
        }
174
175 8
        $cols = $this->getWhereInColumns($cols);
176
177 8
        $pmadb = $cfgCentralColumns['db'];
178 8
        $centralListTable = $cfgCentralColumns['table'];
179 8
        $query = 'SELECT col_name FROM '
180 8
            . Util::backquote($pmadb) . '.' . Util::backquote($centralListTable) . ' WHERE db_name = '
181 8
            . $this->dbi->quoteString($db, ConnectionType::ControlUser) . ' AND col_name IN (' . $cols . ');';
182
183 8
        return $this->dbi->fetchResult($query, null, null, ConnectionType::ControlUser);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->dbi->fetch...ctionType::ControlUser) returns an array which contains values of type array<mixed,null|string> which are incompatible with the documented value type string.
Loading history...
184
    }
185
186
    /**
187
     * return the existing columns in central list among the given list of columns
188
     *
189
     * @param string   $db   the selected database
190
     * @param string[] $cols list of given columns
191
     *
192
     * @return (string|null)[][] list of columns in central columns among given set of columns
193
     */
194 12
    private function findExistingColumns(
195
        string $db,
196
        array $cols,
197
    ): array {
198 12
        $cfgCentralColumns = $this->getParams();
199 12
        if (! is_array($cfgCentralColumns)) {
200
            return [];
201
        }
202
203 12
        $cols = $this->getWhereInColumns($cols);
204
205 12
        $pmadb = $cfgCentralColumns['db'];
206 12
        $centralListTable = $cfgCentralColumns['table'];
207 12
        $query = 'SELECT * FROM '
208 12
            . Util::backquote($pmadb) . '.' . Util::backquote($centralListTable) . ' WHERE db_name = '
209 12
            . $this->dbi->quoteString($db, ConnectionType::ControlUser) . ' AND col_name IN (' . $cols . ');';
210 12
        $hasList = $this->dbi->fetchResult($query, null, null, ConnectionType::ControlUser);
211 12
        $this->handleColumnExtra($hasList);
212
213 12
        return $hasList;
214
    }
215
216
    /**
217
     * build the insert query for central columns list given PMA storage
218
     * db, central_columns table, column name and corresponding definition to be added
219
     *
220
     * @param ColumnFull $def              list of attributes of the column being added
221
     * @param string     $db               PMA configuration storage database name
222
     * @param string     $centralListTable central columns configuration storage table name
223
     *
224
     * @return string query string to insert the given column
225
     * with definition into central list
226
     */
227 8
    private function getInsertQuery(
228
        ColumnFull $def,
229
        string $db,
230
        string $centralListDb,
231
        string $centralListTable,
232
    ): string {
233 8
        $extractedColumnSpec = Util::extractColumnSpec($def->type);
234 8
        $attribute = trim($extractedColumnSpec['attribute']);
235 8
        $type = $extractedColumnSpec['type'];
236 8
        $length = $extractedColumnSpec['spec_in_brackets'];
237
238 8
        $collation = $def->collation ?? '';
239 8
        $isNull = $def->isNull ? '1' : '0';
240 8
        $extra = $def->extra;
241 8
        $default = $def->default ?? '';
242
243 8
        return 'INSERT INTO '
244 8
            . Util::backquote($centralListDb) . '.' . Util::backquote($centralListTable) . ' '
245 8
            . 'VALUES ( ' . $this->dbi->quoteString($db, ConnectionType::ControlUser) . ' ,'
246 8
            . $this->dbi->quoteString($def->field, ConnectionType::ControlUser) . ','
247 8
            . $this->dbi->quoteString($type, ConnectionType::ControlUser) . ','
248 8
            . $this->dbi->quoteString((string) $length, ConnectionType::ControlUser) . ','
249 8
            . $this->dbi->quoteString($collation, ConnectionType::ControlUser) . ','
250 8
            . $this->dbi->quoteString($isNull, ConnectionType::ControlUser) . ','
251 8
            . '\'' . implode(',', [$extra, $attribute])
252 8
            . '\',' . $this->dbi->quoteString($default, ConnectionType::ControlUser) . ');';
253
    }
254
255
    /**
256
     * If $isTable is true then unique columns from given tables as $field_select
257
     * are added to central list otherwise the $field_select is considered as
258
     * list of columns and these columns are added to central list if not already added
259
     *
260
     * @param string[] $fieldSelect     if $isTable is true selected tables list otherwise selected columns list
261
     * @param bool     $isTable         if passed array is of tables or columns
262
     * @param string   $containingTable if $isTable is false, then table name to which columns belong
263
     *
264
     * @return true|Message
265
     */
266 4
    public function syncUniqueColumns(
267
        DatabaseName $databaseName,
268
        array $fieldSelect,
269
        bool $isTable = true,
270
        string $containingTable = '',
271
    ): bool|Message {
272 4
        $cfgCentralColumns = $this->getParams();
273 4
        if (! is_array($cfgCentralColumns)) {
274
            return $this->getStorageNotReadyMessage();
275
        }
276
277 4
        $existingCols = [];
278 4
        $insQuery = [];
279 4
        if ($isTable) {
280 4
            $cols = [];
281 4
            $fields = [];
282 4
            foreach ($fieldSelect as $table) {
283 4
                $fields[$table] = $this->dbi->getColumns($databaseName->getName(), $table, true);
284 4
                $cols = array_merge($cols, array_column($fields[$table], 'field'));
285
            }
286
287 4
            $hasList = $this->findExistingColNames($databaseName->getName(), $cols);
288 4
            foreach ($fieldSelect as $table) {
289 4
                foreach ($fields[$table] as $def) {
290 4
                    $field = $def->field;
291 4
                    if (! in_array($field, $hasList, true)) {
292 4
                        $hasList[] = $field;
293 4
                        $insQuery[] = $this->getInsertQuery(
294 4
                            $def,
295 4
                            $databaseName->getName(),
296 4
                            $cfgCentralColumns['db'],
297 4
                            $cfgCentralColumns['table'],
298 4
                        );
299
                    } else {
300
                        $existingCols[] = "'" . $field . "'";
301
                    }
302
                }
303
            }
304
        } else {
305
            $hasList = $this->findExistingColNames($databaseName->getName(), $fieldSelect);
306
            foreach ($fieldSelect as $column) {
307
                if (! in_array($column, $hasList, true)) {
308
                    $hasList[] = $column;
309
                    $field = $this->dbi->getColumn($databaseName->getName(), $containingTable, $column, true);
310
                    $insQuery[] = $this->getInsertQuery(
311
                        $field,
0 ignored issues
show
Bug introduced by
It seems like $field can also be of type PhpMyAdmin\Column and null; however, parameter $def of PhpMyAdmin\Database\Cent...lumns::getInsertQuery() does only seem to accept PhpMyAdmin\ColumnFull, 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

311
                        /** @scrutinizer ignore-type */ $field,
Loading history...
312
                        $databaseName->getName(),
313
                        $cfgCentralColumns['db'],
314
                        $cfgCentralColumns['table'],
315
                    );
316
                } else {
317
                    $existingCols[] = "'" . $column . "'";
318
                }
319
            }
320
        }
321
322 4
        $message = true;
323 4
        if ($existingCols !== []) {
324
            $existingCols = implode(',', array_unique($existingCols));
325
            $message = Message::notice(
326
                sprintf(
327
                    __(
328
                        'Could not add %1$s as they already exist in central list!',
329
                    ),
330
                    htmlspecialchars($existingCols),
331
                ),
332
            );
333
            $message->addMessage(
334
                Message::notice(
335
                    'Please remove them first from central list if you want to update above columns',
336
                ),
337
            );
338
        }
339
340 4
        foreach ($insQuery as $query) {
341 4
            if (! $this->dbi->tryQuery($query, ConnectionType::ControlUser)) {
342
                $message = Message::error(__('Could not add columns!'));
343
                $message->addMessage(
344
                    Message::rawError($this->dbi->getError(ConnectionType::ControlUser)),
345
                );
346
347
                return $message;
348
            }
349
        }
350
351 4
        return $message;
352
    }
353
354
    /**
355
     * if $isTable is true it removes all columns of given tables as $field_select from
356
     * central columns list otherwise $field_select is columns list and it removes
357
     * given columns if present in central list
358
     *
359
     * @param string   $database    Database name
360
     * @param string[] $fieldSelect if $isTable selected list of tables otherwise
361
     *                            selected list of columns to remove from central list
362
     * @param bool     $isTable     if passed array is of tables or columns
363
     *
364
     * @return true|Message
365
     */
366
    public function deleteColumnsFromList(
367
        string $database,
368
        array $fieldSelect,
369
        bool $isTable = true,
370
    ): bool|Message {
371
        $cfgCentralColumns = $this->getParams();
372
        if (! is_array($cfgCentralColumns)) {
373
            return $this->getStorageNotReadyMessage();
374
        }
375
376
        $pmadb = $cfgCentralColumns['db'];
377
        $centralListTable = $cfgCentralColumns['table'];
378
        $colNotExist = [];
379
        if ($isTable) {
380
            $fields = [];
381
            $cols = [];
382
            foreach ($fieldSelect as $table) {
383
                $fields[$table] = $this->dbi->getColumnNames($database, $table);
384
                $cols = array_merge($cols, $fields[$table]);
385
            }
386
387
            $hasList = $this->findExistingColNames($database, $cols);
388
            foreach ($fieldSelect as $table) {
389
                foreach ($fields[$table] as $column) {
390
                    if (in_array($column, $hasList, true)) {
391
                        continue;
392
                    }
393
394
                    $colNotExist[] = "'" . $column . "'";
395
                }
396
            }
397
        } else {
398
            $cols = $fieldSelect;
399
            $hasList = $this->findExistingColNames($database, $cols);
400
            foreach ($fieldSelect as $column) {
401
                if (in_array($column, $hasList, true)) {
402
                    continue;
403
                }
404
405
                $colNotExist[] = "'" . $column . "'";
406
            }
407
        }
408
409
        $message = true;
410
        if ($colNotExist !== []) {
411
            $colNotExist = implode(',', array_unique($colNotExist));
412
            $message = Message::notice(
413
                sprintf(
414
                    __(
415
                        'Couldn\'t remove Column(s) %1$s as they don\'t exist in central columns list!',
416
                    ),
417
                    htmlspecialchars($colNotExist),
418
                ),
419
            );
420
        }
421
422
        $cols = $this->getWhereInColumns($cols);
423
        $query = 'DELETE FROM '
424
            . Util::backquote($pmadb) . '.' . Util::backquote($centralListTable) . ' WHERE db_name = '
425
            . $this->dbi->quoteString($database, ConnectionType::ControlUser) . ' AND col_name IN (' . $cols . ');';
426
427
        if (! $this->dbi->tryQuery($query, ConnectionType::ControlUser)) {
428
            $message = Message::error(__('Could not remove columns!'));
429
            $message->addHtml('<br>' . htmlspecialchars($cols) . '<br>');
430
            $message->addMessage(
431
                Message::rawError($this->dbi->getError(ConnectionType::ControlUser)),
432
            );
433
        }
434
435
        return $message;
436
    }
437
438
    /**
439
     * Make the columns of given tables consistent with central list of columns.
440
     * Updates only those columns which are not being referenced.
441
     *
442
     * @param string   $db             current database
443
     * @param string[] $selectedTables list of selected tables.
444
     *
445
     * @return true|Message
446
     */
447 4
    public function makeConsistentWithList(
448
        string $db,
449
        array $selectedTables,
450
    ): bool|Message {
451 4
        $message = true;
452 4
        $this->dbi->selectDb($db);
453 4
        foreach ($selectedTables as $table) {
454 4
            $query = 'ALTER TABLE ' . Util::backquote($table);
455 4
            $hasList = $this->findExistingColumns($db, $this->dbi->getColumnNames($db, $table));
456 4
            foreach ($hasList as $column) {
457 4
                $columnStatus = $this->relation->checkChildForeignReferences($db, $table, $column['col_name']);
458
                //column definition can only be changed if
459
                //it is not referenced by another column
460 4
                if (! $columnStatus['isEditable']) {
461
                    continue;
462
                }
463
464 4
                $query .= ' MODIFY ' . Util::backquote($column['col_name']) . ' ' . $column['col_type'];
465 4
                if ($column['col_length']) {
466 4
                    $query .= '(' . $column['col_length'] . ')';
467
                }
468
469 4
                $query .= ' ' . $column['col_attribute'];
470 4
                if ($column['col_isNull']) {
471 4
                    $query .= ' NULL';
472
                } else {
473 4
                    $query .= ' NOT NULL';
474
                }
475
476 4
                $query .= ' ' . $column['col_extra'];
477 4
                if ($column['col_default']) {
478
                    if (
479 4
                        $column['col_default'] !== 'CURRENT_TIMESTAMP'
480 4
                        && $column['col_default'] !== 'current_timestamp()'
481
                    ) {
482 4
                        $query .= ' DEFAULT ' . $this->dbi->quoteString(
483 4
                            $column['col_default'],
484 4
                            ConnectionType::ControlUser,
485 4
                        );
486
                    } else {
487 4
                        $query .= ' DEFAULT ' . $column['col_default'];
488
                    }
489
                }
490
491 4
                $query .= ',';
492
            }
493
494 4
            $query = trim($query, ' ,') . ';';
495 4
            if ($this->dbi->tryQuery($query)) {
496 4
                continue;
497
            }
498
499
            if ($message === true) {
500
                $message = Message::error($this->dbi->getError());
501
            } else {
502
                $message->addText($this->dbi->getError(), '<br>');
503
            }
504
        }
505
506 4
        return $message;
507
    }
508
509
    /**
510
     * update a column in central columns list if a edit is requested
511
     *
512
     * @param string $db           current database
513
     * @param string $origColName  original column name before edit
514
     * @param string $colName      new column name
515
     * @param string $colType      new column type
516
     * @param string $colAttribute new column attribute
517
     * @param string $colLength    new column length
518
     * @param bool   $colIsNull    value 1 if new column isNull is true, 0 otherwise
519
     * @param string $collation    new column collation
520
     * @param string $colExtra     new column extra property
521
     * @param string $colDefault   new column default value
522
     *
523
     * @return true|Message
524
     */
525 8
    public function updateOneColumn(
526
        string $db,
527
        string $origColName,
528
        string $colName,
529
        string $colType,
530
        string $colAttribute,
531
        string $colLength,
532
        bool $colIsNull,
533
        string $collation,
534
        string $colExtra,
535
        string $colDefault,
536
    ): bool|Message {
537 8
        $cfgCentralColumns = $this->getParams();
538 8
        if (! is_array($cfgCentralColumns)) {
539
            return $this->getStorageNotReadyMessage();
540
        }
541
542 8
        $centralListDb = $cfgCentralColumns['db'];
543 8
        $centralTable = $cfgCentralColumns['table'];
544 8
        if ($origColName === '') {
545 4
            $def = new ColumnFull(
546 4
                $colName,
547 4
                $colType . ($colLength !== '' ? '(' . $colLength . ')' : ''),
548 4
                $collation,
549 4
                $colIsNull,
550 4
                '',
551 4
                $colDefault,
552 4
                $colExtra,
553 4
                '',
554 4
                '',
555 4
            );
556 4
            $query = $this->getInsertQuery($def, $db, $centralListDb, $centralTable);
557
        } else {
558 8
            $query = 'UPDATE ' . Util::backquote($centralListDb) . '.' . Util::backquote($centralTable)
559 8
                . ' SET col_type = ' . $this->dbi->quoteString($colType, ConnectionType::ControlUser)
560 8
                . ', col_name = ' . $this->dbi->quoteString($colName, ConnectionType::ControlUser)
561 8
                . ', col_length = ' . $this->dbi->quoteString($colLength, ConnectionType::ControlUser)
562 8
                . ', col_isNull = ' . $colIsNull
563 8
                . ', col_collation = ' . $this->dbi->quoteString($collation, ConnectionType::ControlUser)
564 8
                . ', col_extra = \''
565 8
                . implode(',', [$colExtra, $colAttribute]) . '\''
566 8
                . ', col_default = ' . $this->dbi->quoteString($colDefault, ConnectionType::ControlUser)
567 8
                . ' WHERE db_name = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser)
568 8
                . ' AND col_name = ' . $this->dbi->quoteString($origColName, ConnectionType::ControlUser);
569
        }
570
571 8
        if (! $this->dbi->tryQuery($query, ConnectionType::ControlUser)) {
572
            return Message::error($this->dbi->getError(ConnectionType::ControlUser));
573
        }
574
575 8
        return true;
576
    }
577
578
    /**
579
     * Update Multiple column in central columns list if a change is requested
580
     *
581
     * @param mixed[] $params Request parameters
582
     *
583
     * @return true|Message
584
     */
585 4
    public function updateMultipleColumn(array $params): bool|Message
586
    {
587 4
        $columnDefault = $params['field_default_type'];
588 4
        $columnIsNull = [];
589 4
        $columnExtra = [];
590 4
        $numberCentralFields = count($params['orig_col_name']);
591 4
        for ($i = 0; $i < $numberCentralFields; $i++) {
592 4
            $columnIsNull[$i] = isset($params['field_null'][$i]);
593 4
            $columnExtra[$i] = $params['col_extra'][$i] ?? '';
594
595 4
            if ($columnDefault[$i] === 'NONE') {
596
                $columnDefault[$i] = '';
597 4
            } elseif ($columnDefault[$i] === 'USER_DEFINED') {
598
                $columnDefault[$i] = $params['field_default_value'][$i];
599
            }
600
601 4
            $message = $this->updateOneColumn(
602 4
                $params['db'],
603 4
                $params['orig_col_name'][$i],
604 4
                $params['field_name'][$i],
605 4
                $params['field_type'][$i],
606 4
                $params['field_attribute'][$i],
607 4
                $params['field_length'][$i],
608 4
                $columnIsNull[$i],
609 4
                $params['field_collation'][$i],
610 4
                $columnExtra[$i],
611 4
                $columnDefault[$i],
612 4
            );
613 4
            if (! is_bool($message)) {
614
                return $message;
615
            }
616
        }
617
618 4
        return true;
619
    }
620
621
    /**
622
     * build html for editing a row in central columns table
623
     *
624
     * @param (string|null)[] $row    array contains complete information of a particular row of central list table
625
     * @param int             $rowNum position the row in the table
626
     *
627
     * @return string html of a particular row in the central columns table.
628
     */
629 4
    private function getHtmlForEditTableRow(array $row, int $rowNum): string
630
    {
631 4
        $meta = [];
632 4
        if (! isset($row['col_default']) || $row['col_default'] == '') {
633
            $meta['DefaultType'] = 'NONE';
634 4
        } elseif ($row['col_default'] === 'CURRENT_TIMESTAMP' || $row['col_default'] === 'current_timestamp()') {
635 4
            $meta['DefaultType'] = 'CURRENT_TIMESTAMP';
636 4
        } elseif ($row['col_default'] === 'NULL') {
637
            $meta['DefaultType'] = $row['col_default'];
638
        } else {
639 4
            $meta['DefaultType'] = 'USER_DEFINED';
640 4
            $meta['DefaultValue'] = $row['col_default'];
641
        }
642
643 4
        $defaultValue = '';
644 4
        $typeUpper = mb_strtoupper((string) $row['col_type']);
645
646
        // For a TIMESTAMP, do not show the string "CURRENT_TIMESTAMP" as a default value
647 4
        if (isset($meta['DefaultValue'])) {
648 4
            $defaultValue = $meta['DefaultValue'];
649
650 4
            if ($typeUpper === 'BIT') {
651
                $defaultValue = Util::convertBitDefaultValue($meta['DefaultValue']);
652 4
            } elseif ($typeUpper === 'BINARY' || $typeUpper === 'VARBINARY') {
653
                $defaultValue = bin2hex($meta['DefaultValue']);
654
            }
655
        }
656
657 4
        $charsets = Charsets::getCharsets($this->dbi, $this->disableIs);
658 4
        $collations = Charsets::getCollations($this->dbi, $this->disableIs);
659
660 4
        return $this->template->render('database/central_columns/edit_table_row', [
661 4
            'row_num' => $rowNum,
662 4
            'row' => $row,
663 4
            'max_rows' => $this->maxRows,
664 4
            'meta' => $meta,
665 4
            'default_value' => $defaultValue,
666 4
            'char_editing' => $this->charEditing,
667 4
            'charsets' => $charsets,
668 4
            'collations' => $collations,
669 4
            'attribute_types' => $this->dbi->types->getAttributes(),
670 4
        ]);
671
    }
672
673
    /**
674
     * get the list of columns in given database excluding
675
     * the columns present in current table
676
     *
677
     * @param string $db    selected database
678
     * @param string $table current table name
679
     *
680
     * @return mixed[] encoded list of columns present in central list for the given database
681
     */
682 8
    public function getListRaw(string $db, string $table): array
683
    {
684 8
        $cfgCentralColumns = $this->getParams();
685 8
        if (! is_array($cfgCentralColumns)) {
686
            return [];
687
        }
688
689 8
        $pmadb = $cfgCentralColumns['db'];
690 8
        $centralTable = $cfgCentralColumns['table'];
691 8
        if ($table === '') {
692 4
            $query = 'SELECT * FROM ' . Util::backquote($pmadb) . '.' . Util::backquote($centralTable) . ' '
693 4
                . 'WHERE db_name = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser) . ';';
694
        } else {
695 4
            $columns = $this->dbi->getColumnNames($db, $table);
696 4
            $query = 'SELECT * FROM ' . Util::backquote($pmadb) . '.' . Util::backquote($centralTable) . ' '
697 4
                . 'WHERE db_name = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser);
698 4
            if ($columns !== []) {
699 4
                $query .= ' AND col_name NOT IN (' . $this->getWhereInColumns($columns) . ')';
700
            }
701
702 4
            $query .= ';';
703
        }
704
705 8
        $columnsList = $this->dbi->fetchResult($query, null, null, ConnectionType::ControlUser);
706 8
        $this->handleColumnExtra($columnsList);
707
708 8
        return $columnsList;
709
    }
710
711
    /**
712
     * Column `col_extra` is used to store both extra and attributes for a column.
713
     * This method separates them.
714
     *
715
     * @param mixed[] $columnsList columns list
716
     */
717 24
    private function handleColumnExtra(array &$columnsList): void
718
    {
719 24
        foreach ($columnsList as &$row) {
720 24
            $vals = explode(',', $row['col_extra']);
721
722 24
            if (in_array('BINARY', $vals, true)) {
723 24
                $row['col_attribute'] = 'BINARY';
724 20
            } elseif (in_array('UNSIGNED', $vals, true)) {
725 20
                $row['col_attribute'] = 'UNSIGNED';
726 20
            } elseif (in_array('UNSIGNED ZEROFILL', $vals, true)) {
727
                $row['col_attribute'] = 'UNSIGNED ZEROFILL';
728 20
            } elseif (in_array('on update CURRENT_TIMESTAMP', $vals, true)) {
729 20
                $row['col_attribute'] = 'on update CURRENT_TIMESTAMP';
730
            } else {
731
                $row['col_attribute'] = '';
732
            }
733
734 24
            $row['col_extra'] = in_array('auto_increment', $vals, true) ? 'auto_increment' : '';
735
        }
736
    }
737
738
    /**
739
     * Get HTML for editing page central columns
740
     *
741
     * @param string[] $selectedFld Array containing the selected fields
742
     * @param string   $selectedDb  String containing the name of database
743
     *
744
     * @return string HTML for complete editing page for central columns
745
     */
746 4
    public function getHtmlForEditingPage(array $selectedFld, string $selectedDb): string
747
    {
748 4
        $html = '';
749 4
        $listDetailCols = $this->findExistingColumns($selectedDb, $selectedFld);
750 4
        $rowNum = 0;
751 4
        foreach ($listDetailCols as $row) {
752 4
            $tableHtmlRow = $this->getHtmlForEditTableRow($row, $rowNum);
753 4
            $html .= $tableHtmlRow;
754 4
            $rowNum++;
755
        }
756
757 4
        return $html;
758
    }
759
760
    /**
761
     * get number of columns of given database from central columns list
762
     * starting at offset $from
763
     *
764
     * @param string $db   selected database
765
     * @param int    $from starting offset of first result
766
     * @param int    $num  maximum number of results to return
767
     *
768
     * @return int count of $num columns present in central columns list
769
     * starting at offset $from for the given database
770
     */
771
    public function getColumnsCount(string $db, int $from = 0, int $num = 25): int
772
    {
773
        $cfgCentralColumns = $this->getParams();
774
        if (! is_array($cfgCentralColumns)) {
775
            return 0;
776
        }
777
778
        $pmadb = $cfgCentralColumns['db'];
779
        $centralListTable = $cfgCentralColumns['table'];
780
        //get current values of $db from central column list
781
        $query = 'SELECT COUNT(db_name) FROM ' . Util::backquote($pmadb) . '.' . Util::backquote($centralListTable)
782
            . ' WHERE db_name = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser)
783
            . ($num === 0 ? '' : 'LIMIT ' . $from . ', ' . $num) . ';';
784
        $result = $this->dbi->fetchResult($query, null, null, ConnectionType::ControlUser);
785
786
        if (isset($result[0])) {
787
            return (int) $result[0];
788
        }
789
790
        return -1;
791
    }
792
793
    /** @return string[] */
794 4
    public function getColumnsNotInCentralList(string $db, string $table): array
795
    {
796 4
        $existingColumns = $this->findExistingColNames($db, $this->dbi->getColumnNames($db, $table));
797 4
        $columnNames = $this->dbi->getColumnNames($db, $table);
798
799
        // returns a list of column names less the ones from $existingColumns
800 4
        return array_values(array_diff($columnNames, $existingColumns));
801
    }
802
803
    /**
804
     * Adding a new user defined column to central list
805
     *
806
     * @param string $db        current database
807
     * @param int    $totalRows number of rows in central columns
808
     * @param int    $pos       offset of first result with complete result set
809
     * @param string $textDir   table footer arrow direction
810
     *
811
     * @return mixed[]
812
     */
813
    public function getTemplateVariablesForMain(
814
        string $db,
815
        int $totalRows,
816
        int $pos,
817
        string $textDir,
818
    ): array {
819
        $maxRows = $this->maxRows;
820
        $attributeTypes = $this->dbi->types->getAttributes();
821
822
        $tnPageNow = ($pos / $this->maxRows) + 1;
823
        $tnNbTotalPage = (int) ceil($totalRows / $this->maxRows);
824
        $tnPageSelector = $tnNbTotalPage > 1 ? Util::pageselector(
825
            'pos',
826
            $this->maxRows,
827
            $tnPageNow,
828
            $tnNbTotalPage,
829
        ) : '';
830
        $tables = $this->dbi->getTables($db);
831
        $rowsList = $this->getColumnsList($db, $pos, $maxRows);
832
833
        $defaultValues = [];
834
        $rowsMeta = [];
835
        $typesUpper = [];
836
        $rowNum = 0;
837
        foreach ($rowsList as $row) {
838
            $rowsMeta[$rowNum] = [];
839
            if (! isset($row['col_default']) || $row['col_default'] == '') {
840
                $rowsMeta[$rowNum]['DefaultType'] = 'NONE';
841
            } elseif ($row['col_default'] === 'CURRENT_TIMESTAMP' || $row['col_default'] === 'current_timestamp()') {
842
                $rowsMeta[$rowNum]['DefaultType'] = 'CURRENT_TIMESTAMP';
843
            } elseif ($row['col_default'] === 'NULL') {
844
                $rowsMeta[$rowNum]['DefaultType'] = $row['col_default'];
845
            } else {
846
                $rowsMeta[$rowNum]['DefaultType'] = 'USER_DEFINED';
847
                $rowsMeta[$rowNum]['DefaultValue'] = $row['col_default'];
848
            }
849
850
            $typesUpper[$rowNum] = mb_strtoupper((string) $row['col_type']);
851
852
            // For a TIMESTAMP, do not show the string "CURRENT_TIMESTAMP" as a default value
853
            $defaultValues[$rowNum] = '';
854
            if (isset($rowsMeta[$rowNum]['DefaultValue'])) {
855
                $defaultValues[$rowNum] = $rowsMeta[$rowNum]['DefaultValue'];
856
857
                if ($typesUpper[$rowNum] === 'BIT') {
858
                    $defaultValues[$rowNum] = Util::convertBitDefaultValue($rowsMeta[$rowNum]['DefaultValue']);
859
                } elseif ($typesUpper[$rowNum] === 'BINARY' || $typesUpper[$rowNum] === 'VARBINARY') {
860
                    $defaultValues[$rowNum] = bin2hex($rowsMeta[$rowNum]['DefaultValue']);
861
                }
862
            }
863
864
            $rowNum++;
865
        }
866
867
        $charsets = Charsets::getCharsets($this->dbi, $this->disableIs);
868
        $collations = Charsets::getCollations($this->dbi, $this->disableIs);
869
        $charsetsList = [];
870
        foreach ($charsets as $charset) {
871
            $collationsList = [];
872
            foreach ($collations[$charset->getName()] as $collation) {
873
                $collationsList[] = ['name' => $collation->getName(), 'description' => $collation->getDescription()];
874
            }
875
876
            $charsetsList[] = [
877
                'name' => $charset->getName(),
878
                'description' => $charset->getDescription(),
879
                'collations' => $collationsList,
880
            ];
881
        }
882
883
        return [
884
            'db' => $db,
885
            'total_rows' => $totalRows,
886
            'max_rows' => $maxRows,
887
            'pos' => $pos,
888
            'char_editing' => $this->charEditing,
889
            'attribute_types' => $attributeTypes,
890
            'tn_nbTotalPage' => $tnNbTotalPage,
891
            'tn_page_selector' => $tnPageSelector,
892
            'tables' => $tables,
893
            'rows_list' => $rowsList,
894
            'rows_meta' => $rowsMeta,
895
            'default_values' => $defaultValues,
896
            'types_upper' => $typesUpper,
897
            'text_dir' => $textDir,
898
            'charsets' => $charsetsList,
899
        ];
900
    }
901
902
    /** @param string[] $columns */
903 24
    private function getWhereInColumns(array $columns): string
904
    {
905 24
        return implode(',', array_map(
906 24
            fn (string $string): string => $this->dbi->quoteString($string, ConnectionType::ControlUser),
907 24
            $columns,
908 24
        ));
909
    }
910
911
    private function getStorageNotReadyMessage(): Message
912
    {
913
        return Message::error(
914
            __('The configuration storage is not ready for the central list of columns feature.'),
915
        );
916
    }
917
}
918