diff --git a/editor/components/url-input/index.js b/editor/components/url-input/index.js
index 8f5887bca3cd18..71c3778399e077 100644
--- a/editor/components/url-input/index.js
+++ b/editor/components/url-input/index.js
@@ -70,10 +70,10 @@ class UrlInput extends Component {
loading: true,
} );
this.suggestionsRequest = apiRequest( {
- path: `/wp/v2/posts?${ stringify( {
+ path: `/gutenberg/v1/search?${ stringify( {
search: value,
per_page: 20,
- orderby: 'relevance',
+ type_exclude: 'attachment',
} ) }`,
} );
@@ -227,7 +227,7 @@ class UrlInput extends Component {
onClick={ () => this.selectLink( post.link ) }
aria-selected={ index === selectedSuggestion }
>
- { decodeEntities( post.title.rendered ) || __( '(no title)' ) }
+ { decodeEntities( post.title ) || __( '(no title)' ) }
) ) }
diff --git a/lib/class-wp-rest-search-controller.php b/lib/class-wp-rest-search-controller.php
new file mode 100644
index 00000000000000..bbfeaa11e0c321
--- /dev/null
+++ b/lib/class-wp-rest-search-controller.php
@@ -0,0 +1,473 @@
+namespace = 'gutenberg/v1';
+ $this->rest_base = 'search';
+ }
+
+ /**
+ * Registers the routes for the objects of the controller.
+ *
+ * @since 3.0.0
+ *
+ * @see register_rest_route()
+ */
+ public function register_routes() {
+
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permission_check' ),
+ 'args' => $this->get_collection_params(),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * Checks if a given request has access to search content.
+ *
+ * @since 3.0.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return true|WP_Error True if the request has search access, WP_Error object otherwise.
+ */
+ public function get_items_permission_check( $request ) {
+
+ return true;
+ }
+
+ /**
+ * Retrieves a collection of search results.
+ *
+ * @since 3.0.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function get_items( $request ) {
+
+ // Retrieve the list of registered collection query parameters.
+ $registered = $this->get_collection_params();
+ $query_args = array(
+ 'post_status' => array_keys( $this->get_allowed_post_stati() ),
+ 'post_type' => $request['type'],
+ 'ignore_sticky_posts' => true,
+ );
+
+ // If a search term is given, order by relevance.
+ if ( ! empty( $request['search'] ) ) {
+ $query_args['orderby'] = 'relevance';
+ $query_args['order'] = 'ASC';
+ }
+
+ // Transform 'any' into actual post type list.
+ if ( in_array( 'any', $request['type'], true ) ) {
+ $query_args['post_type'] = array_keys( $this->get_allowed_post_types() );
+ }
+
+ // Ensure to exclude post types as necessary.
+ if ( ! empty( $request['type_exclude'] ) ) {
+ $query_args['post_type'] = array_diff( $query_args['post_type'], $request['type_exclude'] );
+ }
+
+ /*
+ * This array defines mappings between public API query parameters whose
+ * values are accepted as-passed, and their internal WP_Query parameter
+ * name equivalents (some are the same). Only values which are also
+ * present in $registered will be set.
+ */
+ $parameter_mappings = array(
+ 'page' => 'paged',
+ 'search' => 's',
+ );
+
+ /*
+ * For each known parameter which is both registered and present in the request,
+ * set the parameter's value on the $query_args.
+ */
+ foreach ( $parameter_mappings as $api_param => $wp_param ) {
+ if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
+ $query_args[ $wp_param ] = $request[ $api_param ];
+ }
+ }
+
+ // Ensure our per_page parameter overrides any provided posts_per_page filter.
+ if ( isset( $registered['per_page'] ) ) {
+ $query_args['posts_per_page'] = $request['per_page'];
+ }
+
+ $posts_query = new WP_Query();
+ $query_result = $posts_query->query( $query_args );
+
+ $posts = array();
+
+ foreach ( $query_result as $post ) {
+ if ( ! $this->check_read_permission( $post ) ) {
+ continue;
+ }
+
+ $data = $this->prepare_item_for_response( $post, $request );
+ $posts[] = $this->prepare_response_for_collection( $data );
+ }
+
+ $page = (int) $query_args['paged'];
+ $total_posts = $posts_query->found_posts;
+
+ if ( $total_posts < 1 ) {
+ // Out-of-bounds, run the query again without LIMIT for total count.
+ unset( $query_args['paged'] );
+
+ $count_query = new WP_Query();
+ $count_query->query( $query_args );
+ $total_posts = $count_query->found_posts;
+ }
+
+ $max_pages = ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] );
+
+ if ( $page > $max_pages && $total_posts > 0 ) {
+ return new WP_Error( 'rest_post_invalid_page_number', __( 'The page number requested is larger than the number of pages available.', 'gutenberg' ), array( 'status' => 400 ) );
+ }
+
+ $response = rest_ensure_response( $posts );
+
+ $response->header( 'X-WP-Total', (int) $total_posts );
+ $response->header( 'X-WP-TotalPages', (int) $max_pages );
+
+ $request_params = $request->get_query_params();
+ $base = add_query_arg( $request_params, rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
+
+ if ( $page > 1 ) {
+ $prev_page = $page - 1;
+
+ if ( $prev_page > $max_pages ) {
+ $prev_page = $max_pages;
+ }
+
+ $prev_link = add_query_arg( 'page', $prev_page, $base );
+ $response->link_header( 'prev', $prev_link );
+ }
+ if ( $max_pages > $page ) {
+ $next_page = $page + 1;
+ $next_link = add_query_arg( 'page', $next_page, $base );
+
+ $response->link_header( 'next', $next_link );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Checks if a post can be read.
+ *
+ * Correctly handles posts with the inherit status.
+ *
+ * @since 3.0.0
+ *
+ * @param object $post Post object.
+ * @return bool Whether the post can be read.
+ */
+ public function check_read_permission( $post ) {
+
+ // Is the post readable?
+ if ( 'publish' === $post->post_status ) {
+ return true;
+ }
+
+ $post_type = get_post_type_object( $post->post_type );
+ if ( current_user_can( $post_type->cap->read_post, $post->ID ) ) {
+ return true;
+ }
+
+ // Can we read the parent if we're inheriting?
+ if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
+ $parent = get_post( $post->post_parent );
+ if ( $parent ) {
+ return $this->check_read_permission( $parent );
+ }
+ }
+
+ /*
+ * If there isn't a parent, but the status is set to inherit, assume
+ * it's published (as per get_post_status()).
+ */
+ if ( 'inherit' === $post->post_status ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Prepares a single search result for response.
+ *
+ * @since 3.0.0
+ *
+ * @param WP_Post $post Post object.
+ * @param WP_REST_Request $request Request object.
+ * @return WP_REST_Response Response object.
+ */
+ public function prepare_item_for_response( $post, $request ) {
+
+ $GLOBALS['post'] = $post;
+
+ setup_postdata( $post );
+
+ if ( method_exists( $this, 'get_fields_for_response' ) ) {
+ $fields = $this->get_fields_for_response( $request );
+ } else {
+ $schema = $this->get_item_schema();
+ $fields = array_keys( $schema['properties'] );
+ }
+
+ // Base fields for every post.
+ $data = array();
+
+ if ( in_array( 'id', $fields, true ) ) {
+ $data['id'] = $post->ID;
+ }
+
+ if ( in_array( 'type', $fields, true ) ) {
+ $data['type'] = $post->post_type;
+ }
+
+ if ( in_array( 'link', $fields, true ) ) {
+ $data['link'] = get_permalink( $post->ID );
+ }
+
+ if ( in_array( 'title', $fields, true ) ) {
+ if ( post_type_supports( $post->post_type, 'title' ) ) {
+ add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
+
+ $data['title'] = get_the_title( $post->ID );
+
+ remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
+ } else {
+ $data['title'] = '';
+ }
+ }
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+
+ // Wrap the data in a response object.
+ $response = rest_ensure_response( $data );
+
+ $response->add_links( $this->prepare_links( $post ) );
+
+ return $response;
+ }
+
+ /**
+ * Overwrites the default protected title format.
+ *
+ * By default, WordPress will show password protected posts with a title of
+ * "Protected: %s". As the REST API communicates the protected status of a post
+ * in a machine readable format, we remove the "Protected: " prefix.
+ *
+ * @since 3.0.0
+ *
+ * @return string Protected title format.
+ */
+ public function protected_title_format() {
+ return '%s';
+ }
+
+ /**
+ * Retrieves the item schema, conforming to JSON Schema.
+ *
+ * @since 3.0.0
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'search-result',
+ 'type' => 'object',
+ 'properties' => array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the object.', 'gutenberg' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'embed' ),
+ 'readonly' => true,
+ ),
+ 'link' => array(
+ 'description' => __( 'URL to the object.', 'gutenberg' ),
+ 'type' => 'string',
+ 'format' => 'uri',
+ 'context' => array( 'view', 'embed' ),
+ 'readonly' => true,
+ ),
+ 'title' => array(
+ 'description' => __( 'The title for the object.', 'gutenberg' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'embed' ),
+ 'readonly' => true,
+ ),
+ 'type' => array(
+ 'description' => __( 'Type of the object.', 'gutenberg' ),
+ 'type' => 'string',
+ 'enum' => array_keys( $this->get_allowed_post_types() ),
+ 'context' => array( 'view', 'embed' ),
+ 'readonly' => true,
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Retrieves the query params for the search results collection.
+ *
+ * @since 3.0.0
+ *
+ * @return array Collection parameters.
+ */
+ public function get_collection_params() {
+
+ $query_params = parent::get_collection_params();
+
+ $query_params['context']['default'] = 'view';
+
+ $query_params['type'] = array(
+ 'default' => 'any',
+ 'description' => __( 'Limit search results to content of one or more types.', 'gutenberg' ),
+ 'type' => 'array',
+ 'items' => array(
+ 'enum' => array_merge( array_keys( $this->get_allowed_post_types() ), array( 'any' ) ),
+ 'type' => 'string',
+ ),
+ );
+
+ $query_params['type_exclude'] = array(
+ 'default' => array(),
+ 'description' => __( 'Ensure search results exclude content of one or more specific types.', 'gutenberg' ),
+ 'type' => 'array',
+ 'items' => array(
+ 'enum' => array_keys( $this->get_allowed_post_types() ),
+ 'type' => 'string',
+ ),
+ );
+
+ return $query_params;
+ }
+
+ /**
+ * Prepares links for the request.
+ *
+ * @since 3.0.0
+ *
+ * @param WP_Post $post Post object.
+ * @return array Links for the given post.
+ */
+ protected function prepare_links( $post ) {
+
+ $links = array();
+
+ $item_route = $this->detect_rest_item_route( $post );
+ if ( ! empty( $item_route ) ) {
+ $links['self'] = array(
+ 'href' => rest_url( $item_route ),
+ 'embeddable' => true,
+ );
+ }
+
+ $links['collection'] = array(
+ 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
+ );
+
+ $links['about'] = array(
+ 'href' => rest_url( 'wp/v2/types/' . $post->post_type ),
+ );
+
+ return $links;
+ }
+
+ /**
+ * Attempts to detect the route to access a single item.
+ *
+ * @since 3.0.0
+ *
+ * @param WP_Post $post Post object.
+ * @return string REST route relative to the REST base URI, or empty string if not found.
+ */
+ protected function detect_rest_item_route( $post ) {
+ $post_type = get_post_type_object( $post->post_type );
+ if ( ! $post_type ) {
+ return '';
+ }
+
+ // It's currently impossible to detect the REST URL from a custom controller.
+ if ( ! empty( $post_type->rest_controller_class ) && 'WP_REST_Posts_Controller' !== $post_type->rest_controller_class ) {
+ return '';
+ }
+
+ $namespace = 'wp/v2';
+ $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
+
+ return sprintf( '%s/%s/%d', $namespace, $rest_base, $post->ID );
+ }
+
+ /**
+ * Gets the post statuses allowed for search.
+ *
+ * @since 3.0.0
+ *
+ * @return array List of post status objects, keyed by their name.
+ */
+ protected function get_allowed_post_stati() {
+
+ $post_stati = get_post_stati( array(
+ 'public' => true,
+ 'internal' => false,
+ ), 'objects' );
+
+ $post_stati['inherit'] = get_post_status_object( 'inherit' );
+
+ return $post_stati;
+ }
+
+ /**
+ * Gets the post types allowed for search.
+ *
+ * @since 3.0.0
+ *
+ * @return array List of post type objects, keyed by their name.
+ */
+ protected function get_allowed_post_types() {
+
+ return get_post_types( array(
+ 'public' => true,
+ 'show_in_rest' => true,
+ ), 'objects' );
+ }
+}
diff --git a/lib/load.php b/lib/load.php
index f1a8b09c66ce80..24854cd50399ad 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -15,6 +15,7 @@
require dirname( __FILE__ ) . '/class-wp-rest-blocks-controller.php';
require dirname( __FILE__ ) . '/class-wp-rest-autosaves-controller.php';
require dirname( __FILE__ ) . '/class-wp-rest-block-renderer-controller.php';
+ require dirname( __FILE__ ) . '/class-wp-rest-search-controller.php';
require dirname( __FILE__ ) . '/rest-api.php';
}
diff --git a/lib/rest-api.php b/lib/rest-api.php
index 2c691f954e184b..61a7b33a18707c 100644
--- a/lib/rest-api.php
+++ b/lib/rest-api.php
@@ -32,6 +32,9 @@ function gutenberg_register_rest_routes() {
$autosaves_controller->register_routes();
}
}
+
+ $controller = new WP_REST_Search_Controller();
+ $controller->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_rest_routes' );
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 581632857259cb..fa324dd7bc4dc0 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -22,6 +22,7 @@
lib/class-wp-rest-block-renderer-controller.php
+ lib/class-wp-rest-search-controller.php
diff --git a/phpunit/class-rest-search-controller-test.php b/phpunit/class-rest-search-controller-test.php
new file mode 100644
index 00000000000000..b6a7ffbc572767
--- /dev/null
+++ b/phpunit/class-rest-search-controller-test.php
@@ -0,0 +1,334 @@
+post->create_many( 4, array(
+ 'post_title' => 'my-footitle',
+ 'post_type' => 'post',
+ ) );
+
+ self::$my_title_page_ids = $factory->post->create_many( 4, array(
+ 'post_title' => 'my-footitle',
+ 'post_type' => 'page',
+ ) );
+
+ self::$my_title_attachment_ids = $factory->attachment->create_many( 4, array(
+ 'post_title' => 'my-footitle',
+ 'post_type' => 'attachment',
+ ) );
+
+ self::$my_content_post_ids = $factory->post->create_many( 6, array(
+ 'post_content' => 'my-foocontent',
+ ) );
+ }
+
+ /**
+ * Delete our fake data after our tests run.
+ */
+ public static function wpTearDownAfterClass() {
+ $post_ids = array_merge(
+ self::$my_title_post_ids,
+ self::$my_title_page_ids,
+ self::$my_title_attachment_ids,
+ self::$my_content_post_ids
+ );
+
+ foreach ( $post_ids as $post_id ) {
+ wp_delete_post( $post_id, true );
+ }
+ }
+
+ /**
+ * Check that our routes get set up properly.
+ */
+ public function test_register_routes() {
+ $routes = rest_get_server()->get_routes();
+
+ $this->assertArrayHasKey( '/gutenberg/v1/search', $routes );
+ $this->assertCount( 1, $routes['/gutenberg/v1/search'] );
+ }
+
+ /**
+ * Check the context parameter.
+ */
+ public function test_context_param() {
+ $response = $this->do_request_with_params( array(), 'OPTIONS' );
+ $data = $response->get_data();
+
+ $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+ $this->assertEquals( array( 'view', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
+ }
+
+ /**
+ * Search through all content.
+ */
+ public function test_get_items() {
+ $response = $this->do_request_with_params( array(
+ 'per_page' => 100,
+ ) );
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEqualSets(
+ array_merge(
+ self::$my_title_post_ids,
+ self::$my_title_page_ids,
+ self::$my_title_attachment_ids,
+ self::$my_content_post_ids
+ ),
+ wp_list_pluck( $response->get_data(), 'id' )
+ );
+ }
+
+ /**
+ * Search through our posts.
+ */
+ public function test_get_items_search_posts() {
+ $response = $this->do_request_with_params( array(
+ 'per_page' => 100,
+ 'type' => 'post',
+ ) );
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEqualSets(
+ array_merge(
+ self::$my_title_post_ids,
+ self::$my_content_post_ids
+ ),
+ wp_list_pluck( $response->get_data(), 'id' )
+ );
+ }
+
+ /**
+ * Search through our posts and pages.
+ */
+ public function test_get_items_search_posts_and_pages() {
+ $response = $this->do_request_with_params( array(
+ 'per_page' => 100,
+ 'type' => 'post,page',
+ ) );
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEqualSets(
+ array_merge(
+ self::$my_title_post_ids,
+ self::$my_title_page_ids,
+ self::$my_content_post_ids
+ ),
+ wp_list_pluck( $response->get_data(), 'id' )
+ );
+ }
+
+ /**
+ * Search through all content but attachments.
+ */
+ public function test_get_items_search_exclude_attachments() {
+ $response = $this->do_request_with_params( array(
+ 'per_page' => 100,
+ 'type_exclude' => 'attachment',
+ ) );
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEqualSets(
+ array_merge(
+ self::$my_title_post_ids,
+ self::$my_title_page_ids,
+ self::$my_content_post_ids
+ ),
+ wp_list_pluck( $response->get_data(), 'id' )
+ );
+ }
+
+ /**
+ * Search through all that matches a 'footitle' search.
+ */
+ public function test_get_items_search_for_footitle() {
+ $response = $this->do_request_with_params( array(
+ 'per_page' => 100,
+ 'search' => 'footitle',
+ ) );
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEqualSets(
+ array_merge(
+ self::$my_title_post_ids,
+ self::$my_title_page_ids,
+ self::$my_title_attachment_ids
+ ),
+ wp_list_pluck( $response->get_data(), 'id' )
+ );
+ }
+
+ /**
+ * Search through all that matches a 'foocontent' search.
+ */
+ public function test_get_items_search_for_foocontent() {
+ $response = $this->do_request_with_params( array(
+ 'per_page' => 100,
+ 'search' => 'foocontent',
+ ) );
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEqualSets(
+ self::$my_content_post_ids,
+ wp_list_pluck( $response->get_data(), 'id' )
+ );
+ }
+
+ /**
+ * Test retrieving a single item isn't possible.
+ */
+ public function test_get_item() {
+ /** The search controller does not allow getting individual item content */
+ $request = new WP_REST_Request( 'GET', '/gutenberg/v1/search' . self::$my_title_post_ids[0] );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertEquals( 404, $response->get_status() );
+ }
+
+ /**
+ * Test creating an item isn't possible.
+ */
+ public function test_create_item() {
+ /** The search controller does not allow creating content */
+ $request = new WP_REST_Request( 'POST', '/gutenberg/v1/search' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertEquals( 404, $response->get_status() );
+ }
+
+ /**
+ * Test updating an item isn't possible.
+ */
+ public function test_update_item() {
+ /** The search controller does not allow upading content */
+ $request = new WP_REST_Request( 'POST', '/gutenberg/v1/search' . self::$my_title_post_ids[0] );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertEquals( 404, $response->get_status() );
+ }
+
+ /**
+ * Test deleting an item isn't possible.
+ */
+ public function test_delete_item() {
+ /** The search controller does not allow deleting content */
+ $request = new WP_REST_Request( 'DELETE', '/gutenberg/v1/search' . self::$my_title_post_ids[0] );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertEquals( 404, $response->get_status() );
+ }
+
+ /**
+ * Test preparing the data contains the correct fields.
+ */
+ public function test_prepare_item() {
+ $response = $this->do_request_with_params();
+ $this->assertEquals( 200, $response->get_status() );
+
+ $data = $response->get_data();
+ $this->assertEquals( array(
+ 'id',
+ 'type',
+ 'link',
+ 'title',
+ '_links',
+ ), array_keys( $data[0] ) );
+ }
+
+ /**
+ * Test preparing the data with limited fields contains the correct fields.
+ */
+ public function test_prepare_item_limit_fields() {
+ if ( ! method_exists( 'WP_REST_Controller', 'get_fields_for_response' ) ) {
+ $this->markTestSkipped( 'Limiting fields requires the WP_REST_Controller::get_fields_for_response() method.' );
+ }
+
+ $response = $this->do_request_with_params( array(
+ '_fields' => 'id,title',
+ ) );
+ $this->assertEquals( 200, $response->get_status() );
+
+ $data = $response->get_data();
+ $this->assertEquals( array(
+ 'id',
+ 'title',
+ '_links',
+ ), array_keys( $data[0] ) );
+ }
+
+ /**
+ * Tests the item schema is correct.
+ */
+ public function test_get_item_schema() {
+ $request = new WP_REST_Request( 'OPTIONS', '/gutenberg/v1/search' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+ $properties = $data['schema']['properties'];
+
+ $this->assertArrayHasKey( 'id', $properties );
+ $this->assertArrayHasKey( 'link', $properties );
+ $this->assertArrayHasKey( 'title', $properties );
+ $this->assertArrayHasKey( 'type', $properties );
+ }
+
+ /**
+ * Tests that non-public post types are not allowed.
+ */
+ public function test_non_public_type() {
+ $response = $this->do_request_with_params( array(
+ 'type' => 'post,nav_menu_item',
+ ) );
+ $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+ }
+
+ /**
+ * Perform a REST request to our search endpoint with given parameters.
+ */
+ private function do_request_with_params( $params = array(), $method = 'GET' ) {
+ $request = new WP_REST_Request( $method, '/gutenberg/v1/search' );
+
+ foreach ( $params as $param => $value ) {
+ $request->set_param( $param, $value );
+ }
+
+ return rest_get_server()->dispatch( $request );
+ }
+}