Skip to content

Commit dc3204e

Browse files
REST API: Fixes /wp/v2/pattern-directory/patterns endpoint response for slug parameter.
[53218] introduced a bug of a wrong response from the `wp/v2/pattern-directory/patterns` endpoint with a `slug` parameter. As the response is cached, it can result in an incorrect list of available patterns supported by the current theme. This commit resolves by: * Limiting the `slug` to an `array` in the query parameters. * When set, parsing and sorting the slug(s) and then serializing the sorted query args as part of the hashed transient keys. Props antonvlasenko, timothyblynjacobs, spacedmonkey, costdev, hellofromTonya. Follow-up to [53218], [53152], [51208]. Fixes #55617. git-svn-id: https://develop.svn.wordpress.org/trunk@53333 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 9e6e4b5 commit dc3204e

3 files changed

Lines changed: 149 additions & 17 deletions

File tree

src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,7 @@ public function get_items( $request ) {
119119
$query_args['slug'] = $slug;
120120
}
121121

122-
/*
123-
* Include a hash of the query args, so that different requests are stored in
124-
* separate caches.
125-
*
126-
* MD5 is chosen for its speed, low-collision rate, universal availability, and to stay
127-
* under the character limit for `_site_transient_timeout_{...}` keys.
128-
*
129-
* @link https://stackoverflow.com/questions/3665247/fastest-hash-for-non-cryptographic-uses
130-
*/
131-
$transient_key = 'wp_remote_block_patterns_' . md5( implode( '-', $query_args ) );
122+
$transient_key = $this->get_transient_key( $query_args );
132123

133124
/*
134125
* Use network-wide transient to improve performance. The locale is the only site
@@ -337,6 +328,11 @@ public function get_collection_params() {
337328
'minimum' => 1,
338329
);
339330

331+
$query_params['slug'] = array(
332+
'description' => __( 'Limit results to those matching a pattern (slug).' ),
333+
'type' => 'array',
334+
);
335+
340336
/**
341337
* Filter collection parameters for the block pattern directory controller.
342338
*
@@ -346,4 +342,36 @@ public function get_collection_params() {
346342
*/
347343
return apply_filters( 'rest_pattern_directory_collection_params', $query_params );
348344
}
345+
346+
/*
347+
* Include a hash of the query args, so that different requests are stored in
348+
* separate caches.
349+
*
350+
* MD5 is chosen for its speed, low-collision rate, universal availability, and to stay
351+
* under the character limit for `_site_transient_timeout_{...}` keys.
352+
*
353+
* @link https://stackoverflow.com/questions/3665247/fastest-hash-for-non-cryptographic-uses
354+
*
355+
* @since 6.0.0
356+
*
357+
* @param array $query_args Query arguments to generate a transient key from.
358+
* @return string Transient key.
359+
*/
360+
protected function get_transient_key( $query_args ) {
361+
362+
if ( isset( $query_args['slug'] ) ) {
363+
// This is an additional precaution because the "sort" function expects an array.
364+
$query_args['slug'] = wp_parse_list( $query_args['slug'] );
365+
366+
// Empty arrays should not affect the transient key.
367+
if ( empty( $query_args['slug'] ) ) {
368+
unset( $query_args['slug'] );
369+
} else {
370+
// Sort the array so that the transient key doesn't depend on the order of slugs.
371+
sort( $query_args['slug'] );
372+
}
373+
}
374+
375+
return 'wp_remote_block_patterns_' . md5( serialize( $query_args ) );
376+
}
349377
}

tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ class WP_REST_Pattern_Directory_Controller_Test extends WP_Test_REST_Controller_
2121
*/
2222
protected static $contributor_id;
2323

