Passed
Push — master ( 754437...3b285f )
by Maurício
10:47
created

Tracker::handleQuery()   C

Complexity

Conditions 15
Paths 77

Size

Total Lines 116
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 140.2416

Importance

Changes 0
Metric Value
cc 15
eloc 65
nc 77
nop 1
dl 0
loc 116
ccs 11
cts 62
cp 0.1774
crap 140.2416
rs 5.9166
c 0
b 0
f 0

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
 * Tracking changes on databases, tables and views
4
 */
5
6
declare(strict_types=1);
7
8
namespace PhpMyAdmin;
9
10
use PhpMyAdmin\Plugins\Export\ExportSql;
11
use PhpMyAdmin\SqlParser\Parser;
12
use PhpMyAdmin\SqlParser\Statements\AlterStatement;
13
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
14
use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
15
use PhpMyAdmin\SqlParser\Statements\DropStatement;
16
use PhpMyAdmin\SqlParser\Statements\InsertStatement;
17
use PhpMyAdmin\SqlParser\Statements\RenameStatement;
18
use PhpMyAdmin\SqlParser\Statements\TruncateStatement;
19
use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
20
use function array_values;
21
use function count;
22
use function explode;
23
use function is_array;
24
use function mb_strpos;
25
use function mb_strstr;
26
use function mb_substr;
27
use function preg_quote;
28
use function preg_replace;
29
use function rtrim;
30
use function serialize;
31
use function str_replace;
32
use function strtotime;
33
use function substr;
34
use function trim;
35
36
/**
37
 * This class tracks changes on databases, tables and views.
38
 *
39
 * @todo use stristr instead of strstr
40
 */
