Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/wp-includes/meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.
Expand All @@ -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 ] ) ) {
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
109 changes: 109 additions & 0 deletions tests/phpunit/tests/meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, array{mixed}>
*/
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
*/
Expand Down
Loading