diff --git a/src/wp-includes/meta.php b/src/wp-includes/meta.php index dd28b027212ab..6adcf389d5884 100644 --- a/src/wp-includes/meta.php +++ b/src/wp-includes/meta.php @@ -632,6 +632,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. @@ -708,7 +709,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; } @@ -792,6 +794,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. @@ -817,9 +820,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 ] ) ) { @@ -1165,6 +1169,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. * @@ -1219,19 +1224,33 @@ function update_meta_cache( $meta_type, $object_ids ) { return (bool) $check; } - $cache_group = $meta_type . '_meta'; - $non_cached_ids = array(); - $cache = array(); - $cache_values = wp_cache_get_multiple( $object_ids, $cache_group ); + $cache_group = $meta_type . '_meta'; + $non_cached_ids = array(); + $invalid_cache_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_cache_ids[] = $id; } else { $cache[ $id ] = $cached_object; } } + /* + * 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_cache_ids ) ) { + wp_cache_delete_multiple( $invalid_cache_ids, $cache_group ); + } + if ( empty( $non_cached_ids ) ) { return $cache; } @@ -1268,6 +1287,7 @@ function update_meta_cache( $meta_type, $object_ids ) { } $data[ $id ] = $cache[ $id ]; } + 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..3f52916d7d5de 100644 --- a/tests/phpunit/tests/meta.php +++ b/tests/phpunit/tests/meta.php @@ -131,6 +131,121 @@ 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 static 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 $invalid_cached_value Unusable value to place in the meta cache. + */ + public function test_metadata_exists_treats_non_array_cache_value_as_miss( $invalid_cached_value ): void { + $this->assertTrue( wp_cache_set( self::$author->ID, $invalid_cached_value, 'user_meta' ), 'The unusable value should be placed in the cache, check test setup.' ); + + $this->assertTrue( metadata_exists( 'user', self::$author->ID, 'meta_key' ), 'An existing meta key should be reported as existing.' ); + $this->assertFalse( metadata_exists( 'user', self::$author->ID, 'foobarbaz' ), 'A missing meta key should be reported as not existing.' ); + $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 $invalid_cached_value Unusable value to place in the meta cache. + */ + public function test_get_metadata_treats_non_array_cache_value_as_miss( $invalid_cached_value ): void { + $this->assertTrue( wp_cache_set( self::$author->ID, $invalid_cached_value, 'user_meta' ), 'The unusable value should be placed in the cache, check test setup.' ); + + $this->assertSame( 'meta_value', get_metadata( 'user', self::$author->ID, 'meta_key', true ), 'The single meta value should be returned.' ); + $this->assertSame( array( 'meta_value' ), get_metadata( 'user', self::$author->ID, 'meta_key' ), 'The array of meta values should be returned.' ); + $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 $invalid_cached_value Unusable value to place in the meta cache. + */ + public function test_get_metadata_with_empty_key_treats_non_array_cache_value_as_miss( $invalid_cached_value ): void { + $this->assertTrue( wp_cache_set( self::$author->ID, $invalid_cached_value, 'user_meta' ), 'The unusable value should be placed in the cache, check test setup.' ); + + $meta = get_metadata( 'user', self::$author->ID ); + + $this->assertIsArray( $meta, 'All meta for the object should be returned as an array.' ); + $this->assertSame( array( 'meta_value' ), $meta['meta_key'], 'The existing meta key should be included in the returned meta.' ); + } + + /** + * @ticket 66091 + * + * @dataProvider data_non_array_cache_values + * + * @param mixed $invalid_cached_value Unusable value to place in the meta cache. + */ + public function test_update_meta_cache_replaces_non_array_cache_value( $invalid_cached_value ): void { + $this->assertTrue( wp_cache_set( self::$author->ID, $invalid_cached_value, 'user_meta' ), 'The unusable value should be placed in the cache, check test setup.' ); + + $meta_cache = update_meta_cache( 'user', array( self::$author->ID ) ); + + $this->assertIsArray( $meta_cache ); + $this->assertArrayHasKey( self::$author->ID, $meta_cache ); + $this->assertIsArray( $meta_cache[ self::$author->ID ], 'The returned meta cache for the object should be an array.' ); + $this->assertSame( array( 'meta_value' ), $meta_cache[ self::$author->ID ]['meta_key'], 'The returned meta cache should include the existing 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'], 'The replaced cache value should include the existing 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(); + + $this->assertTrue( wp_cache_set( $term_id, new stdClass(), 'term_meta' ), 'The unusable value should be placed in the cache, check test setup.' ); + + $meta_cache = update_meta_cache( 'term', array( $term_id ) ); + $this->assertIsArray( $meta_cache ); + $this->assertArrayHasKey( $term_id, $meta_cache ); + + $this->assertSame( array(), $meta_cache[ $term_id ], 'The returned meta cache for an object without meta should be an empty array.' ); + $this->assertSame( array(), wp_cache_get( $term_id, 'term_meta' ), 'The unusable cache value should have been replaced with an empty array.' ); + } + + /** + * @ticket 66091 + */ + public function test_update_meta_cache_removes_non_array_cache_value_while_cache_addition_is_suspended(): void { + $this->assertTrue( wp_cache_set( self::$author->ID, new stdClass(), 'user_meta' ), 'The unusable value should be placed in the cache, check test setup.' ); + + wp_suspend_cache_addition( true ); + $meta_cache = update_meta_cache( 'user', array( self::$author->ID ) ); + wp_suspend_cache_addition( false ); + $this->assertIsArray( $meta_cache ); + $this->assertIsArray( $meta_cache[ self::$author->ID ] ); + + $this->assertSame( array( 'meta_value' ), $meta_cache[ self::$author->ID ]['meta_key'], 'The meta should still be returned while cache addition is suspended.' ); + $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 */