StructureController::displayTableList()   F
last analyzed

Complexity

Conditions 44
Paths > 20000

Size

Total Lines 302
Code Lines 217

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 193
CRAP Score 55.7098

Importance

Changes 0
Metric Value
eloc 217
dl 0
loc 302
ccs 193
cts 236
cp 0.8178
rs 0
c 0
b 0
f 0
cc 44
nc 4478988
nop 1
crap 55.7098

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\Controllers\Database;
6
7
use DateTimeImmutable;
8
use PhpMyAdmin\Charsets;
9
use PhpMyAdmin\Config;
10
use PhpMyAdmin\Config\PageSettings;
11
use PhpMyAdmin\ConfigStorage\Relation;
12
use PhpMyAdmin\Controllers\InvocableController;
13
use PhpMyAdmin\Current;
14
use PhpMyAdmin\Dbal\DatabaseInterface;
15
use PhpMyAdmin\DbTableExists;
16
use PhpMyAdmin\Favorites\RecentFavoriteTable;
17
use PhpMyAdmin\Favorites\RecentFavoriteTables;
18
use PhpMyAdmin\Favorites\TableType;
19
use PhpMyAdmin\Html\Generator;
20
use PhpMyAdmin\Http\Response;
21
use PhpMyAdmin\Http\ServerRequest;
22
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...
23
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...
24
use PhpMyAdmin\Message;
25
use PhpMyAdmin\Query\Utilities;
26
use PhpMyAdmin\Replication\Replication;
27
use PhpMyAdmin\Replication\ReplicationInfo;
28
use PhpMyAdmin\ResponseRenderer;
29
use PhpMyAdmin\Sanitize;
30
use PhpMyAdmin\StorageEngine;
31
use PhpMyAdmin\Template;
32
use PhpMyAdmin\Tracking\TrackedTable;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Tracking\TrackedTable 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...
33
use PhpMyAdmin\Tracking\Tracker;
34
use PhpMyAdmin\Tracking\TrackingChecker;
35
use PhpMyAdmin\Url;
36
use PhpMyAdmin\Util;
37
use Throwable;
38
39
use function __;
40
use function array_search;
41
use function ceil;
42
use function count;
43
use function htmlspecialchars;
44
use function implode;
45
use function in_array;
46
use function is_string;
47
use function max;
48
use function mb_substr;
49
use function md5;
50
use function preg_match;
51
use function preg_quote;
52
use function sprintf;
53
use function str_replace;
54
use function urlencode;
55
56
/**
57
 * Handles database structure logic
58
 */
