From 83c7cad248d6e55d93712582e9dbd77eb8ad5fb6 Mon Sep 17 00:00:00 2001 From: Ian Dunn Date: Tue, 22 Jun 2021 21:23:19 +0000 Subject: [PATCH] Block Editor: Move caching to endpoint for unique responses. Now that the pattern API request includes the locale and version, the cache key needs to contain a hash of the query args. Props ocean90, dd32, timothyblynjacobs Fixes #53435 git-svn-id: https://develop.svn.wordpress.org/trunk@51208 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-patterns.php | 24 ++--- ...s-wp-rest-pattern-directory-controller.php | 93 +++++++++++++------ .../rest-pattern-directory-controller.php | 40 ++++++++ 3 files changed, 116 insertions(+), 41 deletions(-) diff --git a/src/wp-includes/block-patterns.php b/src/wp-includes/block-patterns.php index 0d539f7a24..38ca373723 100644 --- a/src/wp-includes/block-patterns.php +++ b/src/wp-includes/block-patterns.php @@ -45,7 +45,11 @@ function _register_core_block_patterns_and_categories() { } /** - * Import patterns from wordpress.org/patterns. + * Register Core's official patterns from wordpress.org/patterns. + * + * @since 5.8.0 + * + * @param WP_Screen $current_screen The screen that the current request was triggered from. */ function _load_remote_block_patterns( $current_screen ) { if ( ! $current_screen->is_block_editor ) { @@ -64,18 +68,14 @@ function _load_remote_block_patterns( $current_screen ) { $should_load_remote = apply_filters( 'should_load_remote_block_patterns', true ); if ( $supports_core_patterns && $should_load_remote ) { - $patterns = get_transient( 'wp_remote_block_patterns' ); - if ( ! $patterns ) { - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); - $core_keyword_id = 11; // 11 is the ID for "core". - $request->set_param( 'keyword', $core_keyword_id ); - $response = rest_do_request( $request ); - if ( $response->is_error() ) { - return; - } - $patterns = $response->get_data(); - set_transient( 'wp_remote_block_patterns', $patterns, HOUR_IN_SECONDS ); + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $core_keyword_id = 11; // 11 is the ID for "core". + $request->set_param( 'keyword', $core_keyword_id ); + $response = rest_do_request( $request ); + if ( $response->is_error() ) { + return; } + $patterns = $response->get_data(); foreach ( $patterns as $settings ) { $pattern_name = 'core/' . sanitize_title( $settings['title'] ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php index d758b1346d..046c0a8369 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php @@ -111,38 +111,73 @@ class WP_REST_Pattern_Directory_Controller extends WP_REST_Controller { $query_args['search'] = $search_term; } - $api_url = add_query_arg( - array_map( 'rawurlencode', $query_args ), - 'http://api.wordpress.org/patterns/1.0/' - ); + /* + * Include a hash of the query args, so that different requests are stored in + * separate caches. + * + * MD5 is chosen for its speed, low-collision rate, universal availability, and to stay + * under the character limit for `_site_transient_timeout_{...}` keys. + * + * @link https://stackoverflow.com/questions/3665247/fastest-hash-for-non-cryptographic-uses + */ + $transient_key = 'wp_remote_block_patterns_' . md5( implode( '-', $query_args ) ); - if ( wp_http_supports( array( 'ssl' ) ) ) { - $api_url = set_url_scheme( $api_url, 'https' ); - } + /* + * Use network-wide transient to improve performance. The locale is the only site + * configuration that affects the response, and it's included in the transient key. + */ + $raw_patterns = get_site_transient( $transient_key ); - $wporg_response = wp_remote_get( $api_url ); - $raw_patterns = json_decode( wp_remote_retrieve_body( $wporg_response ) ); - - if ( is_wp_error( $wporg_response ) ) { - $wporg_response->add_data( array( 'status' => 500 ) ); - - return $wporg_response; - } - - // Make sure w.org returned valid data. - if ( ! is_array( $raw_patterns ) ) { - return new WP_Error( - 'pattern_api_failed', - sprintf( - /* translators: %s: Support forums URL. */ - __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.' ), - __( 'https://wordpress.org/support/forums/' ) - ), - array( - 'status' => 500, - 'response' => wp_remote_retrieve_body( $wporg_response ), - ) + if ( ! $raw_patterns ) { + $api_url = add_query_arg( + array_map( 'rawurlencode', $query_args ), + 'http://api.wordpress.org/patterns/1.0/' ); + + if ( wp_http_supports( array( 'ssl' ) ) ) { + $api_url = set_url_scheme( $api_url, 'https' ); + } + + /* + * Default to a short TTL, to mitigate cache stampedes on high-traffic sites. + * This assumes that most errors will be short-lived, e.g., packet loss that causes the + * first request to fail, but a follow-up one will succeed. The value should be high + * enough to avoid stampedes, but low enough to not interfere with users manually + * re-trying a failed request. + */ + $cache_ttl = 5; + $wporg_response = wp_remote_get( $api_url ); + $raw_patterns = json_decode( wp_remote_retrieve_body( $wporg_response ) ); + + if ( is_wp_error( $wporg_response ) ) { + $raw_patterns = $wporg_response; + + } elseif ( ! is_array( $raw_patterns ) ) { + // HTTP request succeeded, but response data is invalid. + $raw_patterns = new WP_Error( + 'pattern_api_failed', + sprintf( + /* translators: %s: Support forums URL. */ + __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.' ), + __( 'https://wordpress.org/support/forums/' ) + ), + array( + 'response' => wp_remote_retrieve_body( $wporg_response ), + ) + ); + + } else { + // Response has valid data. + $cache_ttl = HOUR_IN_SECONDS; + } + + set_site_transient( $transient_key, $raw_patterns, $cache_ttl ); + } + + if ( is_wp_error( $raw_patterns ) ) { + $raw_patterns->add_data( array( 'status' => 500 ) ); + + return $raw_patterns; } $response = array(); diff --git a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php index 50679c4b05..a4a6f549b4 100644 --- a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php +++ b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php @@ -260,6 +260,46 @@ class WP_REST_Pattern_Directory_Controller_Test extends WP_Test_REST_Controller_ $this->assertWPError( $response->as_error() ); } + /** + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @since 5.8.0 + */ + public function test_get_items_prepare_filter() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'browse-all', true ); + + // Test that filter changes uncached values. + add_filter( + 'rest_prepare_block_pattern', + function( $response ) { + return 'initial value'; + } + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $response = rest_do_request( $request ); + $patterns = $response->get_data(); + + $this->assertSame( 'initial value', $patterns[0] ); + + // Test that filter changes cached values (the previous request primed the cache). + add_filter( + 'rest_prepare_block_pattern', + function( $response ) { + return 'modified the cache'; + }, + 11 + ); + + // Test that the filter works against cached values. + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $response = rest_do_request( $request ); + $patterns = $response->get_data(); + + $this->assertSame( 'modified the cache', $patterns[0] ); + } + public function test_get_item() { $this->markTestSkipped( 'Controller does not have get_item route.' ); }