PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 7.2.2
Jetpack – WP Security, Backup, Speed, & Growth v7.2.2
16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 All 503 releases
jetpack / _inc / lib / core-api / class-wpcom-rest-field-controller.php

class-wpcom-rest-field-controller.php in Jetpack – WP Security, Backup, Speed, & Growth 7.2.2, at _inc/lib/core-api/class-wpcom-rest-field-controller.php

331 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // @todo - nicer API for array values?
4
5 /**
6 * `WP_REST_Controller` is basically a wrapper for `register_rest_route()`
7 * `WPCOM_REST_API_V2_Field_Controller` is a mostly-analogous wrapper for `register_rest_field()`
8 */
9 abstract class WPCOM_REST_API_V2_Field_Controller {
10 /**
11 * @var string|string[] $object_type The REST Object Type(s) to which the field should be added.
12 */
13 protected $object_type;
14
15 /**
16 * @var string $field_name The name of the REST API field to add.
17 */
18 protected $field_name;
19
20 public function __construct() {
21 if ( ! $this->object_type ) {
22 /* translators: %s: object_type */
23 _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::$object_type', sprintf( __( "Property '%s' must be overridden.", 'jetpack' ), 'object_type' ), 'Jetpack 6.8' );
24 return;
25 }
26
27 if ( ! $this->field_name ) {
28 /* translators: %s: field_name */
29 _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::$field_name', sprintf( __( "Property '%s' must be overridden.", 'jetpack' ), 'field_name' ), 'Jetpack 6.8' );
30 return;
31 }
32
33 add_action( 'rest_api_init', array( $this, 'register_fields' ) );
34 }
35
36 /**
37 * Registers the field with the appropriate schema and callbacks.
38 */
39 public function register_fields() {
40 foreach ( (array) $this->object_type as $object_type ) {
41 register_rest_field(
42 $object_type,
43 $this->field_name,
44 array(
45 'get_callback' => array( $this, 'get_for_response' ),
46 'update_callback' => array( $this, 'update_from_request' ),
47 'schema' => $this->get_schema(),
48 )
49 );
50 }
51 }
52
53 /**
54 * Ensures the response matches the schema and request context.
55 *
56 * @param mixed $value
57 * @param WP_REST_Request $request
58 * @return mixed
59 */
60 private function prepare_for_response( $value, $request ) {
61 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
62 $schema = $this->get_schema();
63
64 $is_valid = rest_validate_value_from_schema( $value, $schema, $this->field_name );
65 if ( is_wp_error( $is_valid ) ) {
66 return $is_valid;
67 }
68
69 return $this->filter_response_by_context( $value, $schema, $context );
70 }
71
72 /**
73 * Returns the schema's default value
74 *
75 * If there is no default, returns the type's falsey value.
76 *
77 * @param array $schema
78 * @return mixed
79 */
80 final public function get_default_value( $schema ) {
81 if ( isset( $schema['default'] ) ) {
82 return $schema['default'];
83 }
84
85 // If you have something more complicated, use $schema['default'];
86 switch ( isset( $schema['type'] ) ? $schema['type'] : 'null' ) {
87 case 'string':
88 return '';
89 case 'integer':
90 case 'number':
91 return 0;
92 case 'object':
93 return (object) array();
94 case 'array':
95 return array();
96 case 'boolean':
97 return false;
98 case 'null':
99 default:
100 return null;
101 }
102 }
103
104 /**
105 * The field's wrapped getter. Does permission checks and output preparation.
106 *
107 * This cannot be extended: implement `->get()` instead.
108 *
109 * @param mixed $object_data Probably an array. Whatever the endpoint returns.
110 * @param string $field_name Should always match `->field_name`
111 * @param WP_REST_Request $request
112 * @param string $object_type Should always match `->object_type`
113 * @return mixed
114 */
115 final public function get_for_response( $object_data, $field_name, $request, $object_type ) {
116 $permission_check = $this->get_permission_check( $object_data, $request );
117
118 if ( ! $permission_check ) {
119 /* translators: %s: get_permission_check() */
120 _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_permission_check', sprintf( __( "Method '%s' must return either true or WP_Error.", 'jetpack' ), 'get_permission_check' ), 'Jetpack 6.8' );
121 return $this->get_default_value( $this->get_schema() );
122 }
123
124 if ( is_wp_error( $permission_check ) ) {
125 return $this->get_default_value( $this->get_schema() );
126 }
127
128 $value = $this->get( $object_data, $request );
129
130 return $this->prepare_for_response( $value, $request );
131 }
132
133 /**
134 * The field's wrapped setter. Does permission checks.
135 *
136 * This cannot be extended: implement `->update()` instead.
137 *
138 * @param mixed $value The new value for the field.
139 * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
140 * @param string $field_name Should always match `->field_name`
141 * @param WP_REST_Request $request
142 * @param string $object_type Should always match `->object_type`
143 * @return void|WP_Error
144 */
145 final public function update_from_request( $value, $object_data, $field_name, $request, $object_type ) {
146 $permission_check = $this->update_permission_check( $value, $object_data, $request );
147
148 if ( ! $permission_check ) {
149 /* translators: %s: update_permission_check() */
150 _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update_permission_check', sprintf( __( "Method '%s' must return either true or WP_Error.", 'jetpack' ), 'update_permission_check' ), 'Jetpack 6.8' );
151 /* translators: %s: the name of an API response field */
152 return new WP_Error( 'invalid_user_permission', sprintf( __( "You are not allowed to access the '%s' field.", 'jetpack' ), $this->field_name ) );
153 }
154
155 if ( is_wp_error( $permission_check ) ) {
156 return $permission_check;
157 }
158
159 $updated = $this->update( $value, $object_data, $request );
160
161 if ( is_wp_error( $updated ) ) {
162 return $updated;
163 }
164 }
165
166 /**
167 * Permission Check for the field's getter. Must be implemented in the inheriting class.
168 *
169 * @param mixed $object_data Whatever the endpoint would return for its response.
170 * @param WP_REST_Request $request
171 * @return true|WP_Error
172 */
173 public function get_permission_check( $object_data, $request ) {
174 /* translators: %s: get_permission_check() */
175 _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_permission_check', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
176 }
177
178 /**
179 * The field's "raw" getter. Must be implemented in the inheriting class.
180 *
181 * @param mixed $object_data Whatever the endpoint would return for its response.
182 * @param WP_REST_Request $request
183 * @return mixed
184 */
185 public function get( $object_data, $request ) {
186 /* translators: %s: get() */
187 _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
188 }
189
190 /**
191 * Permission Check for the field's setter. Must be implemented in the inheriting class.
192 *
193 * @param mixed $value The new value for the field.
194 * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
195 * @param WP_REST_Request $request
196 * @return true|WP_Error
197 */
198 public function update_permission_check( $value, $object_data, $request ) {
199 /* translators: %s: update_permission_check() */
200 _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update_permission_check', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
201 }
202
203 /**
204 * The field's "raw" setter. Must be implemented in the inheriting class.
205 *
206 * @param mixed $value The new value for the field.
207 * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
208 * @param WP_REST_Request $request
209 * @return mixed
210 */
211 public function update( $value, $object_data, $request ) {
212 /* translators: %s: update() */
213 _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
214 }
215
216 /**
217 * The JSON Schema for the field
218 *
219 * @link https://json-schema.org/understanding-json-schema/
220 * As of WordPress 5.0, Core currently understands:
221 * * type
222 * * string - not minLength, not maxLength, not pattern
223 * * integer - minimum, maximum, exclusiveMinimum, exclusiveMaximum, not multipleOf
224 * * number - minimum, maximum, exclusiveMinimum, exclusiveMaximum, not multipleOf
225 * * boolean
226 * * null
227 * * object - properties, additionalProperties, not propertyNames, not dependencies, not patternProperties, not required
228 * * array: only lists, not tuples - items, not minItems, not maxItems, not uniqueItems, not contains
229 * * enum
230 * * format
231 * * date-time
232 * * email
233 * * ip
234 * * uri
235 * As of WordPress 5.0, Core does not support:
236 * * Multiple type: `type: [ 'string', 'integer' ]`
237 * * $ref, allOf, anyOf, oneOf, not, const
238 *
239 * @return array
240 */
241 public function get_schema() {
242 /* translators: %s: get_schema() */
243 _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_schema', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
244 }
245
246 /**
247 * @param array $schema
248 * @param string $context REST API Request context
249 * @return bool
250 */
251 private function is_valid_for_context( $schema, $context ) {
252 return empty( $schema['context'] ) || in_array( $context, $schema['context'], true );
253 }
254
255 /**
256 * Removes properties that should not appear in the current
257 * request's context
258 *
259 * $context is a Core REST API Framework request attribute that is
260 * always one of:
261 * * view (what you see on the blog)
262 * * edit (what you see in an editor)
263 * * embed (what you see in, e.g., an oembed)
264 *
265 * Fields (and sub-fields, and sub-sub-...) can be flagged for a
266 * set of specific contexts via the field's schema.
267 *
268 * The Core API will filter out top-level fields with the wrong
269 * context, but will not recurse deeply enough into arrays/objects
270 * to remove all levels of sub-fields with the wrong context.
271 *
272 * This function handles that recursion.
273 *
274 * @param mixed $value
275 * @param array $schema
276 * @param string $context REST API Request context
277 * @return mixed Filtered $value
278 */
279 final public function filter_response_by_context( $value, $schema, $context ) {
280 if ( ! $this->is_valid_for_context( $schema, $context ) ) {
281 // We use this intentionally odd looking WP_Error object
282 // internally only in this recursive function (see below
283 // in the `object` case). It will never be output by the REST API.
284 // If we return this for the top level object, Core
285 // correctly remove the top level object from the response
286 // for us.
287 return new WP_Error( '__wrong-context__' );
288 }
289
290 switch ( $schema['type'] ) {
291 case 'array':
292 if ( ! isset( $schema['items'] ) ) {
293 return $value;
294 }
295
296 // Shortcircuit if we know none of the items are valid for this context.
297 // This would only happen in a strangely written schema.
298 if ( ! $this->is_valid_for_context( $schema['items'], $context ) ) {
299 return array();
300 }
301
302 // Recurse to prune sub-properties of each item.
303 foreach ( $value as $key => $item ) {
304 $value[ $key ] = $this->filter_response_by_context( $item, $schema['items'], $context );
305 }
306
307 return $value;
308 case 'object':
309 if ( ! isset( $schema['properties'] ) ) {
310 return $value;
311 }
312
313 foreach ( $value as $field_name => $field_value ) {
314 if ( isset( $schema['properties'][ $field_name ] ) ) {
315 $field_value = $this->filter_response_by_context( $field_value, $schema['properties'][ $field_name ], $context );
316 if ( is_wp_error( $field_value ) && '__wrong-context__' === $field_value->get_error_code() ) {
317 unset( $value[ $field_name ] );
318 } else {
319 // Respect recursion that pruned sub-properties of each property.
320 $value[ $field_name ] = $field_value;
321 }
322 }
323 }
324
325 return (object) $value;
326 }
327
328 return $value;
329 }
330 }
331