From 849c811396dc7651e24e1323a4c67b9fda50a719 Mon Sep 17 00:00:00 2001 From: Kelly Choyce-Dwan Date: Wed, 26 May 2021 01:10:57 +0000 Subject: [PATCH] Block Editor: Add support for the pattern directory. Add an endpoint for fetching block patterns from WordPress.org, and load the block patterns from this new API. Remove the block patterns that have already been moved to WordPress.org/patterns. Props ryelle, iandunn, youknowriad, timothyblynjacobs. Fixes #53246. git-svn-id: https://develop.svn.wordpress.org/trunk@51021 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-patterns.php | 55 ++- src/wp-includes/block-patterns/heading.php | 16 - .../block-patterns/large-header-left.php | 33 -- .../large-header-text-button.php | 35 -- .../media-text-arquitecture.php | 21 - .../block-patterns/media-text-art.php | 21 - .../block-patterns/media-text-nature.php | 28 -- src/wp-includes/block-patterns/quote.php | 30 -- .../text-two-columns-title-offset.php | 53 --- .../block-patterns/text-two-columns-title.php | 31 -- .../block-patterns/text-two-columns.php | 44 -- .../three-columns-media-text.php | 69 --- .../block-patterns/three-columns-text.php | 43 -- .../block-patterns/three-images-gallery.php | 43 -- .../block-patterns/two-buttons.php | 22 - .../block-patterns/two-images-gallery.php | 15 - src/wp-includes/default-filters.php | 1 + src/wp-includes/rest-api.php | 4 + ...s-wp-rest-pattern-directory-controller.php | 297 ++++++++++++ src/wp-settings.php | 1 + .../blocks/pattern-directory/browse-all.json | 50 +++ .../pattern-directory/browse-category-2.json | 34 ++ .../pattern-directory/browse-keyword-11.json | 50 +++ .../pattern-directory/search-button.json | 66 +++ tests/phpunit/tests/blocks/block-editor.php | 11 + .../rest-pattern-directory-controller.php | 424 ++++++++++++++++++ .../tests/rest-api/rest-schema-setup.php | 1 + 27 files changed, 979 insertions(+), 519 deletions(-) delete mode 100644 src/wp-includes/block-patterns/heading.php delete mode 100644 src/wp-includes/block-patterns/large-header-left.php delete mode 100644 src/wp-includes/block-patterns/large-header-text-button.php delete mode 100644 src/wp-includes/block-patterns/media-text-arquitecture.php delete mode 100644 src/wp-includes/block-patterns/media-text-art.php delete mode 100644 src/wp-includes/block-patterns/media-text-nature.php delete mode 100644 src/wp-includes/block-patterns/quote.php delete mode 100644 src/wp-includes/block-patterns/text-two-columns-title-offset.php delete mode 100644 src/wp-includes/block-patterns/text-two-columns-title.php delete mode 100644 src/wp-includes/block-patterns/text-two-columns.php delete mode 100644 src/wp-includes/block-patterns/three-columns-media-text.php delete mode 100644 src/wp-includes/block-patterns/three-columns-text.php delete mode 100644 src/wp-includes/block-patterns/three-images-gallery.php delete mode 100644 src/wp-includes/block-patterns/two-buttons.php delete mode 100644 src/wp-includes/block-patterns/two-images-gallery.php create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php create mode 100644 tests/phpunit/data/blocks/pattern-directory/browse-all.json create mode 100644 tests/phpunit/data/blocks/pattern-directory/browse-category-2.json create mode 100644 tests/phpunit/data/blocks/pattern-directory/browse-keyword-11.json create mode 100644 tests/phpunit/data/blocks/pattern-directory/search-button.json create mode 100644 tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php diff --git a/src/wp-includes/block-patterns.php b/src/wp-includes/block-patterns.php index 0dc01df73f..0d539f7a24 100644 --- a/src/wp-includes/block-patterns.php +++ b/src/wp-includes/block-patterns.php @@ -19,21 +19,6 @@ function _register_core_block_patterns_and_categories() { if ( $should_register_core_patterns ) { $core_block_patterns = array( - 'media-text-nature', - 'two-images-gallery', - 'three-columns-media-text', - 'quote', - 'large-header-left', - 'large-header-text-button', - 'media-text-art', - 'text-two-columns-title', - 'three-columns-text', - 'text-two-columns-title-offset', - 'heading', - 'three-images-gallery', - 'text-two-columns', - 'media-text-arquitecture', - 'two-buttons', 'query-standard-posts', 'query-medium-posts', 'query-small-posts', @@ -58,3 +43,43 @@ function _register_core_block_patterns_and_categories() { register_block_pattern_category( 'text', array( 'label' => _x( 'Text', 'Block pattern category' ) ) ); register_block_pattern_category( 'query', array( 'label' => __( 'Query', 'Block pattern category' ) ) ); } + +/** + * Import patterns from wordpress.org/patterns. + */ +function _load_remote_block_patterns( $current_screen ) { + if ( ! $current_screen->is_block_editor ) { + return; + } + + $supports_core_patterns = get_theme_support( 'core-block-patterns' ); + + /** + * Filter to disable remote block patterns. + * + * @since 5.8.0 + * + * @param bool $should_load_remote + */ + $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 ); + } + + foreach ( $patterns as $settings ) { + $pattern_name = 'core/' . sanitize_title( $settings['title'] ); + register_block_pattern( $pattern_name, (array) $settings ); + } + } +} diff --git a/src/wp-includes/block-patterns/heading.php b/src/wp-includes/block-patterns/heading.php deleted file mode 100644 index 9a3e9f96ac..0000000000 --- a/src/wp-includes/block-patterns/heading.php +++ /dev/null @@ -1,16 +0,0 @@ - _x( 'Heading', 'Block pattern title' ), - 'categories' => array( 'text' ), - 'blockTypes' => array( 'core/heading' ), - 'content' => ' -

