Tracker   F
last analyzed

Complexity

Total Complexity 87

Size/Duplication

Total Lines 661
Duplicated Lines 0 %

Test Coverage

Coverage 64.29%

Importance

Changes 0
Metric Value
wmc 87
eloc 300
dl 0
loc 661
rs 2
c 0
b 0
f 0
ccs 207
cts 322
cp 0.6429

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getLogComment() 0 6 1
A createDatabaseVersion() 0 48 4
A isEnabled() 0 3 1
A enable() 0 3 1
A disable() 0 3 1
A isTracked() 0 31 4
A isActive() 0 10 2
A deactivateTracking() 0 3 1
A activateTracking() 0 3 1
C handleQuery() 0 131 17
A getVersion() 0 30 4
F parseQuery() 0 123 36
A changeTracking() 0 27 2
A isAnyTrackingInProgress() 0 20 2
B createVersion() 0 87 10

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * Tracking changes on databases, tables and views
4
 */
5
6
declare(strict_types=1);
7
8
namespace PhpMyAdmin\Tracking;
9
10
use PhpMyAdmin\Config;
11
use PhpMyAdmin\ConfigStorage\Features\TrackingFeature;
12
use PhpMyAdmin\ConfigStorage\Relation;
13
use PhpMyAdmin\Current;
14
use PhpMyAdmin\Dbal\ConnectionType;
15
use PhpMyAdmin\Dbal\DatabaseInterface;
16
use PhpMyAdmin\Plugins;
17
use PhpMyAdmin\Plugins\Export\ExportSql;
18
use PhpMyAdmin\SqlParser\Parser;
19
use PhpMyAdmin\SqlParser\Statements\AlterStatement;
20
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
21
use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
22
use PhpMyAdmin\SqlParser\Statements\DropStatement;
23
use PhpMyAdmin\SqlParser\Statements\InsertStatement;
24
use PhpMyAdmin\SqlParser\Statements\RenameStatement;
25
use PhpMyAdmin\SqlParser\Statements\TruncateStatement;
26
use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
27
use PhpMyAdmin\Util;
28
29
use function preg_quote;
30
use function preg_replace;
31
use function serialize;
32
use function sprintf;
33
use function str_ends_with;
34
use function str_starts_with;
35
use function trim;
36
37
/**
38
 * This class tracks changes on databases, tables and views.
39
 */
