ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-rest-search-controller.php (10512B)


      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 				_doing_it_wrong(
     72 					__METHOD__,
     73 					/* translators: %s: PHP class name. */
     74 					sprintf( __( 'REST search handlers must extend the %s class.' ), 'WP_REST_Search_Handler' ),
     75 					'5.0.0'
     76 				);
     77 				continue;
     78 			}
     79 
     80 			$this->search_handlers[ $search_handler->get_type() ] = $search_handler;
     81 		}
     82 	}
     83 
     84 	/**
     85 	 * Registers the routes for the search controller.
     86 	 *
     87 	 * @since 5.0.0
     88 	 *
     89 	 * @see register_rest_route()
     90 	 */
     91 	public function register_routes() {
     92 		register_rest_route(
     93 			$this->namespace,
     94 			'/' . $this->rest_base,
     95 			array(
     96 				array(
     97 					'methods'             => WP_REST_Server::READABLE,
     98 					'callback'            => array( $this, 'get_items' ),
     99 					'permission_callback' => array( $this, 'get_items_permission_check' ),
    100 					'args'                => $this->get_collection_params(),
    101 				),
    102 				'schema' => array( $this, 'get_public_item_schema' ),
    103 			)
    104 		);
    105 	}
    106 
    107 	/**
    108 	 * Checks if a given request has access to search content.
    109 	 *
    110 	 * @since 5.0.0
    111 	 *
    112 	 * @param WP_REST_Request $request Full details about the request.
    113 	 * @return true|WP_Error True if the request has search access, WP_Error object otherwise.
    114 	 */
    115 	public function get_items_permission_check( $request ) {
    116 		return true;
    117 	}
    118 
    119 	/**
    120 	 * Retrieves a collection of search results.
    121 	 *
    122 	 * @since 5.0.0
    123 	 *
    124 	 * @param WP_REST_Request $request Full details about the request.
    125 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
    126 	 */
    127 	public function get_items( $request ) {
    128 		$handler = $this->get_search_handler( $request );
    129 		if ( is_wp_error( $handler ) ) {
    130 			return $handler;
    131 		}
    132 
    133 		$result = $handler->search_items( $request );
    134 
    135 		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 ] ) ) {
    136 			return new WP_Error(
    137 				'rest_search_handler_error',
    138 				__( 'Internal search handler error.' ),
    139 				array( 'status' => 500 )
    140 			);
    141 		}
    142 
    143 		$ids = $result[ WP_REST_Search_Handler::RESULT_IDS ];
    144 
    145 		$results = array();
    146 
    147 		foreach ( $ids as $id ) {
    148 			$data      = $this->prepare_item_for_response( $id, $request );
    149 			$results[] = $this->prepare_response_for_collection( $data );
    150 		}
    151 
    152 		$total     = (int) $result[ WP_REST_Search_Handler::RESULT_TOTAL ];
    153 		$page      = (int) $request['page'];
    154 		$per_page  = (int) $request['per_page'];
    155 		$max_pages = ceil( $total / $per_page );
    156 
    157 		if ( $page > $max_pages && $total > 0 ) {
    158 			return new WP_Error(
    159 				'rest_search_invalid_page_number',
    160 				__( 'The page number requested is larger than the number of pages available.' ),
    161 				array( 'status' => 400 )
    162 			);
    163 		}
    164 
    165 		$response = rest_ensure_response( $results );
    166 		$response->header( 'X-WP-Total', $total );
    167 		$response->header( 'X-WP-TotalPages', $max_pages );
    168 
    169 		$request_params = $request->get_query_params();
    170 		$base           = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
    171 
    172 		if ( $page > 1 ) {
    173 			$prev_link = add_query_arg( 'page', $page - 1, $base );
    174 			$response->link_header( 'prev', $prev_link );
    175 		}
    176 		if ( $page < $max_pages ) {
    177 			$next_link = add_query_arg( 'page', $page + 1, $base );
    178 			$response->link_header( 'next', $next_link );
    179 		}
    180 
    181 		return $response;
    182 	}
    183 
    184 	/**
    185 	 * Prepares a single search result for response.
    186 	 *
    187 	 * @since 5.0.0
    188 	 * @since 5.6.0 The `$id` parameter can accept a string.
    189 	 *
    190 	 * @param int|string      $id      ID of the item to prepare.
    191 	 * @param WP_REST_Request $request Request object.
    192 	 * @return WP_REST_Response Response object.
    193 	 */
    194 	public function prepare_item_for_response( $id, $request ) {
    195 		$handler = $this->get_search_handler( $request );
    196 		if ( is_wp_error( $handler ) ) {
    197 			return new WP_REST_Response();
    198 		}
    199 
    200 		$fields = $this->get_fields_for_response( $request );
    201 
    202 		$data = $handler->prepare_item( $id, $fields );
    203 		$data = $this->add_additional_fields_to_object( $data, $request );
    204 
    205 		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
    206 		$data    = $this->filter_response_by_context( $data, $context );
    207 
    208 		$response = rest_ensure_response( $data );
    209 
    210 		$links               = $handler->prepare_item_links( $id );
    211 		$links['collection'] = array(
    212 			'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
    213 		);
    214 		$response->add_links( $links );
    215 
    216 		return $response;
    217 	}
    218 
    219 	/**
    220 	 * Retrieves the item schema, conforming to JSON Schema.
    221 	 *
    222 	 * @since 5.0.0
    223 	 *
    224 	 * @return array Item schema data.
    225 	 */
    226 	public function get_item_schema() {
    227 		if ( $this->schema ) {
    228 			return $this->add_additional_fields_schema( $this->schema );
    229 		}
    230 
    231 		$types    = array();
    232 		$subtypes = array();
    233 
    234 		foreach ( $this->search_handlers as $search_handler ) {
    235 			$types[]  = $search_handler->get_type();
    236 			$subtypes = array_merge( $subtypes, $search_handler->get_subtypes() );
    237 		}
    238 
    239 		$types    = array_unique( $types );
    240 		$subtypes = array_unique( $subtypes );
    241 
    242 		$schema = array(
    243 			'$schema'    => 'http://json-schema.org/draft-04/schema#',
    244 			'title'      => 'search-result',
    245 			'type'       => 'object',
    246 			'properties' => array(
    247 				self::PROP_ID      => array(
    248 					'description' => __( 'Unique identifier for the object.' ),
    249 					'type'        => array( 'integer', 'string' ),
    250 					'context'     => array( 'view', 'embed' ),
    251 					'readonly'    => true,
    252 				),
    253 				self::PROP_TITLE   => array(
    254 					'description' => __( 'The title for the object.' ),
    255 					'type'        => 'string',
    256 					'context'     => array( 'view', 'embed' ),
    257 					'readonly'    => true,
    258 				),
    259 				self::PROP_URL     => array(
    260 					'description' => __( 'URL to the object.' ),
    261 					'type'        => 'string',
    262 					'format'      => 'uri',
    263 					'context'     => array( 'view', 'embed' ),
    264 					'readonly'    => true,
    265 				),
    266 				self::PROP_TYPE    => array(
    267 					'description' => __( 'Object type.' ),
    268 					'type'        => 'string',
    269 					'enum'        => $types,
    270 					'context'     => array( 'view', 'embed' ),
    271 					'readonly'    => true,
    272 				),
    273 				self::PROP_SUBTYPE => array(
    274 					'description' => __( 'Object subtype.' ),
    275 					'type'        => 'string',
    276 					'enum'        => $subtypes,
    277 					'context'     => array( 'view', 'embed' ),
    278 					'readonly'    => true,
    279 				),
    280 			),
    281 		);
    282 
    283 		$this->schema = $schema;
    284 
    285 		return $this->add_additional_fields_schema( $this->schema );
    286 	}
    287 
    288 	/**
    289 	 * Retrieves the query params for the search results collection.
    290 	 *
    291 	 * @since 5.0.0
    292 	 *
    293 	 * @return array Collection parameters.
    294 	 */
    295 	public function get_collection_params() {
    296 		$types    = array();
    297 		$subtypes = array();
    298 
    299 		foreach ( $this->search_handlers as $search_handler ) {
    300 			$types[]  = $search_handler->get_type();
    301 			$subtypes = array_merge( $subtypes, $search_handler->get_subtypes() );
    302 		}
    303 
    304 		$types    = array_unique( $types );
    305 		$subtypes = array_unique( $subtypes );
    306 
    307 		$query_params = parent::get_collection_params();
    308 
    309 		$query_params['context']['default'] = 'view';
    310 
    311 		$query_params[ self::PROP_TYPE ] = array(
    312 			'default'     => $types[0],
    313 			'description' => __( 'Limit results to items of an object type.' ),
    314 			'type'        => 'string',
    315 			'enum'        => $types,
    316 		);
    317 
    318 		$query_params[ self::PROP_SUBTYPE ] = array(
    319 			'default'           => self::TYPE_ANY,
    320 			'description'       => __( 'Limit results to items of one or more object subtypes.' ),
    321 			'type'              => 'array',
    322 			'items'             => array(
    323 				'enum' => array_merge( $subtypes, array( self::TYPE_ANY ) ),
    324 				'type' => 'string',
    325 			),
    326 			'sanitize_callback' => array( $this, 'sanitize_subtypes' ),
    327 		);
    328 
    329 		return $query_params;
    330 	}
    331 
    332 	/**
    333 	 * Sanitizes the list of subtypes, to ensure only subtypes of the passed type are included.
    334 	 *
    335 	 * @since 5.0.0
    336 	 *
    337 	 * @param string|array    $subtypes  One or more subtypes.
    338 	 * @param WP_REST_Request $request   Full details about the request.
    339 	 * @param string          $parameter Parameter name.
    340 	 * @return array|WP_Error List of valid subtypes, or WP_Error object on failure.
    341 	 */
    342 	public function sanitize_subtypes( $subtypes, $request, $parameter ) {
    343 		$subtypes = wp_parse_slug_list( $subtypes );
    344 
    345 		$subtypes = rest_parse_request_arg( $subtypes, $request, $parameter );
    346 		if ( is_wp_error( $subtypes ) ) {
    347 			return $subtypes;
    348 		}
    349 
    350 		// 'any' overrides any other subtype.
    351 		if ( in_array( self::TYPE_ANY, $subtypes, true ) ) {
    352 			return array( self::TYPE_ANY );
    353 		}
    354 
    355 		$handler = $this->get_search_handler( $request );
    356 		if ( is_wp_error( $handler ) ) {
    357 			return $handler;
    358 		}
    359 
    360 		return array_intersect( $subtypes, $handler->get_subtypes() );
    361 	}
    362 
    363 	/**
    364 	 * Gets the search handler to handle the current request.
    365 	 *
    366 	 * @since 5.0.0
    367 	 *
    368 	 * @param WP_REST_Request $request Full details about the request.
    369 	 * @return WP_REST_Search_Handler|WP_Error Search handler for the request type, or WP_Error object on failure.
    370 	 */
    371 	protected function get_search_handler( $request ) {
    372 		$type = $request->get_param( self::PROP_TYPE );
    373 
    374 		if ( ! $type || ! isset( $this->search_handlers[ $type ] ) ) {
    375 			return new WP_Error(
    376 				'rest_search_invalid_type',
    377 				__( 'Invalid type parameter.' ),
    378 				array( 'status' => 400 )
    379 			);
    380 		}
    381 
    382 		return $this->search_handlers[ $type ];
    383 	}
    384 }