diff --git a/src/wp-includes/meta.php b/src/wp-includes/meta.php index 577785c2163a0..96542f993fece 100644 --- a/src/wp-includes/meta.php +++ b/src/wp-includes/meta.php @@ -611,6 +611,7 @@ function get_metadata( $meta_type, $object_id, $meta_key = '', $single = false ) * Retrieves raw metadata value for the specified object. * * @since 5.5.0 + * @since 7.2.0 A cached value that is not an array is now treated as a cache miss. * * @param string $meta_type Type of object metadata is for. Accepts 'blog', 'post', 'comment', 'term', * 'user', or any other object type with an associated meta table. @@ -672,7 +673,8 @@ function get_metadata_raw( $meta_type, $object_id, $meta_key = '', $single = fal $meta_cache = wp_cache_get( $object_id, $meta_type . '_meta' ); - if ( ! $meta_cache ) { + // A cached value that is not an array is unusable, treat it as a cache miss. + if ( ! $meta_cache || ! is_array( $meta_cache ) ) { $meta_cache = update_meta_cache( $meta_type, array( $object_id ) ); $meta_cache = $meta_cache[ $object_id ] ?? null; } @@ -753,6 +755,7 @@ function get_metadata_default( $meta_type, $object_id, $meta_key, $single = fals * Determines if a meta field with the given key exists for the given object ID. * * @since 3.3.0 + * @since 7.2.0 A cached value that is not an array is now treated as a cache miss. * * @param string $meta_type Type of object metadata is for. Accepts 'blog', 'post', 'comment', 'term', * 'user', or any other object type with an associated meta table. @@ -778,9 +781,10 @@ function metadata_exists( $meta_type, $object_id, $meta_key ) { $meta_cache = wp_cache_get( $object_id, $meta_type . '_meta' ); - if ( ! $meta_cache ) { + // A cached value that is not an array is unusable, treat it as a cache miss. + if ( ! $meta_cache || ! is_array( $meta_cache ) ) { $meta_cache = update_meta_cache( $meta_type, array( $object_id ) ); - $meta_cache = $meta_cache[ $object_id ]; + $meta_cache = $meta_cache[ $object_id ] ?? null; } if ( isset( $meta_cache[ $meta_key ] ) ) { @@ -1126,6 +1130,7 @@ function delete_metadata_by_mid( $meta_type, $meta_id ) { * Updates the metadata cache for the specified objects. * * @since 2.9.0 + * @since 7.2.0 A cached value that is not an array is now treated as a cache miss. * * @global wpdb $wpdb WordPress database abstraction object. * @@ -1182,12 +1187,17 @@ function update_meta_cache( $meta_type, $object_ids ) { $cache_group = $meta_type . '_meta'; $non_cached_ids = array(); + $invalid_ids = array(); $cache = array(); $cache_values = wp_cache_get_multiple( $object_ids, $cache_group ); foreach ( $cache_values as $id => $cached_object ) { if ( false === $cached_object ) { $non_cached_ids[] = $id; + } elseif ( ! is_array( $cached_object ) ) { + // A cached value that is not an array is unusable, treat it as a cache miss. + $non_cached_ids[] = $id; + $invalid_ids[] = $id; } else { $cache[ $id ] = $cached_object; } @@ -1229,6 +1239,16 @@ function update_meta_cache( $meta_type, $object_ids ) { } $data[ $id ] = $cache[ $id ]; } + + /* + * Remove unusable cached values so that the regenerated values can be added. + * A delete followed by an add is used instead of wp_cache_set_multiple() + * so that wp_suspend_cache_addition() is still respected. + */ + if ( ! empty( $invalid_ids ) ) { + wp_cache_delete_multiple( $invalid_ids, $cache_group ); + } + wp_cache_add_multiple( $data, $cache_group ); return $cache; diff --git a/tests/phpunit/tests/meta.php b/tests/phpunit/tests/meta.php index 6431a158610a5..c0d6d05df7528 100644 --- a/tests/phpunit/tests/meta.php +++ b/tests/phpunit/tests/meta.php @@ -131,6 +131,115 @@ public function test_metadata_exists_with_filter() { remove_filter( 'get_user_metadata', '__return_zero' ); } + /** + * Non-array values that can not be used as a meta cache entry. + * + * @return array + */ + public function data_non_array_cache_values(): array { + return array( + 'object' => array( new stdClass() ), + 'string' => array( 'meta_value' ), + 'integer' => array( 1 ), + 'float' => array( 1.5 ), + 'true' => array( true ), + ); + } + + /** + * @ticket 66091 + * + * @dataProvider data_non_array_cache_values + * + * @param mixed $cached_value Value to place in the meta cache. + */ + public function test_metadata_exists_treats_non_array_cache_value_as_miss( $cached_value ): void { + wp_cache_set( self::$author->ID, $cached_value, 'user_meta' ); + + $this->assertTrue( metadata_exists( 'user', self::$author->ID, 'meta_key' ) ); + $this->assertFalse( metadata_exists( 'user', self::$author->ID, 'foobarbaz' ) ); + $this->assertIsArray( wp_cache_get( self::$author->ID, 'user_meta' ), 'The unusable cache value should have been replaced.' ); + } + + /** + * @ticket 66091 + * + * @dataProvider data_non_array_cache_values + * + * @param mixed $cached_value Value to place in the meta cache. + */ + public function test_get_metadata_treats_non_array_cache_value_as_miss( $cached_value ): void { + wp_cache_set( self::$author->ID, $cached_value, 'user_meta' ); + + $this->assertSame( 'meta_value', get_metadata( 'user', self::$author->ID, 'meta_key', true ) ); + $this->assertSame( array( 'meta_value' ), get_metadata( 'user', self::$author->ID, 'meta_key' ) ); + $this->assertIsArray( wp_cache_get( self::$author->ID, 'user_meta' ), 'The unusable cache value should have been replaced.' ); + } + + /** + * @ticket 66091 + * + * @dataProvider data_non_array_cache_values + * + * @param mixed $cached_value Value to place in the meta cache. + */ + public function test_get_metadata_with_empty_key_treats_non_array_cache_value_as_miss( $cached_value ): void { + wp_cache_set( self::$author->ID, $cached_value, 'user_meta' ); + + $meta = get_metadata( 'user', self::$author->ID ); + + $this->assertIsArray( $meta ); + $this->assertSame( array( 'meta_value' ), $meta['meta_key'] ); + } + + /** + * @ticket 66091 + * + * @dataProvider data_non_array_cache_values + * + * @param mixed $cached_value Value to place in the meta cache. + */ + public function test_update_meta_cache_replaces_non_array_cache_value( $cached_value ): void { + wp_cache_set( self::$author->ID, $cached_value, 'user_meta' ); + + $meta_cache = update_meta_cache( 'user', array( self::$author->ID ) ); + + $this->assertIsArray( $meta_cache[ self::$author->ID ] ); + $this->assertSame( array( 'meta_value' ), $meta_cache[ self::$author->ID ]['meta_key'] ); + + $cached = wp_cache_get( self::$author->ID, 'user_meta' ); + $this->assertIsArray( $cached, 'The unusable cache value should have been replaced.' ); + $this->assertSame( array( 'meta_value' ), $cached['meta_key'] ); + } + + /** + * @ticket 66091 + */ + public function test_update_meta_cache_replaces_non_array_cache_value_for_object_without_meta(): void { + $term_id = self::factory()->term->create(); + + wp_cache_set( $term_id, new stdClass(), 'term_meta' ); + + $meta_cache = update_meta_cache( 'term', array( $term_id ) ); + + $this->assertSame( array(), $meta_cache[ $term_id ] ); + $this->assertSame( array(), wp_cache_get( $term_id, 'term_meta' ) ); + } + + /** + * @ticket 66091 + */ + public function test_update_meta_cache_removes_non_array_cache_value_while_cache_addition_is_suspended(): void { + wp_cache_set( self::$author->ID, new stdClass(), 'user_meta' ); + + wp_suspend_cache_addition( true ); + $meta_cache = update_meta_cache( 'user', array( self::$author->ID ) ); + wp_suspend_cache_addition( false ); + + $this->assertSame( array( 'meta_value' ), $meta_cache[ self::$author->ID ]['meta_key'] ); + $this->assertFalse( wp_cache_get( self::$author->ID, 'user_meta' ), 'The unusable cache value should be removed but not replaced while cache addition is suspended.' ); + } + /** * @ticket 18158 */