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

CMB2_REST_Controller_Fields   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 514
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 163
c 1
b 0
f 0
dl 0
loc 514
rs 5.04
wmc 57

How to fix   Complexity   

Complex Class

Complex classes like CMB2_REST_Controller_Fields 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 CMB2_REST_Controller_Fields, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * CMB2 objects/fields endpoint for WordPres REST API.
4
 * Allows access to fields registered to a specific box.
5
 *
6
 * @todo  Add better documentation.
7
 * @todo  Research proper schema.
8
 *
9
 * @since 2.2.3
10
 *
11
 * @category  WordPress_Plugin
12
 * @package   CMB2
13
 * @author    CMB2 team
14
 * @license   GPL-2.0+
15
 * @link      https://cmb2.io
16
 */
17
class CMB2_REST_Controller_Fields extends CMB2_REST_Controller_Boxes {
18
19
	/**
20
	 * Register the routes for the objects of the controller.
21
	 *
22
	 * @since 2.2.3
23
	 */
24
	public function register_routes() {
25
		$args = array(
26
			'_embed' => array(
27
				'description' => __( 'Includes the box object which the fields are registered to in the response.', 'cmb2' ),
28
			),
29
			'_rendered' => array(
30
				'description' => __( 'When the \'_rendered\' argument is passed, the renderable field attributes will be returned fully rendered. By default, the names of the callback handers for the renderable attributes will be returned.', 'cmb2' ),
31
			),
32
			'object_id' => array(
33
				'description' => __( 'To view or modify the field\'s value, the \'object_id\' and \'object_type\' arguments are required.', 'cmb2' ),
34
			),
35
			'object_type' => array(
36
				'description' => __( 'To view or modify the field\'s value, the \'object_id\' and \'object_type\' arguments are required.', 'cmb2' ),
37
			),
38
		);
39
40
		// Returns specific box's fields.
41
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<cmb_id>[\w-]+)/fields/', array(
42
			array(
43
				'methods'             => WP_REST_Server::READABLE,
44
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
45
				'callback'            => array( $this, 'get_items' ),
46
				'args'                => $args,
47
			),
48
			'schema' => array( $this, 'get_item_schema' ),
49
		) );
50
51
		$delete_args = $args;
52
		$delete_args['object_id']['required'] = true;
53
		$delete_args['object_type']['required'] = true;
54
55
		// Returns specific field data.
56
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<cmb_id>[\w-]+)/fields/(?P<field_id>[\w-]+)', array(
57
			array(
58
				'methods'             => WP_REST_Server::READABLE,
59
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
60
				'callback'            => array( $this, 'get_item' ),
61
				'args'                => $args,
62
			),
63
			array(
64
				'methods'             => WP_REST_Server::EDITABLE,
65
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
66
				'callback'            => array( $this, 'update_item' ),
67
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
68
				'args'                => $args,
69
			),
70
			array(
71
				'methods'             => WP_REST_Server::DELETABLE,
72
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
73
				'callback'            => array( $this, 'delete_item' ),
74
				'args'                => $delete_args,
75
			),
76
			'schema' => array( $this, 'get_item_schema' ),
77
		) );
78
	}
79
80
	/**
81
	 * Check if a given request has access to get fields.
82
	 * By default, no special permissions needed, but filtering return value.
83
	 *
84
	 * @since 2.2.3
85
	 *
86
	 * @param  WP_REST_Request $request Full data about the request.
87
	 * @return WP_Error|boolean
88
	 */
89
	public function get_items_permissions_check( $request ) {
90
		$this->initiate_rest_read_box( $request, 'fields_read' );
91
		$can_access = true;
92
93
		/**
94
		 * By default, no special permissions needed.
95
		 *
96
		 * @since 2.2.3
97
		 *
98
		 * @param bool   $can_access Whether this CMB2 endpoint can be accessed.
99
		 * @param object $controller This CMB2_REST_Controller object.
100
		 */
101
		return $this->maybe_hook_callback_and_apply_filters( 'cmb2_api_get_fields_permissions_check', $can_access );
102
	}