' . esc_html__( "We're a studio in Berlin with an international practice in architecture, urban planning and interior design. We believe in sharing knowledge and promoting dialogue to increase the creative potential of collaboration." ) . '

- ', - 'description' => _x( 'Heading text', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/large-header-left.php b/src/wp-includes/block-patterns/large-header-left.php deleted file mode 100644 index cdaca2277a..0000000000 --- a/src/wp-includes/block-patterns/large-header-left.php +++ /dev/null @@ -1,33 +0,0 @@ - _x( 'Large header with left-aligned text', 'Block pattern title' ), - 'categories' => array( 'header' ), - 'content' => ' -
-

' . esc_html__( 'Forest.' ) . '

- - - -
-
- - - - -

' . esc_html__( 'Even a child knows how valuable the forest is. The fresh, breathtaking smell of trees. Echoing birds flying above that dense magnitude. A stable climate, a sustainable diverse life and a source of culture. Yet, forests and other ecosystems hang in the balance, threatened to become croplands, pasture, and plantations.' ) . '

-
- - - -
-
-
- ', - 'description' => _x( 'Cover image with quote on top', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/large-header-text-button.php b/src/wp-includes/block-patterns/large-header-text-button.php deleted file mode 100644 index ce39071f4a..0000000000 --- a/src/wp-includes/block-patterns/large-header-text-button.php +++ /dev/null @@ -1,35 +0,0 @@ - _x( 'Large header with text and a button.', 'Block pattern title' ), - 'categories' => array( 'header' ), - 'content' => ' -
-

' . esc_html__( 'Overseas:' ) . '
' . esc_html__( '1500 — 1960' ) . '

- - - -
-
-

' . wp_kses_post( __( 'An exhibition about the different representations of the ocean throughout time, between the sixteenth and the twentieth century. Taking place in our Open Room in Floor 2.' ) ) . '

- - - - -
- - - -
-
-
- ', - 'description' => _x( 'Large header with background image and text and button on top', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/media-text-arquitecture.php b/src/wp-includes/block-patterns/media-text-arquitecture.php deleted file mode 100644 index 41f93e7b09..0000000000 --- a/src/wp-includes/block-patterns/media-text-arquitecture.php +++ /dev/null @@ -1,21 +0,0 @@ - _x( 'Media and text with image on the right', 'Block pattern title' ), - 'categories' => array( 'header' ), - 'content' => ' -
' . esc_attr__( 'Close-up, abstract view of architecture.' ) . '
-

' . esc_html__( 'Open Spaces' ) . '

- - - -

' . esc_html__( 'See case study ↗' ) . '

-
- ', - 'description' => _x( 'Media and text block with image to the left and text to the right', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/media-text-art.php b/src/wp-includes/block-patterns/media-text-art.php deleted file mode 100644 index df84da11ce..0000000000 --- a/src/wp-includes/block-patterns/media-text-art.php +++ /dev/null @@ -1,21 +0,0 @@ - _x( 'Media & text with image on the right', 'Block pattern title' ), - 'categories' => array( 'header' ), - 'content' => ' -
' . esc_attr__( 'A green and brown rural landscape leading into a bright blue ocean and slightly cloudy sky, done in oil paints.' ) . '
-

' . esc_html__( 'Shore with Blue Sea' ) . '

- - - -

' . esc_html__( 'Eleanor Harris (American, 1901-1942)' ) . '

-
- ', - 'description' => _x( 'Media and text block with image to the right and text to the left', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/media-text-nature.php b/src/wp-includes/block-patterns/media-text-nature.php deleted file mode 100644 index 7efd0cbdec..0000000000 --- a/src/wp-includes/block-patterns/media-text-nature.php +++ /dev/null @@ -1,28 +0,0 @@ - _x( 'Media & text in a full height container', 'Block pattern title' ), - 'categories' => array( 'header' ), - 'content' => ' -
-
' . esc_attr__( 'Close-up of dried, cracked earth.' ) . '
-

' . esc_html__( "What's the problem?" ) . '

- - - -

' . esc_html__( 'Trees are more important today than ever before. More than 10,000 products are reportedly made from trees. Through chemistry, the humble woodpile is yielding chemicals, plastics and fabrics that were beyond comprehension when an axe first felled a Texas tree.' ) . '

- - - -
-
- ', - 'description' => _x( 'Media and text block with image to the left and text and button to the right', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/quote.php b/src/wp-includes/block-patterns/quote.php deleted file mode 100644 index 039454330e..0000000000 --- a/src/wp-includes/block-patterns/quote.php +++ /dev/null @@ -1,30 +0,0 @@ - _x( 'Quote', 'Block pattern title' ), - 'categories' => array( 'text' ), - 'blockTypes' => array( 'core/quote' ), - 'content' => ' -
-
- - - -
' . esc_attr__( 'A side profile of a woman in a russet-colored turtleneck and white bag. She looks up with her eyes closed.' ) . '
- - - -

' . esc_html__( "\"Contributing makes me feel like I'm being useful to the planet.\"" ) . '