41
class Tracker
42
{
43
    /**
44
     * Whether tracking is ready.
45
     *
46
     * @var bool
47
     */
48
    protected static $enabled = false;
49
50
    /**
51
     * Cache to avoid quering tracking status multiple times.
52
     *
53
     * @var array
54
     */
55
    protected static $_tracking_cache = [];
56
57
    /**
58
     * Actually enables tracking. This needs to be done after all
59
     * underlaying code is initialized.
60
     *
61
     * @return void
62
     *
63
     * @static
64
     */
65 12
    public static function enable()
66
    {
67 12
        self::$enabled = true;
68 12
    }
69
70
    /**
71
     * Gets the on/off value of the Tracker module, starts initialization.
72
     *
73
     * @return bool (true=on|false=off)
74
     *
75
     * @static
76
     */
77 732
    public static function isActive()
78
    {
79 732
        if (! self::$enabled) {
80 541
            return false;
81
        }
82
        /* We need to avoid attempt to track any queries
83
         * from Relation::getRelationsParam
84
         */
85 197
        self::$enabled = false;
86 197
        $relation = new Relation($GLOBALS['dbi']);
87 197
        $cfgRelation = $relation->getRelationsParam();
88
        /* Restore original state */
89 197
        self::$enabled = true;
90 197
        if (! $cfgRelation['trackingwork']) {
91 192
            return false;
92
        }
93
94 9
        $pma_table = self::getTrackingTable();
95
96 9
        return $pma_table !== null;
97
    }
98
99
    /**
100
     * Parses the name of a table from a SQL statement substring.
101
     *
102
     * @param string $string part of SQL statement
103
     *
104
     * @return string the name of table
105
     *
106
     * @static
107
     */
108 12
    protected static function getTableName($string)
109
    {
110 12
        if (mb_strstr($string, '.')) {
111 8
            $temp = explode('.', $string);
112 8
            $tablename = $temp[1];
113
        } else {
114 4
            $tablename = $string;
115
        }
116
117 12
        $str = explode("\n", $tablename);
118 12
        $tablename = $str[0];
119
120 12
        $tablename = str_replace([';', '`'], '', $tablename);
121 12
        $tablename = trim($tablename);
122
123 12
        return $tablename;
124
    }
125
126
    /**
127
     * Gets the tracking status of a table, is it active or deactive ?
128
     *
129
     * @param string $dbname    name of database
130
     * @param string $tablename name of table
131
     *
132
     * @return bool true or false
133
     *
134
     * @static
135
     */
136 8
    public static function isTracked($dbname, $tablename)
137
    {
138 8
        if (! self::$enabled) {
139 4
            return false;
140
        }
141
142 8
        if (isset(self::$_tracking_cache[$dbname][$tablename])) {
143
            return self::$_tracking_cache[$dbname][$tablename];
144
        }
145
        /* We need to avoid attempt to track any queries
146
         * from Relation::getRelationsParam
147
         */
148 8
        self::$enabled = false;
149 8
        $relation = new Relation($GLOBALS['dbi']);
150 8
        $cfgRelation = $relation->getRelationsParam();
151
        /* Restore original state */
152 8
        self::$enabled = true;
153 8
        if (! $cfgRelation['trackingwork']) {
154 4
            return false;
155
        }
156
157 8
        $sql_query = ' SELECT tracking_active FROM ' . self::getTrackingTable() .
158 8
        " WHERE db_name = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
159 8
        " AND table_name = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " .
160 8
        ' ORDER BY version DESC LIMIT 1';
161
162 8
        $result = $GLOBALS['dbi']->fetchValue($sql_query, 0, 0, DatabaseInterface::CONNECT_CONTROL) == 1;
163
164 8
        self::$_tracking_cache[$dbname][$tablename] = $result;
165
166 8
        return $result;
167
    }
168
169
    /**
170
     * Returns the comment line for the log.
171
     *
172
     * @return string Comment, contains date and username
173
     */
174 12
    public static function getLogComment()
175
    {
176 12
        $date = Util::date('Y-m-d H:i:s');
177 12
        $user = preg_replace('/\s+/', ' ', $GLOBALS['cfg']['Server']['user']);
178
179 12
        return '# log ' . $date . ' ' . $user . "\n";
180
    }
181
182
    /**
183
     * Creates tracking version of a table / view
184
     * (in other words: create a job to track future changes on the table).
185
     *
186
     * @param string $dbname       name of database
187
     * @param string $tablename    name of table
188
     * @param string $version      version
189
     * @param string $tracking_set set of tracking statements
190
     * @param bool   $is_view      if table is a view
191
     *
192
     * @return int result of version insertion
193
     *
194
     * @static
195
     */
196 4
    public static function createVersion(
197
        $dbname,
198
        $tablename,
199
        $version,
200
        $tracking_set = '',
201
        bool $is_view = false
202
    ) {
203 4
        global $sql_backquotes, $export_type;
204
205 4
        $relation = new Relation($GLOBALS['dbi']);
206
207 4
        if ($tracking_set == '') {
208
            $tracking_set
209
                = $GLOBALS['cfg']['Server']['tracking_default_statements'];
210
        }
211
212
        /**
213
         * get Export SQL instance
214
         *
215
         * @var ExportSql $export_sql_plugin
216
         */
217 4
        $export_sql_plugin = Plugins::getPlugin(
218 4
            'export',
219 4
            'sql',
220 4
            'libraries/classes/Plugins/Export/',
221
            [
222 4
                'export_type' => $export_type,
223
                'single_table' => false,
224
            ]
225
        );
226
227 4
        $sql_backquotes = true;
228
229 4
        $date = Util::date('Y-m-d H:i:s');
230
231
        // Get data definition snapshot of table
232
233 4
        $columns = $GLOBALS['dbi']->getColumns($dbname, $tablename, null, true);
234
        // int indices to reduce size
235 4
        $columns = array_values($columns);
236
        // remove Privileges to reduce size
237 4
        for ($i = 0, $nb = count($columns); $i < $nb; $i++) {
238 4
            unset($columns[$i]['Privileges']);
239
        }
240
241 4
        $indexes = $GLOBALS['dbi']->getTableIndexes($dbname, $tablename);
242
243
        $snapshot = [
244 4
            'COLUMNS' => $columns,
245 4
            'INDEXES' => $indexes,
246
        ];
247 4
        $snapshot = serialize($snapshot);
248
249
        // Get DROP TABLE / DROP VIEW and CREATE TABLE SQL statements
250 4
        $sql_backquotes = true;
251
252 4
        $create_sql  = '';
253
254 4
        if ($GLOBALS['cfg']['Server']['tracking_add_drop_table'] == true
255 4
            && $is_view === false
256
        ) {
257
            $create_sql .= self::getLogComment()
258
                . 'DROP TABLE IF EXISTS ' . Util::backquote($tablename) . ";\n";
259
        }
260
261 4
        if ($GLOBALS['cfg']['Server']['tracking_add_drop_view'] == true
262 4
            && $is_view === true
263
        ) {
264 4
            $create_sql .= self::getLogComment()
265 4
                . 'DROP VIEW IF EXISTS ' . Util::backquote($tablename) . ";\n";
266
        }
267
268 4
        $create_sql .= self::getLogComment() .
269 4
            $export_sql_plugin->getTableDef($dbname, $tablename, "\n", '');
270
271
        // Save version
272
273
        $sql_query = "/*NOTRACK*/\n" .
274 4
        'INSERT INTO ' . self::getTrackingTable() . ' (' .
275 4
        'db_name, ' .
276 4
        'table_name, ' .
277 4
        'version, ' .
278 4
        'date_created, ' .
279 4
        'date_updated, ' .
280 4
        'schema_snapshot, ' .
281 4
        'schema_sql, ' .
282 4
        'data_sql, ' .
283 4
        'tracking ' .
284 4
        ') ' .
285 4
        "values (
286 4
        '" . $GLOBALS['dbi']->escapeString($dbname) . "',
287 4
        '" . $GLOBALS['dbi']->escapeString($tablename) . "',
288 4
        '" . $GLOBALS['dbi']->escapeString($version) . "',
289 4
        '" . $GLOBALS['dbi']->escapeString($date) . "',
290 4
        '" . $GLOBALS['dbi']->escapeString($date) . "',
291 4
        '" . $GLOBALS['dbi']->escapeString($snapshot) . "',
292 4
        '" . $GLOBALS['dbi']->escapeString($create_sql) . "',
293 4
        '" . $GLOBALS['dbi']->escapeString("\n") . "',
294 4
        '" . $GLOBALS['dbi']->escapeString($tracking_set)
295 4
        . "' )";
296
297 4
        $result = $relation->queryAsControlUser($sql_query);
298
299 4
        if ($result) {
300
            // Deactivate previous version
301 4
            self::deactivateTracking($dbname, $tablename, (int) $version - 1);
302
        }
303
304 4
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type boolean|resource which is incompatible with the documented return type integer.
Loading history...
305
    }
306
307
    /**
308
     * Removes all tracking data for a table or a version of a table
309
     *
310
     * @param string $dbname    name of database
311
     * @param string $tablename name of table
312
     * @param string $version   version
313
     *
314
     * @return int result of version insertion
315
     *
316
     * @static
317
     */
318 4
    public static function deleteTracking($dbname, $tablename, $version = '')
319
    {
320 4
        $relation = new Relation($GLOBALS['dbi']);
321
322
        $sql_query = "/*NOTRACK*/\n"
323 4
            . 'DELETE FROM ' . self::getTrackingTable()
324 4
            . " WHERE `db_name` = '"
325 4
            . $GLOBALS['dbi']->escapeString($dbname) . "'"
326 4
            . " AND `table_name` = '"
327 4
            . $GLOBALS['dbi']->escapeString($tablename) . "'";
328 4
        if ($version) {
329
            $sql_query .= " AND `version` = '"
330
                . $GLOBALS['dbi']->escapeString($version) . "'";
331
        }
332
333 4
        return $relation->queryAsControlUser($sql_query);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $relation->queryAsControlUser($sql_query) returns the type boolean|resource which is incompatible with the documented return type integer.
Loading history...
334
    }
335
336
    /**
337
     * Creates tracking version of a database
338
     * (in other words: create a job to track future changes on the database).
339
     *
340
     * @param string $dbname       name of database
341
     * @param string $version      version
342
     * @param string $query        query
343
     * @param string $tracking_set set of tracking statements
344
     *
345
     * @return int result of version insertion
346
     *
347
     * @static
348
     */
349 4
    public static function createDatabaseVersion(
350
        $dbname,
351
        $version,
352
        $query,
353
        $tracking_set = 'CREATE DATABASE,ALTER DATABASE,DROP DATABASE'
354
    ) {
355 4
        $relation = new Relation($GLOBALS['dbi']);
356
357 4
        $date = Util::date('Y-m-d H:i:s');
358
359 4
        if ($tracking_set == '') {
360
            $tracking_set
361
                = $GLOBALS['cfg']['Server']['tracking_default_statements'];
362
        }
363
364 4
        $create_sql  = '';
365
366 4
        if ($GLOBALS['cfg']['Server']['tracking_add_drop_database'] == true) {
367
            $create_sql .= self::getLogComment()
368
                . 'DROP DATABASE IF EXISTS ' . Util::backquote($dbname) . ";\n";
369
        }
370
371 4
        $create_sql .= self::getLogComment() . $query;
372
373
        // Save version
374
        $sql_query = "/*NOTRACK*/\n" .
375 4
        'INSERT INTO ' . self::getTrackingTable() . ' (' .
376 4
        'db_name, ' .
377 4
        'table_name, ' .
378 4
        'version, ' .
379 4
        'date_created, ' .
380 4
        'date_updated, ' .
381 4
        'schema_snapshot, ' .
382 4
        'schema_sql, ' .
383 4
        'data_sql, ' .
384 4
        'tracking ' .
385 4
        ') ' .
386 4
        "values (
387 4
        '" . $GLOBALS['dbi']->escapeString($dbname) . "',
388 4
        '" . $GLOBALS['dbi']->escapeString('') . "',
389 4
        '" . $GLOBALS['dbi']->escapeString($version) . "',
390 4
        '" . $GLOBALS['dbi']->escapeString($date) . "',
391 4
        '" . $GLOBALS['dbi']->escapeString($date) . "',
392 4
        '" . $GLOBALS['dbi']->escapeString('') . "',
393 4
        '" . $GLOBALS['dbi']->escapeString($create_sql) . "',
394 4
        '" . $GLOBALS['dbi']->escapeString("\n") . "',
395 4
        '" . $GLOBALS['dbi']->escapeString($tracking_set)
396 4
        . "' )";
397
398 4
        return $relation->queryAsControlUser($sql_query);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $relation->queryAsControlUser($sql_query) returns the type boolean|resource which is incompatible with the documented return type integer.
Loading history...
399
    }
400
401
    /**
402
     * Changes tracking of a table.
403
     *
404
     * @param string $dbname    name of database
405
     * @param string $tablename name of table
406
     * @param string $version   version
407
     * @param int    $new_state the new state of tracking
408
     *
409
     * @return int result of SQL query
410
     *
411
     * @static
412
     */
413 16
    private static function changeTracking(
414
        $dbname,
415
        $tablename,
416
        $version,
417
        $new_state
418
    ) {
419 16
        $relation = new Relation($GLOBALS['dbi']);
420
421 16
        $sql_query = ' UPDATE ' . self::getTrackingTable() .
422 16
        " SET `tracking_active` = '" . $new_state . "' " .
423 16
        " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
424 16
        " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " .
425 16
        " AND `version` = '" . $GLOBALS['dbi']->escapeString((string) $version) . "' ";
426
427 16
        return $relation->queryAsControlUser($sql_query);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $relation->queryAsControlUser($sql_query) returns the type boolean|resource which is incompatible with the documented return type integer.
Loading history...
428
    }
429
430
    /**
431
     * Changes tracking data of a table.
432
     *
433
     * @param string       $dbname    name of database
434
     * @param string       $tablename name of table
435
     * @param string       $version   version
436
     * @param string       $type      type of data(DDL || DML)
437
     * @param string|array $new_data  the new tracking data
438
     *
439
     * @return bool result of change
440
     *
441
     * @static
442
     */
443 4
    public static function changeTrackingData(
444
        $dbname,
445
        $tablename,
446
        $version,
447
        $type,
448
        $new_data
449
    ) {
450 4
        $relation = new Relation($GLOBALS['dbi']);
451
452 4
        if ($type === 'DDL') {
453 4
            $save_to = 'schema_sql';
454 4
        } elseif ($type === 'DML') {
455 4
            $save_to = 'data_sql';
456
        } else {
457 4
            return false;
458
        }
459 4
        $date  = Util::date('Y-m-d H:i:s');
460
461 4
        $new_data_processed = '';
462 4
        if (is_array($new_data)) {
463 4
            foreach ($new_data as $data) {
464 4
                $new_data_processed .= '# log ' . $date . ' ' . $data['username']
465 4
                    . $GLOBALS['dbi']->escapeString($data['statement']) . "\n";
466
            }
467
        } else {
468 4
            $new_data_processed = $new_data;
469
        }
470
471 4
        $sql_query = ' UPDATE ' . self::getTrackingTable() .
472 4
        ' SET `' . $save_to . "` = '" . $new_data_processed . "' " .
473 4
        " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
474 4
        " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " .
475 4
        " AND `version` = '" . $GLOBALS['dbi']->escapeString($version) . "' ";
476
477 4
        $result = $relation->queryAsControlUser($sql_query);
478
479 4
        return (bool) $result;
480
    }
481
482
    /**
483
     * Activates tracking of a table.
484
     *
485
     * @param string $dbname    name of database
486
     * @param string $tablename name of table
487
     * @param string $version   version
488
     *
489
     * @return int result of SQL query
490
     *
491
     * @static
492
     */
493 4
    public static function activateTracking($dbname, $tablename, $version)
494
    {
495 4
        return self::changeTracking($dbname, $tablename, $version, 1);
496
    }
497
498
    /**
499
     * Deactivates tracking of a table.
500
     *
501
     * @param string $dbname    name of database
502
     * @param string $tablename name of table
503
     * @param string $version   version
504
     *
505
     * @return int result of SQL query
506
     *
507
     * @static
508
     */
509 8
    public static function deactivateTracking($dbname, $tablename, $version)
510
    {
511 8
        return self::changeTracking($dbname, $tablename, $version, 0);
512
    }
513
514
    /**
515
     * Gets the newest version of a tracking job
516
     * (in other words: gets the HEAD version).
517
     *
518
     * @param string $dbname    name of database
519
     * @param string $tablename name of table
520
     * @param string $statement tracked statement
521
     *
522
     * @return int (-1 if no version exists | >  0 if a version exists)
523
     *
524
     * @static
525
     */
526 4
    public static function getVersion($dbname, $tablename, $statement = null)
527
    {
528 4
        $relation = new Relation($GLOBALS['dbi']);
529
530 4
        $sql_query = ' SELECT MAX(version) FROM ' . self::getTrackingTable() .
531 4
        " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
532 4
        " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' ";
533
534 4
        if ($statement != '') {
535
            $sql_query .= " AND FIND_IN_SET('"
536
                . $statement . "',tracking) > 0";
537
        }
538 4
        $row = $GLOBALS['dbi']->fetchArray($relation->queryAsControlUser($sql_query));
539
540 4
        return $row[0] ?? -1;
541
    }
542
543
    /**
544
     * Gets the record of a tracking job.
545
     *
546
     * @param string $dbname    name of database
547
     * @param string $tablename name of table
548
     * @param string $version   version number
549
     *
550
     * @return mixed record DDM log, DDL log, structure snapshot, tracked
551
     *         statements.
552
     *
553
     * @static
554
     */
555 8
    public static function getTrackedData($dbname, $tablename, $version)
556
    {
557 8
        $relation = new Relation($GLOBALS['dbi']);
558
559 8
        $sql_query = ' SELECT * FROM ' . self::getTrackingTable() .
560 8
            " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' ";
561 8
        if (! empty($tablename)) {
562
            $sql_query .= " AND `table_name` = '"
563 8
                . $GLOBALS['dbi']->escapeString($tablename) . "' ";
564
        }
565 8
        $sql_query .= " AND `version` = '" . $GLOBALS['dbi']->escapeString($version)
566 8
            . "' ORDER BY `version` DESC LIMIT 1";
567
568 8
        $mixed = $GLOBALS['dbi']->fetchAssoc($relation->queryAsControlUser($sql_query));
569
570
        // PHP 7.4 fix for accessing array offset on null
571 8
        if (! is_array($mixed)) {
572
            $mixed = [
573
                'schema_sql' => null,
574
                'data_sql' => null,
575
                'tracking' => null,
576
                'schema_snapshot' => null,
577
            ];
578
        }
579
580
        // Parse log
581 8
        $log_schema_entries = explode('# log ', (string) $mixed['schema_sql']);
582 8
        $log_data_entries   = explode('# log ', (string) $mixed['data_sql']);
583
584 8
        $ddl_date_from = $date = Util::date('Y-m-d H:i:s');
585
586 8
        $ddlog = [];
587 8
        $first_iteration = true;
588
589
        // Iterate tracked data definition statements
590
        // For each log entry we want to get date, username and statement
591 8
        foreach ($log_schema_entries as $log_entry) {
592 8
            if (trim($log_entry) == '') {
593 8
                continue;
594
            }
595
596 8
            $date      = mb_substr($log_entry, 0, 19);
597 8
            $username  = mb_substr(
598 8
                $log_entry,
599 8
                20,
600 8
                mb_strpos($log_entry, "\n") - 20
601
            );
602 8
            if ($first_iteration) {
603 8
                $ddl_date_from = $date;
604 8
                $first_iteration = false;
605
            }
606 8
            $statement = rtrim(mb_strstr($log_entry, "\n"));
607
608 8
            $ddlog[] = [
609 8
                'date' => $date,
610 8
                'username' => $username,
611 8
                'statement' => $statement,
612
            ];
613
        }
614
615 8
        $date_from = $ddl_date_from;
616 8
        $ddl_date_to = $date;
617
618 8
        $dml_date_from = $date_from;
619
620 8
        $dmlog = [];
621 8
        $first_iteration = true;
622
623
        // Iterate tracked data manipulation statements
624
        // For each log entry we want to get date, username and statement
625 8
        foreach ($log_data_entries as $log_entry) {
626 8
            if (trim($log_entry) == '') {
627 8
                continue;
628
            }
629
630 4
            $date      = mb_substr($log_entry, 0, 19);
631 4
            $username  = mb_substr(
632 4
                $log_entry,
633 4
                20,
634 4
                mb_strpos($log_entry, "\n") - 20
635
            );
636 4
            if ($first_iteration) {
637 4
                $dml_date_from = $date;
638 4
                $first_iteration = false;
639
            }
640 4
            $statement = rtrim(mb_strstr($log_entry, "\n"));
641
642 4
            $dmlog[] = [
643 4
                'date' => $date,
644 4
                'username' => $username,
645 4
                'statement' => $statement,
646
            ];
647
        }
648
649 8
        $dml_date_to = $date;
650
651
        // Define begin and end of date range for both logs
652 8
        $data = [];
653 8
        if (strtotime($ddl_date_from) <= strtotime($dml_date_from)) {
654 8
            $data['date_from'] = $ddl_date_from;
655
        } else {
656
            $data['date_from'] = $dml_date_from;
657
        }
658 8
        if (strtotime($ddl_date_to) >= strtotime($dml_date_to)) {
659 4
            $data['date_to'] = $ddl_date_to;
660
        } else {
661 4
            $data['date_to'] = $dml_date_to;
662
        }
663 8
        $data['ddlog']           = $ddlog;
664 8
        $data['dmlog']           = $dmlog;
665 8
        $data['tracking']        = $mixed['tracking'];
666 8
        $data['schema_snapshot'] = $mixed['schema_snapshot'];
667
668 8
        return $data;
669
    }
670
671
    /**
672
     * Parses a query. Gets
673
     *  - statement identifier (UPDATE, ALTER TABLE, ...)
674
     *  - type of statement, is it part of DDL or DML ?
675
     *  - tablename
676
     *
677
     * @param string $query query
678
     *
679
     * @return array containing identifier, type and tablename.
680
     *
681
     * @static
682
     * @todo: using PMA SQL Parser when possible
683
     * @todo: support multi-table/view drops
684
     */
685 85
    public static function parseQuery($query): array
686
    {
687
        // Usage of PMA_SQP does not work here
688
        //
689
        // require_once("libraries/sqlparser.lib.php");
690
        // $parsed_sql = PMA_SQP_parse($query);
691
        // $sql_info = PMA_SQP_analyze($parsed_sql);
692
693 85
        $parser = new Parser($query);
694
695 85
        $tokens = $parser->list->tokens;
696
697
        // Parse USE statement, need it for SQL dump imports
698 85
        if ($tokens[0]->value === 'USE') {
699
            $GLOBALS['db'] = $tokens[2]->value;
700
        }
701
702 85
        $result = [];
703
704 85
        if (! empty($parser->statements)) {
705 85
            $statement = $parser->statements[0];
706 85
            $options   = isset($statement->options) ? $statement->options->options : null;
707
708
            /*
709
             * DDL statements
710
             */
711 85
            $result['type'] = 'DDL';
712
713
            // Parse CREATE statement
714 85
            if ($statement instanceof CreateStatement) {
715 24
                if (empty($options) || ! isset($options[6])) {
716
                    return $result;
717
                }
718
719 24
                if ($options[6] === 'VIEW' || $options[6] === 'TABLE') {
720 8
                    $result['identifier'] = 'CREATE ' . $options[6];
721 8
                    $result['tablename']  = $statement->name->table;
722 16
                } elseif ($options[6] === 'DATABASE') {
723 4
                    $result['identifier'] = 'CREATE DATABASE';
724 4
                    $result['tablename']  = '';
725
726
                    // In case of CREATE DATABASE, database field of the CreateStatement is the name of the database
727 4
                    $GLOBALS['db']        = $statement->name->database;
728 12
                } elseif ($options[6] === 'INDEX'
729 8
                          || $options[6] === 'UNIQUE INDEX'
730 4
                          || $options[6] === 'FULLTEXT INDEX'
731 12
                          || $options[6] === 'SPATIAL INDEX'
732
                ) {
733 12
                    $result['identifier'] = 'CREATE INDEX';
734
735
                    // In case of CREATE INDEX, we have to get the table name from body of the statement
736 12
                    $result['tablename']  = $statement->body[3]->value === '.' ? $statement->body[4]->value
737 24
                                                                              : $statement->body[2]->value;
738
                }
739 61
            } elseif ($statement instanceof AlterStatement) { // Parse ALTER statement
740 12
                if (empty($options) || ! isset($options[3])) {
741
                    return $result;
742
                }
743
744 12
                if ($options[3] === 'VIEW' || $options[3] === 'TABLE') {
745 8
                    $result['identifier']   = 'ALTER ' . $options[3];
746 8
                    $result['tablename']    = $statement->table->table;
747 4
                } elseif ($options[3] === 'DATABASE') {
748 4
                    $result['identifier']   = 'ALTER DATABASE';
749 4
                    $result['tablename']    = '';
750
751 12
                    $GLOBALS['db']          = $statement->table->table;
752
                }
753 49
            } elseif ($statement instanceof DropStatement) { // Parse DROP statement
754 24
                if (empty($options) || ! isset($options[1])) {
755
                    return $result;
756
                }
757
758 24
                if ($options[1] === 'VIEW' || $options[1] === 'TABLE') {
759 16
                    $result['identifier'] = 'DROP ' . $options[1];
760 16
                    $result['tablename']  = $statement->fields[0]->table;
761 8
                } elseif ($options[1] === 'DATABASE') {
762 4
                    $result['identifier'] = 'DROP DATABASE';
763 4
                    $result['tablename']  = '';
764
765 4
                    $GLOBALS['db']        = $statement->fields[0]->table;
766 4
                } elseif ($options[1] === 'INDEX') {
767 4
                    $result['identifier']   = 'DROP INDEX';
768 24
                    $result['tablename']    = $statement->table->table;
769
                }
770 25
            } elseif ($statement instanceof RenameStatement) { // Parse RENAME statement
771 4
                $result['identifier']               = 'RENAME TABLE';
772 4
                $result['tablename']                = $statement->renames[0]->old->table;
773 4
                $result['tablename_after_rename']   = $statement->renames[0]->new->table;
774
            }
775
776 85
            if (isset($result['identifier'])) {
777 64
                return $result;
778
            }
779
780
            /*
781
             * DML statements
782
             */
783 21
            $result['type'] = 'DML';
784
785
            // Parse UPDATE statement
786 21
            if ($statement instanceof UpdateStatement) {
787 4
                $result['identifier']   = 'UPDATE';
788 4
                $result['tablename']    = $statement->tables[0]->table;
789
            }
790
791
            // Parse INSERT INTO statement
792 21
            if ($statement instanceof InsertStatement) {
793 4
                $result['identifier']   = 'INSERT';
794 4
                $result['tablename']    = $statement->into->dest->table;
795
            }
796
797
            // Parse DELETE statement
798 21
            if ($statement instanceof DeleteStatement) {
799 4
                $result['identifier']   = 'DELETE';
800 4
                $result['tablename']    = $statement->from[0]->table;
801
            }
802
803
            // Parse TRUNCATE statement
804 21
            if ($statement instanceof TruncateStatement) {
805 4
                $result['identifier']   = 'TRUNCATE';
806 4
                $result['tablename']    = $statement->table->table;
807
            }
808
        }
809
810 21
        return $result;
811
    }
812
813
    /**
814
     * Analyzes a given SQL statement and saves tracking data.
815
     *
816
     * @param string $query a SQL query
817
     *
818
     * @return void
819
     *
820
     * @static
821
     */
822 5
    public static function handleQuery($query)
823
    {
824 5
        $relation = new Relation($GLOBALS['dbi']);
825
826
        // If query is marked as untouchable, leave
827 5
        if (mb_strstr($query, '/*NOTRACK*/')) {
828
            return;
829
        }
830
831 5
        if (! (substr($query, -1) === ';')) {
832 4
            $query .= ";\n";
833
        }
834
        // Get some information about query
835 5
        $result = self::parseQuery($query);
836
837
        // Get database name
838 5
        $dbname = trim($GLOBALS['db'] ?? '', '`');
839
        // $dbname can be empty, for example when coming from Synchronize
840
        // and this is a query for the remote server
841 5
        if (empty($dbname)) {
842 4
            return;
843
        }
844
845
        // If we found a valid statement
846 1
        if (! isset($result['identifier'])) {
847 1
            return;
848
        }
849
850
        if (! self::isTracked($dbname, $result['tablename'])) {
851
            return;
852
        }
853
854
        $version = self::getVersion(
855
            $dbname,
856
            $result['tablename'],
857
            $result['identifier']
858
        );
859
860
        // If version not exists and auto-creation is enabled
861
        if ($GLOBALS['cfg']['Server']['tracking_version_auto_create'] == true
862
            && $version == -1
863
        ) {
864
            // Create the version
865
866
            switch ($result['identifier']) {
867
                case 'CREATE TABLE':
868
                    self::createVersion($dbname, $result['tablename'], '1');
869
                    break;
870
                case 'CREATE VIEW':
871
                    self::createVersion(
872
                        $dbname,
873
                        $result['tablename'],
874
                        '1',
875
                        '',
876
                        true
877
                    );
878
                    break;
879
                case 'CREATE DATABASE':
880
                    self::createDatabaseVersion($dbname, '1', $query);
881
                    break;
882
            } // end switch
883
        }
884
885
        // If version exists
886
        if ($version == -1) {
887
            return;
888
        }
889
890
        if ($result['type'] === 'DDL') {
891
            $save_to = 'schema_sql';
892
        } elseif ($result['type'] === 'DML') {
893
            $save_to = 'data_sql';
894
        } else {
895
            $save_to = '';
896
        }
897
        $date  = Util::date('Y-m-d H:i:s');
898
899
        // Cut off `dbname`. from query
900
        $query = preg_replace(
901
            '/`' . preg_quote($dbname, '/') . '`\s?\./',
902
            '',
903
            $query
904
        );
905
906
        // Add log information
907
        $query = self::getLogComment() . $query;
908
909
        // Mark it as untouchable
910
        $sql_query = " /*NOTRACK*/\n"
911
            . ' UPDATE ' . self::getTrackingTable()
912
            . ' SET ' . Util::backquote($save_to)
913
            . ' = CONCAT( ' . Util::backquote($save_to) . ",'\n"
914
            . $GLOBALS['dbi']->escapeString($query) . "') ,"
915
            . " `date_updated` = '" . $date . "' ";
916
917
        // If table was renamed we have to change
918
        // the tablename attribute in pma_tracking too
919
        if ($result['identifier'] === 'RENAME TABLE') {
920
            $sql_query .= ', `table_name` = \''
921
                . $GLOBALS['dbi']->escapeString($result['tablename_after_rename'])
922
                . '\' ';
923
        }
924
925
        // Save the tracking information only for
926
        //     1. the database
927
        //     2. the table / view
928
        //     3. the statements
929
        // we want to track
930
        $sql_query .=
931
        " WHERE FIND_IN_SET('" . $result['identifier'] . "',tracking) > 0" .
932
        " AND `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname ?? '') . "' " .
933
        " AND `table_name` = '"
934
        . $GLOBALS['dbi']->escapeString($result['tablename']) . "' " .
935
        " AND `version` = '" . $GLOBALS['dbi']->escapeString($version ?? '') . "' ";
936
937
        $relation->queryAsControlUser($sql_query);
938
    }
939
940
    /**
941
     * Returns the tracking table
942
     *
943
     * @return string tracking table
944
     */
945 53
    private static function getTrackingTable()
946
    {
947 53
        $relation = new Relation($GLOBALS['dbi']);
948 53
        $cfgRelation = $relation->getRelationsParam();
949
950 53
        return Util::backquote($cfgRelation['db'])
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($cfgRelation['db']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

950
        return /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['db'])
Loading history...
951 53
            . '.' . Util::backquote($cfgRelation['tracking']);
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquo...fgRelation['tracking']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

951
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['tracking']);
Loading history...
952
    }
953
}
954