diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 31e940b2076e1..78d601a5f2a12 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -2835,6 +2835,89 @@ public function get_test_opcode_cache(): array { return $result; } + /** + * Tests whether any of the site's database tables use the legacy utf8/utf8mb3 charset. + * + * The utf8mb3 charset (historically also exposed as `utf8` in MySQL) can only store + * 3-byte characters, so it cannot store emoji and some other multi-byte characters. + * It has been deprecated by MySQL and may be removed in a future release. + * + * @since 7.2.0 + * + * @return array> The test result. + */ + public function get_test_utf8mb3_usage() { + global $wpdb; + + $result = array( + 'label' => __( 'Your database tables use the current character encoding' ), + 'status' => 'good', + 'badge' => array( + 'label' => __( 'Security' ), + 'color' => 'blue', + ), + 'description' => sprintf( + '

%s

', + __( 'The utf8mb3 character set (sometimes shown as utf8) can only store 3-byte characters, so it cannot store emoji and some other characters. It has been deprecated by MySQL and may be removed in a future release.' ) + ), + 'actions' => '', + 'test' => 'utf8mb3_usage', + ); + + if ( empty( $wpdb->is_mysql ) ) { + return $result; + } + + /** + * Filters the list of database tables to check for the legacy utf8/utf8mb3 character set. + * + * @since 7.2.0 + * + * @param string[] $tables Prefixed names of the tables to check. + */ + $tables = apply_filters( 'site_status_utf8mb3_usage_tables', array_values( $wpdb->tables( 'all', true ) ) ); + + $affected_list = array(); + + foreach ( $tables as $table ) { + $columns = $wpdb->get_results( "SHOW FULL COLUMNS FROM `$table`" ); + + if ( ! $columns ) { + continue; + } + + foreach ( $columns as $column ) { + if ( empty( $column->Collation ) ) { + continue; + } + + list( $charset ) = explode( '_', $column->Collation ); + + if ( in_array( strtolower( $charset ), array( 'utf8', 'utf8mb3' ), true ) ) { + $affected_list[] = sprintf( '%s.%s', $table, $column->Field ); + } + } + } + + if ( empty( $affected_list ) ) { + return $result; + } + + $result['status'] = 'recommended'; + $result['label'] = __( 'Some database tables use the outdated utf8mb3 character set' ); + + $result['description'] .= sprintf( + '

%s

', + sprintf( + /* translators: %s: Comma-separated list of affected table and column names. */ + __( 'The following tables and columns use the utf8mb3 character set: %s. Consider converting them to utf8mb4.' ), + implode( ', ', $affected_list ) + ) + ); + + return $result; + } + /** * Returns a set of tests that belong to the site status page. * @@ -2935,6 +3018,10 @@ public static function get_tests() { 'label' => __( 'Opcode cache' ), 'test' => 'opcode_cache', ), + 'utf8mb3_usage' => array( + 'label' => __( 'Database character encoding' ), + 'test' => 'utf8mb3_usage', + ), ), 'async' => array( 'dotorg_communication' => array( diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php index 6080b477f54c3..d754326797758 100644 --- a/tests/phpunit/tests/admin/wpSiteHealth.php +++ b/tests/phpunit/tests/admin/wpSiteHealth.php @@ -707,4 +707,75 @@ public function test_get_test_opcode_cache_result_by_environment() { $this->assertStringContainsString( __( 'Enabling this cache can significantly improve the performance of your site.' ), $result['description'] ); } } + + /** + * Tests get_test_utf8mb3_usage() when no tables use the legacy utf8/utf8mb3 charset. + * + * @ticket 66109 + * + * @covers ::get_test_utf8mb3_usage() + */ + public function test_get_test_utf8mb3_usage_no_legacy_charset() { + global $wpdb; + + if ( empty( $wpdb->is_mysql ) ) { + $this->markTestSkipped( 'This test requires a MySQL server.' ); + } + + $table_name = $wpdb->prefix . 'test_utf8mb3_usage_good'; + $wpdb->query( "DROP TABLE IF EXISTS `$table_name`" ); + $wpdb->query( "CREATE TABLE `$table_name` ( some_column VARCHAR(50) CHARACTER SET utf8mb4 )" ); + + $filter = self::only_table_filter( $table_name ); + add_filter( 'site_status_utf8mb3_usage_tables', $filter ); + $result = $this->instance->get_test_utf8mb3_usage(); + remove_filter( 'site_status_utf8mb3_usage_tables', $filter ); + + $wpdb->query( "DROP TABLE `$table_name`" ); + + $this->assertSame( 'good', $result['status'], 'Status should be "good" when no tables use the legacy utf8/utf8mb3 charset.' ); + $this->assertSame( 'utf8mb3_usage', $result['test'] ); + } + + /** + * Tests get_test_utf8mb3_usage() when a table uses the legacy utf8/utf8mb3 charset. + * + * @ticket 66109 + * + * @covers ::get_test_utf8mb3_usage() + */ + public function test_get_test_utf8mb3_usage_with_legacy_charset() { + global $wpdb; + + if ( empty( $wpdb->is_mysql ) ) { + $this->markTestSkipped( 'This test requires a MySQL server.' ); + } + + $table_name = $wpdb->prefix . 'test_utf8mb3_usage'; + $wpdb->query( "DROP TABLE IF EXISTS `$table_name`" ); + $wpdb->query( "CREATE TABLE `$table_name` ( legacy_column VARCHAR(50) CHARACTER SET utf8mb3, other_column VARCHAR(50) CHARACTER SET utf8mb4 )" ); + + $filter = self::only_table_filter( $table_name ); + add_filter( 'site_status_utf8mb3_usage_tables', $filter ); + $result = $this->instance->get_test_utf8mb3_usage(); + remove_filter( 'site_status_utf8mb3_usage_tables', $filter ); + + $wpdb->query( "DROP TABLE `$table_name`" ); + + $this->assertSame( 'recommended', $result['status'], 'Status should be "recommended" when a table uses the legacy utf8/utf8mb3 charset.' ); + $this->assertStringContainsString( $table_name . '.legacy_column', $result['description'] ); + $this->assertStringNotContainsString( $table_name . '.other_column', $result['description'] ); + } + + /** + * Builds a `site_status_utf8mb3_usage_tables` filter callback scoped to a single table. + * + * @param string $table_name The only table name the filtered test should scan. + * @return callable The filter callback. + */ + private static function only_table_filter( $table_name ) { + return static function () use ( $table_name ) { + return array( $table_name ); + }; + } }