40
class Tracker
41
{
42
    private static bool $enabled = false;
43
44
    /**
45
     * Cache to avoid quering tracking status multiple times.
46
     *
47
     * @var mixed[]
48
     */
49
    protected static array $trackingCache = [];
50
51
    /**
52
     * Cache of checked databases.
53
     *
54
     * @var bool[]
55
     */
56
    private static array $trackedDatabaseCache = [];
57
58
    /**
59
     * Actually enables tracking. This needs to be done after all
60
     * underlaying code is initialized.
61
     */
62 12
    public static function enable(): void
63
    {
64 12
        self::$enabled = true;
65
    }
66
67 120
    public static function disable(): void
68
    {
69 120
        self::$enabled = false;
70
    }
71
72 12
    public static function isEnabled(): bool
73
    {
74 12
        return self::$enabled;
75
    }
76
77
    /**
78
     * Gets the on/off value of the Tracker module, starts initialization.
79
     */
80 8
    public static function isActive(): bool
81
    {
82 8
        if (! self::$enabled) {
83 4
            return false;
84
        }
85
86 8
        $relation = new Relation(DatabaseInterface::getInstance());
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Dbal\DatabaseInterface::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

86
        $relation = new Relation(/** @scrutinizer ignore-deprecated */ DatabaseInterface::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...
87 8
        $relationParameters = $relation->getRelationParameters();
88
89 8
        return $relationParameters->trackingFeature !== null;
90
    }
91
92
    /**
93
     * Gets the tracking status of a table, is it active or disabled ?
94
     *
95
     * @param string $dbName    name of database
96
     * @param string $tableName name of table
97
     */
98 4
    public static function isTracked(string $dbName, string $tableName): bool
99
    {
100 4
        if (! self::$enabled) {
101 4
            return false;
102
        }
103
104 4
        if (isset(self::$trackingCache[$dbName][$tableName])) {
105
            return self::$trackingCache[$dbName][$tableName];
106
        }
107
108 4
        $dbi = DatabaseInterface::getInstance();
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Dbal\DatabaseInterface::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

108
        $dbi = /** @scrutinizer ignore-deprecated */ DatabaseInterface::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...
109 4
        $relation = new Relation($dbi);
110 4
        $trackingFeature = $relation->getRelationParameters()->trackingFeature;
111 4
        if ($trackingFeature === null) {
112 4
            return false;
113
        }
114
115 4
        $sqlQuery = sprintf(
116 4
            'SELECT tracking_active FROM %s.%s WHERE db_name = %s AND table_name = %s'
117 4
                . ' ORDER BY version DESC LIMIT 1',
118 4
            Util::backquote($trackingFeature->database),
119 4
            Util::backquote($trackingFeature->tracking),
120 4
            $dbi->quoteString($dbName, ConnectionType::ControlUser),
121 4
            $dbi->quoteString($tableName, ConnectionType::ControlUser),
122 4
        );
123
124 4
        $result = $dbi->fetchValue($sqlQuery, 0, ConnectionType::ControlUser) == 1;
125
126 4
        self::$trackingCache[$dbName][$tableName] = $result;
127
128 4
        return $result;
129
    }
130
131
    /**
132
     * Returns the comment line for the log.
133
     *
134
     * @return string Comment, contains date and username
135
     */
136 12
    public static function getLogComment(): string
137
    {
138 12
        $date = Util::date('Y-m-d H:i:s');
139 12
        $user = preg_replace('/\s+/', ' ', Config::getInstance()->selectedServer['user']);
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

139
        $user = preg_replace('/\s+/', ' ', /** @scrutinizer ignore-deprecated */ Config::getInstance()->selectedServer['user']);

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...
140
141 12
        return '# log ' . $date . ' ' . $user . "\n";
142
    }
143
144
    /**
145
     * Creates tracking version of a table / view
146
     * (in other words: create a job to track future changes on the table).
147
     *
148
     * @param string $dbName      name of database
149
     * @param string $tableName   name of table
150
     * @param string $version     version
151
     * @param string $trackingSet set of tracking statements
152
     * @param bool   $isView      if table is a view
153
     */
154 4
    public static function createVersion(
155
        string $dbName,
156
        string $tableName,
157
        string $version,
158
        string $trackingSet = '',
159
        bool $isView = false,
160
    ): bool {
161 4
        $dbi = DatabaseInterface::getInstance();
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Dbal\DatabaseInterface::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

161
        $dbi = /** @scrutinizer ignore-deprecated */ DatabaseInterface::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...
162 4
        $relation = new Relation($dbi);
163
164 4
        $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

164
        $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...
165 4
        if ($trackingSet === '') {
166
            $trackingSet = $config->selectedServer['tracking_default_statements'];
167
        }
168
169 4
        $exportSqlPlugin = Plugins::getPlugin('export', 'sql');
170 4
        if (! $exportSqlPlugin instanceof ExportSql) {
171
            return false;
172
        }
173
174 4
        $exportSqlPlugin->useSqlBackquotes(true);
175
176 4
        $date = Util::date('Y-m-d H:i:s');
177
178
        // Get data definition snapshot of table
179
180 4
        $columns = [];
181 4
        foreach ($dbi->getColumns($dbName, $tableName) as $column) {
182 4
            $columns[] = [
183 4
                'Field' => $column->field,
184 4
                'Type' => $column->type,
185 4
                'Collation' => $column->collation,
186 4
                'Null' => $column->isNull ? 'YES' : 'NO',
187 4
                'Key' => $column->key,
188 4
                'Default' => $column->default,
189 4
                'Extra' => $column->extra,
190 4
                'Comment' => $column->comment,
191 4
            ];
192
        }
193
194 4
        $indexes = $dbi->getTableIndexes($dbName, $tableName);
195
196 4
        $snapshot = ['COLUMNS' => $columns, 'INDEXES' => $indexes];
197 4
        $snapshot = serialize($snapshot);
198
199
        // Get DROP TABLE / DROP VIEW and CREATE TABLE SQL statements
200 4
        $createSql = '';
201
202 4
        if ($config->selectedServer['tracking_add_drop_table'] == true && ! $isView) {
203
            $createSql .= self::getLogComment()
204
                . 'DROP TABLE IF EXISTS ' . Util::backquote($tableName) . ";\n";
205
        }
206
207 4
        if ($config->selectedServer['tracking_add_drop_view'] == true && $isView) {
208 4
            $createSql .= self::getLogComment()
209 4
                . 'DROP VIEW IF EXISTS ' . Util::backquote($tableName) . ";\n";
210
        }
211
212 4
        $createSql .= self::getLogComment() . $exportSqlPlugin->getTableDef($dbName, $tableName);
213
214
        // Save version
215 4
        $trackingFeature = $relation->getRelationParameters()->trackingFeature;
216 4
        if ($trackingFeature === null) {
217
            return false;
218
        }
219
220 4
        $sqlQuery = sprintf(
221 4
            '/*NOTRACK*/' . "\n" . 'INSERT INTO %s.%s (db_name, table_name, version,'
222 4
                . ' date_created, date_updated, schema_snapshot, schema_sql, data_sql, tracking)'
223 4
                . ' values (%s, %s, %s, %s, %s, %s, %s, %s, %s)',
224 4
            Util::backquote($trackingFeature->database),
225 4
            Util::backquote($trackingFeature->tracking),
226 4
            $dbi->quoteString($dbName, ConnectionType::ControlUser),
227 4
            $dbi->quoteString($tableName, ConnectionType::ControlUser),
228 4
            $dbi->quoteString($version, ConnectionType::ControlUser),
229 4
            $dbi->quoteString($date, ConnectionType::ControlUser),
230 4
            $dbi->quoteString($date, ConnectionType::ControlUser),
231 4
            $dbi->quoteString($snapshot, ConnectionType::ControlUser),
232 4
            $dbi->quoteString($createSql, ConnectionType::ControlUser),
233 4
            $dbi->quoteString("\n", ConnectionType::ControlUser),
234 4
            $dbi->quoteString($trackingSet, ConnectionType::ControlUser),
235 4
        );
236
237 4
        $dbi->queryAsControlUser($sqlQuery);
238
239
        // Deactivate previous version
240 4
        return self::deactivateTracking($dbName, $tableName, (string) ((int) $version - 1));
241
    }
242
243
    /**
244
     * Creates tracking version of a database
245
     * (in other words: create a job to track future changes on the database).
246
     *
247
     * @param string $dbName      name of database
248
     * @param string $version     version
249
     * @param string $query       query
250
     * @param string $trackingSet set of tracking statements
251
     */
252 4
    public static function createDatabaseVersion(
253
        string $dbName,
254
        string $version,
255
        string $query,
256
        string $trackingSet = 'CREATE DATABASE,ALTER DATABASE,DROP DATABASE',
257
    ): bool {
258 4
        $dbi = DatabaseInterface::getInstance();
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Dbal\DatabaseInterface::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

258
        $dbi = /** @scrutinizer ignore-deprecated */ DatabaseInterface::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...
259 4
        $relation = new Relation($dbi);
260
261 4
        $date = Util::date('Y-m-d H:i:s');
262
263 4
        $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

263
        $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...
264 4
        if ($trackingSet === '') {
265
            $trackingSet = $config->selectedServer['tracking_default_statements'];
266
        }
267
268 4
        $createSql = '';
269
270 4
        if ($config->selectedServer['tracking_add_drop_database'] == true) {
271
            $createSql .= self::getLogComment() . 'DROP DATABASE IF EXISTS ' . Util::backquote($dbName) . ";\n";
272
        }
273
274 4
        $createSql .= self::getLogComment() . $query;
275
276 4
        $trackingFeature = $relation->getRelationParameters()->trackingFeature;
277 4
        if ($trackingFeature === null) {
278
            return false;
279
        }
280
281
        // Save version
282 4
        $sqlQuery = sprintf(
283 4
            '/*NOTRACK*/' . "\n" . 'INSERT INTO %s.%s (db_name, table_name, version,'
284 4
                . ' date_created, date_updated, schema_snapshot, schema_sql, data_sql, tracking)'
285 4
                . ' values (%s, %s, %s, %s, %s, %s, %s, %s, %s)',
286 4
            Util::backquote($trackingFeature->database),
287 4
            Util::backquote($trackingFeature->tracking),
288 4
            $dbi->quoteString($dbName, ConnectionType::ControlUser),
289 4
            $dbi->quoteString('', ConnectionType::ControlUser),
290 4
            $dbi->quoteString($version, ConnectionType::ControlUser),
291 4
            $dbi->quoteString($date, ConnectionType::ControlUser),
292 4
            $dbi->quoteString($date, ConnectionType::ControlUser),
293 4
            $dbi->quoteString('', ConnectionType::ControlUser),
294 4
            $dbi->quoteString($createSql, ConnectionType::ControlUser),
295 4
            $dbi->quoteString("\n", ConnectionType::ControlUser),
296 4
            $dbi->quoteString($trackingSet, ConnectionType::ControlUser),
297 4
        );
298
299 4
        return (bool) $dbi->queryAsControlUser($sqlQuery);
300
    }
301
302
    /**
303
     * Changes tracking of a table.
304
     *
305
     * @param string $dbName    name of database
306
     * @param string $tableName name of table
307
     * @param string $version   version
308
     * @param int    $newState  the new state of tracking
309
     */
310 16
    private static function changeTracking(
311
        string $dbName,
312
        string $tableName,
313
        string $version,
314
        int $newState,
315
    ): bool {
316 16
        $dbi = DatabaseInterface::getInstance();
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Dbal\DatabaseInterface::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

316
        $dbi = /** @scrutinizer ignore-deprecated */ DatabaseInterface::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...
317 16
        $relation = new Relation($dbi);
318 16
        $trackingFeature = $relation->getRelationParameters()->trackingFeature;
319 16
        if ($trackingFeature === null) {
320
            return false;
321
        }
322
323 16
        unset(self::$trackedDatabaseCache[$dbName]); // Clear cache due to the change in tracking status
324
325 16
        $sqlQuery = sprintf(
326 16
            'UPDATE %s.%s SET `tracking_active` = %d'
327 16
                . ' WHERE `db_name` = %s AND `table_name` = %s AND `version` = %s',
328 16
            Util::backquote($trackingFeature->database),
329 16
            Util::backquote($trackingFeature->tracking),
330 16
            $newState,
331 16
            $dbi->quoteString($dbName, ConnectionType::ControlUser),
332 16
            $dbi->quoteString($tableName, ConnectionType::ControlUser),
333 16
            $dbi->quoteString($version, ConnectionType::ControlUser),
334 16
        );
335
336 16
        return (bool) $dbi->queryAsControlUser($sqlQuery);
337
    }
338
339
    /**
340
     * Activates tracking of a table.
341
     *
342
     * @param string $dbname    name of database
343
     * @param string $tablename name of table
344
     * @param string $version   version
345
     */
346 4
    public static function activateTracking(string $dbname, string $tablename, string $version): bool
347
    {
348 4
        return self::changeTracking($dbname, $tablename, $version, 1);
349
    }
350
351
    /**
352
     * Deactivates tracking of a table.
353
     *
354
     * @param string $dbname    name of database
355
     * @param string $tablename name of table
356
     * @param string $version   version
357
     */
358 8
    public static function deactivateTracking(string $dbname, string $tablename, string $version): bool
359
    {
360 8
        return self::changeTracking($dbname, $tablename, $version, 0);
361
    }
362
363
    /**
364
     * Gets the newest version of a tracking job
365
     * (in other words: gets the HEAD version).
366
     *
367
     * @param string      $dbname    name of database
368
     * @param string      $tablename name of table
369
     * @param string|null $statement tracked statement
370
     *
371
     * @return int (-1 if no version exists | >  0 if a version exists)
372
     */
373
    private static function getVersion(string $dbname, string $tablename, string|null $statement = null): int
374
    {
375
        $dbi = DatabaseInterface::getInstance();
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Dbal\DatabaseInterface::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

375
        $dbi = /** @scrutinizer ignore-deprecated */ DatabaseInterface::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...
376
        $relation = new Relation($dbi);
377
        $trackingFeature = $relation->getRelationParameters()->trackingFeature;
378
        if ($trackingFeature === null) {
379
            return -1;
380
        }
381
382
        $sqlQuery = sprintf(
383
            'SELECT MAX(version) FROM %s.%s WHERE `db_name` = %s AND `table_name` = %s',
384
            Util::backquote($trackingFeature->database),
385
            Util::backquote($trackingFeature->tracking),
386
            $dbi->quoteString($dbname, ConnectionType::ControlUser),
387
            $dbi->quoteString($tablename, ConnectionType::ControlUser),
388
        );
389
390
        if ($statement != '') {
391
            $sqlQuery .= " AND FIND_IN_SET('" . $statement . "',tracking) > 0";
392
        }
393
394
        $result = $dbi->tryQueryAsControlUser($sqlQuery);
395
396
        if ($result === false) {
397
            return -1;
398
        }
399
400
        $row = $result->fetchRow();
401
402
        return (int) ($row[0] ?? -1);
403
    }
404
405
    /**
406
     * Parses a query. Gets
407
     *  - statement identifier (UPDATE, ALTER TABLE, ...)
408
     *  - type of statement, is it part of DDL or DML ?
409
     *  - tablename
410
     *
411
     * @param string $query query
412
     *
413
     * @return mixed[] containing identifier, type and tablename.
414
     *
415
     * @todo: using PMA SQL Parser when possible
416
     * @todo: support multi-table/view drops
417
     */
418 84
    public static function parseQuery(string $query): array
419
    {
420
        // Usage of PMA_SQP does not work here
421
        //
422
        // require_once("libraries/sqlparser.lib.php");
423
        // $parsed_sql = PMA_SQP_parse($query);
424
        // $sql_info = PMA_SQP_analyze($parsed_sql);
425
426 84
        $parser = new Parser($query);
427
428 84
        $tokens = $parser->list->tokens;
429
430
        // Parse USE statement, need it for SQL dump imports
431 84
        if ($tokens[0]->value === 'USE') {
432
            Current::$database = $tokens[2]->value;
433
        }
434
435 84
        $result = [];
436
437 84
        if ($parser->statements !== []) {
438 84
            $statement = $parser->statements[0];
439 84
            $options = $statement->options?->options;
440
441
            // DDL statements
442 84
            $result['type'] = 'DDL';
443
444
            // Parse CREATE statement
445 84
            if ($statement instanceof CreateStatement) {
446 28
                if ($options === null || $options === [] || ! isset($options[6])) {
447
                    return $result;
448
                }
449
450 28
                if ($options[6] === 'VIEW' || $options[6] === 'TABLE') {
451 12
                    $result['identifier'] = 'CREATE ' . $options[6];
452 12
                    $result['tablename'] = $statement->name?->table;
453 16
                } elseif ($options[6] === 'DATABASE') {
454 4
                    $result['identifier'] = 'CREATE DATABASE';
455 4
                    $result['tablename'] = '';
456
457
                    // In case of CREATE DATABASE, database field of the CreateStatement is the name of the database
458 4
                    Current::$database = $statement->name?->database;
459
                } elseif (
460 12
                    $options[6] === 'INDEX'
461 8
                          || $options[6] === 'UNIQUE INDEX'
462 4
                          || $options[6] === 'FULLTEXT INDEX'
463 12
                          || $options[6] === 'SPATIAL INDEX'
464
                ) {
465 12
                    $result['identifier'] = 'CREATE INDEX';
466
467
                    // In case of CREATE INDEX, we have to get the table name from body of the statement
468 12
                    $result['tablename'] = $statement->body[3]->value === '.' ? $statement->body[4]->value
469
                                                                              : $statement->body[2]->value;
470
                }
471 56
            } elseif ($statement instanceof AlterStatement) { // Parse ALTER statement
472 12
                if ($options === null || $options === [] || ! isset($options[3])) {
473
                    return $result;
474
                }
475
476 12
                if ($options[3] === 'VIEW' || $options[3] === 'TABLE') {
477 8
                    $result['identifier'] = 'ALTER ' . $options[3];
478 8
                    $result['tablename'] = $statement->table->table;
479 4
                } elseif ($options[3] === 'DATABASE') {
480 4
                    $result['identifier'] = 'ALTER DATABASE';
481 4
                    $result['tablename'] = '';
482
483 4
                    Current::$database = $statement->table->table;
484
                }
485 44
            } elseif ($statement instanceof DropStatement) { // Parse DROP statement
486 24
                if ($options === null || $options === [] || ! isset($options[1])) {
487
                    return $result;
488
                }
489
490 24
                if ($options[1] === 'VIEW' || $options[1] === 'TABLE') {
491 16
                    $result['identifier'] = 'DROP ' . $options[1];
492 16
                    $result['tablename'] = $statement->fields[0]->table;
493 8
                } elseif ($options[1] === 'DATABASE') {
494 4
                    $result['identifier'] = 'DROP DATABASE';
495 4
                    $result['tablename'] = '';
496
497 4
                    Current::$database = $statement->fields[0]->table;
498 4
                } elseif ($options[1] === 'INDEX') {
499 4
                    $result['identifier'] = 'DROP INDEX';
500 4
                    $result['tablename'] = $statement->table->table;
501
                }
502 20
            } elseif ($statement instanceof RenameStatement) { // Parse RENAME statement
503 4
                $result['identifier'] = 'RENAME TABLE';
504 4
                $result['tablename'] = $statement->renames[0]->old->table;
505 4
                $result['tablename_after_rename'] = $statement->renames[0]->new->table;
506
            }
507
508 84
            if (isset($result['identifier'])) {
509 68
                return $result;
510
            }
511
512
            // DML statements
513 16
            $result['type'] = 'DML';
514
515
            // Parse UPDATE statement
516 16
            if ($statement instanceof UpdateStatement) {
517 4
                $result['identifier'] = 'UPDATE';
518 4
                $result['tablename'] = $statement->tables[0]->table;
519
            }
520
521
            // Parse INSERT INTO statement
522 16
            if ($statement instanceof InsertStatement) {
523 4
                $result['identifier'] = 'INSERT';
524 4
                $result['tablename'] = $statement->into->dest->table;
525
            }
526
527
            // Parse DELETE statement
528 16
            if ($statement instanceof DeleteStatement) {
529 4
                $result['identifier'] = 'DELETE';
530 4
                $result['tablename'] = $statement->from[0]->table;
531
            }
532
533
            // Parse TRUNCATE statement
534 16
            if ($statement instanceof TruncateStatement) {
535 4
                $result['identifier'] = 'TRUNCATE';
536 4
                $result['tablename'] = $statement->table->table;
537
            }
538
        }
539
540 16
        return $result;
541
    }
542
543
    /**
544
     * Analyzes a given SQL statement and saves tracking data.
545
     *
546
     * @param string $query a SQL query
547
     */
548 4
    public static function handleQuery(string $query): void
549
    {
550
        // If query is marked as untouchable, leave
551 4
        if (str_starts_with($query, '/*NOTRACK*/')) {
552
            return;
553
        }
554
555 4
        if (! str_ends_with($query, ';')) {
556 4
            $query .= ";\n";
557
        }
558
559
        // Get database name
560 4
        $dbname = trim(Current::$database, '`');
561
        // $dbname can be empty, for example when coming from Synchronize
562
        // and this is a query for the remote server
563 4
        if ($dbname === '') {
564 4
            return;
565
        }
566
567
        $dbi = DatabaseInterface::getInstance();
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Dbal\DatabaseInterface::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

567
        $dbi = /** @scrutinizer ignore-deprecated */ DatabaseInterface::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...
568
        $relation = new Relation($dbi);
569
        $trackingFeature = $relation->getRelationParameters()->trackingFeature;
570
        if ($trackingFeature === null) {
571
            return;
572
        }
573
574
        if (! self::isAnyTrackingInProgress($dbi, $trackingFeature, $dbname)) {
575
            return;
576
        }
577
578
        // Get some information about query
579
        $result = self::parseQuery($query);
580
581
        // If we found a valid statement
582
        if (! isset($result['identifier'])) {
583
            return;
584
        }
585
586
        // The table name was not found, see issue: #16837 as an example
587
        // Also checks if the value is not null
588
        if (! isset($result['tablename'])) {
589
            return;
590
        }
591
592
        $version = self::getVersion($dbname, $result['tablename'], $result['identifier']);
593
594
        // If version not exists and auto-creation is enabled
595
        if (Config::getInstance()->selectedServer['tracking_version_auto_create'] == true && $version == -1) {
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

595
        if (/** @scrutinizer ignore-deprecated */ Config::getInstance()->selectedServer['tracking_version_auto_create'] == true && $version == -1) {

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...
596
            // Create the version
597
598
            switch ($result['identifier']) {
599
                case 'CREATE TABLE':
600
                    self::createVersion($dbname, $result['tablename'], '1');
601
                    break;
602
                case 'CREATE VIEW':
603
                    self::createVersion($dbname, $result['tablename'], '1', '', true);
604
                    break;
605
                case 'CREATE DATABASE':
606
                    self::createDatabaseVersion($dbname, '1', $query);
607
                    break;
608
            }
609
        }
610
611
        // If version exists
612
        if ($version == -1) {
613
            return;
614
        }
615
616
        if (! self::isTracked($dbname, $result['tablename'])) {
617
            return;
618
        }
619
620
        $saveTo = match ($result['type']) {
621
            'DDL' => 'schema_sql',
622
            'DML' => 'data_sql',
623
            default => '',
624
        };
625
626
        $date = Util::date('Y-m-d H:i:s');
627
628
        // Cut off `dbname`. from query
629
        $query = preg_replace(
630
            '/`' . preg_quote($dbname, '/') . '`\s?\./',
631
            '',
632
            $query,
633
        );
634
635
        // Add log information
636
        $query = self::getLogComment() . $query;
637
638
        $relation = new Relation($dbi);
639
        $trackingFeature = $relation->getRelationParameters()->trackingFeature;
640
        if ($trackingFeature === null) {
641
            return;
642
        }
643
644
        // Mark it as untouchable
645
        $sqlQuery = sprintf(
646
            '/*NOTRACK*/' . "\n" . 'UPDATE %s.%s SET %s = CONCAT(%s, %s), `date_updated` = %s',
647
            Util::backquote($trackingFeature->database),
648
            Util::backquote($trackingFeature->tracking),
649
            Util::backquote($saveTo),
650
            Util::backquote($saveTo),
651
            $dbi->quoteString("\n" . $query, ConnectionType::ControlUser),
652
            $dbi->quoteString($date, ConnectionType::ControlUser),
653
        );
654
655
        // If table was renamed we have to change
656
        // the tablename attribute in pma_tracking too
657
        if ($result['identifier'] === 'RENAME TABLE') {
658
            $sqlQuery .= ', `table_name` = '
659
                . $dbi->quoteString($result['tablename_after_rename'], ConnectionType::ControlUser)
660
                . ' ';
661
        }
662
663
        // Save the tracking information only for
664
        //     1. the database
665
        //     2. the table / view
666
        //     3. the statements
667
        // we want to track
668
        $sqlQuery .= sprintf(
669
            " WHERE FIND_IN_SET('" . $result['identifier'] . "',tracking) > 0" .
670
            ' AND `db_name` = %s ' .
671
            ' AND `table_name` = %s ' .
672
            ' AND `version` = %s ',
673
            $dbi->quoteString($dbname, ConnectionType::ControlUser),
674
            $dbi->quoteString($result['tablename'], ConnectionType::ControlUser),
675
            $dbi->quoteString((string) $version, ConnectionType::ControlUser),
676
        );
677
678
        $dbi->queryAsControlUser($sqlQuery);
679
    }
680
681
    private static function isAnyTrackingInProgress(
682
        DatabaseInterface $dbi,
683
        TrackingFeature $trackingFeature,
684
        string $dbname,
685
    ): bool {
686
        if (isset(self::$trackedDatabaseCache[$dbname])) {
687
            return self::$trackedDatabaseCache[$dbname];
688
        }
689
690
        $sqlQuery = sprintf(
691
            '/*NOTRACK*/ SELECT 1 FROM %s.%s WHERE tracking_active = 1 AND db_name = %s LIMIT 1',
692
            Util::backquote($trackingFeature->database),
693
            Util::backquote($trackingFeature->tracking),
694
            $dbi->quoteString($dbname, ConnectionType::ControlUser),
695
        );
696
697
        $isTracked = $dbi->queryAsControlUser($sqlQuery)->fetchValue() !== false;
698
        self::$trackedDatabaseCache[$dbname] = $isTracked;
699
700
        return $isTracked;
701
    }
702
}
703