| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 * @since 5.0.0 23 */ 24 const PROP_ID = 'id'; 25 26 /** 27 * Title property name. 28 * 29 * @since 5.0.0 30 */ 31 const PROP_TITLE = 'title'; 32 33 /** 34 * URL property name. 35 * 36 * @since 5.0.0 37 */ 38 const PROP_URL = 'url'; 39 40 /** 41 * Type property name. 42 * 43 * @since 5.0.0 44 */ 45 const PROP_TYPE = 'type'; 46 47 /** 48 * Subtype property name. 49 * 50 * @since 5.0.0 51 */ 52 const PROP_SUBTYPE = 'subtype'; 53 54 /** 55 * Identifier for the 'any' type. 56 * 57 * @since 5.0.0 58 */ 59 const TYPE_ANY = 'any'; 60 61 /** 62 * Search handlers used by the controller. 63 * 64 * @since 5.0.0 65 * @var WP_REST_Search_Handler[] 66 */ 67 protected $search_handlers = array(); 68 69 /** 70 * Constructor. 71 * 72 * @since 5.0.0 73 * 74 * @param array $search_handlers List of search handlers to use in the controller. Each search 75 * handler instance must extend the `WP_REST_Search_Handler` class. 76 */ 77 public function __construct( array $search_handlers ) { 78 $this->namespace = 'wp/v2'; 79 $this->rest_base = 'search'; 80 81 foreach ( $search_handlers as $search_handler ) { 82 if ( ! $search_handler instanceof WP_REST_Search_Handler ) { 83 _doing_it_wrong( 84 __METHOD__, 85 /* translators: %s: PHP class name. */ 86 sprintf( __( 'REST search handlers must extend the %s class.' ), 'WP_REST_Search_Handler' ), 87 '5.0.0' 88 ); 89 continue; 90 } 91 92 $this->search_handlers[ $search_handler->get_type() ] = $search_handler; 93 } 94 } 95 96 /** 97 * Registers the routes for the search controller. 98 * 99 * @since 5.0.0 100 * 101 * @see register_rest_route() 102 */ 103 public function register_routes() { 104 register_rest_route( 105 $this->namespace, 106 '/' . $this->rest_base, 107 array( 108 array( 109 'methods' => WP_REST_Server::READABLE, 110 'callback' => array( $this, 'get_items' ), 111 'permission_callback' => array( $this, 'get_items_permission_check' ), 112 'args' => $this->get_collection_params(), 113 ), 114 'schema' => array( $this, 'get_public_item_schema' ), 115 ) 116 ); 117 } 118 119 /** 120 * Checks if a given request has access to search content. 121 * 122 * @since 5.0.0 123 * 124 * @param WP_REST_Request $request Full details about the request. 125 * @return true|WP_Error True if the request has search access, WP_Error object otherwise. 126 */ 127 public function get_items_permission_check( $request ) { 128 return true; 129 } 130 131 /** 132 * Retrieves a collection of search results. 133 * 134 * @since 5.0.0 135 * 136 * @param WP_REST_Request $request Full details about the request. 137 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 138 */ 139 public function get_items( $request ) { 140 $handler = $this->get_search_handler( $request ); 141 if ( is_wp_error( $handler ) ) { 142 return $handler; 143 } 144 145 $result = $handler->search_items( $request ); 146 147 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 ] ) ) { 148 return new WP_Error( 149 'rest_search_handler_error', 150 __( 'Internal search handler error.' ), 151 array( 'status' => 500 ) 152 ); 153 } 154 155 $ids = $result[ WP_REST_Search_Handler::RESULT_IDS ]; 156 157 $is_head_request = $request->is_method( 'HEAD' ); 158 if ( ! $is_head_request ) { 159 $results = array(); 160 161 foreach ( $ids as $id ) { 162 $data = $this->prepare_item_for_response( $id, $request ); 163 $results[] = $this->prepare_response_for_collection( $data ); 164 } 165 } 166 167 $total = (int) $result[ WP_REST_Search_Handler::RESULT_TOTAL ]; 168 $page = (int) $request['page']; 169 $per_page = (int) $request['per_page']; 170 $max_pages = (int) ceil( $total / $per_page ); 171 172 if ( $page > $max_pages && $total > 0 ) { 173 return new WP_Error( 174 'rest_search_invalid_page_number', 175 __( 'The page number requested is larger than the number of pages available.' ), 176 array( 'status' => 400 ) 177 ); 178 } 179 180 $response = $is_head_request ? new WP_REST_Response( array() ) : rest_ensure_response( $results ); 181 $response->header( 'X-WP-Total', $total ); 182 $response->header( 'X-WP-TotalPages', $max_pages ); 183 184 $request_params = $request->get_query_params(); 185 $base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); 186 187 if ( $page > 1 ) { 188 $prev_link = add_query_arg( 'page', $page - 1, $base ); 189 $response->link_header( 'prev', $prev_link ); 190 } 191 if ( $page < $max_pages ) { 192 $next_link = add_query_arg( 'page', $page + 1, $base ); 193 $response->link_header( 'next', $next_link ); 194 } 195 196 return $response; 197 } 198 199 /** 200 * Prepares a single search result for response. 201 * 202 * @since 5.0.0 203 * @since 5.6.0 The `$id` parameter can accept a string. 204 * @since 5.9.0 Renamed `$id` to `$item` to match parent class for PHP 8 named parameter support. 205 * 206 * @param int|string $item ID of the item to prepare. 207 * @param WP_REST_Request $request Request object. 208 * @return WP_REST_Response Response object. 209 */ 210 public function prepare_item_for_response( $item, $request ) { 211 // Restores the more descriptive, specific name for use within this method. 212 $item_id = $item; 213 214 $handler = $this->get_search_handler( $request ); 215 if ( is_wp_error( $handler ) ) { 216 return new WP_REST_Response(); 217 } 218 219 $fields = $this->get_fields_for_response( $request ); 220 221 $data = $handler->prepare_item( $item_id, $fields ); 222 $data = $this->add_additional_fields_to_object( $data, $request ); 223 224 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 225 $data = $this->filter_response_by_context( $data, $context ); 226 227 $response = rest_ensure_response( $data ); 228 229 if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { 230 $links = $handler->prepare_item_links( $item_id ); 231 $links['collection'] = array( 232 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), 233 ); 234 $response->add_links( $links ); 235 } 236 237 return $response; 238 } 239 240 /** 241 * Retrieves the item schema, conforming to JSON Schema. 242 * 243 * @since 5.0.0 244 * 245 * @return array Item schema data. 246 */ 247 public function get_item_schema() { 248 if ( $this->schema ) { 249 return $this->add_additional_fields_schema( $this->schema ); 250 } 251 252 $types = array(); 253 $subtypes = array(); 254 255 foreach ( $this->search_handlers as $search_handler ) { 256 $types[] = $search_handler->get_type(); 257 $subtypes = array_merge( $subtypes, $search_handler->get_subtypes() ); 258 } 259 260 $types = array_unique( $types ); 261 $subtypes = array_unique( $subtypes ); 262 263 $schema = array( 264 '$schema' => 'http://json-schema.org/draft-04/schema#', 265 'title' => 'search-result', 266 'type' => 'object', 267 'properties' => array( 268 self::PROP_ID => array( 269 'description' => __( 'Unique identifier for the object.' ), 270 'type' => array( 'integer', 'string' ), 271 'context' => array( 'view', 'embed' ), 272 'readonly' => true, 273 ), 274 self::PROP_TITLE => array( 275 'description' => __( 'The title for the object.' ), 276 'type' => 'string', 277 'context' => array( 'view', 'embed' ), 278 'readonly' => true, 279 ), 280 self::PROP_URL => array( 281 'description' => __( 'URL to the object.' ), 282 'type' => 'string', 283 'format' => 'uri', 284 'context' => array( 'view', 'embed' ), 285 'readonly' => true, 286 ), 287 self::PROP_TYPE => array( 288 'description' => __( 'Object type.' ), 289 'type' => 'string', 290 'enum' => $types, 291 'context' => array( 'view', 'embed' ), 292 'readonly' => true, 293 ), 294 self::PROP_SUBTYPE => array( 295 'description' => __( 'Object subtype.' ), 296 'type' => 'string', 297 'enum' => $subtypes, 298 'context' => array( 'view', 'embed' ), 299 'readonly' => true, 300 ), 301 ), 302 ); 303 304 $this->schema = $schema; 305 306 return $this->add_additional_fields_schema( $this->schema ); 307 } 308 309 /** 310 * Retrieves the query params for the search results collection. 311 * 312 * @since 5.0.0 313 * 314 * @return array Collection parameters. 315 */ 316 public function get_collection_params() { 317 $types = array(); 318 $subtypes = array(); 319 320 foreach ( $this->search_handlers as $search_handler ) { 321 $types[] = $search_handler->get_type(); 322 $subtypes = array_merge( $subtypes, $search_handler->get_subtypes() ); 323 } 324 325 $types = array_unique( $types ); 326 $subtypes = array_unique( $subtypes ); 327 328 $query_params = parent::get_collection_params(); 329 330 $query_params['context']['default'] = 'view'; 331 332 $query_params[ self::PROP_TYPE ] = array( 333 'default' => $types[0], 334 'description' => __( 'Limit results to items of an object type.' ), 335 'type' => 'string', 336 'enum' => $types, 337 ); 338 339 $query_params[ self::PROP_SUBTYPE ] = array( 340 'default' => self::TYPE_ANY, 341 'description' => __( 'Limit results to items of one or more object subtypes.' ), 342 'type' => 'array', 343 'items' => array( 344 'enum' => array_merge( $subtypes, array( self::TYPE_ANY ) ), 345 'type' => 'string', 346 ), 347 'sanitize_callback' => array( $this, 'sanitize_subtypes' ), 348 ); 349 350 $query_params['exclude'] = array( 351 'description' => __( 'Ensure result set excludes specific IDs.' ), 352 'type' => 'array', 353 'items' => array( 354 'type' => 'integer', 355 ), 356 'default' => array(), 357 ); 358 359 $query_params['include'] = array( 360 'description' => __( 'Limit result set to specific IDs.' ), 361 'type' => 'array', 362 'items' => array( 363 'type' => 'integer', 364 ), 365 'default' => array(), 366 ); 367 368 return $query_params; 369 } 370 371 /** 372 * Sanitizes the list of subtypes, to ensure only subtypes of the passed type are included. 373 * 374 * @since 5.0.0 375 * 376 * @param string|array $subtypes One or more subtypes. 377 * @param WP_REST_Request $request Full details about the request. 378 * @param string $parameter Parameter name. 379 * @return string[]|WP_Error List of valid subtypes, or WP_Error object on failure. 380 */ 381 public function sanitize_subtypes( $subtypes, $request, $parameter ) { 382 $subtypes = wp_parse_slug_list( $subtypes ); 383 384 $subtypes = rest_parse_request_arg( $subtypes, $request, $parameter ); 385 if ( is_wp_error( $subtypes ) ) { 386 return $subtypes; 387 } 388 389 // 'any' overrides any other subtype. 390 if ( in_array( self::TYPE_ANY, $subtypes, true ) ) { 391 return array( self::TYPE_ANY ); 392 } 393 394 $handler = $this->get_search_handler( $request ); 395 if ( is_wp_error( $handler ) ) { 396 return $handler; 397 } 398 399 return array_intersect( $subtypes, $handler->get_subtypes() ); 400 } 401 402 /** 403 * Gets the search handler to handle the current request. 404 * 405 * @since 5.0.0 406 * 407 * @param WP_REST_Request $request Full details about the request. 408 * @return WP_REST_Search_Handler|WP_Error Search handler for the request type, or WP_Error object on failure. 409 */ 410 protected function get_search_handler( $request ) { 411 $type = $request->get_param( self::PROP_TYPE ); 412 413 if ( ! $type || ! is_string( $type ) || ! isset( $this->search_handlers[ $type ] ) ) { 414 return new WP_Error( 415 'rest_search_invalid_type', 416 __( 'Invalid type parameter.' ), 417 array( 'status' => 400 ) 418 ); 419 } 420 421 return $this->search_handlers[ $type ]; 422 } 423 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sun Oct 11 08:20:32 2026 | Cross-referenced by PHPXref |