1<?php
2
3declare(strict_types=1);
4
5namespace PhpMyAdmin;
6
7use PhpMyAdmin\Engines\Innodb;
8use PhpMyAdmin\Plugins\Export\ExportSql;
9use function array_merge;
10use function count;
11use function explode;
12use function implode;
13use function mb_strtolower;
14use function str_replace;
15use function strlen;
16use function strtolower;
17use function urldecode;
18
19/**
20 * Set of functions with the operations section in phpMyAdmin
21 */
22class Operations
23{
24    /** @var Relation */
25    private $relation;
26
27    /** @var DatabaseInterface */
28    private $dbi;
29
30    /**
31     * @param DatabaseInterface $dbi      DatabaseInterface object
32     * @param Relation          $relation Relation object
33     */
34    public function __construct(DatabaseInterface $dbi, Relation $relation)
35    {
36        $this->dbi = $dbi;
37        $this->relation = $relation;
38    }
39
40    /**
41     * Run the Procedure definitions and function definitions
42     *
43     * to avoid selecting alternatively the current and new db
44     * we would need to modify the CREATE definitions to qualify
45     * the db name
46     *
47     * @param string $db database name
48     *
49     * @return void
50     */
51    public function runProcedureAndFunctionDefinitions($db)
52    {
53        $procedure_names = $this->dbi->getProceduresOrFunctions($db, 'PROCEDURE');
54        if ($procedure_names) {
55            foreach ($procedure_names as $procedure_name) {
56                $this->dbi->selectDb($db);
57                $tmp_query = $this->dbi->getDefinition(
58                    $db,
59                    'PROCEDURE',
60                    $procedure_name
61                );
62                if ($tmp_query === null) {
63                    continue;
64                }
65
66                // collect for later display
67                $GLOBALS['sql_query'] .= "\n" . $tmp_query;
68                $this->dbi->selectDb($_POST['newname']);
69                $this->dbi->query($tmp_query);
70            }
71        }
72
73        $function_names = $this->dbi->getProceduresOrFunctions($db, 'FUNCTION');
74        if (! $function_names) {
75            return;
76        }
77
78        foreach ($function_names as $function_name) {
79            $this->dbi->selectDb($db);
80            $tmp_query = $this->dbi->getDefinition(
81                $db,
82                'FUNCTION',
83                $function_name
84            );
85            if ($tmp_query === null) {
86                continue;
87            }
88
89            // collect for later display
90            $GLOBALS['sql_query'] .= "\n" . $tmp_query;
91            $this->dbi->selectDb($_POST['newname']);
92            $this->dbi->query($tmp_query);
93        }
94    }
95
96    /**
97     * Create database before copy
98     *
99     * @return void
100     */
101    public function createDbBeforeCopy()
102    {
103        $local_query = 'CREATE DATABASE IF NOT EXISTS '
104            . Util::backquote($_POST['newname']);
105        if (isset($_POST['db_collation'])) {
106            $local_query .= ' DEFAULT'
107                . Util::getCharsetQueryPart($_POST['db_collation'] ?? '');
108        }
109        $local_query .= ';';
110        $GLOBALS['sql_query'] .= $local_query;
111
112        // save the original db name because Tracker.php which
113        // may be called under $this->dbi->query() changes $GLOBALS['db']
114        // for some statements, one of which being CREATE DATABASE
115        $original_db = $GLOBALS['db'];
116        $this->dbi->query($local_query);
117        $GLOBALS['db'] = $original_db;
118
119        // Set the SQL mode to NO_AUTO_VALUE_ON_ZERO to prevent MySQL from creating
120        // export statements it cannot import
121        $sql_set_mode = "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO'";
122        $this->dbi->query($sql_set_mode);
123
124        // rebuild the database list because Table::moveCopy
125        // checks in this list if the target db exists
126        $GLOBALS['dblist']->databases->build();
127    }
128
129    /**
130     * Get views as an array and create SQL view stand-in
131     *
132     * @param array     $tables_full       array of all tables in given db or dbs
133     * @param ExportSql $export_sql_plugin export plugin instance
134     * @param string    $db                database name
135     *
136     * @return array
137     */
138    public function getViewsAndCreateSqlViewStandIn(
139        array $tables_full,
140        $export_sql_plugin,
141        $db
142    ) {
143        $views = [];
144        foreach ($tables_full as $each_table => $tmp) {
145            // to be able to rename a db containing views,
146            // first all the views are collected and a stand-in is created
147            // the real views are created after the tables
148            if (! $this->dbi->getTable($db, (string) $each_table)->isView()) {
149                continue;
150            }
151
152            // If view exists, and 'add drop view' is selected: Drop it!
153            if ($_POST['what'] !== 'nocopy'
154                && isset($_POST['drop_if_exists'])
155                && $_POST['drop_if_exists'] === 'true'
156            ) {
157                $drop_query = 'DROP VIEW IF EXISTS '
158                    . Util::backquote($_POST['newname']) . '.'
159                    . Util::backquote($each_table);
160                $this->dbi->query($drop_query);
161
162                $GLOBALS['sql_query'] .= "\n" . $drop_query . ';';
163            }
164
165            $views[] = $each_table;
166            // Create stand-in definition to resolve view dependencies
167            $sql_view_standin = $export_sql_plugin->getTableDefStandIn(
168                $db,
169                $each_table,
170                "\n"
171            );
172            $this->dbi->selectDb($_POST['newname']);
173            $this->dbi->query($sql_view_standin);
174            $GLOBALS['sql_query'] .= "\n" . $sql_view_standin;
175        }
176
177        return $views;
178    }
179
180    /**
181     * Get sql query for copy/rename table and boolean for whether copy/rename or not
182     *
183     * @param array  $tables_full array of all tables in given db or dbs
184     * @param bool   $move        whether database name is empty or not
185     * @param string $db          database name
186     *
187     * @return array SQL queries for the constraints
188     */
189    public function copyTables(array $tables_full, $move, $db)
190    {
191        $sqlContraints = [];
192        foreach ($tables_full as $each_table => $tmp) {
193            // skip the views; we have created stand-in definitions
194            if ($this->dbi->getTable($db, (string) $each_table)->isView()) {
195                continue;
196            }
197
198            // value of $what for this table only
199            $this_what = $_POST['what'];
200
201            // do not copy the data from a Merge table
202            // note: on the calling FORM, 'data' means 'structure and data'
203            if ($this->dbi->getTable($db, (string) $each_table)->isMerge()) {
204                if ($this_what === 'data') {
205                    $this_what = 'structure';
206                }
207                if ($this_what === 'dataonly') {
208                    $this_what = 'nocopy';
209                }
210            }
211
212            if ($this_what === 'nocopy') {
213                continue;
214            }
215
216            // keep the triggers from the original db+table
217            // (third param is empty because delimiters are only intended
218            //  for importing via the mysql client or our Import feature)
219            $triggers = $this->dbi->getTriggers($db, (string) $each_table, '');
220
221            if (! Table::moveCopy(
222                $db,
223                $each_table,
224                $_POST['newname'],
225                $each_table,
226                ($this_what ?? 'data'),
227                $move,
228                'db_copy'
229            )) {
230                $GLOBALS['_error'] = true;
231                break;
232            }
233            // apply the triggers to the destination db+table
234            if ($triggers) {
235                $this->dbi->selectDb($_POST['newname']);
236                foreach ($triggers as $trigger) {
237                    $this->dbi->query($trigger['create']);
238                    $GLOBALS['sql_query'] .= "\n" . $trigger['create'] . ';';
239                }
240            }
241
242            // this does not apply to a rename operation
243            if (! isset($_POST['add_constraints'])
244                || empty($GLOBALS['sql_constraints_query'])
245            ) {
246                continue;
247            }
248
249            $sqlContraints[] = $GLOBALS['sql_constraints_query'];
250            unset($GLOBALS['sql_constraints_query']);
251        }
252
253        return $sqlContraints;
254    }
255
256    /**
257     * Run the EVENT definition for selected database
258     *
259     * to avoid selecting alternatively the current and new db
260     * we would need to modify the CREATE definitions to qualify
261     * the db name
262     *
263     * @param string $db database name
264     *
265     * @return void
266     */
267    public function runEventDefinitionsForDb($db)
268    {
269        $event_names = $this->dbi->fetchResult(
270            'SELECT EVENT_NAME FROM information_schema.EVENTS WHERE EVENT_SCHEMA= \''
271            . $this->dbi->escapeString($db) . '\';'
272        );
273        if (! $event_names) {
274            return;
275        }
276
277        foreach ($event_names as $event_name) {
278            $this->dbi->selectDb($db);
279            $tmp_query = $this->dbi->getDefinition($db, 'EVENT', $event_name);
280            // collect for later display
281            $GLOBALS['sql_query'] .= "\n" . $tmp_query;
282            $this->dbi->selectDb($_POST['newname']);
283            $this->dbi->query($tmp_query);
284        }
285    }
286
287    /**
288     * Handle the views, return the boolean value whether table rename/copy or not
289     *
290     * @param array  $views views as an array
291     * @param bool   $move  whether database name is empty or not
292     * @param string $db    database name
293     *
294     * @return void
295     */
296    public function handleTheViews(array $views, $move, $db)
297    {
298        // temporarily force to add DROP IF EXIST to CREATE VIEW query,
299        // to remove stand-in VIEW that was created earlier
300        // ( $_POST['drop_if_exists'] is used in moveCopy() )
301        if (isset($_POST['drop_if_exists'])) {
302            $temp_drop_if_exists = $_POST['drop_if_exists'];
303        }
304
305        $_POST['drop_if_exists'] = 'true';
306        foreach ($views as $view) {
307            $copying_succeeded = Table::moveCopy(
308                $db,
309                $view,
310                $_POST['newname'],
311                $view,
312                'structure',
313                $move,
314                'db_copy'
315            );
316            if (! $copying_succeeded) {
317                $GLOBALS['_error'] = true;
318                break;
319            }
320        }
321        unset($_POST['drop_if_exists']);
322
323        if (! isset($temp_drop_if_exists)) {
324            return;
325        }
326
327        // restore previous value
328        $_POST['drop_if_exists'] = $temp_drop_if_exists;
329    }
330
331    /**
332     * Adjust the privileges after Renaming the db
333     *
334     * @param string $oldDb   Database name before renaming
335     * @param string $newname New Database name requested
336     *
337     * @return void
338     */
339    public function adjustPrivilegesMoveDb($oldDb, $newname)
340    {
341        if (! $GLOBALS['db_priv'] || ! $GLOBALS['table_priv']
342            || ! $GLOBALS['col_priv'] || ! $GLOBALS['proc_priv']
343            || ! $GLOBALS['is_reload_priv']
344        ) {
345            return;
346        }
347
348        $this->dbi->selectDb('mysql');
349        $newname = str_replace('_', '\_', $newname);
350        $oldDb = str_replace('_', '\_', $oldDb);
351
352        // For Db specific privileges
353        $query_db_specific = 'UPDATE ' . Util::backquote('db')
354            . 'SET Db = \'' . $this->dbi->escapeString($newname)
355            . '\' where Db = \'' . $this->dbi->escapeString($oldDb) . '\';';
356        $this->dbi->query($query_db_specific);
357
358        // For table specific privileges
359        $query_table_specific = 'UPDATE ' . Util::backquote('tables_priv')
360            . 'SET Db = \'' . $this->dbi->escapeString($newname)
361            . '\' where Db = \'' . $this->dbi->escapeString($oldDb) . '\';';
362        $this->dbi->query($query_table_specific);
363
364        // For column specific privileges
365        $query_col_specific = 'UPDATE ' . Util::backquote('columns_priv')
366            . 'SET Db = \'' . $this->dbi->escapeString($newname)
367            . '\' where Db = \'' . $this->dbi->escapeString($oldDb) . '\';';
368        $this->dbi->query($query_col_specific);
369
370        // For procedures specific privileges
371        $query_proc_specific = 'UPDATE ' . Util::backquote('procs_priv')
372            . 'SET Db = \'' . $this->dbi->escapeString($newname)
373            . '\' where Db = \'' . $this->dbi->escapeString($oldDb) . '\';';
374        $this->dbi->query($query_proc_specific);
375
376        // Finally FLUSH the new privileges
377        $flush_query = 'FLUSH PRIVILEGES;';
378        $this->dbi->query($flush_query);
379    }
380
381    /**
382     * Adjust the privileges after Copying the db
383     *
384     * @param string $oldDb   Database name before copying
385     * @param string $newname New Database name requested
386     *
387     * @return void
388     */
389    public function adjustPrivilegesCopyDb($oldDb, $newname)
390    {
391        if (! $GLOBALS['db_priv'] || ! $GLOBALS['table_priv']
392            || ! $GLOBALS['col_priv'] || ! $GLOBALS['proc_priv']
393            || ! $GLOBALS['is_reload_priv']
394        ) {
395            return;
396        }
397
398        $this->dbi->selectDb('mysql');
399        $newname = str_replace('_', '\_', $newname);
400        $oldDb = str_replace('_', '\_', $oldDb);
401
402        $query_db_specific_old = 'SELECT * FROM '
403            . Util::backquote('db') . ' WHERE '
404            . 'Db = "' . $oldDb . '";';
405
406        $old_privs_db = $this->dbi->fetchResult($query_db_specific_old, 0);
407
408        foreach ($old_privs_db as $old_priv) {
409            $newDb_db_privs_query = 'INSERT INTO ' . Util::backquote('db')
410                . ' VALUES("' . $old_priv[0] . '", "' . $newname . '"';
411            $privCount = count($old_priv);
412            for ($i = 2; $i < $privCount; $i++) {
413                $newDb_db_privs_query .= ', "' . $old_priv[$i] . '"';
414            }
415                $newDb_db_privs_query .= ')';
416
417            $this->dbi->query($newDb_db_privs_query);
418        }
419
420        // For Table Specific privileges
421        $query_table_specific_old = 'SELECT * FROM '
422            . Util::backquote('tables_priv') . ' WHERE '
423            . 'Db = "' . $oldDb . '";';
424
425        $old_privs_table = $this->dbi->fetchResult(
426            $query_table_specific_old,
427            0
428        );
429
430        foreach ($old_privs_table as $old_priv) {
431            $newDb_table_privs_query = 'INSERT INTO ' . Util::backquote(
432                'tables_priv'
433            ) . ' VALUES("' . $old_priv[0] . '", "' . $newname . '", "'
434            . $old_priv[2] . '", "' . $old_priv[3] . '", "' . $old_priv[4]
435            . '", "' . $old_priv[5] . '", "' . $old_priv[6] . '", "'
436            . $old_priv[7] . '");';
437
438            $this->dbi->query($newDb_table_privs_query);
439        }
440
441        // For Column Specific privileges
442        $query_col_specific_old = 'SELECT * FROM '
443            . Util::backquote('columns_priv') . ' WHERE '
444            . 'Db = "' . $oldDb . '";';
445
446        $old_privs_col = $this->dbi->fetchResult(
447            $query_col_specific_old,
448            0
449        );
450
451        foreach ($old_privs_col as $old_priv) {
452            $newDb_col_privs_query = 'INSERT INTO ' . Util::backquote(
453                'columns_priv'
454            ) . ' VALUES("' . $old_priv[0] . '", "' . $newname . '", "'
455            . $old_priv[2] . '", "' . $old_priv[3] . '", "' . $old_priv[4]
456            . '", "' . $old_priv[5] . '", "' . $old_priv[6] . '");';
457
458            $this->dbi->query($newDb_col_privs_query);
459        }
460
461        // For Procedure Specific privileges
462        $query_proc_specific_old = 'SELECT * FROM '
463            . Util::backquote('procs_priv') . ' WHERE '
464            . 'Db = "' . $oldDb . '";';
465
466        $old_privs_proc = $this->dbi->fetchResult(
467            $query_proc_specific_old,
468            0
469        );
470
471        foreach ($old_privs_proc as $old_priv) {
472            $newDb_proc_privs_query = 'INSERT INTO ' . Util::backquote(
473                'procs_priv'
474            ) . ' VALUES("' . $old_priv[0] . '", "' . $newname . '", "'
475            . $old_priv[2] . '", "' . $old_priv[3] . '", "' . $old_priv[4]
476            . '", "' . $old_priv[5] . '", "' . $old_priv[6] . '", "'
477            . $old_priv[7] . '");';
478
479            $this->dbi->query($newDb_proc_privs_query);
480        }
481
482        // Finally FLUSH the new privileges
483        $flush_query = 'FLUSH PRIVILEGES;';
484        $this->dbi->query($flush_query);
485    }
486
487    /**
488     * Create all accumulated constraints
489     *
490     * @param array $sqlConstratints array of sql constraints for the database
491     *
492     * @return void
493     */
494    public function createAllAccumulatedConstraints(array $sqlConstratints)
495    {
496        $this->dbi->selectDb($_POST['newname']);
497        foreach ($sqlConstratints as $one_query) {
498            $this->dbi->query($one_query);
499            // and prepare to display them
500            $GLOBALS['sql_query'] .= "\n" . $one_query;
501        }
502    }
503
504    /**
505     * Duplicate the bookmarks for the db (done once for each db)
506     *
507     * @param bool   $_error whether table rename/copy or not
508     * @param string $db     database name
509     *
510     * @return void
511     */
512    public function duplicateBookmarks($_error, $db)
513    {
514        if ($_error || $db == $_POST['newname']) {
515            return;
516        }
517
518        $get_fields = [
519            'user',
520            'label',
521            'query',
522        ];
523        $where_fields = ['dbase' => $db];
524        $new_fields = ['dbase' => $_POST['newname']];
525        Table::duplicateInfo(
526            'bookmarkwork',
527            'bookmark',
528            $get_fields,
529            $where_fields,
530            $new_fields
531        );
532    }
533
534    /**
535     * Get array of possible row formats
536     *
537     * @return array
538     */
539    public function getPossibleRowFormat()
540    {
541        // the outer array is for engines, the inner array contains the dropdown
542        // option values as keys then the dropdown option labels
543
544        $possible_row_formats = [
545            'ARCHIVE' => ['COMPRESSED' => 'COMPRESSED'],
546            'ARIA'  => [
547                'FIXED'     => 'FIXED',
548                'DYNAMIC'   => 'DYNAMIC',
549                'PAGE'      => 'PAGE',
550            ],
551            'MARIA'  => [
552                'FIXED'     => 'FIXED',
553                'DYNAMIC'   => 'DYNAMIC',
554                'PAGE'      => 'PAGE',
555            ],
556            'MYISAM' => [
557                'FIXED'    => 'FIXED',
558                'DYNAMIC'  => 'DYNAMIC',
559            ],
560            'PBXT'   => [
561                'FIXED'    => 'FIXED',
562                'DYNAMIC'  => 'DYNAMIC',
563            ],
564            'INNODB' => [
565                'COMPACT'  => 'COMPACT',
566                'REDUNDANT' => 'REDUNDANT',
567            ],
568        ];
569
570        /** @var Innodb $innodbEnginePlugin */
571        $innodbEnginePlugin = StorageEngine::getEngine('Innodb');
572        $innodbPluginVersion = $innodbEnginePlugin->getInnodbPluginVersion();
573        $innodb_file_format = '';
574        if (! empty($innodbPluginVersion)) {
575            $innodb_file_format = $innodbEnginePlugin->getInnodbFileFormat() ?? '';
576        }
577        /**
578         * Newer MySQL/MariaDB always return empty a.k.a '' on $innodb_file_format otherwise
579         * old versions of MySQL/MariaDB must be returning something or not empty.
580         * This patch is to support newer MySQL/MariaDB while also for backward compatibilities.
581         */
582        if ((strtolower($innodb_file_format)  === 'barracuda') || ($innodb_file_format == '')
583            && $innodbEnginePlugin->supportsFilePerTable()
584        ) {
585            $possible_row_formats['INNODB']['DYNAMIC'] = 'DYNAMIC';
586            $possible_row_formats['INNODB']['COMPRESSED'] = 'COMPRESSED';
587        }
588
589        return $possible_row_formats;
590    }
591
592    /**
593     * @return array<string, string>
594     */
595    public function getPartitionMaintenanceChoices(): array
596    {
597        global $db, $table;
598
599        $choices = [
600            'ANALYZE' => __('Analyze'),
601            'CHECK' => __('Check'),
602            'OPTIMIZE' => __('Optimize'),
603            'REBUILD' => __('Rebuild'),
604            'REPAIR' => __('Repair'),
605            'TRUNCATE' => __('Truncate'),
606        ];
607
608        $partitionMethod = Partition::getPartitionMethod($db, $table);
609
610        // add COALESCE or DROP option to choices array depending on Partition method
611        if ($partitionMethod === 'RANGE'
612            || $partitionMethod === 'RANGE COLUMNS'
613            || $partitionMethod === 'LIST'
614            || $partitionMethod === 'LIST COLUMNS'
615        ) {
616            $choices['DROP'] = __('Drop');
617        } else {
618            $choices['COALESCE'] = __('Coalesce');
619        }
620
621        return $choices;
622    }
623
624    /**
625     * @param array $urlParams          Array of url parameters.
626     * @param bool  $hasRelationFeature If relation feature is enabled.
627     *
628     * @return array
629     */
630    public function getForeignersForReferentialIntegrityCheck(
631        array $urlParams,
632        $hasRelationFeature
633    ): array {
634        global $db, $table;
635
636        if (! $hasRelationFeature) {
637            return [];
638        }
639
640        $foreigners = [];
641        $this->dbi->selectDb($db);
642        $foreign = $this->relation->getForeigners($db, $table, '', 'internal');
643
644        foreach ($foreign as $master => $arr) {
645            $joinQuery  = 'SELECT '
646                . Util::backquote($table) . '.*'
647                . ' FROM ' . Util::backquote($table)
648                . ' LEFT JOIN '
649                . Util::backquote($arr['foreign_db'])
650                . '.'
651                . Util::backquote($arr['foreign_table']);
652
653            if ($arr['foreign_table'] == $table) {
654                $foreignTable = $table . '1';
655                $joinQuery .= ' AS ' . Util::backquote($foreignTable);
656            } else {
657                $foreignTable = $arr['foreign_table'];
658            }
659
660            $joinQuery .= ' ON '
661                . Util::backquote($table) . '.'
662                . Util::backquote($master)
663                . ' = '
664                . Util::backquote($arr['foreign_db'])
665                . '.'
666                . Util::backquote($foreignTable) . '.'
667                . Util::backquote($arr['foreign_field'])
668                . ' WHERE '
669                . Util::backquote($arr['foreign_db'])
670                . '.'
671                . Util::backquote($foreignTable) . '.'
672                . Util::backquote($arr['foreign_field'])
673                . ' IS NULL AND '
674                . Util::backquote($table) . '.'
675                . Util::backquote($master)
676                . ' IS NOT NULL';
677            $thisUrlParams = array_merge(
678                $urlParams,
679                [
680                    'sql_query' => $joinQuery,
681                    'sql_signature' => Core::signSqlQuery($joinQuery),
682                ]
683            );
684
685            $foreigners[] = [
686                'params' => $thisUrlParams,
687                'master' => $master,
688                'db' => $arr['foreign_db'],
689                'table' => $arr['foreign_table'],
690                'field' => $arr['foreign_field'],
691            ];
692        }
693
694        return $foreigners;
695    }
696
697    /**
698     * Reorder table based on request params
699     *
700     * @return array SQL query and result
701     */
702    public function getQueryAndResultForReorderingTable()
703    {
704        $sql_query = 'ALTER TABLE '
705            . Util::backquote($GLOBALS['table'])
706            . ' ORDER BY '
707            . Util::backquote(urldecode($_POST['order_field']));
708        if (isset($_POST['order_order'])
709            && $_POST['order_order'] === 'desc'
710        ) {
711            $sql_query .= ' DESC';
712        } else {
713            $sql_query .= ' ASC';
714        }
715        $sql_query .= ';';
716        $result = $this->dbi->query($sql_query);
717
718        return [
719            $sql_query,
720            $result,
721        ];
722    }
723
724    /**
725     * Get table alters array
726     *
727     * @param Table  $pma_table           The Table object
728     * @param string $pack_keys           pack keys
729     * @param string $checksum            value of checksum
730     * @param string $page_checksum       value of page checksum
731     * @param string $delay_key_write     delay key write
732     * @param string $row_format          row format
733     * @param string $newTblStorageEngine table storage engine
734     * @param string $transactional       value of transactional
735     * @param string $tbl_collation       collation of the table
736     *
737     * @return array
738     */
739    public function getTableAltersArray(
740        $pma_table,
741        $pack_keys,
742        $checksum,
743        $page_checksum,
744        $delay_key_write,
745        $row_format,
746        $newTblStorageEngine,
747        $transactional,
748        $tbl_collation
749    ) {
750        global $auto_increment;
751
752        $table_alters = [];
753
754        if (isset($_POST['comment'])
755            && urldecode($_POST['prev_comment']) !== $_POST['comment']
756        ) {
757            $table_alters[] = 'COMMENT = \''
758                . $this->dbi->escapeString($_POST['comment']) . '\'';
759        }
760
761        if (! empty($newTblStorageEngine)
762            && mb_strtolower($newTblStorageEngine) !== mb_strtolower($GLOBALS['tbl_storage_engine'])
763        ) {
764            $table_alters[] = 'ENGINE = ' . $newTblStorageEngine;
765        }
766        if (! empty($_POST['tbl_collation'])
767            && $_POST['tbl_collation'] !== $tbl_collation
768        ) {
769            $table_alters[] = 'DEFAULT '
770                . Util::getCharsetQueryPart($_POST['tbl_collation'] ?? '');
771        }
772
773        if ($pma_table->isEngine(['MYISAM', 'ARIA', 'ISAM'])
774            && isset($_POST['new_pack_keys'])
775            && $_POST['new_pack_keys'] != (string) $pack_keys
776        ) {
777            $table_alters[] = 'pack_keys = ' . $_POST['new_pack_keys'];
778        }
779
780        $_POST['new_checksum'] = empty($_POST['new_checksum']) ? '0' : '1';
781        if ($pma_table->isEngine(['MYISAM', 'ARIA'])
782            && $_POST['new_checksum'] !== $checksum
783        ) {
784            $table_alters[] = 'checksum = ' . $_POST['new_checksum'];
785        }
786
787        $_POST['new_transactional']
788            = empty($_POST['new_transactional']) ? '0' : '1';
789        if ($pma_table->isEngine('ARIA')
790            && $_POST['new_transactional'] !== $transactional
791        ) {
792            $table_alters[] = 'TRANSACTIONAL = ' . $_POST['new_transactional'];
793        }
794
795        $_POST['new_page_checksum']
796            = empty($_POST['new_page_checksum']) ? '0' : '1';
797        if ($pma_table->isEngine('ARIA')
798            && $_POST['new_page_checksum'] !== $page_checksum
799        ) {
800            $table_alters[] = 'PAGE_CHECKSUM = ' . $_POST['new_page_checksum'];
801        }
802
803        $_POST['new_delay_key_write']
804            = empty($_POST['new_delay_key_write']) ? '0' : '1';
805        if ($pma_table->isEngine(['MYISAM', 'ARIA'])
806            && $_POST['new_delay_key_write'] !== $delay_key_write
807        ) {
808            $table_alters[] = 'delay_key_write = ' . $_POST['new_delay_key_write'];
809        }
810
811        if ($pma_table->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT', 'ROCKSDB'])
812            && ! empty($_POST['new_auto_increment'])
813            && (! isset($auto_increment)
814            || $_POST['new_auto_increment'] !== $auto_increment)
815            && $_POST['new_auto_increment'] !== $_POST['hidden_auto_increment']
816        ) {
817            $table_alters[] = 'auto_increment = '
818                . $this->dbi->escapeString($_POST['new_auto_increment']);
819        }
820
821        if (! empty($_POST['new_row_format'])) {
822            $newRowFormat = $_POST['new_row_format'];
823            $newRowFormatLower = mb_strtolower($newRowFormat);
824            if ($pma_table->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT'])
825                && (strlen($row_format) === 0
826                || $newRowFormatLower !== mb_strtolower($row_format))
827            ) {
828                $table_alters[] = 'ROW_FORMAT = '
829                    . $this->dbi->escapeString($newRowFormat);
830            }
831        }
832
833        return $table_alters;
834    }
835
836    /**
837     * Get warning messages array
838     *
839     * @return array
840     */
841    public function getWarningMessagesArray()
842    {
843        $warning_messages = [];
844        foreach ($this->dbi->getWarnings() as $warning) {
845            // In MariaDB 5.1.44, when altering a table from Maria to MyISAM
846            // and if TRANSACTIONAL was set, the system reports an error;
847            // I discussed with a Maria developer and he agrees that this
848            // should not be reported with a Level of Error, so here
849            // I just ignore it. But there are other 1478 messages
850            // that it's better to show.
851            if (isset($_POST['new_tbl_storage_engine'])
852                && $_POST['new_tbl_storage_engine'] === 'MyISAM'
853                && $warning['Code'] == '1478'
854                && $warning['Level'] === 'Error'
855            ) {
856                continue;
857            }
858
859            $warning_messages[] = $warning['Level'] . ': #' . $warning['Code']
860                . ' ' . $warning['Message'];
861        }
862
863        return $warning_messages;
864    }
865
866    /**
867     * Get SQL query and result after ran this SQL query for a partition operation
868     * has been requested by the user
869     *
870     * @return array $sql_query, $result
871     */
872    public function getQueryAndResultForPartition()
873    {
874        $sql_query = 'ALTER TABLE '
875            . Util::backquote($GLOBALS['table']) . ' '
876            . $_POST['partition_operation']
877            . ' PARTITION ';
878
879        if ($_POST['partition_operation'] === 'COALESCE') {
880            $sql_query .= count($_POST['partition_name']);
881        } else {
882            $sql_query .= implode(', ', $_POST['partition_name']) . ';';
883        }
884
885        $result = $this->dbi->query($sql_query);
886
887        return [
888            $sql_query,
889            $result,
890        ];
891    }
892
893    /**
894     * Adjust the privileges after renaming/moving a table
895     *
896     * @param string $oldDb    Database name before table renaming/moving table
897     * @param string $oldTable Table name before table renaming/moving table
898     * @param string $newDb    Database name after table renaming/ moving table
899     * @param string $newTable Table name after table renaming/moving table
900     *
901     * @return void
902     */
903    public function adjustPrivilegesRenameOrMoveTable($oldDb, $oldTable, $newDb, $newTable)
904    {
905        if (! $GLOBALS['table_priv'] || ! $GLOBALS['col_priv']
906            || ! $GLOBALS['is_reload_priv']
907        ) {
908            return;
909        }
910
911        $this->dbi->selectDb('mysql');
912
913        // For table specific privileges
914        $query_table_specific = 'UPDATE ' . Util::backquote('tables_priv')
915            . 'SET Db = \'' . $this->dbi->escapeString($newDb)
916            . '\', Table_name = \'' . $this->dbi->escapeString($newTable)
917            . '\' where Db = \'' . $this->dbi->escapeString($oldDb)
918            . '\' AND Table_name = \'' . $this->dbi->escapeString($oldTable)
919            . '\';';
920        $this->dbi->query($query_table_specific);
921
922        // For column specific privileges
923        $query_col_specific = 'UPDATE ' . Util::backquote('columns_priv')
924            . 'SET Db = \'' . $this->dbi->escapeString($newDb)
925            . '\', Table_name = \'' . $this->dbi->escapeString($newTable)
926            . '\' where Db = \'' . $this->dbi->escapeString($oldDb)
927            . '\' AND Table_name = \'' . $this->dbi->escapeString($oldTable)
928            . '\';';
929        $this->dbi->query($query_col_specific);
930
931        // Finally FLUSH the new privileges
932        $flush_query = 'FLUSH PRIVILEGES;';
933        $this->dbi->query($flush_query);
934    }
935
936    /**
937     * Adjust the privileges after copying a table
938     *
939     * @param string $oldDb    Database name before table copying
940     * @param string $oldTable Table name before table copying
941     * @param string $newDb    Database name after table copying
942     * @param string $newTable Table name after table copying
943     *
944     * @return void
945     */
946    public function adjustPrivilegesCopyTable($oldDb, $oldTable, $newDb, $newTable)
947    {
948        if (! $GLOBALS['table_priv'] || ! $GLOBALS['col_priv']
949            || ! $GLOBALS['is_reload_priv']
950        ) {
951            return;
952        }
953
954        $this->dbi->selectDb('mysql');
955
956        // For Table Specific privileges
957        $query_table_specific_old = 'SELECT * FROM '
958            . Util::backquote('tables_priv') . ' where '
959            . 'Db = "' . $oldDb . '" AND Table_name = "' . $oldTable . '";';
960
961        $old_privs_table = $this->dbi->fetchResult(
962            $query_table_specific_old,
963            0
964        );
965
966        foreach ($old_privs_table as $old_priv) {
967            $newDb_table_privs_query = 'INSERT INTO '
968                . Util::backquote('tables_priv') . ' VALUES("'
969                . $old_priv[0] . '", "' . $newDb . '", "' . $old_priv[2] . '", "'
970                . $newTable . '", "' . $old_priv[4] . '", "' . $old_priv[5]
971                . '", "' . $old_priv[6] . '", "' . $old_priv[7] . '");';
972
973            $this->dbi->query($newDb_table_privs_query);
974        }
975
976        // For Column Specific privileges
977        $query_col_specific_old = 'SELECT * FROM '
978            . Util::backquote('columns_priv') . ' WHERE '
979            . 'Db = "' . $oldDb . '" AND Table_name = "' . $oldTable . '";';
980
981        $old_privs_col = $this->dbi->fetchResult(
982            $query_col_specific_old,
983            0
984        );
985
986        foreach ($old_privs_col as $old_priv) {
987            $newDb_col_privs_query = 'INSERT INTO '
988                . Util::backquote('columns_priv') . ' VALUES("'
989                . $old_priv[0] . '", "' . $newDb . '", "' . $old_priv[2] . '", "'
990                . $newTable . '", "' . $old_priv[4] . '", "' . $old_priv[5]
991                . '", "' . $old_priv[6] . '");';
992
993            $this->dbi->query($newDb_col_privs_query);
994        }
995
996        // Finally FLUSH the new privileges
997        $flush_query = 'FLUSH PRIVILEGES;';
998        $this->dbi->query($flush_query);
999    }
1000
1001    /**
1002     * Change all collations and character sets of all columns in table
1003     *
1004     * @param string $db            Database name
1005     * @param string $table         Table name
1006     * @param string $tbl_collation Collation Name
1007     *
1008     * @return void
1009     */
1010    public function changeAllColumnsCollation($db, $table, $tbl_collation)
1011    {
1012        $this->dbi->selectDb($db);
1013
1014        $change_all_collations_query = 'ALTER TABLE '
1015            . Util::backquote($table)
1016            . ' CONVERT TO';
1017
1018        [$charset] = explode('_', $tbl_collation);
1019
1020        $change_all_collations_query .= ' CHARACTER SET ' . $charset
1021            . ($charset == $tbl_collation ? '' : ' COLLATE ' . $tbl_collation);
1022
1023        $this->dbi->query($change_all_collations_query);
1024    }
1025
1026    /**
1027     * Move or copy a table
1028     *
1029     * @param string $db    current database name
1030     * @param string $table current table name
1031     */
1032    public function moveOrCopyTable($db, $table): Message
1033    {
1034        /**
1035         * Selects the database to work with
1036         */
1037        $this->dbi->selectDb($db);
1038
1039        /**
1040         * $_POST['target_db'] could be empty in case we came from an input field
1041         * (when there are many databases, no drop-down)
1042         */
1043        if (empty($_POST['target_db'])) {
1044            $_POST['target_db'] = $db;
1045        }
1046
1047        /**
1048         * A target table name has been sent to this script -> do the work
1049         */
1050        if (Core::isValid($_POST['new_name'])) {
1051            if ($db == $_POST['target_db'] && $table == $_POST['new_name']) {
1052                if (isset($_POST['submit_move'])) {
1053                    $message = Message::error(__('Can\'t move table to same one!'));
1054                } else {
1055                    $message = Message::error(__('Can\'t copy table to same one!'));
1056                }
1057            } else {
1058                Table::moveCopy(
1059                    $db,
1060                    $table,
1061                    $_POST['target_db'],
1062                    $_POST['new_name'],
1063                    $_POST['what'],
1064                    isset($_POST['submit_move']),
1065                    'one_table'
1066                );
1067
1068                if (isset($_POST['adjust_privileges'])
1069                    && ! empty($_POST['adjust_privileges'])
1070                ) {
1071                    if (isset($_POST['submit_move'])) {
1072                        $this->adjustPrivilegesRenameOrMoveTable(
1073                            $db,
1074                            $table,
1075                            $_POST['target_db'],
1076                            $_POST['new_name']
1077                        );
1078                    } else {
1079                        $this->adjustPrivilegesCopyTable(
1080                            $db,
1081                            $table,
1082                            $_POST['target_db'],
1083                            $_POST['new_name']
1084                        );
1085                    }
1086
1087                    if (isset($_POST['submit_move'])) {
1088                        $message = Message::success(
1089                            __(
1090                                'Table %s has been moved to %s. Privileges have been '
1091                                . 'adjusted.'
1092                            )
1093                        );
1094                    } else {
1095                        $message = Message::success(
1096                            __(
1097                                'Table %s has been copied to %s. Privileges have been '
1098                                . 'adjusted.'
1099                            )
1100                        );
1101                    }
1102                } else {
1103                    if (isset($_POST['submit_move'])) {
1104                        $message = Message::success(
1105                            __('Table %s has been moved to %s.')
1106                        );
1107                    } else {
1108                        $message = Message::success(
1109                            __('Table %s has been copied to %s.')
1110                        );
1111                    }
1112                }
1113
1114                $old = Util::backquote($db) . '.'
1115                    . Util::backquote($table);
1116                $message->addParam($old);
1117
1118                $new_name = $_POST['new_name'];
1119                if ($this->dbi->getLowerCaseNames() === '1') {
1120                    $new_name = strtolower($new_name);
1121                }
1122
1123                $GLOBALS['table'] = $new_name;
1124
1125                $new = Util::backquote($_POST['target_db']) . '.'
1126                    . Util::backquote($new_name);
1127                $message->addParam($new);
1128            }
1129        } else {
1130            /**
1131             * No new name for the table!
1132             */
1133            $message = Message::error(__('The table name is empty!'));
1134        }
1135
1136        return $message;
1137    }
1138}
1139