WordPress 4.7
[autoinstalls/wordpress.git] / wp-includes / rest-api / endpoints / class-wp-rest-controller.php
1 <?php
2 /**
3  * REST API: WP_REST_Controller class
4  *
5  * @package WordPress
6  * @subpackage REST_API
7  * @since 4.7.0
8  */
9
10 /**
11  * Core base controller for managing and interacting with REST API items.
12  *
13  * @since 4.7.0
14  */
15 abstract class WP_REST_Controller {
16
17         /**
18          * The namespace of this controller's route.
19          *
20          * @since 4.7.0
21          * @access protected
22          * @var string
23          */
24         protected $namespace;
25
26         /**
27          * The base of this controller's route.
28          *
29          * @since 4.7.0
30          * @access protected
31          * @var string
32          */
33         protected $rest_base;
34
35         /**
36          * Registers the routes for the objects of the controller.
37          *
38          * @since 4.7.0
39          * @access public
40          */
41         public function register_routes() {
42                 _doing_it_wrong( 'WP_REST_Controller::register_routes', __( 'The register_routes() method must be overridden' ), '4.7' );
43         }
44
45         /**
46          * Checks if a given request has access to get items.
47          *
48          * @since 4.7.0
49          * @access public
50          *
51          * @param WP_REST_Request $request Full data about the request.
52          * @return WP_Error|bool True if the request has read access, WP_Error object otherwise.
53          */
54         public function get_items_permissions_check( $request ) {
55                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
56         }
57
58         /**
59          * Retrieves a collection of items.
60          *
61          * @since 4.7.0
62          * @access public
63          *
64          * @param WP_REST_Request $request Full data about the request.
65          * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
66          */
67         public function get_items( $request ) {
68                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
69         }
70
71         /**
72          * Checks if a given request has access to get a specific item.
73          *
74          * @since 4.7.0
75          * @access public
76          *
77          * @param WP_REST_Request $request Full data about the request.
78          * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise.
79          */
80         public function get_item_permissions_check( $request ) {
81                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
82         }
83
84         /**
85          * Retrieves one item from the collection.
86          *
87          * @since 4.7.0
88          * @access public
89          *
90          * @param WP_REST_Request $request Full data about the request.
91          * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
92          */
93         public function get_item( $request ) {
94                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
95         }
96
97         /**
98          * Checks if a given request has access to create items.
99          *
100          * @since 4.7.0
101          * @access public
102          *
103          * @param WP_REST_Request $request Full data about the request.
104          * @return WP_Error|bool True if the request has access to create items, WP_Error object otherwise.
105          */
106         public function create_item_permissions_check( $request ) {
107                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
108         }
109
110         /**
111          * Creates one item from the collection.
112          *
113          * @since 4.7.0
114          * @access public
115          *
116          * @param WP_REST_Request $request Full data about the request.
117          * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
118          */
119         public function create_item( $request ) {
120                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
121         }
122
123         /**
124          * Checks if a given request has access to update a specific item.
125          *
126          * @since 4.7.0
127          * @access public
128          *
129          * @param WP_REST_Request $request Full data about the request.
130          * @return WP_Error|bool True if the request has access to update the item, WP_Error object otherwise.
131          */
132         public function update_item_permissions_check( $request ) {
133                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
134         }
135
136         /**
137          * Updates one item from the collection.
138          *
139          * @since 4.7.0
140          * @access public
141          *
142          * @param WP_REST_Request $request Full data about the request.
143          * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
144          */
145         public function update_item( $request ) {
146                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
147         }
148
149         /**
150          * Checks if a given request has access to delete a specific item.
151          *
152          * @since 4.7.0
153          * @access public
154          *
155          * @param WP_REST_Request $request Full data about the request.
156          * @return WP_Error|bool True if the request has access to delete the item, WP_Error object otherwise.
157          */
158         public function delete_item_permissions_check( $request ) {
159                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
160         }
161
162         /**
163          * Deletes one item from the collection.
164          *
165          * @since 4.7.0
166          * @access public
167          *
168          * @param WP_REST_Request $request Full data about the request.
169          * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
170          */
171         public function delete_item( $request ) {
172                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
173         }
174
175         /**
176          * Prepares one item for create or update operation.
177          *
178          * @since 4.7.0
179          * @access public
180          *
181          * @param WP_REST_Request $request Request object.
182          * @return WP_Error|object The prepared item, or WP_Error object on failure.
183          */
184         protected function prepare_item_for_database( $request ) {
185                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
186         }
187
188         /**
189          * Prepares the item for the REST response.
190          *
191          * @since 4.7.0
192          * @access public
193          *
194          * @param mixed           $item    WordPress representation of the item.
195          * @param WP_REST_Request $request Request object.
196          * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
197          */
198         public function prepare_item_for_response( $item, $request ) {
199                 return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
200         }
201
202         /**
203          * Prepares a response for insertion into a collection.
204          *
205          * @since 4.7.0
206          * @access public
207          *
208          * @param WP_REST_Response $response Response object.
209          * @return array|mixed Response data, ready for insertion into collection data.
210          */
211         public function prepare_response_for_collection( $response ) {
212                 if ( ! ( $response instanceof WP_REST_Response ) ) {
213                         return $response;
214                 }
215
216                 $data   = (array) $response->get_data();
217                 $server = rest_get_server();
218
219                 if ( method_exists( $server, 'get_compact_response_links' ) ) {
220                         $links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
221                 } else {
222                         $links = call_user_func( array( $server, 'get_response_links' ), $response );
223                 }
224
225                 if ( ! empty( $links ) ) {
226                         $data['_links'] = $links;
227                 }
228
229                 return $data;
230         }
231
232         /**
233          * Filters a response based on the context defined in the schema.
234          *
235          * @since 4.7.0
236          * @access public
237          *
238          * @param array  $data    Response data to fiter.
239          * @param string $context Context defined in the schema.
240          * @return array Filtered response.
241          */
242         public function filter_response_by_context( $data, $context ) {
243
244                 $schema = $this->get_item_schema();
245
246                 foreach ( $data as $key => $value ) {
247                         if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) {
248                                 continue;
249                         }
250
251                         if ( ! in_array( $context, $schema['properties'][ $key ]['context'], true ) ) {
252                                 unset( $data[ $key ] );
253                                 continue;
254                         }
255
256                         if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) {
257                                 foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) {
258                                         if ( empty( $details['context'] ) ) {
259                                                 continue;
260                                         }
261
262                                         if ( ! in_array( $context, $details['context'], true ) ) {
263                                                 if ( isset( $data[ $key ][ $attribute ] ) ) {
264                                                         unset( $data[ $key ][ $attribute ] );
265                                                 }
266                                         }
267                                 }
268                         }
269                 }
270
271                 return $data;
272         }
273
274         /**
275          * Retrieves the item's schema, conforming to JSON Schema.
276          *
277          * @since 4.7.0
278          * @access public
279          *
280          * @return array Item schema data.
281          */
282         public function get_item_schema() {
283                 return $this->add_additional_fields_schema( array() );
284         }
285
286         /**
287          * Retrieves the item's schema for display / public consumption purposes.
288          *
289          * @since 4.7.0
290          * @access public
291          *
292          * @return array Public item schema data.
293          */
294         public function get_public_item_schema() {
295
296                 $schema = $this->get_item_schema();
297
298                 foreach ( $schema['properties'] as &$property ) {
299                         unset( $property['arg_options'] );
300                 }
301
302                 return $schema;
303         }
304
305         /**
306          * Retrieves the query params for the collections.
307          *
308          * @since 4.7.0
309          * @access public
310          *
311          * @return array Query parameters for the collection.
312          */
313         public function get_collection_params() {
314                 return array(
315                         'context'                => $this->get_context_param(),
316                         'page'                   => array(
317                                 'description'        => __( 'Current page of the collection.' ),
318                                 'type'               => 'integer',
319                                 'default'            => 1,
320                                 'sanitize_callback'  => 'absint',
321                                 'validate_callback'  => 'rest_validate_request_arg',
322                                 'minimum'            => 1,
323                         ),
324                         'per_page'               => array(
325                                 'description'        => __( 'Maximum number of items to be returned in result set.' ),
326                                 'type'               => 'integer',
327                                 'default'            => 10,
328                                 'minimum'            => 1,
329                                 'maximum'            => 100,
330                                 'sanitize_callback'  => 'absint',
331                                 'validate_callback'  => 'rest_validate_request_arg',
332                         ),
333                         'search'                 => array(
334                                 'description'        => __( 'Limit results to those matching a string.' ),
335                                 'type'               => 'string',
336                                 'sanitize_callback'  => 'sanitize_text_field',
337                                 'validate_callback'  => 'rest_validate_request_arg',
338                         ),
339                 );
340         }
341
342         /**
343          * Retrieves the magical context param.
344          *
345          * Ensures consistent descriptions between endpoints, and populates enum from schema.
346          *
347          * @since 4.7.0
348          * @access public
349          *
350          * @param array $args Optional. Additional arguments for context parameter. Default empty array.
351          * @return array Context parameter details.
352          */
353         public function get_context_param( $args = array() ) {
354                 $param_details = array(
355                         'description'        => __( 'Scope under which the request is made; determines fields present in response.' ),
356                         'type'               => 'string',
357                         'sanitize_callback'  => 'sanitize_key',
358                         'validate_callback'  => 'rest_validate_request_arg',
359                 );
360
361                 $schema = $this->get_item_schema();
362
363                 if ( empty( $schema['properties'] ) ) {
364                         return array_merge( $param_details, $args );
365                 }
366
367                 $contexts = array();
368
369                 foreach ( $schema['properties'] as $attributes ) {
370                         if ( ! empty( $attributes['context'] ) ) {
371                                 $contexts = array_merge( $contexts, $attributes['context'] );
372                         }
373                 }
374
375                 if ( ! empty( $contexts ) ) {
376                         $param_details['enum'] = array_unique( $contexts );
377                         rsort( $param_details['enum'] );
378                 }
379
380                 return array_merge( $param_details, $args );
381         }
382
383         /**
384          * Adds the values from additional fields to a data object.
385          *
386          * @since 4.7.0
387          * @access protected
388          *
389          * @param array           $object  Data object.
390          * @param WP_REST_Request $request Full details about the request.
391          * @return array Modified data object with additional fields.
392          */
393         protected function add_additional_fields_to_object( $object, $request ) {
394
395                 $additional_fields = $this->get_additional_fields();
396
397                 foreach ( $additional_fields as $field_name => $field_options ) {
398
399                         if ( ! $field_options['get_callback'] ) {
400                                 continue;
401                         }
402
403                         $object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request, $this->get_object_type() );
404                 }
405
406                 return $object;
407         }
408
409         /**
410          * Updates the values of additional fields added to a data object.
411          *
412          * @since 4.7.0
413          * @access protected
414          *
415          * @param array           $object  Data Object.
416          * @param WP_REST_Request $request Full details about the request.
417          * @return bool|WP_Error True on success, WP_Error object if a field cannot be updated.
418          */
419         protected function update_additional_fields_for_object( $object, $request ) {
420                 $additional_fields = $this->get_additional_fields();
421
422                 foreach ( $additional_fields as $field_name => $field_options ) {
423                         if ( ! $field_options['update_callback'] ) {
424                                 continue;
425                         }
426
427                         // Don't run the update callbacks if the data wasn't passed in the request.
428                         if ( ! isset( $request[ $field_name ] ) ) {
429                                 continue;
430                         }
431
432                         $result = call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request, $this->get_object_type() );
433
434                         if ( is_wp_error( $result ) ) {
435                                 return $result;
436                         }
437                 }
438
439                 return true;
440         }
441
442         /**
443          * Adds the schema from additional fields to a schema array.
444          *
445          * The type of object is inferred from the passed schema.
446          *
447          * @since 4.7.0
448          * @access protected
449          *
450          * @param array $schema Schema array.
451          * @return array Modified Schema array.
452          */
453         protected function add_additional_fields_schema( $schema ) {
454                 if ( empty( $schema['title'] ) ) {
455                         return $schema;
456                 }
457
458                 // Can't use $this->get_object_type otherwise we cause an inf loop.
459                 $object_type = $schema['title'];
460
461                 $additional_fields = $this->get_additional_fields( $object_type );
462
463                 foreach ( $additional_fields as $field_name => $field_options ) {
464                         if ( ! $field_options['schema'] ) {
465                                 continue;
466                         }
467
468                         $schema['properties'][ $field_name ] = $field_options['schema'];
469                 }
470
471                 return $schema;
472         }
473
474         /**
475          * Retrieves all of the registered additional fields for a given object-type.
476          *
477          * @since 4.7.0
478          * @access protected
479          *
480          * @param  string $object_type Optional. The object type.
481          * @return array Registered additional fields (if any), empty array if none or if the object type could
482          *               not be inferred.
483          */
484         protected function get_additional_fields( $object_type = null ) {
485
486                 if ( ! $object_type ) {
487                         $object_type = $this->get_object_type();
488                 }
489
490                 if ( ! $object_type ) {
491                         return array();
492                 }
493
494                 global $wp_rest_additional_fields;
495
496                 if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) {
497                         return array();
498                 }
499
500                 return $wp_rest_additional_fields[ $object_type ];
501         }
502
503         /**
504          * Retrieves the object type this controller is responsible for managing.
505          *
506          * @since 4.7.0
507          * @access protected
508          *
509          * @return string Object type for the controller.
510          */
511         protected function get_object_type() {
512                 $schema = $this->get_item_schema();
513
514                 if ( ! $schema || ! isset( $schema['title'] ) ) {
515                         return null;
516                 }
517
518                 return $schema['title'];
519         }
520
521         /**
522          * Retrieves an array of endpoint arguments from the item schema for the controller.
523          *
524          * @since 4.7.0
525          * @access public
526          *
527          * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are
528          *                       checked for required values and may fall-back to a given default, this is not done
529          *                       on `EDITABLE` requests. Default WP_REST_Server::CREATABLE.
530          * @return array Endpoint arguments.
531          */
532         public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
533
534                 $schema            = $this->get_item_schema();
535                 $schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
536                 $endpoint_args     = array();
537
538                 foreach ( $schema_properties as $field_id => $params ) {
539
540                         // Arguments specified as `readonly` are not allowed to be set.
541                         if ( ! empty( $params['readonly'] ) ) {
542                                 continue;
543                         }
544
545                         $endpoint_args[ $field_id ] = array(
546                                 'validate_callback' => 'rest_validate_request_arg',
547                                 'sanitize_callback' => 'rest_sanitize_request_arg',
548                         );
549
550                         if ( isset( $params['description'] ) ) {
551                                 $endpoint_args[ $field_id ]['description'] = $params['description'];
552                         }
553
554                         if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
555                                 $endpoint_args[ $field_id ]['default'] = $params['default'];
556                         }
557
558                         if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
559                                 $endpoint_args[ $field_id ]['required'] = true;
560                         }
561
562                         foreach ( array( 'type', 'format', 'enum', 'items' ) as $schema_prop ) {
563                                 if ( isset( $params[ $schema_prop ] ) ) {
564                                         $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
565                                 }
566                         }
567
568                         // Merge in any options provided by the schema property.
569                         if ( isset( $params['arg_options'] ) ) {
570
571                                 // Only use required / default from arg_options on CREATABLE endpoints.
572                                 if ( WP_REST_Server::CREATABLE !== $method ) {
573                                         $params['arg_options'] = array_diff_key( $params['arg_options'], array( 'required' => '', 'default' => '' ) );
574                                 }
575
576                                 $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
577                         }
578                 }
579
580                 return $endpoint_args;
581         }
582
583         /**
584          * Sanitizes the slug value.
585          *
586          * @since 4.7.0
587          * @access public
588          *
589          * @internal We can't use sanitize_title() directly, as the second
590          * parameter is the fallback title, which would end up being set to the
591          * request object.
592          *
593          * @see https://github.com/WP-API/WP-API/issues/1585
594          *
595          * @todo Remove this in favour of https://core.trac.wordpress.org/ticket/34659
596          *
597          * @param string $slug Slug value passed in request.
598          * @return string Sanitized value for the slug.
599          */
600         public function sanitize_slug( $slug ) {
601                 return sanitize_title( $slug );
602         }
603 }