Completed
Pull Request — trunk (#541)
by Justin
04:31
created

CMB2_REST_Endpoints::get_box_fields()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 22
rs 8.6738
cc 5
eloc 13
nc 4
nop 1
1
<?php
2
/**
3
 * Creates CMB2 objects/fields endpoint for WordPres REST API.
4
 * Allows access to fields registered to a specific post type and more.
5
 *
6
 * @todo  Add better documentation.
7
 * @todo  Research proper schema.
8
 *
9
 * @since 2.2.0
10
 *
11
 * @category  WordPress_Plugin
12
 * @package   CMB2
13
 * @author    WebDevStudios
14
 * @license   GPL-2.0+
15
 * @link      http://webdevstudios.com
16
 */
17
class CMB2_REST_Endpoints extends WP_REST_Controller {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
18
19
	/**
20
	 * The current CMB2 REST endpoint version
21
	 * @var string
22
	 * @since 2.2.0
23
	 */
24
	public $version = '1';
25
26
	/**
27
	 * The CMB2 REST namespace
28
	 * @var string
29
	 * @since 2.2.0
30
	 */
31
	public $namespace = 'cmb2/v';
32
33
	/**
34
	 * The current request object
35
	 * @var WP_REST_Request $request
36
	 * @since 2.2.0
37
	 */
38
	public $request;
39
40
	/**
41
	 * Constructor
42
	 * @since 2.2.0
43
	 */
44
	public function __construct() {
45
		$this->namespace .= $this->version;
46
	}
47
48
	/**
49
	 * Register the routes for the objects of the controller.
50
	 *
51
	 * @since 2.2.0
52
	 */
53
	public function register_routes() {
54
55
		// Returns all boxes data.
56
		register_rest_route( $this->namespace, '/boxes/', array(
57
			array(
58
				'methods'         => WP_REST_Server::READABLE,
59
				'callback'        => array( $this, 'get_all_boxes' ),
60
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
61
			),
62
			'schema' => array( $this, 'get_item_schema' ),
63
		) );
64
65
		// Returns specific box's data.
66
		register_rest_route( $this->namespace, '/boxes/(?P<cmb_id>[\w-]+)', array(
67
			array(
68
				'methods'         => WP_REST_Server::READABLE,
69
				'callback'        => array( $this, 'get_box' ),
70
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
71
			),
72
			'schema' => array( $this, 'get_item_schema' ),
73
		) );
74
75
		// Returns specific box's fields.
76
		register_rest_route( $this->namespace, '/boxes/(?P<cmb_id>[\w-]+)/fields/', array(
77
			array(
78
				'methods'         => WP_REST_Server::READABLE,
79
				'callback'        => array( $this, 'get_box_fields' ),
80
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
81
			),
82
			'schema' => array( $this, 'get_item_schema' ),
83
		) );
84
85
		// Returns specific field data.
86
		register_rest_route( $this->namespace, '/boxes/(?P<cmb_id>[\w-]+)/fields/(?P<field_id>[\w-]+)', array(
87
			array(
88
				'methods'         => WP_REST_Server::READABLE,
89
				'callback'        => array( $this, 'get_field' ),
90
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
91
			),
92
			'schema' => array( $this, 'get_item_schema' ),
93
		) );
94
95
	}
96
97
	/**
98
	 * Get all public fields
99
	 *
100
	 * @since 2.2.0
101
	 *
102
	 * @param WP_REST_Request $request The API request object.
103
	 * @return array
104
	 */
105
	public function get_all_boxes( $request ) {
106
		$this->request = $request;
107
108
		$boxes = CMB2_Boxes::get_by_property( 'show_in_rest', false );
109
110
		if ( empty( $boxes ) ) {
111
			return $this->prepare_item( array( 'error' => __( 'No boxes found.', 'cmb2' ) ), $this->request );
112
		}
113
114
		$boxes_data = array();
115
		// Loop boxes and get specific field.
116
		foreach ( $boxes as $key => $cmb ) {
117
			$boxes_data[ $cmb->cmb_id ] = $this->get_rest_box( $cmb );
118
		}
119
120
		return $this->prepare_item( $boxes_data );
121
	}
122
123
	/**
124
	 * Get all public fields
125
	 *
126
	 * @since 2.2.0
127
	 *
128
	 * @param WP_REST_Request $request The API request object.
129
	 * @return array
130
	 */
131
	public function get_box( $request ) {
132
		$this->request = $request;
133
134
		$cmb_id = $this->request->get_param( 'cmb_id' );
135
136
		if ( $cmb_id && ( $cmb = cmb2_get_metabox( $cmb_id ) ) ) {
137
			return $this->prepare_item( $this->get_rest_box( $cmb ) );
138
		}
139
140
		return $this->prepare_item( array( 'error' => __( 'No box found by that id.', 'cmb2' ) ) );
141
	}
142
143
	/**
144
	 * Get all box fields
145
	 *
146
	 * @since 2.2.0
147
	 *
148
	 * @param WP_REST_Request $request The API request object.
149
	 * @return array
150
	 */
151
	public function get_box_fields( $request ) {
152
		$this->request = $request;
153
154
		$cmb_id = $this->request->get_param( 'cmb_id' );
155
156
		if ( $cmb_id && ( $cmb = cmb2_get_metabox( $cmb_id ) ) ) {
157
			$fields = array();
158
			foreach ( $cmb->prop( 'fields', array() ) as $field ) {
159
				$field = $this->get_rest_field( $cmb, $field['id'] );
160
161
				if ( ! is_wp_error( $field ) ) {
162
					$fields[ $field['id'] ] = $field;
163
				} else {
164
					$fields[ $field['id'] ] = array( 'error' => $field->get_error_message() );
165
				}
166
			}
167
168
			return $this->prepare_item( $fields );
169
		}
170
171
		return $this->prepare_item( array( 'error' => __( 'No box found by that id.', 'cmb2' ) ) );
172
	}
173
174
	/**
175
	 * Get a CMB2 box prepared for REST
176
	 *
177
	 * @since 2.2.0
178
	 *
179
	 * @param CMB2 $cmb
180
	 * @return array
181
	 */
182
	public function get_rest_box( $cmb ) {
183
		// TODO: handle callable properties
184
		$boxes_data = $cmb->meta_box;
185
186
		unset( $boxes_data['fields'] );
187
188
		$base = $this->namespace . '/boxes/' . $cmb->cmb_id;
189
		$boxbase = $base . '/' . $cmb->cmb_id;
190
191
		$response = new WP_REST_Response( $boxes_data );
192
		$response->add_links( array(
193
			'self' => array(
194
				'href' => rest_url( trailingslashit( $boxbase ) ),
195
			),
196
			'collection' => array(
197
				'href' => rest_url( trailingslashit( $base ) ),
198
			),
199
			'fields' => array(
200
				'href' => rest_url( trailingslashit( $boxbase ) . 'fields/' ),
201
			),
202
		) );
203
204
		$boxes_data['_links'] = $response->get_links();
205
206
		return $boxes_data;
207
	}
208
209
	/**
210
	 * Get a specific field
211
	 *
212
	 * @since 2.2.0
213
	 *
214
	 * @param WP_REST_Request $request The API request object.
215
	 * @return array|WP_Error
216
	 */
217
	public function get_field( $request ) {
218
		$this->request = $request;
219
220
		$cmb = cmb2_get_metabox( $this->request->get_param( 'cmb_id' ) );
221
222
		if ( ! $cmb ) {
223
			return $this->prepare_item( array( 'error' => __( 'No box found by that id.', 'cmb2' ) ) );
224
		}
225
226
		$field = $this->get_rest_field( $cmb, $this->request->get_param( 'field_id' ) );
227
228
		if ( is_wp_error( $field ) ) {
229
			return $this->prepare_item( array( 'error' => $field->get_error_message() ) );
230
		}
231
232
		return $this->prepare_item( $field );
233
	}
234
235
	/**
236
	 * Get a specific field
237
	 *
238
	 * @since 2.2.0
239
	 *
240
	 * @param CMB2 $cmb
241
	 * @return array|WP_Error
242
	 */
243
	public function get_rest_field( $cmb, $field_id ) {
0 ignored issues
show
Coding Style introduced by
get_rest_field uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
244
		// TODO: more robust show_in_rest checking. use rest_read/rest_write properties.
245
		if ( ! $cmb->prop( 'show_in_rest' ) ) {
246
			return new WP_Error( 'cmb2_rest_error', __( "You don't have permission to view this field.", 'cmb2' ) );
247
		}
248
249
		$field = $cmb->get_field( $field_id );
250
251
		if ( ! $field ) {
252
			return new WP_Error( 'cmb2_rest_error', __( 'No field found by that id.', 'cmb2' ) );
253
		}
254
255
		// TODO: check for show_in_rest property.
256
		// $can_read = $this->can_read
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
257
		// 	? 'write_only' !== $show_in_rest
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
258
		// 	: in_array( $show_in_rest, array( 'read_and_write', 'read_only' ), true );
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
259
260
261
		$field_data = array();
262
		$params_to_ignore = array( 'show_on_cb', 'show_in_rest', 'options' );
263
		$params_to_rename = array(
264
			'label_cb' => 'label',
265
			'options_cb' => 'options',
266
		);
267
268
		foreach ( $field->args() as $key => $value ) {
269
			if ( in_array( $key, $params_to_ignore, true ) ) {
270
				continue;
271
			}
272
273
			if ( 'render_row_cb' === $key ) {
274
				$value = '';
275
				// TODO: Use request get object
276
				if ( isset( $_GET['rendered'] ) && ( $cb = $field->maybe_callback( 'render_row_cb' ) ) ) {
277
					// Ok, callback is good, let's run it.
278
					ob_start();
279
					call_user_func( $cb, $field->args(), $this );
280
					$field_data[ 'rendered' ] = ob_get_clean();
281
				}
282
				continue;
283
			}
284
285
			if ( 'options_cb' === $key ) {
286
				$value = $field->options();
287
			} elseif ( in_array( $key, CMB2_Field::$callable_fields ) ) {
288
				$value = $field->get_param_callback_result( $key );
289
			}
290
291
			$key = isset( $params_to_rename[ $key ] ) ? $params_to_rename[ $key ] : $key;
292
293
			if ( empty( $value ) || is_scalar( $value ) || is_array( $value ) ) {
294
				$field_data[ $key ] = $value;
295
			} else {
296
				$field_data[ $key ] = __( 'Value Error', 'cmb2' );
297
			}
298
		}
299
		$field_data['value'] = $field->get_data();
300
301
		$base = $this->namespace . '/boxes/' . $cmb->cmb_id;
302
303
		// Entity meta
304
		$field_data['_links'] = array(
305
			'self' => array(
306
				'href' => rest_url( trailingslashit( $base ) . 'fields/' . $field->_id() ),
307
			),
308
			'collection' => array(
309
				'href' => rest_url( trailingslashit( $base ) . 'fields/' ),
310
			),
311
			'box' => array(
312
				'href' => rest_url( trailingslashit( $base ) ),
313
			),
314
		);
315
316
		return $field_data;
317
	}
318
319
	/**
320
	 * Check if a given request has access to a field or box.
321
	 * By default, no special permissions needed, but filtering return value.
322
	 *
323
	 * @since 2.2.0
324
	 *
325
	 * @param  WP_REST_Request $request Full details about the request.
326
	 * @return bool
327
	 */
328
	public function get_item_permissions_check( $request ) {
329
		$this->request = $request;
330
331
		/**
332
		 * By default, no special permissions needed.
333
		 *
334
		 * @since 2.2.0
335
		 *
336
		 * @param object $request        The WP_REST_Request object
337
		 * @param object $cmb2_endpoints This endpoints object
338
		 */
339
		return apply_filters( 'cmb2_request_permissions_check', true, $this->request );
340
	}
341
342
	/**
343
	 * Prepare a CMB2 object for serialization
344
	 *
345
	 * @since 2.2.0
346
	 *
347
	 * @param  mixed $data
0 ignored issues
show
Bug introduced by
There is no parameter named $data. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
348
	 * @return array $data
349
	 */
350
	public function prepare_item( $post ) {
351
		return $this->prepare_item_for_response( $post, $this->request );
352
	}
353
354
	/**
355
	 * Prepare a CMB2 object for serialization
356
	 *
357
	 * @since 2.2.0
358
	 *
359
	 * @param  mixed           $data
360
	 * @param  WP_REST_Request $request Request object
361
	 * @return array $data
362
	 */
363
	public function prepare_item_for_response( $data, $request ) {
364
365
		$context = ! empty( $this->request['context'] ) ? $this->request['context'] : 'view';
366
		$data = $this->filter_response_by_context( $data, $context );
367
368
		/**
369
		 * Filter the prepared CMB2 item response.
370
		 *
371
		 * @since 2.2.0
372
		 *
373
		 * @param mixed  $data           Prepared data
374
		 * @param object $request        The WP_REST_Request object
375
		 * @param object $cmb2_endpoints This endpoints object
376
		 */
377
		return apply_filters( 'cmb2_rest_prepare', $data, $this->request, $this );
378
	}
379
380
	/**
381
	 * Get CMB2 fields schema, conforming to JSON Schema
382
	 *
383
	 * @since 2.2.0
384
	 *
385
	 * @return array
386
	 */
387
	public function get_item_schema() {
388
		$schema = array(
389
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
390
			'title'                => 'CMB2',
391
			'type'                 => 'object',
392
			'properties'           => array(
393
				'description' => array(
394
					'description'  => 'A human-readable description of the object.',
395
					'type'         => 'string',
396
					'context'      => array( 'view' ),
397
					),
398
					'name'             => array(
399
						'description'  => 'The id for the object.',
400
						'type'         => 'integer',
401
						'context'      => array( 'view' ),
402
					),
403
				'name' => array(
404
					'description'  => 'The title for the object.',
405
					'type'         => 'string',
406
					'context'      => array( 'view' ),
407
				),
408
			),
409
		);
410
		return $this->add_additional_fields_schema( $schema );
411
	}
412
413
}
414