103
104
	/**
105
	 * Get all public CMB2 box fields.
106
	 *
107
	 * @since 2.2.3
108
	 *
109
	 * @param  WP_REST_Request $request Full data about the request.
110
	 * @return WP_Error|WP_REST_Response
111
	 */
112
	public function get_items( $request ) {
113
		if ( ! $this->rest_box ) {
114
			$this->initiate_rest_read_box( $request, 'fields_read' );
115
		}
116
117
		if ( is_wp_error( $this->rest_box ) ) {
118
			return $this->rest_box;
119
		}
120
121
		$fields = array();
122
		foreach ( $this->rest_box->cmb->prop( 'fields', array() ) as $field ) {
123
124
			// Make sure this field can be read.
125
			$this->field = $this->rest_box->field_can_read( $field['id'], true );
126
127
			// And make sure current user can view this box.
128
			if ( $this->field && $this->get_item_permissions_check_filter() ) {
129
				$fields[ $field['id'] ] = $this->server->response_to_data(
130
					$this->prepare_field_response(),
131
					isset( $this->request['_embed'] )
132
				);
133
			}
134
		}
135
136
		return $this->prepare_item( $fields );
137
	}
138
139
	/**
140
	 * Check if a given request has access to a field.
141
	 * By default, no special permissions needed, but filtering return value.
142
	 *
143
	 * @since 2.2.3
144
	 *
145
	 * @param  WP_REST_Request $request Full details about the request.
146
	 * @return WP_Error|boolean
147
	 */
148
	public function get_item_permissions_check( $request ) {
149
		$this->initiate_rest_read_box( $request, 'field_read' );
150
		if ( ! is_wp_error( $this->rest_box ) ) {
151
			$this->field = $this->rest_box->field_can_read( $this->request->get_param( 'field_id' ), true );
152
		}
153
154
		return $this->get_item_permissions_check_filter();
155
	}
156
157
	/**
158
	 * Check by filter if a given request has access to a field.
159
	 * By default, no special permissions needed, but filtering return value.
160
	 *
161
	 * @since 2.2.3
162
	 *
163
	 * @param  bool $can_access Whether the current request has access to view the field by default.
164
	 * @return WP_Error|boolean
165
	 */
166
	public function get_item_permissions_check_filter( $can_access = true ) {
167
		/**
168
		 * By default, no special permissions needed.
169
		 *
170
		 * @since 2.2.3
171
		 *
172
		 * @param bool   $can_access Whether this CMB2 endpoint can be accessed.
173
		 * @param object $controller This CMB2_REST_Controller object.
174
		 */
175
		return $this->maybe_hook_callback_and_apply_filters( 'cmb2_api_get_field_permissions_check', $can_access );
176
	}
177
178
	/**
179
	 * Get one CMB2 field from the collection.
180
	 *
181
	 * @since 2.2.3
182
	 *
183
	 * @param  WP_REST_Request $request Full data about the request.
184
	 * @return WP_Error|WP_REST_Response
185
	 */
186
	public function get_item( $request ) {
187
		$this->initiate_rest_read_box( $request, 'field_read' );
188
189
		if ( is_wp_error( $this->rest_box ) ) {
190
			return $this->rest_box;
191
		}
192
193
		return $this->prepare_read_field( $this->request->get_param( 'field_id' ) );
194
	}
195
196
	/**
197
	 * Check if a given request has access to update a field value.
198
	 * By default, requires 'edit_others_posts' capability, but filtering return value.
199
	 *
200
	 * @since 2.2.3
201
	 *
202
	 * @param  WP_REST_Request $request Full details about the request.
203
	 * @return WP_Error|boolean
204
	 */
