| @@ -11,10 +11,8 @@ | ||
| 11 | 11 | if ( ! defined( 'ABSPATH' ) ) { |
| 12 | 12 | exit; |
| 13 | 13 | } |
| 14 | 14 | |
| 15 | -// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter | |
| 16 | - | |
| 17 | 15 | /** |
| 18 | 16 | * Class Vigilante_Database_Prefix |
| 19 | 17 | * |
| 20 | 18 | * Changes the WordPress database prefix safely |
| @@ -76,9 +74,9 @@ | ||
| 76 | 74 | if ( ! is_multisite() ) { |
| 77 | 75 | return true; |
| 78 | 76 | } |
| 79 | 77 | |
| 80 | - if ( ! is_main_site() ) { | |
| 78 | + if ( ! is_main_site() || ! is_main_network() ) { | |
| 81 | 79 | return new WP_Error( |
| 82 | 80 | 'multisite_not_main_site', |
| 83 | 81 | __( 'The database prefix is shared by the whole network. Change it from the main site of the network.', 'vigilante' ) |
| 84 | 82 | ); |
| @@ -274,8 +272,9 @@ | ||
| 274 | 272 | * |
| 275 | 273 | * @return array|WP_Error |
| 276 | 274 | */ |
| 277 | 275 | private function get_site_prefix_map() { |
| 276 | + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name comes from the validated prefix map and all values use placeholders. | |
| 278 | 277 | if ( ! is_multisite() ) { |
| 279 | 278 | return array( |
| 280 | 279 | array( |
| 281 | 280 | 'blog_id' => 1, |
| @@ -313,8 +312,9 @@ | ||
| 313 | 312 | ); |
| 314 | 313 | } |
| 315 | 314 | |
| 316 | 315 | return $map; |
| 316 | + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter | |
| 317 | 317 | } |
| 318 | 318 | |
| 319 | 319 | /** |
| 320 | 320 | * Get all tables with the current prefix |
| @@ -321,8 +321,9 @@ | ||
| 321 | 321 | * |
| 322 | 322 | * @return array Table names. |
| 323 | 323 | */ |
| 324 | 324 | private function get_prefixed_tables() { |
| 325 | + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- %i placeholder requires WP 6.2+, and the sniff reports inside the multiline prepare(). | |
| 325 | 326 | return $this->wpdb->get_col( |
| 326 | 327 | $this->wpdb->prepare( |
| 327 | 328 | 'SHOW TABLES LIKE %s', |
| 328 | 329 | $this->wpdb->esc_like( $this->old_prefix ) . '%' |
| @@ -327,8 +328,9 @@ | ||
| 327 | 328 | 'SHOW TABLES LIKE %s', |
| 328 | 329 | $this->wpdb->esc_like( $this->old_prefix ) . '%' |
| 329 | 330 | ) |
| 330 | 331 | ); |
| 332 | + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared | |
| 331 | 333 | } |
| 332 | 334 | |
| 333 | 335 | /** |
| 334 | 336 | * Check if tables exist with a given prefix |
| @@ -336,8 +338,9 @@ | ||
| 336 | 338 | * @param string $prefix Prefix to check. |
| 337 | 339 | * @return bool |
| 338 | 340 | */ |
| 339 | 341 | private function prefix_tables_exist( $prefix ) { |
| 342 | + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- %i placeholder requires WP 6.2+, and the sniff reports inside the multiline prepare(). | |
| 340 | 343 | $result = $this->wpdb->get_var( |
| 341 | 344 | $this->wpdb->prepare( |
| 342 | 345 | 'SHOW TABLES LIKE %s', |
| 343 | 346 | $this->wpdb->esc_like( $prefix ) . '%' |
| @@ -344,8 +347,9 @@ | ||
| 344 | 347 | ) |
| 345 | 348 | ); |
| 346 | 349 | |
| 347 | 350 | return ! empty( $result ); |
| 351 | + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared | |
| 348 | 352 | } |
| 349 | 353 | |
| 350 | 354 | /** |
| 351 | 355 | * Rename all tables from old prefix to new prefix |
| @@ -353,8 +357,9 @@ | ||
| 353 | 357 | * @param array $tables List of table names. |
| 354 | 358 | * @return true|WP_Error |
| 355 | 359 | */ |
| 356 | 360 | private function rename_tables( $tables ) { |
| 361 | + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- %i placeholder requires WP 6.2+, and the sniff reports inside the multiline prepare(). | |
| 357 | 362 | $renamed = array(); |
| 358 | 363 | |
| 359 | 364 | foreach ( $tables as $old_name ) { |
| 360 | 365 | $new_name = $this->new_prefix . substr( $old_name, strlen( $this->old_prefix ) ); |
| @@ -393,8 +398,9 @@ | ||
| 393 | 398 | $renamed[ $new_name ] = $old_name; |
| 394 | 399 | } |
| 395 | 400 | |
| 396 | 401 | return true; |
| 402 | + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared | |
| 397 | 403 | } |
| 398 | 404 | |
| 399 | 405 | /** |
| 400 | 406 | * Rollback table renames |
| @@ -401,8 +407,9 @@ | ||
| 401 | 407 | * |
| 402 | 408 | * @param array $original_tables Original table names. |
| 403 | 409 | */ |
| 404 | 410 | private function rollback_tables( $original_tables ) { |
| 411 | + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- %i placeholder requires WP 6.2+, and the sniff reports inside the multiline prepare(). | |
| 405 | 412 | foreach ( $original_tables as $old_name ) { |
| 406 | 413 | $new_name = $this->new_prefix . substr( $old_name, strlen( $this->old_prefix ) ); |
| 407 | 414 | |
| 408 | 415 | // Check if new name exists (it was renamed) |
| @@ -417,8 +424,9 @@ | ||
| 417 | 424 | ) |
| 418 | 425 | ); |
| 419 | 426 | } |
| 420 | 427 | } |
| 428 | + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared | |
| 421 | 429 | } |
| 422 | 430 | |
| 423 | 431 | /** |
| 424 | 432 | * Update $table_prefix in wp-config.php |
| @@ -432,14 +440,24 @@ | ||
| 432 | 440 | if ( false === $content ) { |
| 433 | 441 | return new WP_Error( 'read_error', __( 'Cannot read wp-config.php.', 'vigilante' ) ); |
| 434 | 442 | } |
| 435 | 443 | |
| 436 | - // Back up the original file | |
| 437 | - $backup_path = $this->wpconfig_path . '.vigilante-backup-' . gmdate( 'YmdHis' ); | |
| 438 | - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents | |
| 439 | - if ( ! file_put_contents( $backup_path, $content ) ) { | |
| 440 | - return new WP_Error( 'backup_error', __( 'Cannot create wp-config.php backup.', 'vigilante' ) ); | |
| 441 | - } | |
| 444 | + /* | |
| 445 | + * The original file is held in $content, in memory, and that is the | |
| 446 | + * whole backup this needs. | |
| 447 | + * | |
| 448 | + * Until 2.11.3 it also wrote a copy next to wp-config.php, named | |
| 449 | + * wp-config.php.vigilante-backup-<timestamp>. On a standard install | |
| 450 | + * that is the site root, the name is a predictable timestamp and it | |
| 451 | + * carries no .php extension, so a server hands it over as plain text | |
| 452 | + * with the database credentials and the eight salts inside. It was | |
| 453 | + * deleted straight after, but a request that died in between left it | |
| 454 | + * there for good, and that is exactly the moment when the owner is busy | |
| 455 | + * with a site that will not load. Present since 1.2.0. | |
| 456 | + * | |
| 457 | + * Nothing is lost by removing it: that file was never read back. The | |
| 458 | + * restore below, the only path that undoes anything, uses $content. | |
| 459 | + */ | |
| 442 | 460 | |
| 443 | 461 | // Match the $table_prefix line (handles single and double quotes, with/without spaces) |
| 444 | 462 | $pattern = '/(\$table_prefix\s*=\s*)([\'"]).+?\\2(\s*;)/'; |
| 445 | 463 | $replacement = '${1}\'' . $this->new_prefix . '\'${3}'; |
| @@ -446,10 +464,8 @@ | ||
| 446 | 464 | |
| 447 | 465 | $new_content = preg_replace( $pattern, $replacement, $content, 1, $count ); |
| 448 | 466 | |
| 449 | 467 | if ( 0 === $count || null === $new_content ) { |
| 450 | - // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink | |
| 451 | - unlink( $backup_path ); | |
| 452 | 468 | return new WP_Error( 'replace_error', __( 'Cannot find $table_prefix in wp-config.php.', 'vigilante' ) ); |
| 453 | 469 | } |
| 454 | 470 | |
| 455 | 471 | // Write updated content |
| @@ -456,20 +472,14 @@ | ||
| 456 | 472 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents |
| 457 | 473 | $result = file_put_contents( $this->wpconfig_path, $new_content ); |
| 458 | 474 | |
| 459 | 475 | if ( false === $result ) { |
| 460 | - // Restore backup | |
| 476 | + // Restore from memory, which is where the original has been all along. | |
| 461 | 477 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents |
| 462 | 478 | file_put_contents( $this->wpconfig_path, $content ); |
| 463 | - // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink | |
| 464 | - unlink( $backup_path ); | |
| 465 | 479 | return new WP_Error( 'write_error', __( 'Cannot write to wp-config.php.', 'vigilante' ) ); |
| 466 | 480 | } |
| 467 | 481 | |
| 468 | - // Clean up backup after successful write | |
| 469 | - // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink | |
| 470 | - unlink( $backup_path ); | |
| 471 | - | |
| 472 | 482 | $this->invalidate_wpconfig_opcode_cache(); |
| 473 | 483 | |
| 474 | 484 | return true; |
| 475 | 485 | } |
| @@ -674,8 +684,9 @@ | ||
| 674 | 684 | * @param string $old_prefix Prefix to strip. |
| 675 | 685 | * @param string $new_prefix Prefix to write. |
| 676 | 686 | */ |
| 677 | 687 | private function bulk_rename_options( $table, $old_prefix, $new_prefix ) { |
| 688 | + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- %i placeholder requires WP 6.2+, and the sniff reports inside the multiline prepare(), and the table name comes from the validated prefix map and all values use placeholders. | |
| 678 | 689 | $this->wpdb->query( |
| 679 | 690 | $this->wpdb->prepare( |
| 680 | 691 | "UPDATE `{$table}` SET option_name = CONCAT( %s, SUBSTRING( option_name, %d ) ) WHERE option_name LIKE %s", |
| 681 | 692 | $new_prefix, |
| @@ -682,8 +693,9 @@ | ||
| 682 | 693 | strlen( $old_prefix ) + 1, |
| 683 | 694 | $this->wpdb->esc_like( $old_prefix ) . '%' |
| 684 | 695 | ) |
| 685 | 696 | ); |
| 697 | + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter | |
| 686 | 698 | } |
| 687 | 699 | |
| 688 | 700 | /** |
| 689 | 701 | * Rename every usermeta key that starts with a given prefix |
| @@ -695,8 +707,9 @@ | ||
| 695 | 707 | * @param string $old_prefix Prefix to strip. |
| 696 | 708 | * @param string $new_prefix Prefix to write. |
| 697 | 709 | */ |
| 698 | 710 | private function bulk_rename_usermeta( $table, $old_prefix, $new_prefix ) { |
| 711 | + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- %i placeholder requires WP 6.2+, and the sniff reports inside the multiline prepare(), and the table name comes from the validated prefix map and all values use placeholders. | |
| 699 | 712 | // Prefix migration has to rewrite meta_key values by definition, so the |
| 700 | 713 | // slow-query rule does not apply here. |
| 701 | 714 | // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key |
| 702 | 715 | $this->wpdb->query( |
| @@ -707,8 +720,9 @@ | ||
| 707 | 720 | $this->wpdb->esc_like( $old_prefix ) . '%' |
| 708 | 721 | ) |
| 709 | 722 | ); |
| 710 | 723 | // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key |
| 724 | + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter | |
| 711 | 725 | } |
| 712 | 726 | |
| 713 | 727 | /** |
| 714 | 728 | * Rename a single option, without ever overwriting an existing one |
| @@ -718,8 +732,9 @@ | ||
| 718 | 732 | * @param string $new_name Wanted option name. |
| 719 | 733 | * @return bool Whether the option was renamed. |
| 720 | 734 | */ |
| 721 | 735 | private function rename_option( $table, $old_name, $new_name ) { |
| 736 | + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- %i placeholder requires WP 6.2+, and the sniff reports inside the multiline prepare(), and the table name comes from the validated prefix map and all values use placeholders. | |
| 722 | 737 | if ( $old_name === $new_name || $this->option_exists( $table, $new_name ) ) { |
| 723 | 738 | return false; |
| 724 | 739 | } |
| 725 | 740 | |
| @@ -737,8 +752,9 @@ | ||
| 737 | 752 | array( 'option_id' => (int) $option_id ), |
| 738 | 753 | array( '%s' ), |
| 739 | 754 | array( '%d' ) |
| 740 | 755 | ); |
| 756 | + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter | |
| 741 | 757 | } |
| 742 | 758 | |
| 743 | 759 | /** |
| 744 | 760 | * Rename every row of a usermeta key |
| @@ -747,8 +763,9 @@ | ||
| 747 | 763 | * @param string $old_key Current meta key. |
| 748 | 764 | * @param string $new_key Wanted meta key. |
| 749 | 765 | */ |
| 750 | 766 | private function rename_usermeta( $table, $old_key, $new_key ) { |
| 767 | + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- %i placeholder requires WP 6.2+, and the sniff reports inside the multiline prepare(), and the table name comes from the validated prefix map and all values use placeholders. | |
| 751 | 768 | if ( $old_key === $new_key ) { |
| 752 | 769 | return; |
| 753 | 770 | } |
| 754 | 771 | |
| @@ -762,8 +779,9 @@ | ||
| 762 | 779 | $old_key |
| 763 | 780 | ) |
| 764 | 781 | ); |
| 765 | 782 | // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key |
| 783 | + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter | |
| 766 | 784 | } |
| 767 | 785 | |
| 768 | 786 | /** |
| 769 | 787 | * Whether an option name exists in a given options table |
| @@ -772,13 +790,15 @@ | ||
| 772 | 790 | * @param string $option_name Option name. |
| 773 | 791 | * @return bool |
| 774 | 792 | */ |
| 775 | 793 | private function option_exists( $table, $option_name ) { |
| 794 | + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- %i placeholder requires WP 6.2+, and the sniff reports inside the multiline prepare(), and the table name comes from the validated prefix map and all values use placeholders. | |
| 776 | 795 | $found = $this->wpdb->get_var( |
| 777 | 796 | $this->wpdb->prepare( "SELECT option_id FROM `{$table}` WHERE option_name = %s LIMIT 1", $option_name ) |
| 778 | 797 | ); |
| 779 | 798 | |
| 780 | 799 | return ! empty( $found ); |
| 800 | + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter | |
| 781 | 801 | } |
| 782 | 802 | |
| 783 | 803 | /** |
| 784 | 804 | * Whether a table exists |
| @@ -786,13 +806,15 @@ | ||
| 786 | 806 | * @param string $table Table name. |
| 787 | 807 | * @return bool |
| 788 | 808 | */ |
| 789 | 809 | private function table_exists( $table ) { |
| 810 | + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- %i placeholder requires WP 6.2+, and the sniff reports inside the multiline prepare(). | |
| 790 | 811 | $found = $this->wpdb->get_var( |
| 791 | 812 | $this->wpdb->prepare( 'SHOW TABLES LIKE %s', $this->wpdb->esc_like( $table ) ) |
| 792 | 813 | ); |
| 793 | 814 | |
| 794 | 815 | return ! empty( $found ); |
| 816 | + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared | |
| 795 | 817 | } |
| 796 | 818 | |
| 797 | 819 | /** |
| 798 | 820 | * Drop cached copies of everything the rename touched |