Completed
Push — add/rest-api-field-controller ( 9b83b7 )
by
unknown
16:01 queued 08:39
created

WPCOM_REST_API_V2_Field_Controller::update()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 3
dl 0
loc 4
rs 10
c 0
b 0
f 0
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( $object_type, $this->field_name, array(
42
				'get_callback' => array( $this, 'get_for_response' ),
43
				'update_callback' => array( $this, 'update_from_request' ),
44
				'schema' => $this->get_schema(),
45
			) );
46
		}
47
	}
48
49
	/**
50
	 * Ensures the response matches the schema and request context.
51
	 *
52
	 * You shouldn't have to extend this method.
53
	 *
54
	 * @param mixed $value
55
	 * @param WP_REST_Request $request
56
	 * @return mixed
57
	 */
58
	function prepare_for_response( $value, $request ) {
59
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
60
		$schema = $this->get_schema();
61
62
		$is_valid = rest_validate_value_from_schema( $value, $schema, $this->field_name );
63
		if ( is_wp_error( $is_valid ) ) {
64
			return $is_valid;
65
		}
66
67
		return $this->filter_response_by_context( $value, $schema, $context );
0 ignored issues
show
Bug introduced by
It seems like $schema defined by $this->get_schema() on line 60 can also be of type null; however, WPCOM_REST_API_V2_Field_...r_response_by_context() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
68
	}
69
70
	/**
71
	 * Returns the schema's default value
72
	 *
73
	 * If there is no default, returns the type's falsey value.
74
	 *
75
	 * @param array $schema
76
	 * @return mixed
77
	 */
78
	final public function get_default_value( $schema ) {
79
		if ( isset( $schema['default'] ) ) {
80
			return $schema['default'];
81
		}
82
83
		// If you have something more complicated, use $schema['default'];
84
		switch ( isset( $schema['type'] ) ? $schema['type'] : 'null' ) {
85
			case 'string' :
86
				return '';
87
			case 'integer' :
88
			case 'number' :
89
				return 0;
90
			case 'object' :
91
				return (object) array();
92
			case 'array' :
93
				return array();
94
			case 'boolean' :
95
				return false;
96
			case 'null' :
97
			default :
98
				return null;
99
		}
100
	}
101
102
	/**
103
	 * The field's wrapped getter. Does permission checks and output preparation.
104
	 *
105
	 * This cannot be extended: implement `->get()` instead.
106
	 *
107
	 * @param mixed $object_data Probably an array. Whatever the endpoint returns.
108
	 * @param string $field_name Should always match `->field_name`
109
	 * @param WP_REST_Request $request
110
	 * @param $object_type Should always match `->object_type`
111
	 * @return mixed
112
	 */
113
	final public function get_for_response( $object_data, $field_name, $request, $object_type ) {
114
		$permission_check = $this->get_permission_check( $object_data, $request );
115
116
		if ( ! $permission_check || is_wp_error( $permission_check ) ) {
117
			return $this->get_default_value( $this->get_schema() );
0 ignored issues
show
Bug introduced by
It seems like $this->get_schema() targeting WPCOM_REST_API_V2_Field_Controller::get_schema() can also be of type null; however, WPCOM_REST_API_V2_Field_...er::get_default_value() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
118
		}
119
120
		$value = $this->get( $object_data, $request );
121
122
		return $this->prepare_for_response( $value, $request );
123
	}
124
125
	/**
126
	 * The field's wrapped setter. Does permission checks.
127
	 *
128
	 * This cannot be extended: implement `->update()` instead.
129
	 *
130
	 * @param mixed $value The new value for the field.
131
	 * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
132
	 * @param string $field_name Should always match `->field_name`
133
	 * @param WP_REST_Request $request
134
	 * @param $object_type Should always match `->object_type`
135
	 * @return void|WP_Error
136
	 */
137
	final public function update_from_request( $value, $object_data, $field_name, $request, $object_type ) {
138
		$permission_check = $this->update_permission_check( $value, $object_data, $request );
139
140
		if ( ! $permission_check ) {
141
			/* translators: %s: get_permission_check() */
142
			_doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update_permission_check', sprintf( __( "Method '%s' must return either true or WP_Error." ), 'update_permission_check' ), 'Jetpack 6.8' );
143
			return;
144
		}
145
146
		if ( is_wp_error( $permission_check ) ) {
147
			return $permission_check;
148
		}
149
150
		$updated = $this->update( $value, $object_data, $request );
151
152
		if ( is_wp_error( $updated ) ) {
153
			return $updated;
154
		}
155
	}
156
157
	/**
158
	 * Permission Check for the field's getter. Must be implemented in the inheriting class.
159
	 *
160
	 * @param mixed $object_data Whatever the endpoint would returnn for its response.
161
	 * @param WP_REST_Request
162
	 * @return true|WP_Error
163
	 */
164
	public function get_permission_check( $object_data, $request ) {
165
		/* translators: %s: get_permission_check() */
166
                _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_permission_check', sprintf( __( "Method '%s' must be overridden." ), __METHOD__ ), 'Jetpack 6.8' );
167
	}
168
169
	/**
170
	 * The field's "raw" getter. Must be implemented in the inheriting class.
171
	 *
172
	 * @param mixed $object_data Whatever the endpoint would returnn for its response.
173
	 * @param WP_REST_Request
174
	 * @return mixed
175
	 */
176
	public function get( $object_data, $request ) {
177
		/* translators: %s: get() */
178
                _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get', sprintf( __( "Method '%s' must be overridden." ), __METHOD__ ), 'Jetpack 6.8' );
179
	}
