PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 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 All 508 releases
← All changes | _inc/lib/core-api/class-wpcom-rest-field-controller.php +442 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,442 @@
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 +}