205
	public function update_item_permissions_check( $request ) {
206
		$this->initiate_rest_read_box( $request, 'field_value_update' );
207
		if ( ! is_wp_error( $this->rest_box ) ) {
208
			$this->field = $this->rest_box->field_can_edit( $this->request->get_param( 'field_id' ), true );
209
		}
210
211
		$can_update = current_user_can( 'edit_others_posts' );
212
213
		/**
214
		 * By default, 'edit_others_posts' is required capability.
215
		 *
216
		 * @since 2.2.3
217
		 *
218
		 * @param bool   $can_update Whether this CMB2 endpoint can be accessed.
219
		 * @param object $controller This CMB2_REST_Controller object.
220
		 */
221
		return $this->maybe_hook_callback_and_apply_filters( 'cmb2_api_update_field_value_permissions_check', $can_update );
222
	}
223
224
	/**
225
	 * Update CMB2 field value.
226
	 *
227
	 * @since 2.2.3
228
	 *
229
	 * @param  WP_REST_Request $request Full data about the request.
230
	 * @return WP_Error|WP_REST_Response
231
	 */
232
	public function update_item( $request ) {
233
		$this->initiate_rest_read_box( $request, 'field_value_update' );
234
235
		if ( ! $this->request['value'] ) {
236
			return new WP_Error( 'cmb2_rest_update_field_error', __( 'CMB2 Field value cannot be updated without the value parameter specified.', 'cmb2' ), array(
237
				'status' => 400,
238
			) );
239
		}
240
241
		return $this->modify_field_value( 'updated' );
242
	}
243
244
	/**
245
	 * Check if a given request has access to delete a field value.
246
	 * By default, requires 'delete_others_posts' capability, but filtering return value.
247
	 *
248
	 * @since 2.2.3
249
	 *
250
	 * @param  WP_REST_Request $request Full details about the request.
251
	 * @return WP_Error|boolean
252
	 */
253
	public function delete_item_permissions_check( $request ) {
254
		$this->initiate_rest_read_box( $request, 'field_value_delete' );
255
		if ( ! is_wp_error( $this->rest_box ) ) {
256
			$this->field = $this->rest_box->field_can_edit( $this->request->get_param( 'field_id' ), true );
257
		}
258
259
		$can_delete = current_user_can( 'delete_others_posts' );
260
261
		/**
262
		 * By default, 'delete_others_posts' is required capability.
263
		 *
264
		 * @since 2.2.3
265
		 *
266
		 * @param bool   $can_delete Whether this CMB2 endpoint can be accessed.
267
		 * @param object $controller This CMB2_REST_Controller object.
268
		 */
269
		return $this->maybe_hook_callback_and_apply_filters( 'cmb2_api_delete_field_value_permissions_check', $can_delete );
270
	}
271
272
	/**
273
	 * Delete CMB2 field value.
274
	 *
275
	 * @since 2.2.3
276
	 *
277
	 * @param  WP_REST_Request $request Full data about the request.
278
	 * @return WP_Error|WP_REST_Response
279
	 */
280
	public function delete_item( $request ) {
281
		$this->initiate_rest_read_box( $request, 'field_value_delete' );
282
283
		return $this->modify_field_value( 'deleted' );
284
	}
285
286
	/**
287
	 * Modify CMB2 field value.
288
	 *
289
	 * @since 2.2.3
290
	 *
291
	 * @param  string $activity The modification activity (updated or deleted).
292
	 * @return WP_Error|WP_REST_Response
293
	 */
294
	public function modify_field_value( $activity ) {
295
296
		if ( ! $this->request['object_id'] || ! $this->request['object_type'] ) {
297
			return new WP_Error( 'cmb2_rest_modify_field_value_error', __( 'CMB2 Field value cannot be modified without the object_id and object_type parameters specified.', 'cmb2' ), array(
298
				'status' => 400,
299
			) );
300
		}
301
302
		if ( is_wp_error( $this->rest_box ) ) {
303
			return $this->rest_box;
304
		}
305
306
		$this->field = $this->rest_box->field_can_edit(
307
			$this->field ? $this->field : $this->request->get_param( 'field_id' ),
308
			true
309
		);
310
311
		if ( ! $this->field ) {
312
			return new WP_Error( 'cmb2_rest_no_field_by_id_error', __( 'No field found by that id.', 'cmb2' ), array(
313
				'status' => 403,
314
			) );
315
		}
316
317
		$this->field->args[ "value_{$activity}" ] = (bool) 'deleted' === $activity
318
			? $this->field->remove_data()
319
			: $this->field->save_field( $this->request['value'] );
320
321
		// If options page, save the $activity options
322
		if ( 'options-page' == $this->request['object_type'] ) {
323
			$this->field->args[ "value_{$activity}" ] = cmb2_options( $this->request['object_id'] )->set();
324
		}
325
326
		return $this->prepare_read_field( $this->field );
327
	}
