Passed
Push — trunk ( 35f7f8...44999f )
by Justin
01:59
created

includes/rest-api/CMB2_REST_Controller_Fields.php (1 issue)

Labels
Severity
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 1
	public function register_routes() {
25
		$args = array(
26
			'_embed' => array(
27 1
				'description' => __( 'Includes the box object which the fields are registered to in the response.', 'cmb2' ),
28 1
			),
29
			'_rendered' => array(
30 1
				'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 1
			),
32
			'object_id' => array(
33 1
				'description' => __( 'To view or modify the field\'s value, the \'object_id\' and \'object_type\' arguments are required.', 'cmb2' ),
34 1
			),
35
			'object_type' => array(
36 1
				'description' => __( 'To view or modify the field\'s value, the \'object_id\' and \'object_type\' arguments are required.', 'cmb2' ),
37 1
			),
38 1
		);
39
40
		// Returns specific box's fields.
41 1
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<cmb_id>[\w-]+)/fields/', array(
42
			array(
43 1
				'methods'             => WP_REST_Server::READABLE,
44 1
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
45 1
				'callback'            => array( $this, 'get_items' ),
46 1
				'args'                => $args,
47 1
			),
48 1
			'schema' => array( $this, 'get_item_schema' ),
49 1
		) );
50
51 1
		$delete_args = $args;
52 1
		$delete_args['object_id']['required'] = true;
53 1
		$delete_args['object_type']['required'] = true;
54
55
		// Returns specific field data.
56 1
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<cmb_id>[\w-]+)/fields/(?P<field_id>[\w-]+)', array(
57
			array(
58 1
				'methods'             => WP_REST_Server::READABLE,
59 1
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
60 1
				'callback'            => array( $this, 'get_item' ),
61 1
				'args'                => $args,
62 1
			),
63
			array(
64 1
				'methods'             => WP_REST_Server::EDITABLE,
65 1
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
66 1
				'callback'            => array( $this, 'update_item' ),
67 1
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
68 1
				'args'                => $args,
69 1
			),
70
			array(
71 1
				'methods'             => WP_REST_Server::DELETABLE,
72 1
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
73 1
				'callback'            => array( $this, 'delete_item' ),
74 1
				'args'                => $delete_args,
75 1
			),
76 1
			'schema' => array( $this, 'get_item_schema' ),
77 1
		) );
78 1
	}
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
1 ignored issue
show
The type WP_Error was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
88
	 */