' . wp_kses_post( __( '— Anna Wong, Volunteer' ) ) . '
- - - -
-
- ', - 'description' => _x( 'Testimonial quote with portrait', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/text-two-columns-title-offset.php b/src/wp-includes/block-patterns/text-two-columns-title-offset.php deleted file mode 100644 index 00d1404d77..0000000000 --- a/src/wp-includes/block-patterns/text-two-columns-title-offset.php +++ /dev/null @@ -1,53 +0,0 @@ - _x( 'Two columns of text with offset heading', 'Block pattern title' ), - 'categories' => array( 'columns', 'text' ), - 'content' => ' -
- - - - -
-
-

' . esc_html__( 'Oceanic Inspiration' ) . '

-
- - - -
-
-
-
- - - -
-
- - - -
-

' . esc_html__( 'Winding veils round their heads, the women walked on deck. They were now moving steadily down the river, passing the dark shapes of ships at anchor, and London was a swarm of lights with a pale yellow canopy drooping above it. There were the lights of the great theatres, the lights of the long streets, lights that indicated huge squares of domestic comfort, lights that hung high in air.' ) . '

-
- - - -
-

' . esc_html__( 'No darkness would ever settle upon those lamps, as no darkness had settled upon them for hundreds of years. It seemed dreadful that the town should blaze for ever in the same spot; dreadful at least to people going away to adventure upon the sea, and beholding it as a circumscribed mound, eternally burnt, eternally scarred. From the deck of the ship the great city appeared a crouched and cowardly figure, a sedentary miser.' ) . '

-
-
- - - - -
- ', - 'description' => _x( 'Two columns of text with offset heading', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/text-two-columns-title.php b/src/wp-includes/block-patterns/text-two-columns-title.php deleted file mode 100644 index dc155c9926..0000000000 --- a/src/wp-includes/block-patterns/text-two-columns-title.php +++ /dev/null @@ -1,31 +0,0 @@ - _x( 'Two columns text and title', 'Block pattern title' ), - 'categories' => array( 'columns', 'text' ), - 'content' => ' -
-

' . esc_html__( 'The voyage had begun, and had begun happily with a soft blue sky, and a calm sea.' ) . '

- - - -
-
-

' . esc_html__( 'They followed her on to the deck. All the smoke and the houses had disappeared, and the ship was out in a wide space of sea very fresh and clear though pale in the early light. They had left London sitting on its mud. A very thin line of shadow tapered on the horizon, scarcely thick enough to stand the burden of Paris, which nevertheless rested upon it. They were free of roads, free of mankind, and the same exhilaration at their freedom ran through them all.' ) . '

-
- - - -
-

' . esc_html__( "The ship was making her way steadily through small waves which slapped her and then fizzled like effervescing water, leaving a little border of bubbles and foam on either side. The colourless October sky above was thinly clouded as if by the trail of wood-fire smoke, and the air was wonderfully salt and brisk. Indeed it was too cold to stand still. Mrs. Ambrose drew her arm within her husband's, and as they moved off it could be seen from the way in which her sloping cheek turned up to his that she had something private to communicate." ) . '

-
-
-
- ', - 'description' => _x( 'Two columns text and title', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/text-two-columns.php b/src/wp-includes/block-patterns/text-two-columns.php deleted file mode 100644 index b1d0abd184..0000000000 --- a/src/wp-includes/block-patterns/text-two-columns.php +++ /dev/null @@ -1,44 +0,0 @@ - _x( 'Two columns of text', 'Block pattern title' ), - 'categories' => array( 'columns', 'text' ), - 'content' => ' -
-
- - - -

' . esc_html__( 'We have worked with:' ) . '

- - - -

' . wp_kses_post( __( 'EARTHFUND™
ARCHWEEKLY
FUTURE ROADS
BUILDING NY' ) ) . '

- - - - -
- - - - -
- ', - 'description' => _x( 'Two columns of text', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/three-columns-media-text.php b/src/wp-includes/block-patterns/three-columns-media-text.php deleted file mode 100644 index 758b6ab6b7..0000000000 --- a/src/wp-includes/block-patterns/three-columns-media-text.php +++ /dev/null @@ -1,69 +0,0 @@ - _x( 'Three columns with images and text', 'Block pattern title' ), - 'categories' => array( 'columns' ), - 'content' => ' -
-
-
- - - - -
' . esc_html__( 'ECOLOGY' ) . '
- - - -

' . esc_html__( 'Natural resources.' ) . '

- - -
-
- - - -
-
-

' . wp_kses_post( __( 'Nature, in the common sense, refers to essences unchanged by man; space, the air, the river, the leaf. Art is applied to the mixture of his will with the same things, as in a house, a canal, a statue, a picture. But his operations taken together are so insignificant, a little chipping, baking, patching, and washing, that in an impression so grand as that of the world on the human mind, they do not vary the result.' ) ) . '

-
- - - -
- - - - -
' . esc_attr__( 'The sun setting through a dense forest of trees.' ) . '
-
- - - -
-
' . esc_attr__( 'Wind turbines standing on a grassy plain, against a blue sky.' ) . '
-
-
- - - -
-
-
' . esc_attr__( 'The sun shining over a ridge leading down into the shore. In the distance, a car drives down a road.' ) . '
-
- - - -
-

' . esc_html__( "Undoubtedly we have no questions to ask which are unanswerable. We must trust the perfection of the creation so far, as to believe that whatever curiosity the order of things has awakened in our minds, the order of things can satisfy. Every man's condition is a solution in hieroglyphic to those inquiries he would put." ) . '

