PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
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 16.3-a.1, at _inc/lib/core-api/class-wpcom-rest-field-controller.php

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