Skip to content

Commit 8060280

Browse files
rootroot
authored andcommitted
Fix #12671: Installer page doesn't check if MySQL tables were created successfully
1 parent 8510818 commit 8060280

File tree

3 files changed

+185
-8
lines changed

3 files changed

+185
-8
lines changed

src/wp-admin/includes/upgrade.php

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,59 @@
1919
/** WordPress Schema API */
2020
require_once ABSPATH . 'wp-admin/includes/schema.php';
2121

22+
/**
23+
* Validates that the required database tables exist after installation schema creation.
24+
*
25+
* @since 6.9.0
26+
*
27+
* @global wpdb $wpdb WordPress database abstraction object.
28+
*
29+
* @param string $tables Optional. Which set of tables to validate. Default 'all'.
30+
* @return true|WP_Error True when the required tables exist, WP_Error otherwise.
31+
*/
32+
function wp_install_validate_schema( $tables = 'all' ) {
33+
global $wpdb;
34+
35+
$database_error = $wpdb->last_error;
36+
$missing_tables = array();
37+
$suppress = $wpdb->suppress_errors();
38+
39+
foreach ( $wpdb->tables( $tables, true ) as $table ) {
40+
$found_table = $wpdb->get_var(
41+
$wpdb->prepare(
42+
'SHOW TABLES LIKE %s',
43+
$wpdb->esc_like( $table )
44+
)
45+
);
46+
47+
if ( $found_table !== $table ) {
48+
$missing_tables[] = $table;
49+
}
50+
}
51+
52+
$wpdb->suppress_errors( $suppress );
53+
54+
if ( empty( $missing_tables ) ) {
55+
return true;
56+
}
57+
58+
$message = __( 'WordPress could not create the database tables required for installation. Please verify that the database user has permission to create tables.' );
59+
60+
if ( ! empty( $database_error ) ) {
61+
/* translators: 1: Generic database table creation failure message, 2: Database error message. */
62+
$message = sprintf( __( '%1$s Database error: %2$s' ), $message, $database_error );
63+
}
64+
65+
return new WP_Error(
66+
'db_install_missing_tables',
67+
$message,
68+
array(
69+
'db_error' => $database_error,
70+
'missing_tables' => $missing_tables,
71+
)
72+
);
73+
}
74+
2275
if ( ! function_exists( 'wp_install' ) ) :
2376
/**
2477
* Installs the site.
@@ -35,14 +88,7 @@
3588
* @param string $deprecated Optional. Not used.
3689
* @param string $user_password Optional. User's chosen password. Default empty (random password).
3790
* @param string $language Optional. Language chosen. Default empty.
38-
* @return array {
39-
* Data for the newly installed site.
40-
*
41-
* @type string $url The URL of the site.
42-
* @type int $user_id The ID of the site owner.
43-
* @type string $password The password of the site owner, if their user account didn't already exist.
44-
* @type string $password_message The explanatory message regarding the password.
45-
* }
91+
* @return array|WP_Error Data for the newly installed site, or WP_Error on failure.
4692
*/
4793
function wp_install(
4894
$blog_title,
@@ -62,6 +108,11 @@ function wp_install(
62108
wp_cache_flush();
63109
make_db_current_silent();
64110

111+
$schema_result = wp_install_validate_schema();
112+
if ( is_wp_error( $schema_result ) ) {
113+
return $schema_result;
114+
}
115+
65116
/*
66117
* Ensure update checks are delayed after installation.
67118
*
@@ -124,6 +175,10 @@ function wp_install(
124175
$message = __( 'User already exists. Password inherited.' );
125176
}
126177

178+
if ( is_wp_error( $user_id ) ) {
179+
return $user_id;
180+
}
181+
127182
$user = new WP_User( $user_id );
128183
$user->set_role( 'administrator' );
129184

src/wp-admin/install.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,11 @@ function display_setup_form( $error = null ) {
440440
if ( false === $error ) {
441441
$wpdb->show_errors();
442442
$result = wp_install( $weblog_title, $user_name, $admin_email, $public, '', wp_slash( $admin_password ), $loaded_language );
443+
444+
if ( is_wp_error( $result ) ) {
445+
display_setup_form( $result->get_error_message() );
446+
break;
447+
}
443448
?>
444449

445450
<h1><?php _e( 'Success!' ); ?></h1>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
/**
4+
* @group admin
5+
* @group upgrade
6+
*/
7+
class Tests_Admin_IncludesUpgrade extends WP_UnitTestCase {
8+
9+
/**
10+
* The original database table prefix.
11+
*
12+
* @var string
13+
*/
14+
private $original_prefix;
15+
16+
/**
17+
* The database table prefix used during the test.
18+
*
19+
* @var string
20+
*/
21+
private $test_prefix = 'wptests_install_failure_';
22+
23+
/**
24+
* Loads the upgrade functions before the tests run.
25+
*
26+
* @param WP_UnitTest_Factory $factory Test factory.
27+
*/
28+
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
29+
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
30+
}
31+
32+
/**
33+
* Sets up the test fixture.
34+
*/
35+
public function set_up() {
36+
global $wpdb, $table_prefix;
37+
38+
parent::set_up();
39+
40+
$this->original_prefix = $wpdb->base_prefix;
41+
42+
$wpdb->set_prefix( $this->test_prefix );
43+
$table_prefix = $this->test_prefix;
44+
45+
$this->drop_test_tables();
46+
}
47+
48+
/**
49+
* Tears down the test fixture.
50+
*/
51+
public function tear_down() {
52+
global $wpdb, $table_prefix;
53+
54+
remove_filter( 'query', array( $this, 'force_create_table_failure' ) );
55+
56+
$this->drop_test_tables();
57+
58+
$wpdb->set_prefix( $this->original_prefix );
59+
$table_prefix = $this->original_prefix;
60+
61+
parent::tear_down();
62+
}
63+
64+
/**
65+
* Ensures installation fails when schema creation fails.
66+
*/
67+
public function test_wp_install_returns_wp_error_when_schema_creation_fails() {
68+
global $wpdb;
69+
70+
$show_errors = $wpdb->hide_errors();
71+
$suppress_errors = $wpdb->suppress_errors();
72+
73+
add_filter( 'query', array( $this, 'force_create_table_failure' ) );
74+
75+
$result = wp_install( 'WordPress Develop', 'admin', 'test@example.com', 1, '', 'password' );
76+
77+
remove_filter( 'query', array( $this, 'force_create_table_failure' ) );
78+
79+
$wpdb->show_errors( $show_errors );
80+
$wpdb->suppress_errors( $suppress_errors );
81+
82+
$this->assertWPError( $result );
83+
$this->assertSame( 'db_install_missing_tables', $result->get_error_code() );
84+
$this->assertStringContainsString( 'could not create the database tables required for installation', $result->get_error_message() );
85+
$this->assertContains( $wpdb->options, $result->get_error_data()['missing_tables'] );
86+
}
87+
88+
/**
89+
* Forces CREATE TABLE queries to fail for the installer schema step.
90+
*
91+
* @param string $query Database query.
92+
* @return string Filtered query.
93+
*/
94+
public function force_create_table_failure( $query ) {
95+
if ( 0 === strpos( trim( $query ), 'CREATE TABLE' ) ) {
96+
return 'CREATE TABL broken_installer_schema';
97+
}
98+
99+
return $query;
100+
}
101+
102+
/**
103+
* Drops any tables that were created with the test prefix.
104+
*/
105+
private function drop_test_tables() {
106+
global $wpdb;
107+
108+
$tables = $wpdb->tables( 'all', true );
109+
110+
if ( empty( $tables ) ) {
111+
return;
112+
}
113+
114+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
115+
$wpdb->query( 'DROP TABLE IF EXISTS ' . implode( ', ', $tables ) );
116+
}
117+
}

0 commit comments

Comments
 (0)