328
329
	/**
330
	 * Get a response object for a specific field ID.
331
	 *
332
	 * @since 2.2.3
333
	 *
334
	 * @param  string\CMB2_Field Field id or Field object.
335
	 * @return WP_Error|WP_REST_Response
336
	 */
337
	public function prepare_read_field( $field ) {
338
		$this->field = $this->rest_box->field_can_read( $field, true );
339
340
		if ( ! $this->field ) {
341
			return new WP_Error( 'cmb2_rest_no_field_by_id_error', __( 'No field found by that id.', 'cmb2' ), array(
342
				'status' => 403,
343
			) );
344
		}
345
346
		return $this->prepare_item( $this->prepare_field_response() );
347
	}
348
349
	/**
350
	 * Get a specific field response.
351
	 *
352
	 * @since 2.2.3
353
	 *
354
	 * @param  CMB2_Field Field object.
355
	 * @return array      Response array.
356
	 */
357
	public function prepare_field_response() {
358
		$field_data = $this->prepare_field_data( $this->field );
359
		$response = rest_ensure_response( $field_data );
360
361
		$response->add_links( $this->prepare_links( $this->field ) );
362
363
		return $response;
364
	}
365
366
	/**
367
	 * Prepare the field data array for JSON.
368
	 *
369
	 * @since  2.2.3
370
	 *
371
	 * @param  CMB2_Field $field field object.
372
	 *
373
	 * @return array             Array of field data.
374
	 */
375
	protected function prepare_field_data( CMB2_Field $field ) {
376
		$field_data = array();
377
		$params_to_ignore = array( 'show_in_rest', 'options' );
378
		$params_to_rename = array(
379
			'label_cb' => 'label',
380
			'options_cb' => 'options',
381
		);
382
383
		// Run this first so the js_dependencies arg is populated.
384
		$rendered = ( $cb = $field->maybe_callback( 'render_row_cb' ) )
385
			// Ok, callback is good, let's run it.
386
			? $this->get_cb_results( $cb, $field->args(), $field )
387
			: false;
388
389
		$field_args = $field->args();
390
391
		foreach ( $field_args as $key => $value ) {
392
			if ( in_array( $key, $params_to_ignore, true ) ) {
393
				continue;
394
			}
395
396
			if ( 'options_cb' === $key ) {
397
				$value = $field->options();
398
			} elseif ( in_array( $key, CMB2_Field::$callable_fields, true ) ) {
399
400
				if ( isset( $this->request['_rendered'] ) ) {
401
					$value = $key === 'render_row_cb' ? $rendered : $field->get_param_callback_result( $key );
402
				} elseif ( is_array( $value ) ) {
403
					// We need to rewrite callbacks as string as they will cause
404
					// JSON recursion errors.
405
					$class = is_string( $value[0] ) ? $value[0] : get_class( $value[0] );
406
					$value = $class . '::' . $value[1];
407
				}
408
			}
409
410
			$key = isset( $params_to_rename[ $key ] ) ? $params_to_rename[ $key ] : $key;
411
412
			if ( empty( $value ) || is_scalar( $value ) || is_array( $value ) ) {
413
				$field_data[ $key ] = $value;
414
			} else {
415
				$field_data[ $key ] = sprintf( __( 'Value Error for %s', 'cmb2' ), $key );
416
			}
417
		}
418
419
		if ( $field->args( 'has_supporting_data' ) ) {
420
			$field_data = $this->get_supporting_data( $field_data, $field );
421
		}
422
423
		if ( $this->request['object_id'] && $this->request['object_type'] ) {
424
			$field_data['value'] = $field->get_rest_value();
425
		}
426
427
		return $field_data;
428
	}
