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