24+
/**
25+
* An instance of WP_REST_Pattern_Directory_Controller class.
26+
*
27+
* @since 6.0.0
28+
*
29+
* @var WP_REST_Pattern_Directory_Controller
30+
*/
31+
private static $controller;
32+
2433
/**
2534
* Set up class test fixtures.
2635
*
@@ -34,6 +43,8 @@ public static function wpSetUpBeforeClass( $factory ) {
3443
'role' => 'contributor',
3544
)
3645
);
46+
47+
static::$controller = new WP_REST_Pattern_Directory_Controller();
3748
}
3849

3950
/**
@@ -42,7 +53,7 @@ public static function wpSetUpBeforeClass( $factory ) {
4253
* @param WP_REST_Response[] $pattern An individual pattern from the REST API response.
4354
*/
4455
public function assertPatternMatchesSchema( $pattern ) {
45-
$schema = ( new WP_REST_Pattern_Directory_Controller() )->get_item_schema();
56+
$schema = static::$controller->get_item_schema();
4657
$pattern_id = isset( $pattern->id ) ? $pattern->id : '{pattern ID is missing}';
4758

4859
$this->assertTrue(
@@ -322,12 +333,11 @@ public function test_delete_item() {
322333
* @since 5.8.0
323334
*/
324335
public function test_prepare_item() {
325-
$controller = new WP_REST_Pattern_Directory_Controller();
326336
$raw_patterns = json_decode( self::get_raw_response( 'browse-all' ) );
327337
$raw_patterns[0]->extra_field = 'this should be removed';
328338

329-
$prepared_pattern = $controller->prepare_response_for_collection(
330-
$controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() )
339+
$prepared_pattern = static::$controller->prepare_response_for_collection(
340+
static::$controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() )
331341
);
332342

333343
$this->assertPatternMatchesSchema( $prepared_pattern );
@@ -340,12 +350,11 @@ public function test_prepare_item() {
340350
* @since 5.8.0
341351
*/
342352
public function test_prepare_item_search() {
343-
$controller = new WP_REST_Pattern_Directory_Controller();
344353
$raw_patterns = json_decode( self::get_raw_response( 'search' ) );
345354
$raw_patterns[0]->extra_field = 'this should be removed';
346355

347-
$prepared_pattern = $controller->prepare_response_for_collection(
348-
$controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() )
356+
$prepared_pattern = static::$controller->prepare_response_for_collection(
357+
static::$controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() )
349358
);
350359

351360
$this->assertPatternMatchesSchema( $prepared_pattern );
@@ -399,6 +408,96 @@ public function test_get_item_schema() {
399408
$this->markTestSkipped( "The controller's schema is hardcoded, so tests would not be meaningful." );
400409
}
401410

411+
/**
412+
* Tests if the transient key gets generated correctly.
413+
*
414+
* @dataProvider data_get_query_parameters
415+
*
416+
* @covers WP_REST_Pattern_Directory_Controller::get_transient_key
417+
*
418+
* @since 6.0.0
419+
*
420+
* @ticket 55617
421+
*
422+
* @param array $parameters_1 Expected query arguments.
423+
* @param array $parameters_2 Actual query arguments.
424+
* @param string $message An error message to display.
425+
* @param bool $assert_same Assertion type (assertSame vs assertNotSame).
426+
*/
427+
public function test_transient_keys_get_generated_correctly( $parameters_1, $parameters_2, $message, $assert_same = true ) {
428+
$reflection_method = new ReflectionMethod( static::$controller, 'get_transient_key' );
429+
$reflection_method->setAccessible( true );
430+
431+
$result_1 = $reflection_method->invoke( self::$controller, $parameters_1 );
432+
$result_2 = $reflection_method->invoke( self::$controller, $parameters_2 );
433+
434+
$this->assertIsString( $result_1, 'Transient key #1 must be a string.' );
435+
$this->assertNotEmpty( $result_1, 'Transient key #1 must not be empty.' );
436+
437+
$this->assertIsString( $result_2, 'Transient key #2 must be a string.' );
438+
$this->assertNotEmpty( $result_2, 'Transient key #2 must not be empty.' );
439+
440+
if ( $assert_same ) {
441+
$this->assertSame( $result_1, $result_2, $message );
442+
} else {
443+
$this->assertNotSame( $result_1, $result_2, $message );
444+
}
445+
446+
}
447+
448+
/**
449+
* @since 6.0.0
450+
*
451+
* @ticket 55617
452+
*/
453+
public function data_get_query_parameters() {
454+
return array(
455+
'same key and empty slugs' => array(
456+
'parameters_1' => array(
457+
'parameter_1' => 1,
458+
'slug' => array(),
459+
),
460+
'parameters_2' => array(
461+
'parameter_1' => 1,
462+
),
463+
'message' => 'Empty slugs should not affect the transient key.',
464+
),
465+
'same key and slugs in different order' => array(
466+
'parameters_1' => array(
467+
'parameter_1' => 1,
468+
'slug' => array( 0, 2 ),
469+
),
470+
'parameters_2' => array(
471+
'parameter_1' => 1,
472+
'slug' => array( 2, 0 ),
473+
),
474+
'message' => 'The order of slugs should not affect the transient key.',
475+
),
476+
'same key and different slugs' => array(
477+
'parameters_1' => array(
478+
'parameter_1' => 1,
479+
'slug' => array( 'some_slug' ),
480+
),
481+
'parameters_2' => array(
482+
'parameter_1' => 1,
483+
'slug' => array( 'some_other_slug' ),
484+
),
485+
'message' => 'Transient keys must not match.',
486+
false,
487+
),
488+
'different keys' => array(
489+
'parameters_1' => array(
490+
'parameter_1' => 1,
491+
),
492+
'parameters_2' => array(
493+
'parameter_2' => 1,
494+
),
495+
'message' => 'Transient keys must depend on array keys.',
496+
false,
497+
),
498+
);
499+
}
500+
402501
/**
403502
* Simulate a successful outbound HTTP requests, to keep tests pure and performant.
404503
*

tests/qunit/fixtures/wp-api-generated.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10385,6 +10385,11 @@ mockedApiResponse.Schema = {
1038510385
"type": "integer",
1038610386
"minimum": 1,
1038710387
"required": false
10388+
},
10389+
"slug": {
10390+
"description": "Limit results to those matching a pattern (slug).",
10391+
"type": "array",
10392+
"required": false
1038810393
}
1038910394
}
1039010395
}

0 commit comments

Comments
 (0)