180
181
	/**
182
	 * Permission Check for the field's setter. Must be implemented in the inheriting class.
183
	 *
184
	 * @param mixed $value The new value for the field.
185
	 * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
186
	 * @param WP_REST_Request
187
	 * @return true|WP_Error
188
	 */
189
	public function update_permission_check( $value, $object_data, $request ) {
190
		/* translators: %s: update_permission_check() */
191
                _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update_permission_check', sprintf( __( "Method '%s' must be overridden." ), __METHOD__ ), 'Jetpack 6.8' );
192
	}
193
194
	/**
195
	 * The field's "raw" setter. Must be implemented in the inheriting class.
196
	 *
197
	 * @param mixed $value The new value for the field.
198
	 * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
199
	 * @param WP_REST_Request
200
	 * @return mixed
201
	 */
202
	public function update( $value, $object_data, $request ) {
203
		/* translators: %s: update() */
204
                _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update', sprintf( __( "Method '%s' must be overridden." ), __METHOD__ ), 'Jetpack 6.8' );
205
	}
206
207
	/**
208
	 * The JSON Schema for the field
209
	 * @link https://json-schema.org/understanding-json-schema/
210
	 * As of WordPress 5.0, Core currently understands:
211
	 * * type
212
	 *   * string - not minLength, not maxLength, not pattern
213
	 *   * integer - minimum, maximum, exclusiveMinimum, exclusiveMaximum, not multipleOf
214
	 *   * number  - minimum, maximum, exclusiveMinimum, exclusiveMaximum, not multipleOf
215
	 *   * boolean
216
	 *   * null
217
	 *   * object - properties, additionalProperties, not propertyNames, not dependencies, not patternProperties, not required
218
	 *   * array: only lists, not tuples - items, not minItems, not maxItems, not uniqueItems, not contains
219
	 * * enum
220
	 * * format
221
	 *   * date-time
222
	 *   * email
223
	 *   * ip
224
	 *   * uri
225
	 * As of WordPress 5.0, Core does not support:
226
	 * * Multiple type: `type: [ 'string', 'integer' ]`
227
	 * * $ref, allOf, anyOf, oneOf, not, const
228
	 *
229
	 * @return array
230
	 */
231
	public function get_schema() {
232
		/* translators: %s: get_schema() */
233
                _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_schema', sprintf( __( "Method '%s' must be overridden." ), __METHOD__ ), 'Jetpack 6.8' );
234
	}
235
236
	/**
237
	 * @param array $schema
238
	 * @param string $context REST API Request context
239
	 * @return bool
240
	 */
241
	private function is_valid_for_context( $schema, $context ) {
242
		return empty( $schema['context'] ) || in_array( $context, $schema['context'], true );
243
	}
244
245
	/**
246
	 * Removes properties that should not appear in the current
247
	 * request's context
248
	 *
249
	 * $context is a Core REST API Framework request attribute that is
250
	 * always one of:
251
	 * * view (what you see on the blog)
252
	 * * edit (what you see in an editor)
253
	 * * embed (what you see in, e.g., an oembed)
254
	 *
255
	 * Fields (and sub-fields, and sub-sub-...) can be flagged for a
256
	 * set of specific contexts via the field's schema.
257
	 *
258
	 * The Core API will filter out top-level fields with the wrong
259
	 * context, but will not recurse deeply enough into arrays/objects
260
	 * to remove all levels of sub-fields with the wrong context.
261
	 *
262
	 * This function handles that recursion.
263
	 *
264
	 * @param mixed $value
265
	 * @param array $schema
266
	 * @param string $context REST API Request context
267
	 * @return mixed Filtered $value
268
	 */
269
	final function filter_response_by_context( $value, $schema, $context ) {
270
		if ( ! $this->is_valid_for_context( $schema, $context ) ) {
271
			// We use this intentionally odd looking WP_Error object
272
			// internally only in this recursive function (see below
273
			// in the `object` case). It will never be output by the REST API.
274
			// If we return this for the top level object, Core
275
			// correctly remove the top level object from the response
276
			// for us.
277
			return new WP_Error( '__wrong-context__' );
278
		}
279
280
		switch ( $schema['type'] ) {
281
			case 'array' :
282
				if ( ! isset( $schema['items'] ) ) {
283
					return $value;
284
				}
285
286
				// Shortcircuit if we know none of the items are valid for this context.
287
				// This would only happen in a strangely written schema.
288
				if ( ! $this->is_valid_for_context( $schema['items'], $context ) ) {
289
					return array();
290
				}
291
292
				// Recurse to prune sub-properties of each item.
293
294
				$keys = array_keys( $value );
295
296
				$items = array_map(
297
					array( $this, 'filter_response_by_context' ),
298
					$value,
299
					array_fill( 0, count( $keys ), $schema['items'] ),
300
					array_fill( 0, count( $keys ), $context )
301
				);
302
303
				return array_combine( $keys, $items );
304
			case 'object' :
305
				if ( ! isset( $schema['properties'] ) ) {
306
					return $value;
307
				}
308
309
				foreach ( $value as $field_name => $field_value ) {
310
					if ( isset( $schema['properties'][$field_name] ) ) {
311
						$field_value = $this->filter_response_by_context( $field_value, $schema['properties'][$field_name], $context );
312
						if ( is_wp_error( $field_value ) && '__wrong-context__' === $field_value->get_error_code() ) {
313
							unset( $value[$field_name] );
314
						} else {
315
							// Respect recursion that pruned sub-properties of each property.
316
							$value[$field_name] = $field_value;
317
						}
318
					}
319
				}
320
321
				return (object) $value;
322
		}
323
324
		return $value;
325
	}
326
}
327