89 1
	public function get_items_permissions_check( $request ) {
90 1
		$this->initiate_rest_read_box( $request, 'fields_read' );
91 1
		$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 1
		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 1
	public function get_items( $request ) {
113 1
		if ( ! $this->rest_box ) {
114
			$this->initiate_rest_read_box( $request, 'fields_read' );
115
		}
116
117 1
		if ( is_wp_error( $this->rest_box ) ) {
118
			return $this->rest_box;
119
		}
120
121 1
		$fields = array();
122 1
		foreach ( $this->rest_box->cmb->prop( 'fields', array() ) as $field ) {
123
124
			// Make sure this field can be read.
125 1
			$this->field = $this->rest_box->field_can_read( $field['id'], true );
126
127
			// And make sure current user can view this box.
128 1
			if ( $this->field && $this->get_item_permissions_check_filter() ) {
129 1
				$fields[ $field['id'] ] = $this->server->response_to_data(
130 1
					$this->prepare_field_response(),
131 1
					isset( $this->request['_embed'] )
132 1
				);
133 1
			}
134 1
		}
135
136 1
		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 4
	public function get_item_permissions_check( $request ) {
149 4
		$this->initiate_rest_read_box( $request, 'field_read' );
150 4
		if ( ! is_wp_error( $this->rest_box ) ) {
151 4
			$this->field = $this->rest_box->field_can_read( $this->request->get_param( 'field_id' ), true );
152 4
		}
153
154 4
		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 5
	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 5
		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 2
	public function get_item( $request ) {
187 2
		$this->initiate_rest_read_box( $request, 'field_read' );
188
189 2
		if ( is_wp_error( $this->rest_box ) ) {
190
			return $this->rest_box;
191
		}
192
193 2
		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 5
	public function update_item_permissions_check( $request ) {
206 5
		$this->initiate_rest_read_box( $request, 'field_value_update' );
207 5
		if ( ! is_wp_error( $this->rest_box ) ) {
208 5
			$this->field = $this->rest_box->field_can_edit( $this->request->get_param( 'field_id' ), true );
209 5
		}
210
211 5
		$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 5
		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 2
	public function update_item( $request ) {
233 2
		$this->initiate_rest_read_box( $request, 'field_value_update' );
234
235 2
		if ( ! $this->request['value'] ) {
236 1
			return new WP_Error( 'cmb2_rest_update_field_error', __( 'CMB2 Field value cannot be updated without the value parameter specified.', 'cmb2' ), array(
237 1
				'status' => 400,
238 1
			) );
239
		}
240
241 2
		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 10
	public function delete_item_permissions_check( $request ) {
254 3
		$this->initiate_rest_read_box( $request, 'field_value_delete' );
255 3
		if ( ! is_wp_error( $this->rest_box ) ) {
256 10
			$this->field = $this->rest_box->field_can_edit( $this->request->get_param( 'field_id' ), true );
257 3
		}
258
259 3
		$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 3
		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 1
	public function delete_item( $request ) {
281 1
		$this->initiate_rest_read_box( $request, 'field_value_delete' );
282
283 1
		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 3
	public function modify_field_value( $activity ) {
295
296 3
		if ( ! $this->request['object_id'] || ! $this->request['object_type'] ) {
297 1
			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 1
				'status' => 400,
299 1
			) );
300
		}
301
302 2
		if ( is_wp_error( $this->rest_box ) ) {
303
			return $this->rest_box;
304
		}
305
306 2
		$this->field = $this->rest_box->field_can_edit(
307 2
			$this->field ? $this->field : $this->request->get_param( 'field_id' ),
308
			true
309 2
		);
310
311 2
		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 2
		$this->field->args[ "value_{$activity}" ] = (bool) 'deleted' === $activity
318 2
			? $this->field->remove_data()
319 2
			: $this->field->save_field( $this->request['value'] );
320
321
		// If options page, save the $activity options
322 2
		if ( 'options-page' == $this->request['object_type'] ) {
323
			$this->field->args[ "value_{$activity}" ] = cmb2_options( $this->request['object_id'] )->set();
324
		}
325
326 2
		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 4
	public function prepare_read_field( $field ) {
338 4
		$this->field = $this->rest_box->field_can_read( $field, true );
339
340 4
		if ( ! $this->field ) {
341 1
			return new WP_Error( 'cmb2_rest_no_field_by_id_error', __( 'No field found by that id.', 'cmb2' ), array(
342 1
				'status' => 403,
343 1
			) );
344
		}
345
346 4
		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 5
	public function prepare_field_response() {
358 5
		$field_data = $this->prepare_field_data( $this->field );
359 5
		$response = rest_ensure_response( $field_data );
360
361 5
		$response->add_links( $this->prepare_links( $this->field ) );
362
363 5
		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 5
	protected function prepare_field_data( CMB2_Field $field ) {
376 5
		$field_data = array();
377 5
		$params_to_ignore = array( 'show_in_rest', 'options' );
378
		$params_to_rename = array(
379 5
			'label_cb' => 'label',
380 5
			'options_cb' => 'options',
381 5
		);
382
383
		// Run this first so the js_dependencies arg is populated.
384 5
		$rendered = ( $cb = $field->maybe_callback( 'render_row_cb' ) )
385
			// Ok, callback is good, let's run it.
386 5
			? $this->get_cb_results( $cb, $field->args(), $field )
387 5
			: false;
388
389 5
		$field_args = $field->args();
390
391 5
		foreach ( $field_args as $key => $value ) {
392 5
			if ( in_array( $key, $params_to_ignore, true ) ) {
393 5
				continue;
394
			}
395
396 5
			if ( 'options_cb' === $key ) {
397 5
				$value = $field->options();
398 5
			} elseif ( in_array( $key, CMB2_Field::$callable_fields, true ) ) {
399
400 5
				if ( isset( $this->request['_rendered'] ) ) {
401
					$value = $key === 'render_row_cb' ? $rendered : $field->get_param_callback_result( $key );
402 5
				} elseif ( is_array( $value ) ) {
403
					// We need to rewrite callbacks as string as they will cause
404
					// JSON recursion errors.
405 5
					$class = is_string( $value[0] ) ? $value[0] : get_class( $value[0] );
406 5
					$value = $class . '::' . $value[1];
407 5
				}
408 5
			}
409
410 5
			$key = isset( $params_to_rename[ $key ] ) ? $params_to_rename[ $key ] : $key;
411
412 5
			if ( empty( $value ) || is_scalar( $value ) || is_array( $value ) ) {
413 5
				$field_data[ $key ] = $value;
414 5
			} else {
415
				$field_data[ $key ] = sprintf( __( 'Value Error for %s', 'cmb2' ), $key );
416
			}
417 5
		}
418
419 5
		if ( $this->request['object_id'] && $this->request['object_type'] ) {
420 3
			$field_data['value'] = $field->get_data();
421 3
		}
422
423 5
		return $field_data;
424
	}
425
426
	/**
427
	 * Return an array of contextual links for field/fields.
428
	 *
429
	 * @since  2.2.3
430
	 *
431
	 * @param  CMB2_Field $field Field object to build links from.
432
	 *
433
	 * @return array             Array of links
434
	 */
435 5
	protected function prepare_links( $field ) {
436 5
		$boxbase      = $this->namespace_base . '/' . $this->rest_box->cmb->cmb_id;
437 5
		$query_string = $this->get_query_string();
438
439 3
		$links = array(
440
			'self' => array(
441 5
				'href' => rest_url( trailingslashit( $boxbase ) . 'fields/' . $field->_id() . $query_string ),
442 5
			),
443
			'collection' => array(
444 5
				'href' => rest_url( trailingslashit( $boxbase ) . 'fields' . $query_string ),
445 5
			),
446
			'up' => array(
447 5
				'embeddable' => true,
448 5
				'href' => rest_url( $boxbase . $query_string ),
449 5
			),
450 5
		);
451
452 5
		return $links;
453
	}
454
455
	/**
456
	 * Checks if the CMB2 box or field has any registered callback parameters for the given filter.
457
	 *
458
	 * The registered handlers will have a property name which matches the filter, except:
459
	 * - The 'cmb2_api' prefix will be removed
460
	 * - A '_cb' suffix will be added (to stay inline with other '*_cb' parameters).
461
	 *
462
	 * @since  2.2.3
463
	 *
464
	 * @param  string $filter      The filter name.
465
	 * @param  bool   $default_val The default filter value.
466
	 *
467
	 * @return bool                The possibly-modified filter value (if the _cb param is a non-callable).
468
	 */
469 10
	public function maybe_hook_registered_callback( $filter, $default_val ) {
470 10
		$default_val = parent::maybe_hook_registered_callback( $filter, $default_val );
471
472 10
		if ( $this->field ) {
473
474
			// Hook field specific filter callbacks.
475 9
			$val = $this->field->maybe_hook_parameter( $filter, $default_val );
476 9
			if ( null !== $val ) {
477 9
				$default_val = $val;
478 9
			}
479 9
		}
480
481 10
		return $default_val;
482
	}
483
484
	/**
485
	 * Unhooks any CMB2 box or field registered callback parameters for the given filter.
486
	 *
487
	 * @since  2.2.3
488
	 *
489
	 * @param  string $filter The filter name.
490
	 *
491
	 * @return void
492
	 */
493 9
	public function maybe_unhook_registered_callback( $filter ) {
494 9
		parent::maybe_unhook_registered_callback( $filter );
495
496 9
		if ( $this->field ) {
497
			// Unhook field specific filter callbacks.
498 9
			$this->field->maybe_hook_parameter( $filter, null, 'remove_filter' );
499 9
		}
500 9
	}
501
502
}
503