wp/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php
changeset 9 177826044cd9
child 16 a86126ab1dd4
equal deleted inserted replaced
8:c7c34916027a 9:177826044cd9
       
     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.0.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( urlencode_deep( $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 }