-
-
-
- ', - 'description' => _x( 'Three columns with images and text', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/three-columns-text.php b/src/wp-includes/block-patterns/three-columns-text.php deleted file mode 100644 index bdd8c0331d..0000000000 --- a/src/wp-includes/block-patterns/three-columns-text.php +++ /dev/null @@ -1,43 +0,0 @@ - _x( 'Three columns of text', 'Block pattern title' ), - 'categories' => array( 'columns', 'text' ), - 'content' => ' -
-
-

' . esc_html__( 'Virtual Tour ↗' ) . '

- - - -

' . esc_html__( 'Get a virtual tour of the museum. Ideal for schools and events.' ) . '

-
- - - -
-

' . esc_html__( 'Current Shows ↗' ) . '

- - - -

' . esc_html__( 'Stay updated and see our current exhibitions here.' ) . '

-
- - - -
-

' . esc_html__( 'Useful Info ↗' ) . '

- - - -

' . esc_html__( 'Get to know our opening times, ticket prices and discounts.' ) . '

-
-
- ', - 'description' => _x( 'Three columns of text', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/three-images-gallery.php b/src/wp-includes/block-patterns/three-images-gallery.php deleted file mode 100644 index c8f059fe09..0000000000 --- a/src/wp-includes/block-patterns/three-images-gallery.php +++ /dev/null @@ -1,43 +0,0 @@ - _x( 'Three columns with offset images', 'Block pattern title' ), - 'categories' => array( 'gallery' ), - 'content' => ' -
-
-
' . esc_attr__( 'Close-up, abstract view of geometric architecture.' ) . '
-
- - - -
- - - - - - - - -
' . esc_attr__( 'Close-up, angled view of a window on a white building.' ) . '
-
- - - -
-
' . esc_attr__( 'Close-up of the corner of a white, geometric building with both sharp points and round corners.' ) . '
- - - - -
-
- ', - 'description' => _x( 'Three columns with offset images', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/two-buttons.php b/src/wp-includes/block-patterns/two-buttons.php deleted file mode 100644 index d75490aa44..0000000000 --- a/src/wp-includes/block-patterns/two-buttons.php +++ /dev/null @@ -1,22 +0,0 @@ - _x( 'Two buttons', 'Block pattern title' ), - 'content' => ' -
- - - - - -
- ', - 'viewportWidth' => 500, - 'categories' => array( 'buttons' ), - 'description' => _x( 'Two buttons, one filled and one outlined, side by side.', 'Block pattern description' ), -); diff --git a/src/wp-includes/block-patterns/two-images-gallery.php b/src/wp-includes/block-patterns/two-images-gallery.php deleted file mode 100644 index dd29b19370..0000000000 --- a/src/wp-includes/block-patterns/two-images-gallery.php +++ /dev/null @@ -1,15 +0,0 @@ - _x( 'Two images side by side', 'Block pattern title' ), - 'categories' => array( 'gallery' ), - 'content' => ' - - ', - 'description' => _x( 'Two images side by side', 'Block pattern description' ), -); diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 2dee4e2639..d2a89ed4d1 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -315,6 +315,7 @@ add_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); add_action( 'template_redirect', 'wp_shortlink_header', 11, 0 ); add_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); add_action( 'init', '_register_core_block_patterns_and_categories' ); +add_action( 'current_screen', '_load_remote_block_patterns' ); add_action( 'init', 'check_theme_switched', 99 ); add_action( 'init', array( 'WP_Block_Supports', 'init' ), 22 ); add_action( 'after_switch_theme', '_wp_menus_changed' ); diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index c08c85b539..9a615419b9 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -329,6 +329,10 @@ function create_initial_rest_routes() { $controller = new WP_REST_Block_Directory_Controller(); $controller->register_routes(); + // Pattern Directory. + $controller = new WP_REST_Pattern_Directory_Controller(); + $controller->register_routes(); + // Site Health. $site_health = WP_Site_Health::get_instance(); $controller = new WP_REST_Site_Health_Controller( $site_health ); 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 new file mode 100644 index 0000000000..c233c9499f --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php @@ -0,0 +1,297 @@ +namespace = 'wp/v2'; + $this->rest_base = 'pattern-directory'; + } + + /** + * Registers the necessary REST API routes. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/patterns', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Checks whether a given request has permission to view the local pattern directory. + * + * @since 5.8.0 + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|bool True if the request has permission, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { + if ( current_user_can( 'edit_posts' ) ) { + return true; + } + + foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { + if ( current_user_can( $post_type->cap->edit_posts ) ) { + return true; + } + } + + return new WP_Error( + 'rest_pattern_directory_cannot_view', + __( 'Sorry, you are not allowed to browse the local block pattern directory.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + /** + * Search and retrieve block patterns metadata + * + * @since 5.8.0 + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $query_args = array(); + $category_id = $request['category']; + $keyword_id = $request['keyword']; + $search_term = $request['search']; + + if ( $category_id ) { + $query_args['pattern-categories'] = $category_id; + } + + if ( $keyword_id ) { + $query_args['pattern-keywords'] = $keyword_id; + } + + if ( $search_term ) { + $query_args['search'] = $search_term; + } + + $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' ); + } + + $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 ), + ) + ); + } + + $response = array(); + + if ( $raw_patterns ) { + foreach ( $raw_patterns as $pattern ) { + $response[] = $this->prepare_response_for_collection( + $this->prepare_item_for_response( $pattern, $request ) + ); + } + } + + return new WP_REST_Response( $response ); + } + + /** + * Prepare a raw pattern before it's output in an API response. + * + * @since 5.8.0 + * + * @param object $raw_pattern A pattern from api.wordpress.org, before any changes. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response + */ + public function prepare_item_for_response( $raw_pattern, $request ) { + $prepared_pattern = array( + 'id' => absint( $raw_pattern->id ), + 'title' => sanitize_text_field( $raw_pattern->title->rendered ), + 'content' => wp_kses_post( $raw_pattern->pattern_content ), + 'categories' => array_map( 'sanitize_title', $raw_pattern->category_slugs ), + 'keywords' => array_map( 'sanitize_title', $raw_pattern->keyword_slugs ), + 'description' => sanitize_text_field( $raw_pattern->meta->wpop_description ), + 'viewport_width' => absint( $raw_pattern->meta->wpop_viewport_width ), + ); + + $prepared_pattern = $this->add_additional_fields_to_object( $prepared_pattern, $request ); + + $response = new WP_REST_Response( $prepared_pattern ); + + /** + * Filters the REST API response for a pattern. + * + * @since 5.8.0 + * + * @param WP_REST_Response $response The response object. + * @param object $raw_pattern The unprepared pattern. + * @param WP_REST_Request $request The request object. + */ + return apply_filters( 'rest_prepare_block_pattern', $response, $raw_pattern, $request ); + } + + /** + * Retrieves the pattern's schema, conforming to JSON Schema. + * + * @since 5.8.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'pattern-directory-item', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'The pattern ID.' ), + 'type' => 'integer', + 'minimum' => 1, + 'context' => array( 'view', 'embed' ), + ), + + 'title' => array( + 'description' => __( 'The pattern title, in human readable format.' ), + 'type' => 'string', + 'minLength' => 1, + 'context' => array( 'view', 'embed' ), + ), + + 'content' => array( + 'description' => __( 'The pattern content.' ), + 'type' => 'string', + 'minLength' => 1, + 'context' => array( 'view', 'embed' ), + ), + + 'categories' => array( + 'description' => __( "The pattern's category slugs." ), + 'type' => 'array', + 'uniqueItems' => true, + 'items' => array( 'type' => 'string' ), + 'context' => array( 'view', 'embed' ), + ), + + 'keywords' => array( + 'description' => __( "The pattern's keyword slugs." ), + 'type' => 'array', + 'uniqueItems' => true, + 'items' => array( 'type' => 'string' ), + 'context' => array( 'view', 'embed' ), + ), + + 'description' => array( + 'description' => __( 'A description of the pattern.' ), + 'type' => 'string', + 'minLength' => 1, + 'context' => array( 'view', 'embed' ), + ), + + 'viewport_width' => array( + 'description' => __( 'The preferred width of the viewport when previewing a pattern, in pixels.' ), + 'type' => 'integer', + 'context' => array( 'view', 'embed' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the search params for the patterns collection. + * + * @since 5.8.0 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + // Pagination is not supported. + unset( $query_params['page'] ); + unset( $query_params['per_page'] ); + + $query_params['search']['minLength'] = 1; + $query_params['context']['default'] = 'view'; + + $query_params['category'] = array( + 'description' => __( 'Limit results to those matching a category ID.' ), + 'type' => 'integer', + 'minimum' => 1, + ); + + $query_params['keyword'] = array( + 'description' => __( 'Limit results to those matching a keyword ID.' ), + 'type' => 'integer', + 'minimum' => 1, + ); + + /** + * Filter collection parameters for the pattern directory controller. + * + * @since 5.8.0 + * + * @param array $query_params JSON Schema-formatted collection parameters. + */ + return apply_filters( 'rest_pattern_directory_collection_params', $query_params ); + } +} diff --git a/src/wp-settings.php b/src/wp-settings.php index 48b648e803..75fa9e5881 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -268,6 +268,7 @@ require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-settings-controller require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-themes-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-plugins-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-directory-controller.php'; +require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-application-passwords-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-site-health-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-sidebars-controller.php'; diff --git a/tests/phpunit/data/blocks/pattern-directory/browse-all.json b/tests/phpunit/data/blocks/pattern-directory/browse-all.json new file mode 100644 index 0000000000..26131e33bb --- /dev/null +++ b/tests/phpunit/data/blocks/pattern-directory/browse-all.json @@ -0,0 +1,50 @@ +[ + { + "id": 31, + "title": { "rendered": "Heading and paragraph" }, + "content": { + "rendered": "\n
\n

2.
Which treats of the first sally the ingenious Don Quixote made from home

\n\n\n\n

These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.

\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "A heading preceded by a chapter number, and followed by a paragraph.", + "wpop_viewport_width": 1000 + }, + "category_slugs": [ "text" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n

2.
Which treats of the first sally the ingenious Don Quixote made from home

\n\n\n\n

These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.

\n
\n" + }, + { + "id": 25, + "title": { "rendered": "Large header with a heading" }, + "content": { + "rendered": "\n
\n

Don Quixote

\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "A large hero section with an example background image and a heading in the center.", + "wpop_viewport_width": 1000 + }, + "category_slugs": [ "header" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n

Don Quixote

\n
\n" + }, + { + "id": 26, + "title": { "rendered": "Large header with a heading and a button" }, + "content": { + "rendered": "\n
\n
\n
\n
\n
\n\n\n\n
\n
\n\n\n\n

Thou hast seen
nothing yet

\n\n\n\n\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "A large hero section with a bright gradient background, a big heading and a filled button.", + "wpop_viewport_width": 1000 + }, + "category_slugs": [ "header" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n
\n
\n
\n
\n\n\n\n
\n
\n\n\n\n

Thou hast seen
nothing yet

\n\n\n\n\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n
\n" + } +] diff --git a/tests/phpunit/data/blocks/pattern-directory/browse-category-2.json b/tests/phpunit/data/blocks/pattern-directory/browse-category-2.json new file mode 100644 index 0000000000..947da16bfd --- /dev/null +++ b/tests/phpunit/data/blocks/pattern-directory/browse-category-2.json @@ -0,0 +1,34 @@ +[ + { + "id": 15, + "title": { "rendered": "Three buttons" }, + "content": { + "rendered": "\n
\n\n\n\n\n\n\n\n\n\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "Three filled buttons with rounded corners, side by side.", + "wpop_viewport_width": 600 + }, + "category_slugs": [ "buttons" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n\n\n\n\n\n\n\n\n\n
\n" + }, + { + "id": 5, + "title": { "rendered": "Two buttons" }, + "content": { + "rendered": "\n
\n\n\n\n\n\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "Two buttons, one filled and one outlined, side by side.", + "wpop_viewport_width": 500 + }, + "category_slugs": [ "buttons" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n\n\n\n\n\n
\n" + } +] diff --git a/tests/phpunit/data/blocks/pattern-directory/browse-keyword-11.json b/tests/phpunit/data/blocks/pattern-directory/browse-keyword-11.json new file mode 100644 index 0000000000..26131e33bb --- /dev/null +++ b/tests/phpunit/data/blocks/pattern-directory/browse-keyword-11.json @@ -0,0 +1,50 @@ +[ + { + "id": 31, + "title": { "rendered": "Heading and paragraph" }, + "content": { + "rendered": "\n
\n

2.
Which treats of the first sally the ingenious Don Quixote made from home

\n\n\n\n

These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.

\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "A heading preceded by a chapter number, and followed by a paragraph.", + "wpop_viewport_width": 1000 + }, + "category_slugs": [ "text" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n

2.
Which treats of the first sally the ingenious Don Quixote made from home

\n\n\n\n

These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.

\n
\n" + }, + { + "id": 25, + "title": { "rendered": "Large header with a heading" }, + "content": { + "rendered": "\n
\n

Don Quixote

\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "A large hero section with an example background image and a heading in the center.", + "wpop_viewport_width": 1000 + }, + "category_slugs": [ "header" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n

Don Quixote

\n
\n" + }, + { + "id": 26, + "title": { "rendered": "Large header with a heading and a button" }, + "content": { + "rendered": "\n
\n
\n
\n
\n
\n\n\n\n
\n
\n\n\n\n

Thou hast seen
nothing yet

\n\n\n\n\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "A large hero section with a bright gradient background, a big heading and a filled button.", + "wpop_viewport_width": 1000 + }, + "category_slugs": [ "header" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n
\n
\n
\n
\n\n\n\n
\n
\n\n\n\n

Thou hast seen
nothing yet

\n\n\n\n\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n
\n" + } +] diff --git a/tests/phpunit/data/blocks/pattern-directory/search-button.json b/tests/phpunit/data/blocks/pattern-directory/search-button.json new file mode 100644 index 0000000000..b135e0ac5a --- /dev/null +++ b/tests/phpunit/data/blocks/pattern-directory/search-button.json @@ -0,0 +1,66 @@ +[ + { + "id": 26, + "title": { "rendered": "Large header with a heading and a button" }, + "content": { + "rendered": "\n
\n
\n
\n
\n
\n\n\n\n
\n
\n\n\n\n

Thou hast seen
nothing yet

\n\n\n\n\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "A large hero section with a bright gradient background, a big heading and a filled button.", + "wpop_viewport_width": 1000 + }, + "category_slugs": [ "header" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n
\n
\n
\n
\n\n\n\n
\n
\n\n\n\n

Thou hast seen
nothing yet

\n\n\n\n\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n
\n" + }, + { + "id": 28, + "title": { "rendered": "Three columns of text with buttons" }, + "content": { + "rendered": "\n
\n
\n
\n

Which treats of the character and pursuits of the famous Don Quixote of La Mancha.

\n\n\n\n\n
\n\n\n\n
\n

Which treats of the first sally the ingenious Don Quixote made from home.

\n\n\n\n\n
\n\n\n\n
\n

Wherein is related the droll way in which Don Quixote had himself dubbed a knight.

\n\n\n\n\n
\n
\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "Three small columns of text, each with an outlined button with rounded corners at the bottom.", + "wpop_viewport_width": 1000 + }, + "category_slugs": [ "columns" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n
\n
\n

Which treats of the character and pursuits of the famous Don Quixote of La Mancha.

\n\n\n\n\n
\n\n\n\n
\n

Which treats of the first sally the ingenious Don Quixote made from home.

\n\n\n\n\n
\n\n\n\n
\n

Wherein is related the droll way in which Don Quixote had himself dubbed a knight.

\n\n\n\n\n
\n
\n
\n" + }, + { + "id": 15, + "title": { "rendered": "Three buttons" }, + "content": { + "rendered": "\n
\n\n\n\n\n\n\n\n\n\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "Three filled buttons with rounded corners, side by side.", + "wpop_viewport_width": 600 + }, + "category_slugs": [ "buttons" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n\n\n\n\n\n\n\n\n\n
\n" + }, + { + "id": 5, + "title": { "rendered": "Two buttons" }, + "content": { + "rendered": "\n
\n\n\n\n\n\n
\n", + "protected": false + }, + "meta": { + "spay_email": "", + "wpop_description": "Two buttons, one filled and one outlined, side by side.", + "wpop_viewport_width": 500 + }, + "category_slugs": [ "buttons" ], + "keyword_slugs": [ "core" ], + "pattern_content": "\n
\n\n\n\n\n\n
\n" + } +] diff --git a/tests/phpunit/tests/blocks/block-editor.php b/tests/phpunit/tests/blocks/block-editor.php index 896a1ae36f..bd294ef6a5 100644 --- a/tests/phpunit/tests/blocks/block-editor.php +++ b/tests/phpunit/tests/blocks/block-editor.php @@ -29,6 +29,17 @@ class WP_Test_Block_Editor extends WP_UnitTestCase { ); $post = $this->factory()->post->create_and_get( $args ); + + global $wp_rest_server; + $wp_rest_server = new Spy_REST_Server; + do_action( 'rest_api_init', $wp_rest_server ); + } + + public function tearDown() { + /** @var WP_REST_Server $wp_rest_server */ + global $wp_rest_server; + $wp_rest_server = null; + parent::tearDown(); } function filter_set_block_categories_post( $block_categories, $post ) { diff --git a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php new file mode 100644 index 0000000000..50679c4b05 --- /dev/null +++ b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php @@ -0,0 +1,424 @@ +user->create( + array( + 'role' => 'contributor', + ) + ); + } + + /** + * Asserts that the pattern matches the expected response schema. + * + * @param WP_REST_Response[] $pattern An individual pattern from the REST API response. + */ + public function assertPatternMatchesSchema( $pattern ) { + $schema = ( new WP_REST_Pattern_Directory_Controller() )->get_item_schema(); + $pattern_id = isset( $pattern->id ) ? $pattern->id : '{pattern ID is missing}'; + + $this->assertTrue( + rest_validate_value_from_schema( $pattern, $schema ), + "Pattern ID `$pattern_id` doesn't match the response schema." + ); + + $this->assertSame( + array_keys( $schema['properties'] ), + array_keys( $pattern ), + "Pattern ID `$pattern_id` doesn't contain all of the fields expected from the schema." + ); + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::register_routes + * + * @since 5.8.0 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + + $this->assertArrayHasKey( '/wp/v2/pattern-directory/patterns', $routes ); + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_context_param + * + * @since 5.8.0 + */ + public function test_context_param() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pattern-directory/patterns' ); + $response = rest_get_server()->dispatch( $request ); + $patterns = $response->get_data(); + + $this->assertSame( 'view', $patterns['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed' ), $patterns['endpoints'][0]['args']['context']['enum'] ); + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @since 5.8.0 + */ + public function test_get_items() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'browse-all', true ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $response = rest_do_request( $request ); + $patterns = $response->get_data(); + + $this->assertNotWPError( $response->as_error() ); + $this->assertSame( 200, $response->status ); + $this->assertGreaterThan( 0, count( $patterns ) ); + + array_walk( $patterns, array( $this, 'assertPatternMatchesSchema' ) ); + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @since 5.8.0 + */ + public function test_get_items_by_category() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'browse-category', true ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $request->set_query_params( array( 'category' => 2 ) ); + $response = rest_do_request( $request ); + $patterns = $response->get_data(); + + $this->assertNotWPError( $response->as_error() ); + $this->assertSame( 200, $response->status ); + $this->assertGreaterThan( 0, count( $patterns ) ); + + array_walk( $patterns, array( $this, 'assertPatternMatchesSchema' ) ); + + foreach ( $patterns as $pattern ) { + $this->assertContains( 'buttons', $pattern['categories'] ); + } + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @since 5.8.0 + */ + public function test_get_items_by_keyword() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'browse-keyword', true ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $request->set_query_params( array( 'keyword' => 11 ) ); + $response = rest_do_request( $request ); + $patterns = $response->get_data(); + + $this->assertNotWPError( $response->as_error() ); + $this->assertSame( 200, $response->status ); + $this->assertGreaterThan( 0, count( $patterns ) ); + + array_walk( $patterns, array( $this, 'assertPatternMatchesSchema' ) ); + + foreach ( $patterns as $pattern ) { + $this->assertContains( 'core', $pattern['keywords'] ); + } + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @since 5.8.0 + */ + public function test_get_items_search() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'search', true ); + + $search_term = 'button'; + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $request->set_query_params( array( 'search' => $search_term ) ); + $response = rest_do_request( $request ); + $patterns = $response->get_data(); + + $this->assertNotWPError( $response->as_error() ); + $this->assertSame( 200, $response->status ); + $this->assertGreaterThan( 0, count( $patterns ) ); + + array_walk( $patterns, array( $this, 'assertPatternMatchesSchema' ) ); + + foreach ( $patterns as $pattern ) { + $search_field_values = $pattern['title'] . ' ' . $pattern['description']; + + $this->assertNotFalse( stripos( $search_field_values, $search_term ) ); + } + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @since 5.8.0 + */ + public function test_get_items_wdotorg_unavailable() { + wp_set_current_user( self::$contributor_id ); + self::prevent_requests_to_host( 'api.wordpress.org' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $response = rest_do_request( $request ); + + $this->assertErrorResponse( 'patterns_api_failed', $response, 500 ); + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @since 5.8.0 + */ + public function test_get_items_logged_out() { + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $request->set_query_params( array( 'search' => 'button' ) ); + $response = rest_do_request( $request ); + + $this->assertErrorResponse( 'rest_pattern_directory_cannot_view', $response ); + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @since 5.8.0 + */ + public function test_get_items_no_results() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'browse-all', false ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $request->set_query_params( array( 'category' => PHP_INT_MAX ) ); + $response = rest_do_request( $request ); + $patterns = $response->get_data(); + + $this->assertSame( 200, $response->status ); + $this->assertSame( array(), $patterns ); + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @since 5.8.0 + */ + public function test_get_items_search_no_results() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'search', false ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $request->set_query_params( array( 'search' => '0c4549ee68f24eaaed46a49dc983ecde' ) ); + $response = rest_do_request( $request ); + $patterns = $response->get_data(); + + $this->assertSame( 200, $response->status ); + $this->assertSame( array(), $patterns ); + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @since 5.8.0 + */ + public function test_get_items_invalid_response_data() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'invalid-data', true ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $response = rest_do_request( $request ); + + $this->assertSame( 500, $response->status ); + $this->assertWPError( $response->as_error() ); + } + + public function test_get_item() { + $this->markTestSkipped( 'Controller does not have get_item route.' ); + } + + public function test_create_item() { + $this->markTestSkipped( 'Controller does not have create_item route.' ); + } + + public function test_update_item() { + $this->markTestSkipped( 'Controller does not have update_item route.' ); + } + + public function test_delete_item() { + $this->markTestSkipped( 'Controller does not have delete_item route.' ); + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::prepare_item_for_response + * + * @since 5.8.0 + */ + public function test_prepare_item() { + $controller = new WP_REST_Pattern_Directory_Controller(); + $raw_patterns = json_decode( self::get_raw_response( 'browse-all' ) ); + $raw_patterns[0]->extra_field = 'this should be removed'; + + $prepared_pattern = $controller->prepare_response_for_collection( + $controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() ) + ); + + $this->assertPatternMatchesSchema( $prepared_pattern ); + $this->assertArrayNotHasKey( 'extra_field', $prepared_pattern ); + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::prepare_item_for_response + * + * @since 5.8.0 + */ + public function test_prepare_item_search() { + $controller = new WP_REST_Pattern_Directory_Controller(); + $raw_patterns = json_decode( self::get_raw_response( 'search' ) ); + $raw_patterns[0]->extra_field = 'this should be removed'; + + $prepared_pattern = $controller->prepare_response_for_collection( + $controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() ) + ); + + $this->assertPatternMatchesSchema( $prepared_pattern ); + $this->assertArrayNotHasKey( 'extra_field', $prepared_pattern ); + } + + /** + * Get a mocked raw response from api.wordpress.org. + * + * @return string + */ + private static function get_raw_response( $action ) { + $fixtures_dir = DIR_TESTDATA . '/blocks/pattern-directory'; + + switch ( $action ) { + default: + case 'browse-all': + // Response from https://api.wordpress.org/patterns/1.0/. + $response = file_get_contents( $fixtures_dir . '/browse-all.json' ); + break; + + case 'browse-category': + // Response from https://api.wordpress.org/patterns/1.0/?pattern-categories=2. + $response = file_get_contents( $fixtures_dir . '/browse-category-2.json' ); + break; + + case 'browse-keyword': + // Response from https://api.wordpress.org/patterns/1.0/?pattern-keywords=11. + $response = file_get_contents( $fixtures_dir . '/browse-keyword-11.json' ); + break; + + case 'search': + // Response from https://api.wordpress.org/patterns/1.0/?search=button. + $response = file_get_contents( $fixtures_dir . '/search-button.json' ); + break; + + case 'invalid-data': + $response = ''; // Any HTTP 200 response from w.org should be in JSON, even if it contains an error message. + break; + } + + return $response; + } + + /** + * @covers WP_REST_Pattern_Directory_Controller::get_item_schema + * + * @since 5.8.0 + */ + public function test_get_item_schema() { + $this->markTestSkipped( "The controller's schema is hardcoded, so tests would not be meaningful." ); + } + + /** + * Simulate a successful outbound HTTP requests, to keep tests pure and performant. + * + * @param string $action Pass a case from `get_raw_response()` to determine returned data. + * @param bool $expects_results Pass `true` to get results, or `false` to get 0 results. + * + * @since 5.8.0 + */ + private static function mock_successful_response( $action, $expects_results ) { + add_filter( + 'pre_http_request', + static function ( $preempt, $args, $url ) use ( $action, $expects_results ) { + + if ( 'api.wordpress.org' !== wp_parse_url( $url, PHP_URL_HOST ) ) { + return $preempt; + } + + $response = array( + 'headers' => array(), + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'body' => $expects_results ? self::get_raw_response( $action ) : '[]', + 'cookies' => array(), + 'filename' => null, + ); + + return $response; + }, + 10, + 3 + ); + } + + /** + * Simulate a network failure on outbound http requests to a given hostname. + * + * @since 5.8.0 + * + * @param string $blocked_host The host to block connections to. + */ + private static function prevent_requests_to_host( $blocked_host = 'api.wordpress.org' ) { + add_filter( + 'pre_http_request', + static function ( $return, $args, $url ) use ( $blocked_host ) { + + if ( wp_parse_url( $url, PHP_URL_HOST ) === $blocked_host ) { + return new WP_Error( + 'patterns_api_failed', + "An expected error occurred connecting to $blocked_host because of a unit test.", + "cURL error 7: Failed to connect to $blocked_host port 80: Connection refused" + ); + + } + + return $return; + }, + 10, + 3 + ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 359af6cc9d..edfce2b39d 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -95,6 +95,7 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase { '/wp/v2/pages/(?P[\\d]+)/revisions/(?P[\\d]+)', '/wp/v2/pages/(?P[\\d]+)/autosaves', '/wp/v2/pages/(?P[\\d]+)/autosaves/(?P[\\d]+)', + '/wp/v2/pattern-directory/patterns', '/wp/v2/media', '/wp/v2/media/(?P[\\d]+)', '/wp/v2/media/(?P[\\d]+)/post-process',