Ticket #39965: 39965.2.diff
File 39965.2.diff, 41.8 KB (added by , 6 years ago) |
---|
-
src/wp-includes/rest-api.php
diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index e1403d71bd..adcd8b957b 100644
a b function create_initial_rest_routes() { 230 230 $controller = new WP_REST_Comments_Controller; 231 231 $controller->register_routes(); 232 232 233 /** 234 * Filters the search handlers to use in the REST search controller. 235 * 236 * @since 5.0.0 237 * 238 * @param array $search_handlers List of search handlers to use in the controller. Each search 239 * handler instance must extend the `WP_REST_Search_Handler` class. 240 * Default is only a handler for posts. 241 */ 242 $search_handlers = apply_filters( 'wp_rest_search_handlers', array( new WP_REST_Post_Search_Handler() ) ); 243 244 $controller = new WP_REST_Search_Controller( $search_handlers ); 245 $controller->register_routes(); 246 233 247 // Settings. 234 248 $controller = new WP_REST_Settings_Controller; 235 249 $controller->register_routes(); -
new file src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php
diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php new file mode 100644 index 0000000000..eb5493a9da
- + 1 <?php 2 /** 3 * REST API: WP_REST_Search_Controller class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 5.0.0 8 */ 9 10 /** 11 * Core class to search through all WordPress content via the REST API. 12 * 13 * @since 5.0.0 14 * 15 * @see WP_REST_Controller 16 */ 17 class WP_REST_Search_Controller extends WP_REST_Controller { 18 19 /** 20 * ID property name. 21 */ 22 const PROP_ID = 'id'; 23 24 /** 25 * Title property name. 26 */ 27 const PROP_TITLE = 'title'; 28 29 /** 30 * URL property name. 31 */ 32 const PROP_URL = 'url'; 33 34 /** 35 * Type property name. 36 */ 37 const PROP_TYPE = 'type'; 38 39 /** 40 * Subtype property name. 41 */ 42 const PROP_SUBTYPE = 'subtype'; 43 44 /** 45 * Identifier for the 'any' type. 46 */ 47 const TYPE_ANY = 'any'; 48 49 /** 50 * Search handlers used by the controller. 51 * 52 * @since 5.0.0 53 * @var array 54 */ 55 protected $search_handlers = array(); 56 57 /** 58 * Constructor. 59 * 60 * @since 5.0.0 61 * 62 * @param array $search_handlers List of search handlers to use in the controller. Each search 63 * handler instance must extend the `WP_REST_Search_Handler` class. 64 */ 65 public function __construct( array $search_handlers ) { 66 $this->namespace = 'wp/v2'; 67 $this->rest_base = 'search'; 68 69 foreach ( $search_handlers as $search_handler ) { 70 if ( ! $search_handler instanceof WP_REST_Search_Handler ) { 71 72 /* translators: %s: PHP class name */ 73 _doing_it_wrong( __METHOD__, sprintf( __( 'REST search handlers must extend the %s class.' ), 'WP_REST_Search_Handler' ), '5.5.0' ); 74 continue; 75 } 76 77 $this->search_handlers[ $search_handler->get_type() ] = $search_handler; 78 } 79 } 80 81 /** 82 * Registers the routes for the objects of the controller. 83 * 84 * @since 5.0.0 85 * 86 * @see register_rest_route() 87 */ 88 public function register_routes() { 89 register_rest_route( 90 $this->namespace, 91 '/' . $this->rest_base, 92 array( 93 array( 94 'methods' => WP_REST_Server::READABLE, 95 'callback' => array( $this, 'get_items' ), 96 'permission_callback' => array( $this, 'get_items_permission_check' ), 97 'args' => $this->get_collection_params(), 98 ), 99 'schema' => array( $this, 'get_public_item_schema' ), 100 ) 101 ); 102 } 103 104 /** 105 * Checks if a given request has access to search content. 106 * 107 * @since 5.0.0 108 * 109 * @param WP_REST_Request $request Full details about the request. 110 * @return true|WP_Error True if the request has search access, WP_Error object otherwise. 111 */ 112 public function get_items_permission_check( $request ) { 113 return true; 114 } 115 116 /** 117 * Retrieves a collection of search results. 118 * 119 * @since 5.0.0 120 * 121 * @param WP_REST_Request $request Full details about the request. 122 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 123 */ 124 public function get_items( $request ) { 125 $handler = $this->get_search_handler( $request ); 126 if ( is_wp_error( $handler ) ) { 127 return $handler; 128 } 129 130 $result = $handler->search_items( $request ); 131 132 if ( ! isset( $result[ WP_REST_Search_Handler::RESULT_IDS ] ) || ! is_array( $result[ WP_REST_Search_Handler::RESULT_IDS ] ) || ! isset( $result[ WP_REST_Search_Handler::RESULT_TOTAL ] ) ) { 133 return new WP_Error( 'rest_search_handler_error', __( 'Internal search handler error.' ), array( 'status' => 500 ) ); 134 } 135 136 $ids = array_map( 'absint', $result[ WP_REST_Search_Handler::RESULT_IDS ] ); 137 138 $results = array(); 139 foreach ( $ids as $id ) { 140 $data = $this->prepare_item_for_response( $id, $request ); 141 $results[] = $this->prepare_response_for_collection( $data ); 142 } 143 144 $total = (int) $result[ WP_REST_Search_Handler::RESULT_TOTAL ]; 145 $page = (int) $request['page']; 146 $per_page = (int) $request['per_page']; 147 $max_pages = ceil( $total / $per_page ); 148 149 if ( $page > $max_pages && $total > 0 ) { 150 return new WP_Error( 'rest_search_invalid_page_number', __( 'The page number requested is larger than the number of pages available.' ), array( 'status' => 400 ) ); 151 } 152 153 $response = rest_ensure_response( $results ); 154 $response->header( 'X-WP-Total', $total ); 155 $response->header( 'X-WP-TotalPages', $max_pages ); 156 157 $request_params = $request->get_query_params(); 158 $base = add_query_arg( $request_params, rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); 159 160 if ( $page > 1 ) { 161 $prev_link = add_query_arg( 'page', $page - 1, $base ); 162 $response->link_header( 'prev', $prev_link ); 163 } 164 if ( $page < $max_pages ) { 165 $next_link = add_query_arg( 'page', $page + 1, $base ); 166 $response->link_header( 'next', $next_link ); 167 } 168 169 return $response; 170 } 171 172 /** 173 * Prepares a single search result for response. 174 * 175 * @since 5.0.0 176 * 177 * @param int $id ID of the item to prepare. 178 * @param WP_REST_Request $request Request object. 179 * @return WP_REST_Response Response object. 180 */ 181 public function prepare_item_for_response( $id, $request ) { 182 $handler = $this->get_search_handler( $request ); 183 if ( is_wp_error( $handler ) ) { 184 return new WP_REST_Response(); 185 } 186 187 $fields = $this->get_fields_for_response( $request ); 188 189 $data = $handler->prepare_item( $id, $fields ); 190 $data = $this->add_additional_fields_to_object( $data, $request ); 191 192 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 193 $data = $this->filter_response_by_context( $data, $context ); 194 195 $response = rest_ensure_response( $data ); 196 197 $links = $handler->prepare_item_links( $id ); 198 $links['collection'] = array( 199 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), 200 ); 201 $response->add_links( $links ); 202 203 return $response; 204 } 205 206 /** 207 * Retrieves the item schema, conforming to JSON Schema. 208 * 209 * @since 5.0.0 210 * 211 * @return array Item schema data. 212 */ 213 public function get_item_schema() { 214 $types = array(); 215 $subtypes = array(); 216 foreach ( $this->search_handlers as $search_handler ) { 217 $types[] = $search_handler->get_type(); 218 $subtypes = array_merge( $subtypes, $search_handler->get_subtypes() ); 219 } 220 221 $types = array_unique( $types ); 222 $subtypes = array_unique( $subtypes ); 223 224 $schema = array( 225 '$schema' => 'http://json-schema.org/draft-04/schema#', 226 'title' => 'search-result', 227 'type' => 'object', 228 'properties' => array( 229 self::PROP_ID => array( 230 'description' => __( 'Unique identifier for the object.' ), 231 'type' => 'integer', 232 'context' => array( 'view', 'embed' ), 233 'readonly' => true, 234 ), 235 self::PROP_TITLE => array( 236 'description' => __( 'The title for the object.' ), 237 'type' => 'string', 238 'context' => array( 'view', 'embed' ), 239 'readonly' => true, 240 ), 241 self::PROP_URL => array( 242 'description' => __( 'URL to the object.' ), 243 'type' => 'string', 244 'format' => 'uri', 245 'context' => array( 'view', 'embed' ), 246 'readonly' => true, 247 ), 248 self::PROP_TYPE => array( 249 'description' => __( 'Object type.' ), 250 'type' => 'string', 251 'enum' => $types, 252 'context' => array( 'view', 'embed' ), 253 'readonly' => true, 254 ), 255 self::PROP_SUBTYPE => array( 256 'description' => __( 'Object subtype.' ), 257 'type' => 'string', 258 'enum' => $subtypes, 259 'context' => array( 'view', 'embed' ), 260 'readonly' => true, 261 ), 262 ), 263 ); 264 265 return $this->add_additional_fields_schema( $schema ); 266 } 267 268 /** 269 * Retrieves the query params for the search results collection. 270 * 271 * @since 5.0.0 272 * 273 * @return array Collection parameters. 274 */ 275 public function get_collection_params() { 276 $types = array(); 277 $subtypes = array(); 278 foreach ( $this->search_handlers as $search_handler ) { 279 $types[] = $search_handler->get_type(); 280 $subtypes = array_merge( $subtypes, $search_handler->get_subtypes() ); 281 } 282 283 $types = array_unique( $types ); 284 $subtypes = array_unique( $subtypes ); 285 286 $query_params = parent::get_collection_params(); 287 288 $query_params['context']['default'] = 'view'; 289 290 $query_params[ self::PROP_TYPE ] = array( 291 'default' => $types[0], 292 'description' => __( 'Limit results to items of an object type.' ), 293 'type' => 'string', 294 'enum' => $types, 295 ); 296 297 $query_params[ self::PROP_SUBTYPE ] = array( 298 'default' => self::TYPE_ANY, 299 'description' => __( 'Limit results to items of one or more object subtypes.' ), 300 'type' => 'array', 301 'items' => array( 302 'enum' => array_merge( $subtypes, array( self::TYPE_ANY ) ), 303 'type' => 'string', 304 ), 305 'sanitize_callback' => array( $this, 'sanitize_subtypes' ), 306 ); 307 308 return $query_params; 309 } 310 311 /** 312 * Sanitizes the list of subtypes, to ensure only subtypes of the passed type are included. 313 * 314 * @since 5.0.0 315 * 316 * @param string|array $subtypes One or more subtypes. 317 * @param WP_REST_Request $request Full details about the request. 318 * @param string $parameter Parameter name. 319 * @return array|WP_Error List of valid subtypes, or WP_Error object on failure. 320 */ 321 public function sanitize_subtypes( $subtypes, $request, $parameter ) { 322 $subtypes = wp_parse_slug_list( $subtypes ); 323 324 $subtypes = rest_parse_request_arg( $subtypes, $request, $parameter ); 325 if ( is_wp_error( $subtypes ) ) { 326 return $subtypes; 327 } 328 329 // 'any' overrides any other subtype. 330 if ( in_array( self::TYPE_ANY, $subtypes, true ) ) { 331 return array( self::TYPE_ANY ); 332 } 333 334 $handler = $this->get_search_handler( $request ); 335 if ( is_wp_error( $handler ) ) { 336 return $handler; 337 } 338 339 return array_intersect( $subtypes, $handler->get_subtypes() ); 340 } 341 342 /** 343 * Gets the search handler to handle the current request. 344 * 345 * @since 5.0.0 346 * 347 * @param WP_REST_Request $request Full details about the request. 348 * @return WP_REST_Search_Handler|WP_Error Search handler for the request type, or WP_Error object on failure. 349 */ 350 protected function get_search_handler( $request ) { 351 $type = $request->get_param( self::PROP_TYPE ); 352 353 if ( ! $type || ! isset( $this->search_handlers[ $type ] ) ) { 354 return new WP_Error( 'rest_search_invalid_type', __( 'Invalid type parameter.' ), array( 'status' => 400 ) ); 355 } 356 357 return $this->search_handlers[ $type ]; 358 } 359 } -
new file src/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php
diff --git a/src/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php b/src/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php new file mode 100644 index 0000000000..804be3b31b
- + 1 <?php 2 /** 3 * REST API: WP_REST_Post_Search_Handler class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 5.0.0 8 */ 9 10 /** 11 * Core class representing a search handler for posts in the REST API. 12 * 13 * @since 5.0.0 14 */ 15 class WP_REST_Post_Search_Handler extends WP_REST_Search_Handler { 16 17 /** 18 * Constructor. 19 * 20 * @since 5.0.0 21 */ 22 public function __construct() { 23 $this->type = 'post'; 24 25 // Support all public post types except attachments. 26 $this->subtypes = array_diff( 27 array_values( 28 get_post_types( 29 array( 30 'public' => true, 31 'show_in_rest' => true, 32 ), 33 'names' 34 ) 35 ), 36 array( 'attachment' ) 37 ); 38 } 39 40 /** 41 * Searches the object type content for a given search request. 42 * 43 * @since 5.0.0 44 * 45 * @param WP_REST_Request $request Full REST request. 46 * @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing 47 * an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the 48 * total count for the matching search results. 49 */ 50 public function search_items( WP_REST_Request $request ) { 51 52 // Get the post types to search for the current request. 53 $post_types = $request[ WP_REST_Search_Controller::PROP_SUBTYPE ]; 54 if ( in_array( WP_REST_Search_Controller::TYPE_ANY, $post_types, true ) ) { 55 $post_types = $this->subtypes; 56 } 57 58 $query_args = array( 59 'post_type' => $post_types, 60 'post_status' => 'publish', 61 'paged' => (int) $request['page'], 62 'posts_per_page' => (int) $request['per_page'], 63 'ignore_sticky_posts' => true, 64 'fields' => 'ids', 65 ); 66 67 if ( ! empty( $request['search'] ) ) { 68 $query_args['s'] = $request['search']; 69 } 70 71 $query = new WP_Query(); 72 $found_ids = $query->query( $query_args ); 73 $total = $query->found_posts; 74 75 return array( 76 self::RESULT_IDS => $found_ids, 77 self::RESULT_TOTAL => $total, 78 ); 79 } 80 81 /** 82 * Prepares the search result for a given ID. 83 * 84 * @since 5.0.0 85 * 86 * @param int $id Item ID. 87 * @param array $fields Fields to include for the item. 88 * @return array Associative array containing all fields for the item. 89 */ 90 public function prepare_item( $id, array $fields ) { 91 $post = get_post( $id ); 92 93 $data = array(); 94 95 if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) { 96 $data[ WP_REST_Search_Controller::PROP_ID ] = (int) $post->ID; 97 } 98 99 if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) { 100 if ( post_type_supports( $post->post_type, 'title' ) ) { 101 add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); 102 $data[ WP_REST_Search_Controller::PROP_TITLE ] = get_the_title( $post->ID ); 103 remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); 104 } else { 105 $data[ WP_REST_Search_Controller::PROP_TITLE ] = ''; 106 } 107 } 108 109 if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) { 110 $data[ WP_REST_Search_Controller::PROP_URL ] = get_permalink( $post->ID ); 111 } 112 113 if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) { 114 $data[ WP_REST_Search_Controller::PROP_TYPE ] = $this->type; 115 } 116 117 if ( in_array( WP_REST_Search_Controller::PROP_SUBTYPE, $fields, true ) ) { 118 $data[ WP_REST_Search_Controller::PROP_SUBTYPE ] = $post->post_type; 119 } 120 121 return $data; 122 } 123 124 /** 125 * Prepares links for the search result of a given ID. 126 * 127 * @since 5.0.0 128 * 129 * @param int $id Item ID. 130 * @return array Links for the given item. 131 */ 132 public function prepare_item_links( $id ) { 133 $post = get_post( $id ); 134 135 $links = array(); 136 137 $item_route = $this->detect_rest_item_route( $post ); 138 if ( ! empty( $item_route ) ) { 139 $links['self'] = array( 140 'href' => rest_url( $item_route ), 141 'embeddable' => true, 142 ); 143 } 144 145 $links['about'] = array( 146 'href' => rest_url( 'wp/v2/types/' . $post->post_type ), 147 ); 148 149 return $links; 150 } 151 152 /** 153 * Overwrites the default protected title format. 154 * 155 * By default, WordPress will show password protected posts with a title of 156 * "Protected: %s". As the REST API communicates the protected status of a post 157 * in a machine readable format, we remove the "Protected: " prefix. 158 * 159 * @since 5.0.0 160 * 161 * @return string Protected title format. 162 */ 163 public function protected_title_format() { 164 return '%s'; 165 } 166 167 /** 168 * Attempts to detect the route to access a single item. 169 * 170 * @since 5.0.0 171 * 172 * @param WP_Post $post Post object. 173 * @return string REST route relative to the REST base URI, or empty string if unknown. 174 */ 175 protected function detect_rest_item_route( $post ) { 176 $post_type = get_post_type_object( $post->post_type ); 177 if ( ! $post_type ) { 178 return ''; 179 } 180 181 // It's currently impossible to detect the REST URL from a custom controller. 182 if ( ! empty( $post_type->rest_controller_class ) && 'WP_REST_Posts_Controller' !== $post_type->rest_controller_class ) { 183 return ''; 184 } 185 186 $namespace = 'wp/v2'; 187 $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name; 188 189 return sprintf( '%s/%s/%d', $namespace, $rest_base, $post->ID ); 190 } 191 192 } -
new file src/wp-includes/rest-api/search/class-wp-rest-search-handler.php
diff --git a/src/wp-includes/rest-api/search/class-wp-rest-search-handler.php b/src/wp-includes/rest-api/search/class-wp-rest-search-handler.php new file mode 100644 index 0000000000..4799e1c981
- + 1 <?php 2 /** 3 * REST API: WP_REST_Search_Handler class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 5.0.0 8 */ 9 10 /** 11 * Core base class representing a search handler for an object type in the REST API. 12 * 13 * @since 5.0.0 14 */ 15 abstract class WP_REST_Search_Handler { 16 17 /** 18 * Field containing the IDs in the search result. 19 */ 20 const RESULT_IDS = 'ids'; 21 22 /** 23 * Field containing the total count in the search result. 24 */ 25 const RESULT_TOTAL = 'total'; 26 27 /** 28 * Object type managed by this search handler. 29 * 30 * @since 5.0.0 31 * @var string 32 */ 33 protected $type = ''; 34 35 /** 36 * Object subtypes managed by this search handler. 37 * 38 * @since 5.0.0 39 * @var array 40 */ 41 protected $subtypes = array(); 42 43 /** 44 * Gets the object type managed by this search handler. 45 * 46 * @since 5.0.0 47 * 48 * @return string Object type identifier. 49 */ 50 public function get_type() { 51 return $this->type; 52 } 53 54 /** 55 * Gets the object subtypes managed by this search handler. 56 * 57 * @since 5.0.0 58 * 59 * @return array Array of object subtype identifiers. 60 */ 61 public function get_subtypes() { 62 return $this->subtypes; 63 } 64 65 /** 66 * Searches the object type content for a given search request. 67 * 68 * @since 5.0.0 69 * 70 * @param WP_REST_Request $request Full REST request. 71 * @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing 72 * an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the 73 * total count for the matching search results. 74 */ 75 abstract public function search_items( WP_REST_Request $request ); 76 77 /** 78 * Prepares the search result for a given ID. 79 * 80 * @since 5.0.0 81 * 82 * @param int $id Item ID. 83 * @param array $fields Fields to include for the item. 84 * @return array Associative array containing all fields for the item. 85 */ 86 abstract public function prepare_item( $id, array $fields ); 87 88 /** 89 * Prepares links for the search result of a given ID. 90 * 91 * @since 5.0.0 92 * 93 * @param int $id Item ID. 94 * @return array Links for the given item. 95 */ 96 abstract public function prepare_item_links( $id ); 97 } -
src/wp-settings.php
diff --git a/src/wp-settings.php b/src/wp-settings.php index 44c8a91a07..d17325974f 100644
a b require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-taxonomies-control 233 233 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-terms-controller.php' ); 234 234 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-users-controller.php' ); 235 235 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-comments-controller.php' ); 236 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-search-controller.php' ); 236 237 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-settings-controller.php' ); 237 238 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-themes-controller.php' ); 238 239 require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php' ); … … require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.p 240 241 require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php' ); 241 242 require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-term-meta-fields.php' ); 242 243 require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-user-meta-fields.php' ); 244 require( ABSPATH . WPINC . '/rest-api/search/class-wp-rest-search-handler.php' ); 245 require( ABSPATH . WPINC . '/rest-api/search/class-wp-rest-post-search-handler.php' ); 243 246 244 247 $GLOBALS['wp_embed'] = new WP_Embed(); 245 248 -
tests/phpunit/includes/bootstrap.php
diff --git a/tests/phpunit/includes/bootstrap.php b/tests/phpunit/includes/bootstrap.php index c3b52cc553..a5bac2bcb0 100644
a b require dirname( __FILE__ ) . '/testcase-canonical.php'; 117 117 require dirname( __FILE__ ) . '/exceptions.php'; 118 118 require dirname( __FILE__ ) . '/utils.php'; 119 119 require dirname( __FILE__ ) . '/spy-rest-server.php'; 120 require dirname( __FILE__ ) . '/class-wp-rest-test-search-handler.php'; 120 121 121 122 /** 122 123 * A child class of the PHP test runner. -
new file tests/phpunit/includes/class-wp-rest-test-search-handler.php
diff --git a/tests/phpunit/includes/class-wp-rest-test-search-handler.php b/tests/phpunit/includes/class-wp-rest-test-search-handler.php new file mode 100644 index 0000000000..10a686972b
- + 1 <?php 2 /** 3 * REST API: WP_REST_Test_Search_Handler class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 */ 8 9 /** 10 * Test class extending WP_REST_Search_Handler 11 */ 12 class WP_REST_Test_Search_Handler extends WP_REST_Search_Handler { 13 14 protected $items = array(); 15 16 public function __construct( $amount = 10 ) { 17 $this->type = 'test'; 18 19 $this->subtypes = array( 'test_first_type', 'test_second_type' ); 20 21 $this->items = array(); 22 for ( $i = 1; $i <= $amount; $i++ ) { 23 $subtype = $i > $amount / 2 ? 'test_second_type' : 'test_first_type'; 24 25 $this->items[ $i ] = (object) array( 26 'test_id' => $i, 27 'test_title' => sprintf( 'Title %d', $i ), 28 'test_url' => sprintf( home_url( '/tests/%d' ), $i ), 29 'test_type' => $subtype, 30 ); 31 } 32 } 33 34 public function search_items( WP_REST_Request $request ) { 35 $subtypes = $request[ WP_REST_Search_Controller::PROP_SUBTYPE ]; 36 if ( in_array( WP_REST_Search_Controller::TYPE_ANY, $subtypes, true ) ) { 37 $subtypes = $this->subtypes; 38 } 39 40 $results = array(); 41 foreach ( $subtypes as $subtype ) { 42 $results = array_merge( $results, wp_list_filter( array_values( $this->items ), array( 'test_type' => $subtype ) ) ); 43 } 44 45 $results = wp_list_sort( $results, 'test_id', 'DESC' ); 46 47 $number = (int) $request['per_page']; 48 $offset = (int) $request['per_page'] * ( (int) $request['page'] - 1 ); 49 50 $total = count( $results ); 51 52 $results = array_slice( $results, $offset, $number ); 53 54 return array( 55 self::RESULT_IDS => wp_list_pluck( $results, 'test_id' ), 56 self::RESULT_TOTAL => $total, 57 ); 58 } 59 60 public function prepare_item( $id, array $fields ) { 61 $test = $this->items[ $id ]; 62 63 $data = array(); 64 65 if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) { 66 $data[ WP_REST_Search_Controller::PROP_ID ] = (int) $test->test_id; 67 } 68 69 if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) { 70 $data[ WP_REST_Search_Controller::PROP_TITLE ] = $test->test_title; 71 } 72 73 if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) { 74 $data[ WP_REST_Search_Controller::PROP_URL ] = $test->test_url; 75 } 76 77 if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) { 78 $data[ WP_REST_Search_Controller::PROP_TYPE ] = $this->type; 79 } 80 81 if ( in_array( WP_REST_Search_Controller::PROP_SUBTYPE, $fields, true ) ) { 82 $data[ WP_REST_Search_Controller::PROP_SUBTYPE ] = $test->test_type; 83 } 84 85 return $data; 86 } 87 88 public function prepare_item_links( $id ) { 89 return array(); 90 } 91 } -
tests/phpunit/tests/rest-api/rest-schema-setup.php
diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index e51fcc796d..1b868316f0 100644
a b class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase { 110 110 '/wp/v2/users/me', 111 111 '/wp/v2/comments', 112 112 '/wp/v2/comments/(?P<id>[\\d]+)', 113 '/wp/v2/search', 113 114 '/wp/v2/settings', 114 115 '/wp/v2/themes', 115 116 ); -
new file tests/phpunit/tests/rest-api/rest-search-controller.php
diff --git a/tests/phpunit/tests/rest-api/rest-search-controller.php b/tests/phpunit/tests/rest-api/rest-search-controller.php new file mode 100644 index 0000000000..60d1ef0bfb
- + 1 <?php 2 /** 3 * WP_REST_Search_Controller tests 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 */ 8 9 /** 10 * Tests for WP_REST_Search_Controller. 11 * 12 * @group restapi 13 */ 14 class WP_Test_REST_Search_Controller extends WP_Test_REST_Controller_Testcase { 15 16 /** 17 * Posts with title 'my-footitle'. 18 * 19 * @var array 20 */ 21 private static $my_title_post_ids = array(); 22 23 /** 24 * Pages with title 'my-footitle'. 25 * 26 * @var array 27 */ 28 private static $my_title_page_ids = array(); 29 30 /** 31 * Posts with content 'my-foocontent'. 32 * 33 * @var array 34 */ 35 private static $my_content_post_ids = array(); 36 37 /** 38 * Create fake data before our tests run. 39 * 40 * @param WP_UnitTest_Factory $factory Helper that lets us create fake data. 41 */ 42 public static function wpSetUpBeforeClass( $factory ) { 43 self::$my_title_post_ids = $factory->post->create_many( 44 4, 45 array( 46 'post_title' => 'my-footitle', 47 'post_type' => 'post', 48 ) 49 ); 50 51 self::$my_title_page_ids = $factory->post->create_many( 52 4, 53 array( 54 'post_title' => 'my-footitle', 55 'post_type' => 'page', 56 ) 57 ); 58 59 self::$my_content_post_ids = $factory->post->create_many( 60 6, 61 array( 62 'post_content' => 'my-foocontent', 63 ) 64 ); 65 } 66 67 /** 68 * Delete our fake data after our tests run. 69 */ 70 public static function wpTearDownAfterClass() { 71 $post_ids = array_merge( 72 self::$my_title_post_ids, 73 self::$my_title_page_ids, 74 self::$my_content_post_ids 75 ); 76 77 foreach ( $post_ids as $post_id ) { 78 wp_delete_post( $post_id, true ); 79 } 80 } 81 82 /** 83 * Check that our routes get set up properly. 84 */ 85 public function test_register_routes() { 86 $routes = rest_get_server()->get_routes(); 87 88 $this->assertArrayHasKey( '/wp/v2/search', $routes ); 89 $this->assertCount( 1, $routes['/wp/v2/search'] ); 90 } 91 92 /** 93 * Check the context parameter. 94 */ 95 public function test_context_param() { 96 $response = $this->do_request_with_params( array(), 'OPTIONS' ); 97 $data = $response->get_data(); 98 99 $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] ); 100 $this->assertEquals( array( 'view', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] ); 101 } 102 103 /** 104 * Search through all content. 105 */ 106 public function test_get_items() { 107 $response = $this->do_request_with_params( 108 array( 109 'per_page' => 100, 110 ) 111 ); 112 113 $this->assertEquals( 200, $response->get_status() ); 114 $this->assertEqualSets( 115 array_merge( 116 self::$my_title_post_ids, 117 self::$my_title_page_ids, 118 self::$my_content_post_ids 119 ), 120 wp_list_pluck( $response->get_data(), 'id' ) 121 ); 122 } 123 124 /** 125 * Search through all content with a low limit. 126 */ 127 public function test_get_items_with_limit() { 128 $response = $this->do_request_with_params( 129 array( 130 'per_page' => 3, 131 ) 132 ); 133 134 $this->assertEquals( 200, $response->get_status() ); 135 $this->assertEquals( 3, count( $response->get_data() ) ); 136 } 137 138 /** 139 * Search through posts of any post type. 140 */ 141 public function test_get_items_search_type_post() { 142 $response = $this->do_request_with_params( 143 array( 144 'per_page' => 100, 145 'type' => 'post', 146 ) 147 ); 148 149 $this->assertEquals( 200, $response->get_status() ); 150 $this->assertEqualSets( 151 array_merge( 152 self::$my_title_post_ids, 153 self::$my_title_page_ids, 154 self::$my_content_post_ids 155 ), 156 wp_list_pluck( $response->get_data(), 'id' ) 157 ); 158 } 159 160 /** 161 * Search through posts of post type 'post'. 162 */ 163 public function test_get_items_search_type_post_subtype_post() { 164 $response = $this->do_request_with_params( 165 array( 166 'per_page' => 100, 167 'type' => 'post', 168 'subtype' => 'post', 169 ) 170 ); 171 172 $this->assertEquals( 200, $response->get_status() ); 173 $this->assertEqualSets( 174 array_merge( 175 self::$my_title_post_ids, 176 self::$my_content_post_ids 177 ), 178 wp_list_pluck( $response->get_data(), 'id' ) 179 ); 180 } 181 182 /** 183 * Search through posts of post type 'page'. 184 */ 185 public function test_get_items_search_type_post_subtype_page() { 186 $response = $this->do_request_with_params( 187 array( 188 'per_page' => 100, 189 'type' => 'post', 190 'subtype' => 'page', 191 ) 192 ); 193 194 $this->assertEquals( 200, $response->get_status() ); 195 $this->assertEqualSets( 196 self::$my_title_page_ids, 197 wp_list_pluck( $response->get_data(), 'id' ) 198 ); 199 } 200 201 /** 202 * Search through an invalid type 203 */ 204 public function test_get_items_search_type_invalid() { 205 $response = $this->do_request_with_params( 206 array( 207 'per_page' => 100, 208 'type' => 'invalid', 209 ) 210 ); 211 212 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 213 } 214 215 /** 216 * Search through posts of an invalid post type. 217 */ 218 public function test_get_items_search_type_post_subtype_invalid() { 219 $response = $this->do_request_with_params( 220 array( 221 'per_page' => 100, 222 'type' => 'post', 223 'subtype' => 'invalid', 224 ) 225 ); 226 227 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 228 } 229 230 /** 231 * Search through posts and pages. 232 */ 233 public function test_get_items_search_posts_and_pages() { 234 $response = $this->do_request_with_params( 235 array( 236 'per_page' => 100, 237 'type' => 'post', 238 'subtype' => 'post,page', 239 ) 240 ); 241 242 $this->assertEquals( 200, $response->get_status() ); 243 $this->assertEqualSets( 244 array_merge( 245 self::$my_title_post_ids, 246 self::$my_title_page_ids, 247 self::$my_content_post_ids 248 ), 249 wp_list_pluck( $response->get_data(), 'id' ) 250 ); 251 } 252 253 /** 254 * Search through all that matches a 'footitle' search. 255 */ 256 public function test_get_items_search_for_footitle() { 257 $response = $this->do_request_with_params( 258 array( 259 'per_page' => 100, 260 'search' => 'footitle', 261 ) 262 ); 263 264 $this->assertEquals( 200, $response->get_status() ); 265 $this->assertEqualSets( 266 array_merge( 267 self::$my_title_post_ids, 268 self::$my_title_page_ids 269 ), 270 wp_list_pluck( $response->get_data(), 'id' ) 271 ); 272 } 273 274 /** 275 * Search through all that matches a 'foocontent' search. 276 */ 277 public function test_get_items_search_for_foocontent() { 278 $response = $this->do_request_with_params( 279 array( 280 'per_page' => 100, 281 'search' => 'foocontent', 282 ) 283 ); 284 285 $this->assertEquals( 200, $response->get_status() ); 286 $this->assertEqualSets( 287 self::$my_content_post_ids, 288 wp_list_pluck( $response->get_data(), 'id' ) 289 ); 290 } 291 292 /** 293 * Test retrieving a single item isn't possible. 294 */ 295 public function test_get_item() { 296 /** The search controller does not allow getting individual item content */ 297 $request = new WP_REST_Request( 'GET', '/wp/v2/search' . self::$my_title_post_ids[0] ); 298 $response = rest_get_server()->dispatch( $request ); 299 $this->assertEquals( 404, $response->get_status() ); 300 } 301 302 /** 303 * Test creating an item isn't possible. 304 */ 305 public function test_create_item() { 306 /** The search controller does not allow creating content */ 307 $request = new WP_REST_Request( 'POST', '/wp/v2/search' ); 308 $response = rest_get_server()->dispatch( $request ); 309 $this->assertEquals( 404, $response->get_status() ); 310 } 311 312 /** 313 * Test updating an item isn't possible. 314 */ 315 public function test_update_item() { 316 /** The search controller does not allow upading content */ 317 $request = new WP_REST_Request( 'POST', '/wp/v2/search' . self::$my_title_post_ids[0] ); 318 $response = rest_get_server()->dispatch( $request ); 319 $this->assertEquals( 404, $response->get_status() ); 320 } 321 322 /** 323 * Test deleting an item isn't possible. 324 */ 325 public function test_delete_item() { 326 /** The search controller does not allow deleting content */ 327 $request = new WP_REST_Request( 'DELETE', '/wp/v2/search' . self::$my_title_post_ids[0] ); 328 $response = rest_get_server()->dispatch( $request ); 329 $this->assertEquals( 404, $response->get_status() ); 330 } 331 332 /** 333 * Test preparing the data contains the correct fields. 334 */ 335 public function test_prepare_item() { 336 $response = $this->do_request_with_params(); 337 $this->assertEquals( 200, $response->get_status() ); 338 339 $data = $response->get_data(); 340 $this->assertEquals( 341 array( 342 'id', 343 'title', 344 'url', 345 'type', 346 'subtype', 347 '_links', 348 ), 349 array_keys( $data[0] ) 350 ); 351 } 352 353 /** 354 * Test preparing the data with limited fields contains the correct fields. 355 */ 356 public function test_prepare_item_limit_fields() { 357 if ( ! method_exists( 'WP_REST_Controller', 'get_fields_for_response' ) ) { 358 $this->markTestSkipped( 'Limiting fields requires the WP_REST_Controller::get_fields_for_response() method.' ); 359 } 360 361 $response = $this->do_request_with_params( 362 array( 363 '_fields' => 'id,title', 364 ) 365 ); 366 $this->assertEquals( 200, $response->get_status() ); 367 368 $data = $response->get_data(); 369 $this->assertEquals( 370 array( 371 'id', 372 'title', 373 '_links', 374 ), 375 array_keys( $data[0] ) 376 ); 377 } 378 379 /** 380 * Tests the item schema is correct. 381 */ 382 public function test_get_item_schema() { 383 $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/search' ); 384 $response = rest_get_server()->dispatch( $request ); 385 $data = $response->get_data(); 386 $properties = $data['schema']['properties']; 387 388 $this->assertArrayHasKey( 'id', $properties ); 389 $this->assertArrayHasKey( 'title', $properties ); 390 $this->assertArrayHasKey( 'url', $properties ); 391 $this->assertArrayHasKey( 'type', $properties ); 392 $this->assertArrayHasKey( 'subtype', $properties ); 393 } 394 395 /** 396 * Tests that non-public post types are not allowed. 397 */ 398 public function test_non_public_post_type() { 399 $response = $this->do_request_with_params( 400 array( 401 'type' => 'post', 402 'subtype' => 'post,nav_menu_item', 403 ) 404 ); 405 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 406 } 407 408 /** 409 * Test getting items directly with a custom search handler. 410 */ 411 public function test_custom_search_handler_get_items() { 412 $controller = new WP_REST_Search_Controller( array( new WP_REST_Test_Search_Handler( 10 ) ) ); 413 414 $request = $this->get_request( 415 array( 416 'page' => 1, 417 'per_page' => 10, 418 'type' => 'test', 419 'subtype' => array( WP_REST_Search_Controller::TYPE_ANY ), 420 ) 421 ); 422 $response = $controller->get_items( $request ); 423 $this->assertEqualSets( range( 1, 10 ), wp_list_pluck( $response->get_data(), 'id' ) ); 424 425 $request = $this->get_request( 426 array( 427 'page' => 1, 428 'per_page' => 10, 429 'type' => 'test', 430 'subtype' => array( 'test_first_type' ), 431 ) 432 ); 433 $response = $controller->get_items( $request ); 434 $this->assertEqualSets( range( 1, 5 ), wp_list_pluck( $response->get_data(), 'id' ) ); 435 } 436 437 /** 438 * Test preparing an item directly with a custom search handler. 439 */ 440 public function test_custom_search_handler_prepare_item() { 441 $controller = new WP_REST_Search_Controller( array( new WP_REST_Test_Search_Handler( 10 ) ) ); 442 443 $request = $this->get_request( 444 array( 445 'type' => 'test', 446 'subtype' => array( WP_REST_Search_Controller::TYPE_ANY ), 447 ) 448 ); 449 $response = $controller->prepare_item_for_response( 1, $request ); 450 $data = $response->get_data(); 451 $this->assertEquals( 452 array( 453 'id', 454 'title', 455 'url', 456 'type', 457 'subtype', 458 ), 459 array_keys( $data ) 460 ); 461 } 462 463 /** 464 * Test preparing an item directly with a custom search handler with limited fields. 465 */ 466 public function test_custom_search_handler_prepare_item_limit_fields() { 467 if ( ! method_exists( 'WP_REST_Controller', 'get_fields_for_response' ) ) { 468 $this->markTestSkipped( 'Limiting fields requires the WP_REST_Controller::get_fields_for_response() method.' ); 469 } 470 471 $controller = new WP_REST_Search_Controller( array( new WP_REST_Test_Search_Handler( 10 ) ) ); 472 473 $request = $this->get_request( 474 array( 475 'type' => 'test', 476 'subtype' => array( WP_REST_Search_Controller::TYPE_ANY ), 477 '_fields' => 'id,title', 478 ) 479 ); 480 $response = $controller->prepare_item_for_response( 1, $request ); 481 $data = $response->get_data(); 482 $this->assertEquals( 483 array( 484 'id', 485 'title', 486 ), 487 array_keys( $data ) 488 ); 489 } 490 491 /** 492 * Test getting the collection params directly with a custom search handler. 493 */ 494 public function test_custom_search_handler_get_collection_params() { 495 $controller = new WP_REST_Search_Controller( array( new WP_REST_Test_Search_Handler( 10 ) ) ); 496 497 $params = $controller->get_collection_params(); 498 $this->assertEquals( 'test', $params[ WP_REST_Search_Controller::PROP_TYPE ]['default'] ); 499 $this->assertEqualSets( array( 'test' ), $params[ WP_REST_Search_Controller::PROP_TYPE ]['enum'] ); 500 $this->assertEqualSets( array( 'test_first_type', 'test_second_type', WP_REST_Search_Controller::TYPE_ANY ), $params[ WP_REST_Search_Controller::PROP_SUBTYPE ]['items']['enum'] ); 501 } 502 503 /** 504 * Perform a REST request to our search endpoint with given parameters. 505 */ 506 private function do_request_with_params( $params = array(), $method = 'GET' ) { 507 $request = $this->get_request( $params, $method ); 508 509 return rest_get_server()->dispatch( $request ); 510 } 511 512 /** 513 * Get a REST request object for given parameters. 514 */ 515 private function get_request( $params = array(), $method = 'GET' ) { 516 $request = new WP_REST_Request( $method, '/wp/v2/search' ); 517 518 foreach ( $params as $param => $value ) { 519 $request->set_param( $param, $value ); 520 } 521 522 return $request; 523 } 524 525 } -
tests/qunit/fixtures/wp-api-generated.js
diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 6b9d530b46..5f3875ca04 100644
a b mockedApiResponse.Schema = { 3406 3406 } 3407 3407 ] 3408 3408 }, 3409 "/wp/v2/search": { 3410 "namespace": "wp/v2", 3411 "methods": [ 3412 "GET" 3413 ], 3414 "endpoints": [ 3415 { 3416 "methods": [ 3417 "GET" 3418 ], 3419 "args": { 3420 "context": { 3421 "required": false, 3422 "default": "view", 3423 "enum": [ 3424 "view", 3425 "embed" 3426 ], 3427 "description": "Scope under which the request is made; determines fields present in response.", 3428 "type": "string" 3429 }, 3430 "page": { 3431 "required": false, 3432 "default": 1, 3433 "description": "Current page of the collection.", 3434 "type": "integer" 3435 }, 3436 "per_page": { 3437 "required": false, 3438 "default": 10, 3439 "description": "Maximum number of items to be returned in result set.", 3440 "type": "integer" 3441 }, 3442 "search": { 3443 "required": false, 3444 "description": "Limit results to those matching a string.", 3445 "type": "string" 3446 }, 3447 "type": { 3448 "required": false, 3449 "default": "post", 3450 "enum": [ 3451 "post" 3452 ], 3453 "description": "Limit results to items of an object type.", 3454 "type": "string" 3455 }, 3456 "subtype": { 3457 "required": false, 3458 "default": "any", 3459 "description": "Limit results to items of one or more object subtypes.", 3460 "type": "array", 3461 "items": { 3462 "enum": [ 3463 "post", 3464 "page", 3465 "any" 3466 ], 3467 "type": "string" 3468 } 3469 } 3470 } 3471 } 3472 ], 3473 "_links": { 3474 "self": "http://example.org/index.php?rest_route=/wp/v2/search" 3475 } 3476 }, 3409 3477 "/wp/v2/settings": { 3410 3478 "namespace": "wp/v2", 3411 3479 "methods": [