Passed
Push — master ( 40a760...c02299 )
by Chris
17:18 queued 13:08
created

WP_REST_Controller   F

Complexity

Total Complexity 71

Size/Duplication

Total Lines 536
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 160
c 1
b 0
f 0
dl 0
loc 536
rs 2.7199
wmc 71

How to fix   Complexity   

Complex Class

Complex classes like WP_REST_Controller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WP_REST_Controller, and based on these observations, apply Extract Interface, too.

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