59
final class StructureController implements InvocableController
60
{
61
    /** @var int Number of tables */
62
    private int $numTables = 0;
63
64
    /** @var int Current position in the list */
65
    private int $position = 0;
66
67
    /** @var bool DB is information_schema */
68
    private bool $dbIsSystemSchema = false;
69
70
    /** @var int Number of tables */
71
    private int $totalNumTables = 0;
72
73
    /** @var mixed[] Tables in the database */
74
    private array $tables = [];
75
76
    /** @var bool whether stats show or not */
77
    private bool $isShowStats = false;
78
79
    private ReplicationInfo $replicationInfo;
80
81 20
    public function __construct(
82
        private readonly ResponseRenderer $response,
83
        private readonly Template $template,
84
        private readonly Relation $relation,
85
        private readonly Replication $replication,
86
        private readonly DatabaseInterface $dbi,
87
        private readonly TrackingChecker $trackingChecker,
88
        private readonly PageSettings $pageSettings,
89
        private readonly DbTableExists $dbTableExists,
90
        private readonly Config $config,
91
    ) {
92 20
        $this->replicationInfo = new ReplicationInfo($this->dbi);
93
    }
94
95
    /**
96
     * Retrieves database information for further use.
97
     */
98 4
    private function getDatabaseInfo(ServerRequest $request): void
99
    {
100 4
        [$tables, $totalNumTables] = Util::getDbInfo($request, Current::$database);
101
102 4
        $this->tables = $tables;
103 4
        $this->numTables = count($tables);
104 4
        $this->position = Util::getTableListPosition($request, Current::$database);
105 4
        $this->totalNumTables = $totalNumTables;
106
107
        /**
108
         * whether to display extended stats
109
         */
110 4
        $this->isShowStats = $this->config->settings['ShowStats'];
111
112
        /**
113
         * whether selected db is information_schema
114
         */
115 4
        $this->dbIsSystemSchema = false;
116
117 4
        if (! Utilities::isSystemSchema(Current::$database)) {
118 4
            return;
119
        }
120
121
        $this->isShowStats = false;
122
        $this->dbIsSystemSchema = true;
123
    }
124
125
    public function __invoke(ServerRequest $request): Response
126
    {
127
        $parameters = ['sort' => $_REQUEST['sort'] ?? null, 'sort_order' => $_REQUEST['sort_order'] ?? null];
128
129
        if (Current::$database === '') {
130
            return $this->response->missingParameterError('db');
131
        }
132
133
        $databaseName = DatabaseName::tryFrom($request->getParam('db'));
134
        if ($databaseName === null || ! $this->dbTableExists->selectDatabase($databaseName)) {
135
            if ($request->isAjax()) {
136
                $this->response->setRequestStatus(false);
137
                $this->response->addJSON('message', Message::error(__('No databases selected.')));
138
139
                return $this->response->response();
140
            }
141
142
            $this->response->redirectToRoute('/', ['reload' => true, 'message' => __('No databases selected.')]);
143
144
            return $this->response->response();
145
        }
146
147
        $this->response->addScriptFiles(['database/structure.js', 'table/change.js']);
148
149
        // Gets the database structure
150
        $this->getDatabaseInfo($request);
151
152
        // Checks if there are any tables to be shown on current page.
153
        // If there are no tables, the user is redirected to the last page
154
        // having any.
155
        if ($this->totalNumTables > 0 && $this->position > $this->totalNumTables) {
156
            $this->response->redirectToRoute('/database/structure', [
157
                'db' => Current::$database,
158
                'pos' => max(0, $this->totalNumTables - $this->config->settings['MaxTableList']),
159
                'reload' => 1,
160
            ]);
161
        }
162
163
        $this->replicationInfo->load($request->getParsedBodyParamAsStringOrNull('primary_connection'));
164
        $replicaInfo = $this->replicationInfo->getReplicaInfo();
165
166
        $this->pageSettings->init('DbStructure');
167
        $this->response->addHTML($this->pageSettings->getErrorHTML());
168
        $this->response->addHTML($this->pageSettings->getHTML());
169
170
        if ($this->numTables > 0) {
171
            $urlParams = ['pos' => $this->position, 'db' => Current::$database];
172
            if (isset($parameters['sort'])) {
173
                $urlParams['sort'] = $parameters['sort'];
174
            }
175
176
            if (isset($parameters['sort_order'])) {
177
                $urlParams['sort_order'] = $parameters['sort_order'];
178
            }
179
180
            $listNavigator = Generator::getListNavigator(
181
                $this->totalNumTables,
182
                $this->position,
183
                $urlParams,
184
                Url::getFromRoute('/database/structure'),
185
                'frame_content',
186
                $this->config->settings['MaxTableList'],
187
            );
188
189
            $tableList = $this->displayTableList($replicaInfo);
190
        }
191
192
        $createTable = '';
193
        if (! $this->dbIsSystemSchema) {
194
            $createTable = $this->template->render('database/create_table', ['db' => Current::$database]);
195
        }
196
197
        $this->response->render('database/structure/index', [
198
            'database' => Current::$database,
199
            'has_tables' => $this->numTables > 0,
200
            'list_navigator_html' => $listNavigator ?? '',
201
            'table_list_html' => $tableList ?? '',
202
            'is_system_schema' => $this->dbIsSystemSchema,
203
            'create_table_html' => $createTable,
204
        ]);
205
206
        return $this->response->response();
207
    }
208
209
    /** @param mixed[] $replicaInfo */
210 4
    private function displayTableList(array $replicaInfo): string
211
    {
212 4
        $html = '';
213
214
        // filtering
215 4
        $html .= $this->template->render('filter', ['filter_value' => '']);
216
217 4
        $i = $sumEntries = 0;
218 4
        $overheadCheck = false;
219 4
        $createTimeAll = null;
220 4
        $updateTimeAll = null;
221 4
        $checkTimeAll = null;
222 4
        $numColumns = $this->config->settings['PropertiesNumColumns'] > 1
223
            ? ceil($this->numTables / $this->config->settings['PropertiesNumColumns']) + 1
224 4
            : 0;
225 4
        $rowCount = 0;
226 4
        $sumSize = 0;
227 4
        $overheadSize = 0;
228
229 4
        $hiddenFields = [];
230 4
        $overallApproxRows = false;
231 4
        $structureTableRows = [];
232 4
        $trackedTables = $this->trackingChecker->getTrackedTables(Current::$database);
233 4
        $recentFavoriteTables = RecentFavoriteTables::getInstance(TableType::Favorite);
234
        /** @var mixed[] $currentTable */
235 4
        foreach ($this->tables as $currentTable) {
236
            // Get valid statistics whatever is the table type
237
238 4
            $dropQuery = '';
239 4
            $dropMessage = '';
240 4
            $overhead = '';
241 4
            $inputClass = ['checkall'];
242
243
            // Sets parameters for links
244 4
            $tableUrlParams = ['db' => Current::$database, 'table' => $currentTable['TABLE_NAME']];
245
            // do not list the previous table's size info for a view
246
247 4
            [
248 4
                $currentTable,
249 4
                $formattedSize,
250 4
                $unit,
251 4
                $formattedOverhead,
252 4
                $overheadUnit,
253 4
                $overheadSize,
254 4
                $tableIsView,
255 4
                $sumSize,
256 4
            ] = $this->getStuffForEngineTypeTable($currentTable, $sumSize, $overheadSize);
257
258 4
            $curTable = $this->dbi
259 4
                ->getTable(Current::$database, $currentTable['TABLE_NAME']);
260 4
            if (! $curTable->isMerge()) {
261 4
                $sumEntries += $currentTable['TABLE_ROWS'];
262
            }
263
264 4
            $collationDefinition = '---';
265 4
            if (isset($currentTable['Collation'])) {
266
                $tableCollation = Charsets::findCollationByName(
267
                    $this->dbi,
268
                    $this->config->selectedServer['DisableIS'],
269
                    $currentTable['Collation'],
270
                );
271
                if ($tableCollation !== null) {
272
                    $collationDefinition = $this->template->render('database/structure/collation_definition', [
273
                        'valueTitle' => $tableCollation->getDescription(),
274
                        'value' => $tableCollation->getName(),
275
                    ]);
276
                }
277
            }
278
279 4
            if ($this->isShowStats) {
280 4
                $overhead = '-';
281 4
                if ($formattedOverhead != '') {
282 4
                    $overhead = $this->template->render('database/structure/overhead', [
283 4
                        'table_url_params' => $tableUrlParams,
284 4
                        'formatted_overhead' => $formattedOverhead,
285 4
                        'overhead_unit' => $overheadUnit,
286 4
                    ]);
287 4
                    $overheadCheck = true;
288 4
                    $inputClass[] = 'tbl-overhead';
289
                }
290
            }
291
292 4
            if ($this->config->settings['ShowDbStructureCharset']) {
293
                $charset = '';
294
                if (isset($tableCollation)) {
295
                    $charset = $tableCollation->getCharset();
296
                }
297
            }
298
299 4
            $createTime = null;
300 4
            if ($this->config->settings['ShowDbStructureCreation'] && isset($currentTable['Create_time'])) {
301
                $createTime = $this->createDateTime($currentTable['Create_time']);
302
                if ($createTime !== null && ($createTimeAll === null || $createTime < $createTimeAll)) {
303
                    $createTimeAll = $createTime;
304
                }
305
            }
306
307 4
            $updateTime = null;
308 4
            if ($this->config->settings['ShowDbStructureLastUpdate'] && isset($currentTable['Update_time'])) {
309
                $updateTime = $this->createDateTime($currentTable['Update_time']);
310
                if ($updateTime !== null && ($updateTimeAll === null || $updateTime < $updateTimeAll)) {
311
                    $updateTimeAll = $updateTime;
312
                }
313
            }
314
315 4
            $checkTime = null;
316 4
            if ($this->config->settings['ShowDbStructureLastCheck'] && isset($currentTable['Check_time'])) {
317
                $checkTime = $this->createDateTime($currentTable['Check_time']);
318
                if ($checkTime !== null && ($checkTimeAll === null || $checkTime < $checkTimeAll)) {
319
                    $checkTimeAll = $checkTime;
320
                }
321
            }
322
323 4
            $truename = $currentTable['TABLE_NAME'];
324
325 4
            $i++;
326
327 4
            $rowCount++;
328 4
            if ($tableIsView) {
329
                $hiddenFields[] = '<input type="hidden" name="views[]" value="'
330
                    . htmlspecialchars($currentTable['TABLE_NAME']) . '">';
331
            }
332
333
            /**
334
             * Always activate links for Browse, Search and Empty, even if
335
             * the icons are greyed, because
336
             * 1. for views, we don't know the number of rows at this point
337
             * 2. for tables, another source could have populated them since the
338
             *    page was generated
339
             *
340
             * I could have used the PHP ternary conditional operator but I find
341
             * the code easier to read without this operator.
342
             */
343 4
            $mayHaveRows = $currentTable['TABLE_ROWS'] > 0 || $tableIsView;
0 ignored issues
show
introduced by
The condition $tableIsView is always true.
Loading history...
344
345 4
            if (! $this->dbIsSystemSchema) {
346 4
                $dropQuery = sprintf(
347 4
                    'DROP %s %s',
348 4
                    $tableIsView ? 'VIEW' : 'TABLE',
349 4
                    Util::backquote(
350 4
                        $currentTable['TABLE_NAME'],
351 4
                    ),
352 4
                );
353 4
                $dropMessage = sprintf(
354 4
                    ($tableIsView ? __('View %s has been dropped.') : __('Table %s has been dropped.')),
355 4
                    str_replace(
356 4
                        ' ',
357 4
                        '&nbsp;',
358 4
                        htmlspecialchars($currentTable['TABLE_NAME']),
359 4
                    ),
360 4
                );
361
            }
362
363 4
            if ($numColumns > 0 && $this->numTables > $numColumns && ($rowCount % $numColumns) === 0) {
364
                $rowCount = 1;
365
366
                $html .= $this->template->render('database/structure/table_header', [
367
                    'db' => Current::$database,
368
                    'db_is_system_schema' => $this->dbIsSystemSchema,
369
                    'replication' => $replicaInfo['status'],
370
                    'properties_num_columns' => $this->config->settings['PropertiesNumColumns'],
371
                    'is_show_stats' => $this->isShowStats,
372
                    'show_charset' => $this->config->settings['ShowDbStructureCharset'],
373
                    'show_comment' => $this->config->settings['ShowDbStructureComment'],
374
                    'show_creation' => $this->config->settings['ShowDbStructureCreation'],
375
                    'show_last_update' => $this->config->settings['ShowDbStructureLastUpdate'],
376
                    'show_last_check' => $this->config->settings['ShowDbStructureLastCheck'],
377
                    'num_favorite_tables' => $this->config->settings['NumFavoriteTables'],
378
                    'structure_table_rows' => $structureTableRows,
379
                ]);
380
                $structureTableRows = [];
381
            }
382
383 4
            [$approxRows, $showSuperscript] = $this->isRowCountApproximated($currentTable, $tableIsView);
384
385 4
            [$do, $ignored] = $this->getReplicationStatus($replicaInfo, $truename);
386
387 4
            $structureTableRows[] = [
388 4
                'table_name_hash' => md5($currentTable['TABLE_NAME']),
389 4
                'db_table_name_hash' => md5(Current::$database . '.' . $currentTable['TABLE_NAME']),
390 4
                'db' => Current::$database,
391 4
                'curr' => $i,
392 4
                'input_class' => implode(' ', $inputClass),
393 4
                'table_is_view' => $tableIsView,
394 4
                'current_table' => $currentTable,
395 4
                'may_have_rows' => $mayHaveRows,
396 4
                'browse_table_label_title' => htmlspecialchars($currentTable['TABLE_COMMENT']),
397 4
                'browse_table_label_truename' => $truename,
398 4
                'empty_table_sql_query' => 'TRUNCATE ' . Util::backquote($currentTable['TABLE_NAME']),
399 4
                'empty_table_message_to_show' => urlencode(
400 4
                    sprintf(
401 4
                        __('Table %s has been emptied.'),
402 4
                        htmlspecialchars(
403 4
                            $currentTable['TABLE_NAME'],
404 4
                        ),
405 4
                    ),
406 4
                ),
407 4
                'tracking_icon' => $this->getTrackingIcon($truename, $trackedTables[$truename] ?? null),
408 4
                'server_replica_status' => $replicaInfo['status'],
409 4
                'table_url_params' => $tableUrlParams,
410 4
                'db_is_system_schema' => $this->dbIsSystemSchema,
411 4
                'drop_query' => $dropQuery,
412 4
                'drop_message' => $dropMessage,
413 4
                'collation' => $collationDefinition,
414 4
                'formatted_size' => $formattedSize,
415 4
                'unit' => $unit,
416 4
                'overhead' => $overhead,
417 4
                'create_time' => $createTime !== null ? Util::localisedDate($createTime) : '-',
418 4
                'update_time' => $updateTime !== null ? Util::localisedDate($updateTime) : '-',
419 4
                'check_time' => $checkTime !== null ? Util::localisedDate($checkTime) : '-',
420 4
                'charset' => $charset ?? '',
421 4
                'is_show_stats' => $this->isShowStats,
422 4
                'ignored' => $ignored,
423 4
                'do' => $do,
424 4
                'approx_rows' => $approxRows,
425 4
                'show_superscript' => $showSuperscript,
426 4
                'already_favorite' => $recentFavoriteTables->contains(
427 4
                    new RecentFavoriteTable(
428 4
                        DatabaseName::from(Current::$database),
429 4
                        TableName::from($currentTable['TABLE_NAME']),
430 4
                    ),
431 4
                ),
432 4
                'num_favorite_tables' => $this->config->settings['NumFavoriteTables'],
433 4
                'properties_num_columns' => $this->config->settings['PropertiesNumColumns'],
434 4
                'limit_chars' => $this->config->settings['LimitChars'],
435 4
                'show_charset' => $this->config->settings['ShowDbStructureCharset'],
436 4
                'show_comment' => $this->config->settings['ShowDbStructureComment'],
437 4
                'show_creation' => $this->config->settings['ShowDbStructureCreation'],
438 4
                'show_last_update' => $this->config->settings['ShowDbStructureLastUpdate'],
439 4
                'show_last_check' => $this->config->settings['ShowDbStructureLastCheck'],
440 4
            ];
441
442 4
            $overallApproxRows = $overallApproxRows || $approxRows;
443
        }
444
445 4
        $databaseCollation = [];
446 4
        $databaseCharset = '';
447 4
        $collation = Charsets::findCollationByName(
448 4
            $this->dbi,
449 4
            $this->config->selectedServer['DisableIS'],
450 4
            $this->dbi->getDbCollation(Current::$database),
451 4
        );
452 4
        if ($collation !== null) {
453
            $databaseCollation = ['name' => $collation->getName(), 'description' => $collation->getDescription()];
454
            $databaseCharset = $collation->getCharset();
455
        }
456
457 4
        $relationParameters = $this->relation->getRelationParameters();
458
459 4
        $defaultStorageEngine = '';
460 4
        if ($this->config->settings['PropertiesNumColumns'] < 2) {
461
            // MySQL <= 5.5.2
462 4
            $defaultStorageEngine = $this->dbi->fetchValue('SELECT @@storage_engine;');
463 4
            if (! is_string($defaultStorageEngine) || $defaultStorageEngine === '') {
464
                // MySQL >= 5.5.3
465 4
                $defaultStorageEngine = $this->dbi->fetchValue('SELECT @@default_storage_engine;');
466
            }
467
        }
468
469 4
        return $html . $this->template->render('database/structure/table_header', [
470 4
            'db' => Current::$database,
471 4
            'db_is_system_schema' => $this->dbIsSystemSchema,
472 4
            'replication' => $replicaInfo['status'],
473 4
            'properties_num_columns' => $this->config->settings['PropertiesNumColumns'],
474 4
            'is_show_stats' => $this->isShowStats,
475 4
            'show_charset' => $this->config->settings['ShowDbStructureCharset'],
476 4
            'show_comment' => $this->config->settings['ShowDbStructureComment'],
477 4
            'show_creation' => $this->config->settings['ShowDbStructureCreation'],
478 4
            'show_last_update' => $this->config->settings['ShowDbStructureLastUpdate'],
479 4
            'show_last_check' => $this->config->settings['ShowDbStructureLastCheck'],
480 4
            'num_favorite_tables' => $this->config->settings['NumFavoriteTables'],
481 4
            'structure_table_rows' => $structureTableRows,
482 4
            'body_for_table_summary' => [
483 4
                'num_tables' => $this->numTables,
484 4
                'server_replica_status' => $replicaInfo['status'],
485 4
                'db_is_system_schema' => $this->dbIsSystemSchema,
486 4
                'sum_entries' => $sumEntries,
487 4
                'database_collation' => $databaseCollation,
488 4
                'is_show_stats' => $this->isShowStats,
489 4
                'database_charset' => $databaseCharset,
490 4
                'sum_size' => $sumSize,
491 4
                'overhead_size' => $overheadSize,
492 4
                'create_time_all' => $createTimeAll !== null ? Util::localisedDate($createTimeAll) : '-',
493 4
                'update_time_all' => $updateTimeAll !== null ? Util::localisedDate($updateTimeAll) : '-',
494 4
                'check_time_all' => $checkTimeAll !== null ? Util::localisedDate($checkTimeAll) : '-',
495 4
                'approx_rows' => $overallApproxRows,
496 4
                'num_favorite_tables' => $this->config->settings['NumFavoriteTables'],
497 4
                'db' => Current::$database,
498 4
                'properties_num_columns' => $this->config->settings['PropertiesNumColumns'],
499 4
                'default_storage_engine' => $defaultStorageEngine,
500 4
                'show_charset' => $this->config->settings['ShowDbStructureCharset'],
501 4
                'show_comment' => $this->config->settings['ShowDbStructureComment'],
502 4
                'show_creation' => $this->config->settings['ShowDbStructureCreation'],
503 4
                'show_last_update' => $this->config->settings['ShowDbStructureLastUpdate'],
504 4
                'show_last_check' => $this->config->settings['ShowDbStructureLastCheck'],
505 4
            ],
506 4
            'check_all_tables' => [
507 4
                'overhead_check' => $overheadCheck,
508 4
                'db_is_system_schema' => $this->dbIsSystemSchema,
509 4
                'hidden_fields' => $hiddenFields,
510 4
                'disable_multi_table' => $this->config->config->DisableMultiTableMaintenance,
511 4
                'central_columns_work' => $relationParameters->centralColumnsFeature !== null,
512 4
            ],
513 4
        ]);
514
    }
515
516
    /**
517
     * Returns the tracking icon if the table is tracked
518
     *
519
     * @return string HTML for tracking icon
520
     */
521 4
    private function getTrackingIcon(string $table, TrackedTable|null $trackedTable): string
522
    {
523 4
        $trackingIcon = '';
524 4
        if (Tracker::isActive() && $trackedTable !== null) {
525
            $trackingIcon = $this->template->render('database/structure/tracking_icon', [
526
                'db' => Current::$database,
527
                'table' => $table,
528
                'is_tracked' => $trackedTable->active,
529
            ]);
530
        }
531
532 4
        return $trackingIcon;
533
    }
534
535
    /**
536
     * Returns whether the row count is approximated
537
     *
538
     * @param mixed[] $currentTable array containing details about the table
539
     * @param bool    $tableIsView  whether the table is a view
540
     *
541
     * @return array{bool, string}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{bool, string} at position 2 could not be parsed: Expected ':' at position 2, but found 'bool'.
Loading history...
542
     */
543 4
    private function isRowCountApproximated(
544
        array $currentTable,
545
        bool $tableIsView,
546
    ): array {
547 4
        $approxRows = false;
548 4
        $showSuperscript = '';
549
550
        // there is a null value in the ENGINE
551
        // - when the table needs to be repaired, or
552
        // - when it's a view
553
        //  so ensure that we'll display "in use" below for a table
554
        //  that needs to be repaired
555 4
        if (isset($currentTable['TABLE_ROWS']) && ($currentTable['ENGINE'] != null || $tableIsView)) {
556
            // InnoDB/TokuDB table: we did not get an accurate row count
557 4
            $approxRows = ! $tableIsView
558 4
                && in_array($currentTable['ENGINE'], ['CSV', 'InnoDB', 'TokuDB'], true)
559 4
                && ! $currentTable['COUNTED'];
560
561 4
            if ($tableIsView && $currentTable['TABLE_ROWS'] >= $this->config->settings['MaxExactCountViews']) {
562
                $approxRows = true;
563
                $showSuperscript = Generator::showHint(
564
                    Sanitize::convertBBCode(
565
                        sprintf(
566
                            __(
567
                                'This view has at least this number of rows. Please refer to %sdocumentation%s.',
568
                            ),
569
                            '[doc@cfg_MaxExactCountViews]',
570
                            '[/doc]',
571
                        ),
572
                    ),
573
                );
574
            }
575
        }
576
577 4
        return [$approxRows, $showSuperscript];
578
    }
579
580
    /**
581
     * Returns the replication status of the table.
582
     *
583
     * @param mixed[] $replicaInfo
584
     * @param string  $table       table name
585
     *
586
     * @return array{bool, bool}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{bool, bool} at position 2 could not be parsed: Expected ':' at position 2, but found 'bool'.
Loading history...
587
     */
588 4
    private function getReplicationStatus(array $replicaInfo, string $table): array
589
    {
590 4
        $do = $ignored = false;
591 4
        if ($replicaInfo['status']) {
592
            $nbServReplicaDoDb = count($replicaInfo['Do_DB']);
593
            $nbServReplicaIgnoreDb = count($replicaInfo['Ignore_DB']);
594
            $searchDoDBInTruename = array_search($table, $replicaInfo['Do_DB']);
595
            $searchDoDBInDB = array_search(Current::$database, $replicaInfo['Do_DB']);
596
597
            $do = (is_string($searchDoDBInTruename) && $searchDoDBInTruename !== '')
598
                || (is_string($searchDoDBInDB) && $searchDoDBInDB !== '')
599
                || ($nbServReplicaDoDb == 0 && $nbServReplicaIgnoreDb == 0)
600
                || $this->hasTable($replicaInfo['Wild_Do_Table'], $table);
601
602
            $searchDb = array_search(Current::$database, $replicaInfo['Ignore_DB']);
603
            $searchTable = array_search($table, $replicaInfo['Ignore_Table']);
604
            $ignored = (is_string($searchTable) && $searchTable !== '')
605
                || (is_string($searchDb) && $searchDb !== '')
606
                || $this->hasTable($replicaInfo['Wild_Ignore_Table'], $table);
607
        }
608
609 4
        return [$do, $ignored];
610
    }
611
612
    /**
613
     * Find table with truename
614
     *
615
     * @param mixed[] $db       DB to look into
616
     * @param string  $truename Table name
617
     */
618 4
    private function hasTable(array $db, string $truename): bool
619
    {
620 4
        foreach ($db as $dbTable) {
621
            if (
622 4
                Current::$database === $this->replication->extractDbOrTable($dbTable)
623 4
                && preg_match(
624 4
                    '@^' .
625 4
                    preg_quote(mb_substr($this->replication->extractDbOrTable($dbTable, 'table'), 0, -1), '@') . '@',
626 4
                    $truename,
627 4
                ) === 1
628
            ) {
629 4
                return true;
630
            }
631
        }
632
633 4
        return false;
634
    }
635
636
    /**
637
     * Get the value set for ENGINE table,
638
     *
639
     * @internal param bool $table_is_view whether table is view or not
640
     *
641
     * @param mixed[] $currentTable current table
642
     * @param int     $sumSize      total table size
643
     * @param int     $overheadSize overhead size
644
     *
645
     * @psalm-return list{mixed[], string, string, string, string, int, bool, int}
646
     */
647 4
    private function getStuffForEngineTypeTable(
648
        array $currentTable,
649
        int $sumSize,
650
        int $overheadSize,
651
    ): array {
652 4
        $formattedSize = '-';
653 4
        $unit = '';
654 4
        $formattedOverhead = '';
655 4
        $overheadUnit = '';
656 4
        $tableIsView = false;
657
658 4
        switch ($currentTable['ENGINE']) {
659
            // MyISAM, ISAM or Heap table: Row count, data size and index size
660
            // are accurate; data size is accurate for ARCHIVE
661 4
            case 'MyISAM':
662 4
            case 'ISAM':
663 4
            case 'HEAP':
664 4
            case 'MEMORY':
665 4
            case 'ARCHIVE':
666 4
            case 'Aria':
667 4
            case 'Maria':
668 4
                [
669 4
                    $currentTable,
670 4
                    $formattedSize,
671 4
                    $unit,
672 4
                    $formattedOverhead,
673 4
                    $overheadUnit,
674 4
                    $overheadSize,
675 4
                    $sumSize,
676 4
                ] = $this->getValuesForAriaTable(
677 4
                    $currentTable,
678 4
                    $sumSize,
679 4
                    $overheadSize,
680 4
                    $formattedSize,
681 4
                    $unit,
682 4
                    $formattedOverhead,
683 4
                    $overheadUnit,
684 4
                );
685 4
                break;
686
            case 'InnoDB':
687
            case 'PBMS':
688
            case 'TokuDB':
689
            case 'ROCKSDB':
690
                // InnoDB table: Row count is not accurate but data and index sizes are.
691
                // PBMS table in Drizzle: TABLE_ROWS is taken from table cache,
692
                // so it may be unavailable
693
                [$currentTable, $formattedSize, $unit, $sumSize] = $this->getValuesForInnodbTable(
694
                    $currentTable,
695
                    $sumSize,
696
                );
697
                break;
698
            case 'CSV':
699
                [$currentTable, $formattedSize, $unit, $sumSize] = $this->getValuesForCsvTable($currentTable, $sumSize);
700
                break;
701
            // Mysql 5.0.x (and lower) uses MRG_MyISAM
702
            // and MySQL 5.1.x (and higher) uses MRG_MYISAM
703
            // Both are aliases for MERGE
704
            case 'MRG_MyISAM':
705
            case 'MRG_MYISAM':
706
            case 'MERGE':
707
            case 'BerkeleyDB':
708
                // Merge or BerkleyDB table: Only row count is accurate.
709
                if ($this->isShowStats) {
710
                    $formattedSize = ' - ';
711
                }
712
713
                break;
714
            // for a view, the ENGINE is sometimes reported as null,
715
            // or on some servers it's reported as "SYSTEM VIEW"
716
            case null:
717
            case 'SYSTEM VIEW':
718
                // possibly a view, do nothing
719
                break;
720
            case 'Mroonga':
721
                // The idea is to show the size only if Mroonga is available,
722
                // in other case the old unknown message will appear
723
                if (StorageEngine::hasMroongaEngine()) {
724
                    [$currentTable, $formattedSize, $unit, $sumSize] = $this->getValuesForMroongaTable(
725
                        $currentTable,
726
                        $sumSize,
727
                    );
728
                    break;
729
                }
730
                // no break, go to default case
731
            default:
732
                // Unknown table type.
733
                if ($this->isShowStats) {
734
                    $formattedSize = __('unknown');
735
                }
736
        }
737
738 4
        if ($currentTable['TABLE_TYPE'] === 'VIEW' || $currentTable['TABLE_TYPE'] === 'SYSTEM VIEW') {
739
            // countRecords() takes care of $cfg['MaxExactCountViews']
740
            $currentTable['TABLE_ROWS'] = $this->dbi
741
                ->getTable(Current::$database, $currentTable['TABLE_NAME'])
742
                ->countRecords(true);
743
            $tableIsView = true;
744
        }
745
746 4
        return [
747 4
            $currentTable,
748 4
            $formattedSize,
749 4
            $unit,
750 4
            $formattedOverhead,
751 4
            $overheadUnit,
752 4
            $overheadSize,
753 4
            $tableIsView,
754 4
            $sumSize,
755 4
        ];
756
    }
757
758
    /**
759
     * Get values for ARIA/MARIA tables
760
     *
761
     * @param mixed[] $currentTable      current table
762
     * @param int     $sumSize           sum size
763
     * @param int     $overheadSize      overhead size
764
     * @param string  $formattedSize     formatted size
765
     * @param string  $unit              unit
766
     * @param string  $formattedOverhead overhead formatted
767
     * @param string  $overheadUnit      overhead unit
768
     *
769
     * @return mixed[]
770
     */
771 8
    private function getValuesForAriaTable(
772
        array $currentTable,
773
        int $sumSize,
774
        int $overheadSize,
775
        string $formattedSize,
776
        string $unit,
777
        string $formattedOverhead,
778
        string $overheadUnit,
779
    ): array {
780 8
        if ($this->dbIsSystemSchema) {
781 4
            $currentTable['Rows'] = $this->dbi
782 4
                ->getTable(Current::$database, $currentTable['Name'])
783 4
                ->countRecords();
784
        }
785
786 8
        if ($this->isShowStats) {
787
            /** @var int $tblsize */
788 8
            $tblsize = $currentTable['Data_length']
789 8
                + $currentTable['Index_length'];
790 8
            $sumSize += $tblsize;
791 8
            [$formattedSize, $unit] = Util::formatByteDown($tblsize, 3, $tblsize > 0 ? 1 : 0);
792 8
            if (isset($currentTable['Data_free']) && $currentTable['Data_free'] > 0) {
793 8
                [$formattedOverhead, $overheadUnit] = Util::formatByteDown($currentTable['Data_free'], 3, 1);
794 8
                $overheadSize += $currentTable['Data_free'];
795
            }
796
        }
797
798 8
        return [$currentTable, $formattedSize, $unit, $formattedOverhead, $overheadUnit, $overheadSize, $sumSize];
799
    }
800
801
    /**
802
     * Get values for InnoDB table
803
     *
804
     * @param mixed[] $currentTable current table
805
     * @param int     $sumSize      sum size
806
     *
807
     * @return mixed[]
808
     */
809 4
    private function getValuesForInnodbTable(
810
        array $currentTable,
811
        int $sumSize,
812
    ): array {
813 4
        $formattedSize = $unit = '';
814
815
        if (
816 4
            (in_array($currentTable['ENGINE'], ['InnoDB', 'TokuDB'], true)
817 4
            && $currentTable['TABLE_ROWS'] < $this->config->settings['MaxExactCount'])
818 4
            || ! isset($currentTable['TABLE_ROWS'])
819
        ) {
820 4
            $currentTable['COUNTED'] = true;
821 4
            $currentTable['TABLE_ROWS'] = $this->dbi
822 4
                ->getTable(Current::$database, $currentTable['TABLE_NAME'])
823 4
                ->countRecords(true);
824
        } else {
825 4
            $currentTable['COUNTED'] = false;
826
        }
827
828 4
        if ($this->isShowStats) {
829
            /** @var int $tblsize */
830 4
            $tblsize = $currentTable['Data_length']
831 4
                + $currentTable['Index_length'];
832 4
            $sumSize += $tblsize;
833 4
            [$formattedSize, $unit] = Util::formatByteDown($tblsize, 3, $tblsize > 0 ? 1 : 0);
834
        }
835
836 4
        return [$currentTable, $formattedSize, $unit, $sumSize];
837
    }
838
839
    /**
840
     * Get values for CSV table
841
     *
842
     * https://bugs.mysql.com/bug.php?id=53929
843
     *
844
     * @param mixed[] $currentTable
845
     *
846
     * @return mixed[]
847
     */
848
    private function getValuesForCsvTable(array $currentTable, int $sumSize): array
849
    {
850
        $formattedSize = $unit = '';
851
852
        if ($currentTable['ENGINE'] === 'CSV') {
853
            $currentTable['COUNTED'] = true;
854
            $currentTable['TABLE_ROWS'] = $this->dbi
855
                ->getTable(Current::$database, $currentTable['TABLE_NAME'])
856
                ->countRecords(true);
857
        } else {
858
            $currentTable['COUNTED'] = false;
859
        }
860
861
        if ($this->isShowStats) {
862
            // Only count columns that have double quotes
863
            $columnCount = (int) $this->dbi->fetchValue(
864
                'SELECT COUNT(COLUMN_NAME) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '
865
                . $this->dbi->quoteString(Current::$database) . ' AND TABLE_NAME = '
866
                . $this->dbi->quoteString($currentTable['TABLE_NAME']) . ' AND NUMERIC_SCALE IS NULL;',
867
            );
868
869
            // Get column names
870
            $columnNames = $this->dbi->fetchValue(
871
                'SELECT GROUP_CONCAT(COLUMN_NAME) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '
872
                . $this->dbi->quoteString(Current::$database) . ' AND TABLE_NAME = '
873
                . $this->dbi->quoteString($currentTable['TABLE_NAME']) . ';',
874
            );
875
876
            // 10Mb buffer for CONCAT_WS
877
            // not sure if is needed
878
            $this->dbi->query('SET SESSION group_concat_max_len = 10 * 1024 * 1024');
879
880
            // Calculate data length
881
            $dataLength = (int) $this->dbi->fetchValue('
882
                SELECT SUM(CHAR_LENGTH(REPLACE(REPLACE(REPLACE(
883
                    CONCAT_WS(\',\', ' . $columnNames . '),
0 ignored issues
show
Bug introduced by
Are you sure $columnNames of type false|null|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

883
                    CONCAT_WS(\',\', ' . /** @scrutinizer ignore-type */ $columnNames . '),
Loading history...
884
                    UNHEX(\'0A\'), \'nn\'), UNHEX(\'22\'), \'nn\'), UNHEX(\'5C\'), \'nn\'
885
                ))) FROM ' . Util::backquote(Current::$database) . '.' . Util::backquote($currentTable['TABLE_NAME']));
886
887
            // Calculate quotes length
888
            $quotesLength = $currentTable['TABLE_ROWS'] * $columnCount * 2;
889
890
            /** @var int $tblsize */
891
            $tblsize = $dataLength + $quotesLength + $currentTable['TABLE_ROWS'];
892
893
            $sumSize += $tblsize;
894
            [$formattedSize, $unit] = Util::formatByteDown($tblsize, 3, $tblsize > 0 ? 1 : 0);
895
        }
896
897
        return [$currentTable, $formattedSize, $unit, $sumSize];
898
    }
899
900
    /**
901
     * Get values for Mroonga table
902
     *
903
     * @param mixed[] $currentTable current table
904
     * @param int     $sumSize      sum size
905
     *
906
     * @return mixed[]
907
     */
908 4
    private function getValuesForMroongaTable(
909
        array $currentTable,
910
        int $sumSize,
911
    ): array {
912 4
        $formattedSize = '';
913 4
        $unit = '';
914
915 4
        if ($this->isShowStats) {
916
            /** @var int $tblsize */
917 4
            $tblsize = $currentTable['Data_length'] + $currentTable['Index_length'];
918 4
            $sumSize += $tblsize;
919 4
            [$formattedSize, $unit] = Util::formatByteDown($tblsize, 3, $tblsize > 0 ? 1 : 0);
920
        }
921
922 4
        return [$currentTable, $formattedSize, $unit, $sumSize];
923
    }
924
925
    private function createDateTime(mixed $dateTime): DateTimeImmutable|null
926
    {
927
        if (! is_string($dateTime) || $dateTime === '') {
928
            return null;
929
        }
930
931
        try {
932
            return new DateTimeImmutable($dateTime);
933
        } catch (Throwable) {
934
            return null;
935
        }
936
    }
937
}
938