From 34b0ac5baea7c4e7e449ea20806fb8ff617ecb1c Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 27 Aug 2024 08:04:23 +1000 Subject: [PATCH 1/8] Tests for get_option(). --- tests/phpunit/tests/option/getOptions.php | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/phpunit/tests/option/getOptions.php b/tests/phpunit/tests/option/getOptions.php index 7435dce1882d1..e5f3a1ce568d9 100644 --- a/tests/phpunit/tests/option/getOptions.php +++ b/tests/phpunit/tests/option/getOptions.php @@ -88,4 +88,63 @@ public function test_get_options_with_nonexistent_options() { $this->assertFalse( $options['nonexistent_option'], 'nonexistent_option is present in option.' ); } + + /** + * Test get_option() returns the same type when cached and uncached. + * + * @ticket 32848 + * + * @dataProvider data_get_option_return_type_cached_and_uncached + * + * @param mixed $option_vale The value to test. + */ + public function test_get_option_return_type_cached_and_uncached( $option_vale ) { + $option_name = 'option_for_type_testing'; + + // Set the option value. + update_option( $option_name, $option_vale, false ); + + // Get the option while cached. + $option_cached = get_option( $option_name ); + + // Clear the cache. + wp_cache_delete( $option_name, 'options' ); + + // Get the option while uncached. + $option_uncached = get_option( $option_name ); + + // Check that the return type is the same. + $this->assertSame( gettype( $option_cached ), gettype( $option_uncached ), 'The return type is not the same.' ); + $this->assertEqualsCanonicalizing( $option_cached, $option_uncached, 'The option values are not the same.' ); + } + + /** + * Data provider for test_get_option_return_type_cached_and_uncached(). + * + * @return array[] + */ + public function data_get_option_return_type_cached_and_uncached() { + return array( + 'an empty string' => array( '' ), + 'a string with spaces' => array( ' ' ), + 'a string with tabs' => array( "\t" ), + 'a string with new lines' => array( "\n" ), + 'a string with carriage returns' => array( "\r" ), + 'int -1' => array( -1 ), + 'int 0' => array( 0 ), + 'int 1' => array( 1 ), + 'float -1.0' => array( -1.0 ), + 'float 0.0' => array( 0.0 ), + 'float 1.0' => array( 1.0 ), + 'false' => array( false ), + 'true' => array( true ), + 'null' => array( null ), + 'an empty array' => array( array() ), + 'a non-empty array' => array( array( 'value' ) ), + 'an empty object' => array( new stdClass() ), + 'a non-empty object' => array( (object) array( 'value' ) ), + 'INF' => array( INF ), + 'NAN' => array( NAN ), + ); + } } From 40c7bdec4fce2203867299f50319341943119c21 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 27 Aug 2024 08:16:12 +1000 Subject: [PATCH 2/8] Tests for get_network_option() Cache key. CS I will never remember. --- tests/phpunit/tests/option/networkOption.php | 68 ++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/phpunit/tests/option/networkOption.php b/tests/phpunit/tests/option/networkOption.php index a4247d4926cec..33e0458a670ab 100644 --- a/tests/phpunit/tests/option/networkOption.php +++ b/tests/phpunit/tests/option/networkOption.php @@ -412,4 +412,72 @@ public function test_delete_network_option_does_not_use_single_site_notoptions_c $this->assertIsArray( $network_notoptions_cache, 'Multisite notoptions cache should be set.' ); $this->assertArrayHasKey( 'ticket_61730_notoption', $network_notoptions_cache, 'The option should be in the notoptions cache.' ); } + + /** + * Test get_network_option() returns the same type when cached and uncached. + * + * @ticket 32848 + * + * @covers ::get_network_option + + * @dataProvider data_get_network_option_return_type_cached_and_uncached + * + * @param mixed $option_vale The value to test. + */ + public function test_get_network_option_return_type_cached_and_uncached( $option_vale ) { + $option_name = 'option_for_type_testing'; + if ( is_multisite() ) { + $cache_key = "1:$option_name"; + $cache_group = 'site-options'; + } else { + $cache_key = $option_name; + $cache_group = 'options'; + } + + // Set the option value. + update_network_option( 1, $option_name, $option_vale, false ); + + // Get the option while cached. + $option_cached = get_network_option( 1, $option_name ); + + // Clear the cache. + wp_cache_delete( $cache_key, $cache_group ); + + // Get the option while uncached. + $option_uncached = get_network_option( 1, $option_name ); + + // Check that the return type is the same. + $this->assertSame( gettype( $option_cached ), gettype( $option_uncached ), 'The return type is not the same.' ); + $this->assertEqualsCanonicalizing( $option_cached, $option_uncached, 'The option values are not the same.' ); + } + + /** + * Data provider for test_get_network_option_return_type_cached_and_uncached(). + * + * @return array[] + */ + public function data_get_network_option_return_type_cached_and_uncached() { + return array( + 'an empty string' => array( '' ), + 'a string with spaces' => array( ' ' ), + 'a string with tabs' => array( "\t" ), + 'a string with new lines' => array( "\n" ), + 'a string with carriage returns' => array( "\r" ), + 'int -1' => array( -1 ), + 'int 0' => array( 0 ), + 'int 1' => array( 1 ), + 'float -1.0' => array( -1.0 ), + 'float 0.0' => array( 0.0 ), + 'float 1.0' => array( 1.0 ), + 'false' => array( false ), + 'true' => array( true ), + 'null' => array( null ), + 'an empty array' => array( array() ), + 'a non-empty array' => array( array( 'value' ) ), + 'an empty object' => array( new stdClass() ), + 'a non-empty object' => array( (object) array( 'value' ) ), + 'INF' => array( INF ), + 'NAN' => array( NAN ), + ); + } } From ec26ed8759e45623b280fd47d54f5fc884bfd8a0 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sun, 4 Aug 2024 10:19:35 +1000 Subject: [PATCH 3/8] Cast option DB values to strings. --- src/wp-includes/option.php | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index 537f7aafd0aba..ba242348a43c7 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -922,6 +922,15 @@ function update_option( $option, $value, $autoload = null ) { */ do_action( 'update_option', $option, $old_value, $value ); + /* + * Ensure the serialized value is a string. + * + * This ensure that the option is stored in the cache in the same format as the + * option is stored in the database. Rather than type casting, sprintf is used to + * match the process used by wpdb::prepare(). + */ + $serialized_value = sprintf( '%s', $serialized_value ); + $update_args = array( 'option_value' => $serialized_value, ); @@ -1117,6 +1126,15 @@ function add_option( $option, $value = '', $deprecated = '', $autoload = null ) */ do_action( 'add_option', $option, $value ); + /* + * Ensure the serialized value is a string. + * + * This ensure that the option is stored in the cache in the same format as the + * option is stored in the database. Rather than type casting, sprintf is used to + * match the process used by wpdb::prepare(). + */ + $serialized_value = sprintf( '%s', $serialized_value ); + $result = $wpdb->query( $wpdb->prepare( "INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE `option_name` = VALUES(`option_name`), `option_value` = VALUES(`option_value`), `autoload` = VALUES(`autoload`)", $option, $serialized_value, $autoload ) ); if ( ! $result ) { return false; @@ -2147,6 +2165,16 @@ function add_network_option( $network_id, $option, $value ) { $value = sanitize_option( $option, $value ); $serialized_value = maybe_serialize( $value ); + + /* + * Ensure the serialized value is a string. + * + * This ensure that the option is stored in the cache in the same format as the + * option is stored in the database. Rather than type casting, sprintf is used to + * match the process used by wpdb::prepare(). + */ + $serialized_value = sprintf( '%s', $serialized_value ); + $result = $wpdb->insert( $wpdb->sitemeta, array( @@ -2390,6 +2418,15 @@ function update_network_option( $network_id, $option, $value ) { $value = sanitize_option( $option, $value ); $serialized_value = maybe_serialize( $value ); + /* + * Ensure the serialized value is a string. + * + * This ensure that the option is stored in the cache in the same format as the + * option is stored in the database. Rather than type casting, sprintf is used to + * match the process used by wpdb::prepare(). + */ + $serialized_value = sprintf( '%s', $serialized_value ); + $result = $wpdb->update( $wpdb->sitemeta, array( 'meta_value' => $serialized_value ), From f6f5cf47893bfacba5cfb158fd9b6d968b2d07ad Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sun, 4 Aug 2024 10:35:03 +1000 Subject: [PATCH 4/8] Avoid updates. --- src/wp-includes/option.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index ba242348a43c7..db8866843d75f 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -891,6 +891,8 @@ function update_option( $option, $value, $autoload = null ) { */ $value = apply_filters( 'pre_update_option', $value, $option, $old_value ); + $serialized_value = maybe_serialize( $value ); + /* * If the new and old values are the same, no need to update. * @@ -900,7 +902,7 @@ function update_option( $option, $value, $autoload = null ) { * * See https://core.trac.wordpress.org/ticket/38903 */ - if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) { + if ( sprintf( '%s', $serialized_value ) === $old_value || maybe_serialize( $old_value ) === $serialized_value ) { return false; } @@ -909,8 +911,6 @@ function update_option( $option, $value, $autoload = null ) { return add_option( $option, $value, '', $autoload ); } - $serialized_value = maybe_serialize( $value ); - /** * Fires immediately before an option value is updated. * @@ -2387,6 +2387,8 @@ function update_network_option( $network_id, $option, $value ) { */ $value = apply_filters( "pre_update_site_option_{$option}", $value, $old_value, $option, $network_id ); + $serialized_value = maybe_serialize( $value ); + /* * If the new and old values are the same, no need to update. * @@ -2396,7 +2398,7 @@ function update_network_option( $network_id, $option, $value ) { * * See https://core.trac.wordpress.org/ticket/44956 */ - if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) { + if ( sprintf( '%s', $serialized_value ) === $old_value || maybe_serialize( $old_value ) === $serialized_value ) { return false; } @@ -2417,7 +2419,6 @@ function update_network_option( $network_id, $option, $value ) { } else { $value = sanitize_option( $option, $value ); - $serialized_value = maybe_serialize( $value ); /* * Ensure the serialized value is a string. * From d2f5231beb16c04f09dff9b2d011054588fd4c32 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 20 Aug 2024 10:12:54 +1000 Subject: [PATCH 5/8] Cast various option values as required. --- .../twentytwenty/classes/class-twentytwenty-walker-page.php | 2 +- src/wp-includes/blocks.php | 4 ++-- tests/phpunit/tests/https-migration.php | 2 +- tests/phpunit/tests/taxonomy.php | 4 ++-- tests/phpunit/tests/term/splitSharedTerm.php | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/wp-content/themes/twentytwenty/classes/class-twentytwenty-walker-page.php b/src/wp-content/themes/twentytwenty/classes/class-twentytwenty-walker-page.php index 74ff332eed9b7..a83785a2cc18a 100644 --- a/src/wp-content/themes/twentytwenty/classes/class-twentytwenty-walker-page.php +++ b/src/wp-content/themes/twentytwenty/classes/class-twentytwenty-walker-page.php @@ -63,7 +63,7 @@ public function start_el( &$output, $data_object, $depth = 0, $args = array(), $ } elseif ( $_current_page && $page->ID === $_current_page->post_parent ) { $css_class[] = 'current_page_parent'; } - } elseif ( get_option( 'page_for_posts' ) === $page->ID ) { + } elseif ( (int) get_option( 'page_for_posts' ) === $page->ID ) { $css_class[] = 'current_page_parent'; } diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 9b24b989d4024..d50343a7c6881 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -2547,8 +2547,8 @@ function build_comment_query_vars_from_block( $block ) { $comment_args['hierarchical'] = false; } - if ( get_option( 'page_comments' ) === '1' || get_option( 'page_comments' ) === true ) { - $per_page = get_option( 'comments_per_page' ); + if ( (bool) get_option( 'page_comments' ) === true ) { + $per_page = (int) get_option( 'comments_per_page' ); $default_page = get_option( 'default_comments_page' ); if ( $per_page > 0 ) { $comment_args['number'] = $per_page; diff --git a/tests/phpunit/tests/https-migration.php b/tests/phpunit/tests/https-migration.php index ae66738603e13..f7c243c0396d3 100644 --- a/tests/phpunit/tests/https-migration.php +++ b/tests/phpunit/tests/https-migration.php @@ -109,7 +109,7 @@ public function test_wp_update_https_migration_required() { // Changing HTTP to HTTPS on a site with content should result in flag being set, requiring migration. update_option( 'fresh_site', '0' ); wp_update_https_migration_required( 'http://example.org', 'https://example.org' ); - $this->assertTrue( get_option( 'https_migration_required' ) ); + $this->assertTrue( (bool) get_option( 'https_migration_required' ) ); // Changing another part than the scheme should delete/reset the flag because changing those parts (e.g. the // domain) can have further implications. diff --git a/tests/phpunit/tests/taxonomy.php b/tests/phpunit/tests/taxonomy.php index 2a4ee3b560d08..8a80bec432e81 100644 --- a/tests/phpunit/tests/taxonomy.php +++ b/tests/phpunit/tests/taxonomy.php @@ -1056,7 +1056,7 @@ public function test_default_term_for_custom_taxonomy() { // Test default term. $term = wp_get_post_terms( $post_id, $tax ); - $this->assertSame( get_option( 'default_term_' . $tax ), $term[0]->term_id ); + $this->assertSame( get_option( 'default_term_' . $tax ), (string) $term[0]->term_id ); // Test default term deletion. $this->assertSame( wp_delete_term( $term[0]->term_id, $tax ), 0 ); @@ -1077,7 +1077,7 @@ public function test_default_term_for_custom_taxonomy() { // Test default term. $term = wp_get_post_terms( $post_id, $tax ); - $this->assertSame( get_option( 'default_term_' . $tax ), $term[0]->term_id ); + $this->assertSame( get_option( 'default_term_' . $tax ), (string) $term[0]->term_id ); // wp_set_object_terms() should not assign default term. wp_set_object_terms( $post_id, array(), $tax ); diff --git a/tests/phpunit/tests/term/splitSharedTerm.php b/tests/phpunit/tests/term/splitSharedTerm.php index ece1d534ada51..353215abf3926 100644 --- a/tests/phpunit/tests/term/splitSharedTerm.php +++ b/tests/phpunit/tests/term/splitSharedTerm.php @@ -183,12 +183,12 @@ public function test_should_update_default_category_on_term_split() { ); clean_term_cache( $t1['term_id'], 'category' ); - $this->assertSame( $t1['term_id'], get_option( 'default_category', -1 ) ); + $this->assertSame( $t1['term_id'], (int) get_option( 'default_category', -1 ) ); $new_term_id = _split_shared_term( $t1['term_id'], $t1['term_taxonomy_id'] ); $this->assertNotEquals( $new_term_id, $t1['term_id'] ); - $this->assertSame( $new_term_id, get_option( 'default_category', -1 ) ); + $this->assertSame( $new_term_id, (int) get_option( 'default_category', -1 ) ); } /** From 77822e73325af539460a743e142a2b380da8bbb6 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 20 Aug 2024 10:57:34 +1000 Subject: [PATCH 6/8] Additional type casting here and there. --- src/wp-includes/class-wp-site.php | 2 +- src/wp-includes/ms-functions.php | 2 +- tests/phpunit/tests/ajax/wpAjaxWpCompressionTest.php | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-site.php b/src/wp-includes/class-wp-site.php index fcd07cd5e3175..7f21be15b499a 100644 --- a/src/wp-includes/class-wp-site.php +++ b/src/wp-includes/class-wp-site.php @@ -329,7 +329,7 @@ private function get_details() { } $details->blogname = get_option( 'blogname' ); $details->siteurl = get_option( 'siteurl' ); - $details->post_count = get_option( 'post_count' ); + $details->post_count = (int) get_option( 'post_count' ); $details->home = get_option( 'home' ); restore_current_blog(); diff --git a/src/wp-includes/ms-functions.php b/src/wp-includes/ms-functions.php index 913f22543f2bf..a46196b151eb9 100644 --- a/src/wp-includes/ms-functions.php +++ b/src/wp-includes/ms-functions.php @@ -2558,7 +2558,7 @@ function get_space_allowed() { $space_allowed = get_option( 'blog_upload_space' ); if ( ! is_numeric( $space_allowed ) ) { - $space_allowed = get_site_option( 'blog_upload_space' ); + $space_allowed = (int) get_site_option( 'blog_upload_space' ); } if ( ! is_numeric( $space_allowed ) ) { diff --git a/tests/phpunit/tests/ajax/wpAjaxWpCompressionTest.php b/tests/phpunit/tests/ajax/wpAjaxWpCompressionTest.php index c7fa70528b264..63bc5ef173bb0 100644 --- a/tests/phpunit/tests/ajax/wpAjaxWpCompressionTest.php +++ b/tests/phpunit/tests/ajax/wpAjaxWpCompressionTest.php @@ -148,7 +148,7 @@ public function test_set_yes() { } // Check the site option is not changed due to lack of nonce. - $this->assertSame( 0, get_site_option( 'can_compress_scripts' ) ); + $this->assertSame( '0', get_site_option( 'can_compress_scripts' ) ); // Add a nonce. $_GET['_ajax_nonce'] = wp_create_nonce( 'update_can_compress_scripts' ); @@ -161,7 +161,7 @@ public function test_set_yes() { } // Check the site option is changed. - $this->assertSame( 1, get_site_option( 'can_compress_scripts' ) ); + $this->assertSame( '1', get_site_option( 'can_compress_scripts' ) ); } /** @@ -186,7 +186,7 @@ public function test_set_no() { } // Check the site option is not changed due to lack of nonce. - $this->assertSame( 1, get_site_option( 'can_compress_scripts' ) ); + $this->assertSame( '1', get_site_option( 'can_compress_scripts' ) ); // Add a nonce. $_GET['_ajax_nonce'] = wp_create_nonce( 'update_can_compress_scripts' ); @@ -199,7 +199,7 @@ public function test_set_no() { } // Check the site option is changed. - $this->assertSame( 0, get_site_option( 'can_compress_scripts' ) ); + $this->assertSame( '0', get_site_option( 'can_compress_scripts' ) ); } /** From 667df5480b55f10aad5c9fb90773cd55feb113ec Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 20 Aug 2024 11:11:11 +1000 Subject: [PATCH 7/8] Cast space allowed to an integer later in the function. --- src/wp-includes/ms-functions.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/ms-functions.php b/src/wp-includes/ms-functions.php index a46196b151eb9..059a0d1b7ea6b 100644 --- a/src/wp-includes/ms-functions.php +++ b/src/wp-includes/ms-functions.php @@ -2558,13 +2558,16 @@ function get_space_allowed() { $space_allowed = get_option( 'blog_upload_space' ); if ( ! is_numeric( $space_allowed ) ) { - $space_allowed = (int) get_site_option( 'blog_upload_space' ); + $space_allowed = get_site_option( 'blog_upload_space' ); } if ( ! is_numeric( $space_allowed ) ) { $space_allowed = 100; } + // Cast the value to an integer. + $space_allowed = (int) $space_allowed; + /** * Filters the upload quota for the current site. * From 3750a6d2c66bfd47694ac20f58ba4df6440bf803 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 27 Aug 2024 08:20:37 +1000 Subject: [PATCH 8/8] Comment on why the tests are as they are. --- tests/phpunit/tests/option/getOptions.php | 6 ++++++ tests/phpunit/tests/option/networkOption.php | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/tests/phpunit/tests/option/getOptions.php b/tests/phpunit/tests/option/getOptions.php index e5f3a1ce568d9..950884a4aa885 100644 --- a/tests/phpunit/tests/option/getOptions.php +++ b/tests/phpunit/tests/option/getOptions.php @@ -115,6 +115,12 @@ public function test_get_option_return_type_cached_and_uncached( $option_vale ) // Check that the return type is the same. $this->assertSame( gettype( $option_cached ), gettype( $option_uncached ), 'The return type is not the same.' ); + /* + * Check canonicalized value. + * + * This is done separately from the above check to avoid false negatives + * for objects as assertSame checks for the same instance. + */ $this->assertEqualsCanonicalizing( $option_cached, $option_uncached, 'The option values are not the same.' ); } diff --git a/tests/phpunit/tests/option/networkOption.php b/tests/phpunit/tests/option/networkOption.php index 33e0458a670ab..766f13cc665af 100644 --- a/tests/phpunit/tests/option/networkOption.php +++ b/tests/phpunit/tests/option/networkOption.php @@ -448,6 +448,12 @@ public function test_get_network_option_return_type_cached_and_uncached( $option // Check that the return type is the same. $this->assertSame( gettype( $option_cached ), gettype( $option_uncached ), 'The return type is not the same.' ); + /* + * Check canonicalized value. + * + * This is done separately from the above check to avoid false negatives + * for objects as assertSame checks for the same instance. + */ $this->assertEqualsCanonicalizing( $option_cached, $option_uncached, 'The option values are not the same.' ); }