429
430
	/**
431
	 * Gets field supporting data (field id and value).
432
	 *
433
	 * @since  2.7.0
434
	 *
435
	 * @param  CMB2_Field $field      Field object.
436
	 * @param  array      $field_data Array of field data.
437
	 *
438
	 * @return array                  Array of field data.
439
	 */
440
	public function get_supporting_data( $field_data, $field ) {
441
442
		// Reset placement of this property.
443
		unset( $field_data['has_supporting_data'] );
444
		$field_data['has_supporting_data'] = true;
445
446
		$field = $field->get_supporting_field();
447
		$field_data['supporting_data'] = array(
448
			'id' => $field->_id( '', false ),
449
		);
450
451
		if ( $this->request['object_id'] && $this->request['object_type'] ) {
452
			$field_data['supporting_data']['value'] = $field->get_rest_value();
453
		}
454
455
		return $field_data;
456
	}
457
458
	/**
459
	 * Return an array of contextual links for field/fields.
460
	 *
461
	 * @since  2.2.3
462
	 *
463
	 * @param  CMB2_Field $field Field object to build links from.
464
	 *
465
	 * @return array             Array of links
466
	 */
467
	protected function prepare_links( $field ) {
468
		$boxbase      = $this->namespace_base . '/' . $this->rest_box->cmb->cmb_id;
469
		$query_string = $this->get_query_string();
470
471
		$links = array(
472
			'self' => array(
473
				'href' => rest_url( trailingslashit( $boxbase ) . 'fields/' . $field->_id( '', false ) . $query_string ),
474
			),
475
			'collection' => array(
476
				'href' => rest_url( trailingslashit( $boxbase ) . 'fields' . $query_string ),
477
			),
478
			'up' => array(
479
				'embeddable' => true,
480
				'href' => rest_url( $boxbase . $query_string ),
481
			),
482
		);
483
484
		return $links;
485
	}
486
487
	/**
488
	 * Checks if the CMB2 box or field has any registered callback parameters for the given filter.
489
	 *
490
	 * The registered handlers will have a property name which matches the filter, except:
491
	 * - The 'cmb2_api' prefix will be removed
492
	 * - A '_cb' suffix will be added (to stay inline with other '*_cb' parameters).
493
	 *
494
	 * @since  2.2.3
495
	 *
496
	 * @param  string $filter      The filter name.
497
	 * @param  bool   $default_val The default filter value.
498
	 *
499
	 * @return bool                The possibly-modified filter value (if the _cb param is a non-callable).
500
	 */
501
	public function maybe_hook_registered_callback( $filter, $default_val ) {
502
		$default_val = parent::maybe_hook_registered_callback( $filter, $default_val );
503
504
		if ( $this->field ) {
505
506
			// Hook field specific filter callbacks.
507
			$val = $this->field->maybe_hook_parameter( $filter, $default_val );
508
			if ( null !== $val ) {
509
				$default_val = $val;
510
			}
511
		}
512
513
		return $default_val;
514
	}
515
516
	/**
517
	 * Unhooks any CMB2 box or field registered callback parameters for the given filter.
518
	 *
519
	 * @since  2.2.3
520
	 *
521
	 * @param  string $filter The filter name.
522
	 *
523
	 * @return void
524
	 */
525
	public function maybe_unhook_registered_callback( $filter ) {
526
		parent::maybe_unhook_registered_callback( $filter );
527
528
		if ( $this->field ) {
529
			// Unhook field specific filter callbacks.
530
			$this->field->maybe_hook_parameter( $filter, null, 'remove_filter' );
531
		}
532
	}
533
534
}
535