PluginProbe
Gutenberg / 4.3.0
Gutenberg v4.3.0
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / class-wp-rest-search-controller.php

class-wp-rest-search-controller.php in Gutenberg 4.3.0, at lib/class-wp-rest-search-controller.php

364 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API: WP_REST_Search_Controller class
4 *
5 * @package gutenberg
6 * @since 3.3.0
7 */
8
9 /**
10 * Core class to search through all WordPress content via the REST API.
11 *
12 * @since 3.3.0
13 *
14 * @see WP_REST_Controller
15 */
16 class WP_REST_Search_Controller extends WP_REST_Controller {
17
18 /**
19 * ID property name.
20 */
21 const PROP_ID = 'id';
22
23 /**
24 * Title property name.
25 */
26 const PROP_TITLE = 'title';
27
28 /**
29 * URL property name.
30 */
31 const PROP_URL = 'url';
32
33 /**
34 * Type property name.
35 */
36 const PROP_TYPE = 'type';
37
38 /**
39 * Subtype property name.
40 */
41 const PROP_SUBTYPE = 'subtype';
42
43 /**
44 * Identifier for the 'any' type.
45 */
46 const TYPE_ANY = 'any';
47
48 /**
49 * Search handlers used by the controller.
50 *
51 * @since 3.3.0
52 * @var array
53 */
54 protected $search_handlers = array();
55
56 /**
57 * Constructor.
58 *
59 * @since 3.3.0
60 *
61 * @param array $search_handlers List of search handlers to use in the controller. Each search
62 * handler instance must extend the `WP_REST_Search_Handler` class.
63 */
64 public function __construct( array $search_handlers ) {
65 $this->namespace = 'wp/v2';
66 $this->rest_base = 'search';
67
68 foreach ( $search_handlers as $search_handler ) {
69 if ( ! $search_handler instanceof WP_REST_Search_Handler ) {
70
71 /* translators: %s: PHP class name */
72 _doing_it_wrong( __METHOD__, sprintf( __( 'REST search handlers must extend the %s class.', 'gutenberg' ), 'WP_REST_Search_Handler' ), '3.3.0' );
73 continue;
74 }
75
76 $this->search_handlers[ $search_handler->get_type() ] = $search_handler;
77 }
78 }
79
80 /**
81 * Registers the routes for the objects of the controller.
82 *
83 * @since 3.3.0
84 *
85 * @see register_rest_route()
86 */
87 public function register_routes() {
88 register_rest_route(
89 $this->namespace,
90 '/' . $this->rest_base,
91 array(
92 array(
93 'methods' => WP_REST_Server::READABLE,
94 'callback' => array( $this, 'get_items' ),
95 'permission_callback' => array( $this, 'get_items_permission_check' ),
96 'args' => $this->get_collection_params(),
97 ),
98 'schema' => array( $this, 'get_public_item_schema' ),
99 )
100 );
101 }
102
103 /**
104 * Checks if a given request has access to search content.
105 *
106 * @since 3.3.0
107 *
108 * @param WP_REST_Request $request Full details about the request.
109 * @return true|WP_Error True if the request has search access, WP_Error object otherwise.
110 */
111 public function get_items_permission_check( $request ) {
112 return true;
113 }
114
115 /**
116 * Retrieves a collection of search results.
117 *
118 * @since 3.3.0
119 *
120 * @param WP_REST_Request $request Full details about the request.
121 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
122 */
123 public function get_items( $request ) {
124 $handler = $this->get_search_handler( $request );
125 if ( is_wp_error( $handler ) ) {
126 return $handler;
127 }
128
129 $result = $handler->search_items( $request );
130
131 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 ] ) ) {
132 return new WP_Error( 'rest_search_handler_error', __( 'Internal search handler error.', 'gutenberg' ), array( 'status' => 500 ) );
133 }
134
135 $ids = array_map( 'absint', $result[ WP_REST_Search_Handler::RESULT_IDS ] );
136
137 $results = array();
138 foreach ( $ids as $id ) {
139 $data = $this->prepare_item_for_response( $id, $request );
140 $results[] = $this->prepare_response_for_collection( $data );
141 }
142
143 $total = (int) $result[ WP_REST_Search_Handler::RESULT_TOTAL ];
144 $page = (int) $request['page'];
145 $per_page = (int) $request['per_page'];
146 $max_pages = ceil( $total / $per_page );
147
148 if ( $page > $max_pages && $total > 0 ) {
149 return new WP_Error( 'rest_search_invalid_page_number', __( 'The page number requested is larger than the number of pages available.', 'gutenberg' ), array( 'status' => 400 ) );
150 }
151
152 $response = rest_ensure_response( $results );
153 $response->header( 'X-WP-Total', $total );
154 $response->header( 'X-WP-TotalPages', $max_pages );
155
156 $request_params = $request->get_query_params();
157 $base = add_query_arg( $request_params, rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
158
159 if ( $page > 1 ) {
160 $prev_link = add_query_arg( 'page', $page - 1, $base );
161 $response->link_header( 'prev', $prev_link );
162 }
163 if ( $page < $max_pages ) {
164 $next_link = add_query_arg( 'page', $page + 1, $base );
165 $response->link_header( 'next', $next_link );
166 }
167
168 return $response;
169 }
170
171 /**
172 * Prepares a single search result for response.
173 *
174 * @since 3.3.0
175 *
176 * @param int $id ID of the item to prepare.
177 * @param WP_REST_Request $request Request object.
178 * @return WP_REST_Response Response object.
179 */
180 public function prepare_item_for_response( $id, $request ) {
181 $handler = $this->get_search_handler( $request );
182 if ( is_wp_error( $handler ) ) {
183 return new WP_REST_Response();
184 }
185
186 if ( method_exists( $this, 'get_fields_for_response' ) ) {
187 $fields = $this->get_fields_for_response( $request );
188 } else {
189 $schema = $this->get_item_schema();
190 $fields = array_keys( $schema['properties'] );
191 }
192
193 $data = $handler->prepare_item( $id, $fields );
194 $data = $this->add_additional_fields_to_object( $data, $request );
195
196 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
197 $data = $this->filter_response_by_context( $data, $context );
198
199 $response = rest_ensure_response( $data );
200
201 $links = $handler->prepare_item_links( $id );
202 $links['collection'] = array(
203 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
204 );
205 $response->add_links( $links );
206
207 return $response;
208 }
209
210 /**
211 * Retrieves the item schema, conforming to JSON Schema.
212 *
213 * @since 3.3.0
214 *
215 * @return array Item schema data.
216 */
217 public function get_item_schema() {
218 $types = array();
219 $subtypes = array();
220 foreach ( $this->search_handlers as $search_handler ) {
221 $types[] = $search_handler->get_type();
222 $subtypes = array_merge( $subtypes, $search_handler->get_subtypes() );
223 }
224
225 $types = array_unique( $types );
226 $subtypes = array_unique( $subtypes );
227
228 $schema = array(
229 '$schema' => 'http://json-schema.org/draft-04/schema#',
230 'title' => 'search-result',
231 'type' => 'object',
232 'properties' => array(
233 self::PROP_ID => array(
234 'description' => __( 'Unique identifier for the object.', 'gutenberg' ),
235 'type' => 'integer',
236 'context' => array( 'view', 'embed' ),
237 'readonly' => true,
238 ),
239 self::PROP_TITLE => array(
240 'description' => __( 'The title for the object.', 'gutenberg' ),
241 'type' => 'string',
242 'context' => array( 'view', 'embed' ),
243 'readonly' => true,
244 ),
245 self::PROP_URL => array(
246 'description' => __( 'URL to the object.', 'gutenberg' ),
247 'type' => 'string',
248 'format' => 'uri',
249 'context' => array( 'view', 'embed' ),
250 'readonly' => true,
251 ),
252 self::PROP_TYPE => array(
253 'description' => __( 'Object type.', 'gutenberg' ),
254 'type' => 'string',
255 'enum' => $types,
256 'context' => array( 'view', 'embed' ),
257 'readonly' => true,
258 ),
259 self::PROP_SUBTYPE => array(
260 'description' => __( 'Object subtype.', 'gutenberg' ),
261 'type' => 'string',
262 'enum' => $subtypes,
263 'context' => array( 'view', 'embed' ),
264 'readonly' => true,
265 ),
266 ),
267 );
268
269 return $this->add_additional_fields_schema( $schema );
270 }
271
272 /**
273 * Retrieves the query params for the search results collection.
274 *
275 * @since 3.3.0
276 *
277 * @return array Collection parameters.
278 */
279 public function get_collection_params() {
280 $types = array();
281 $subtypes = array();
282 foreach ( $this->search_handlers as $search_handler ) {
283 $types[] = $search_handler->get_type();
284 $subtypes = array_merge( $subtypes, $search_handler->get_subtypes() );
285 }
286
287 $types = array_unique( $types );
288 $subtypes = array_unique( $subtypes );
289
290 $query_params = parent::get_collection_params();
291
292 $query_params['context']['default'] = 'view';
293
294 $query_params[ self::PROP_TYPE ] = array(
295 'default' => $types[0],
296 'description' => __( 'Limit results to items of an object type.', 'gutenberg' ),
297 'type' => 'string',
298 'enum' => $types,
299 );
300
301 $query_params[ self::PROP_SUBTYPE ] = array(
302 'default' => self::TYPE_ANY,
303 'description' => __( 'Limit results to items of one or more object subtypes.', 'gutenberg' ),
304 'type' => 'array',
305 'items' => array(
306 'enum' => array_merge( $subtypes, array( self::TYPE_ANY ) ),
307 'type' => 'string',
308 ),
309 'sanitize_callback' => array( $this, 'sanitize_subtypes' ),
310 );
311
312 return $query_params;
313 }
314
315 /**
316 * Sanitizes the list of subtypes, to ensure only subtypes of the passed type are included.
317 *
318 * @since 3.3.0
319 *
320 * @param string|array $subtypes One or more subtypes.
321 * @param WP_REST_Request $request Full details about the request.
322 * @param string $parameter Parameter name.
323 * @return array|WP_Error List of valid subtypes, or WP_Error object on failure.
324 */
325 public function sanitize_subtypes( $subtypes, $request, $parameter ) {
326 $subtypes = wp_parse_slug_list( $subtypes );
327
328 $subtypes = rest_parse_request_arg( $subtypes, $request, $parameter );
329 if ( is_wp_error( $subtypes ) ) {
330 return $subtypes;
331 }
332
333 // 'any' overrides any other subtype.
334 if ( in_array( self::TYPE_ANY, $subtypes, true ) ) {
335 return array( self::TYPE_ANY );
336 }
337
338 $handler = $this->get_search_handler( $request );
339 if ( is_wp_error( $handler ) ) {
340 return $handler;
341 }
342
343 return array_intersect( $subtypes, $handler->get_subtypes() );
344 }
345
346 /**
347 * Gets the search handler to handle the current request.
348 *
349 * @since 3.3.0
350 *
351 * @param WP_REST_Request $request Full details about the request.
352 * @return WP_REST_Search_Handler|WP_Error Search handler for the request type, or WP_Error object on failure.
353 */
354 protected function get_search_handler( $request ) {
355 $type = $request->get_param( self::PROP_TYPE );
356
357 if ( ! $type || ! isset( $this->search_handlers[ $type ] ) ) {
358 return new WP_Error( 'rest_search_invalid_type', __( 'Invalid type parameter.', 'gutenberg' ), array( 'status' => 400 ) );
359 }
360
361 return $this->search_handlers[ $type ];
362 }
363 }
364