Completed
Pull Request — trunk (#541)
by Justin
15:40
created

CMB2_REST_Endpoints::get_box()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 9
rs 9.6667
cc 3
eloc 5
nc 2
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
	 * Constructor
35
	 * @since 2.2.0
36
	 */
37
	public function __construct() {
38
		$this->namespace .= $this->version;
39
	}
40
41
	/**
42
	 * Register the routes for the objects of the controller.
43
	 */
44
	public function register_routes() {
45
46
		// Returns all boxes data.
47
		register_rest_route( $this->namespace, '/boxes/', array(
48
			array(
49
				'methods'         => WP_REST_Server::READABLE,
50
				'callback'        => array( $this, 'get_all_boxes' ),
51
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
52
			),
53
			'schema' => array( $this, 'get_item_schema' ),
54
		) );
55
56
		// Returns specific box's data.
57
		register_rest_route( $this->namespace, '/boxes/(?P<cmb_id>[\w-]+)', array(
58
			array(
59
				'methods'         => WP_REST_Server::READABLE,
60
				'callback'        => array( $this, 'get_box' ),
61
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
62
			),
63
			'schema' => array( $this, 'get_item_schema' ),
64
		) );
65
66
		// Returns specific box's fields.
67
		register_rest_route( $this->namespace, '/boxes/(?P<cmb_id>[\w-]+)/fields/', array(
68
			array(
69
				'methods'         => WP_REST_Server::READABLE,
70
				'callback'        => array( $this, 'get_box_fields' ),
71
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
72
			),
73
			'schema' => array( $this, 'get_item_schema' ),
74
		) );
75
76
		// Returns specific field data.
77
		register_rest_route( $this->namespace, '/boxes/(?P<cmb_id>[\w-]+)/fields/(?P<field_id>[\w-]+)', array(
78
			array(
79
				'methods'         => WP_REST_Server::READABLE,
80
				'callback'        => array( $this, 'get_field' ),
81
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
82
			),
83
			'schema' => array( $this, 'get_item_schema' ),
84
		) );
85
86
	}
87
88
	/**
89
	 * Get all public fields
90
	 *
91
	 * @param WP_REST_Request $request The API request object.
92
	 * @return array
93
	 */
94
	public function get_all_boxes( $request ) {
95
		$boxes = CMB2_Boxes::get_by_property( 'show_in_rest', false );
96
97
		if ( empty( $boxes ) ) {
98
			return $this->prepare_item_for_response( array( 'error' => __( 'No boxes found.', 'cmb2' ) ), $request );
99
		}
100
101
		$boxes_data = array();
102
		// Loop boxes and get specific field.
103
		foreach ( $boxes as $key => $cmb ) {
104
			$boxes_data[ $cmb->cmb_id ] = $this->get_rest_box( $cmb );
105
		}
106
107
		return $this->prepare_item_for_response( $boxes_data, $request );
108
	}
109
110
	/**
111
	 * Get all public fields
112
	 *
113
	 * @param WP_REST_Request $request The API request object.
114
	 * @return array
115
	 */
116
	public function get_box( $request ) {
117
		$cmb_id = $request->get_param( 'cmb_id' );
118
119
		if ( $cmb_id && ( $cmb = cmb2_get_metabox( $cmb_id ) ) ) {
120
			return $this->prepare_item_for_response( $this->get_rest_box( $cmb ), $request );
121
		}
122
123
		return $this->prepare_item_for_response( array( 'error' => __( 'No box found by that id.', 'cmb2' ) ), $request );
124
	}
125
126
	/**
127
	 * Get all box fields
128
	 *
129
	 * @param WP_REST_Request $request The API request object.
130
	 * @return array
131
	 */
132
	public function get_box_fields( $request ) {
133
		$cmb_id = $request->get_param( 'cmb_id' );
134
135
		if ( $cmb_id && ( $cmb = cmb2_get_metabox( $cmb_id ) ) ) {
136
			$fields = array();
137
			foreach ( $cmb->prop( 'fields', array() ) as $field ) {
138
				$field = $this->get_rest_field( $cmb, $field['id'] );
139
140
				if ( ! is_wp_error( $field ) ) {
141
					$fields[ $field['id'] ] = $field;
142
				} else {
143
					$fields[ $field['id'] ] = array( 'error' => $field->get_error_message() );
144
				}
145
			}
146
147
			return $this->prepare_item_for_response( $fields, $request );
148
		}
149
150
		return $this->prepare_item_for_response( array( 'error' => __( 'No box found by that id.', 'cmb2' ) ), $request );
151
	}
152
153
	/**
154
	 * Get a CMB2 box prepared for REST
155
	 *
156
	 * @param CMB2 $cmb
157
	 * @return array
158
	 */
159
	public function get_rest_box( $cmb ) {
160
		// TODO: handle callable properties
161
		$boxes_data = $cmb->meta_box;
162
163
		unset( $boxes_data['fields'] );
164
165
		// TODO research adding links the proper way
166
		// $data->add_links( $this->prepare_links( $post ) );
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...
167
168
		$base = $this->namespace . '/boxes/' . $cmb->cmb_id;
169
		$boxbase = $base . '/' . $cmb->cmb_id;
170
171
		// Entity meta
172
		$boxes_data['_links'] = array(
173
			'self' => array(
174
				'href' => rest_url( trailingslashit( $boxbase ) ),
175
			),
176
			'collection' => array(
177
				'href' => rest_url( trailingslashit( $base ) ),
178
			),
179
			'fields' => array(
180
				'href' => rest_url( trailingslashit( $boxbase ) . 'fields/' ),
181
			),
182
		);
183
184
		return $boxes_data;
185
	}
186
187
	/**
188
	 * Get a specific field
189
	 *
190
	 * @param WP_REST_Request $request The API request object.
191
	 * @return array|WP_Error
192
	 */
193
	public function get_field( $request ) {
194
		$cmb = cmb2_get_metabox( $request->get_param( 'cmb_id' ) );
195
196
		if ( ! $cmb ) {
197
			return $this->prepare_item_for_response( array( 'error' => __( 'No box found by that id.', 'cmb2' ) ), $request );
198
		}
199
200
		$field = $this->get_rest_field( $cmb, $request->get_param( 'field_id' ) );
201
202
		if ( is_wp_error( $field ) ) {
203
			return $this->prepare_item_for_response( array( 'error' => $field->get_error_message() ), $request );
204
		}
205
206
		return $this->prepare_item_for_response( $field, $request );
207
	}
208
209
	/**
210
	 * Get a specific field
211
	 *
212
	 * @param CMB2 $cmb
213
	 * @return array|WP_Error
214
	 */
215
	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...
216
		// TODO: more robust show_in_rest checking. use rest_read/rest_write properties.
217
		if ( ! $cmb->prop( 'show_in_rest' ) ) {
218
			return new WP_Error( 'cmb2_rest_error', __( "You don't have permission to view this field.", 'cmb2' ) );
219
		}
220
221
		$field = $cmb->get_field( $field_id );
222
223
		if ( ! $field ) {
224
			return new WP_Error( 'cmb2_rest_error', __( 'No field found by that id.', 'cmb2' ) );
225
		}
226
227
		// TODO: check for show_in_rest property.
228
		// $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...
229
		// 	? '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...
230
		// 	: 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...
231
232
233
		$field_data = array();
234
		$params_to_ignore = array( 'show_on_cb', 'show_in_rest', 'options' );
235
		$params_to_rename = array(
236
			'label_cb' => 'label',
237
			'options_cb' => 'options',
238
		);
239
240
		foreach ( $field->args() as $key => $value ) {
241
			if ( in_array( $key, $params_to_ignore, true ) ) {
242
				continue;
243
			}
244
245
			if ( 'render_row_cb' === $key ) {
246
				$value = '';
247
				// TODO: Use request get object
248
				if ( isset( $_GET['rendered'] ) && ( $cb = $field->maybe_callback( 'render_row_cb' ) ) ) {
249
					// Ok, callback is good, let's run it.
250
					ob_start();
251
					call_user_func( $cb, $field->args(), $this );
252
					$field_data[ 'rendered' ] = ob_get_clean();
253
				}
254
				continue;
255
			}
256
257
			if ( 'options_cb' === $key ) {
258
				$value = $field->options();
259
			} elseif ( in_array( $key, CMB2_Field::$callable_fields ) ) {
260
				$value = $field->get_param_callback_result( $key );
261
			}
262
263
			$key = isset( $params_to_rename[ $key ] ) ? $params_to_rename[ $key ] : $key;
264
265
			if ( empty( $value ) || is_scalar( $value ) || is_array( $value ) ) {
266
				$field_data[ $key ] = $value;
267
			} else {
268
				$field_data[ $key ] = __( 'Value Error', 'cmb2' );
269
			}
270
		}
271
		$field_data['value'] = $field->get_data();
272
273
		$base = $this->namespace . '/boxes/' . $cmb->cmb_id;
274
275
		// Entity meta
276
		$field_data['_links'] = array(
277
			'self' => array(
278
				'href' => rest_url( trailingslashit( $base ) . 'fields/' . $field->_id() ),
279
			),
280
			'collection' => array(
281
				'href' => rest_url( trailingslashit( $base ) . 'fields/' ),
282
			),
283
			'box' => array(
284
				'href' => rest_url( trailingslashit( $base ) ),
285
			),
286
		);
287
288
		return $field_data;
289
	}
290
291
	/**
292
	 * Check if a given request has access to a field or box
293
	 *
294
	 * @param  WP_REST_Request $request Full details about the request.
295
	 * @return bool
296
	 */
297
	public function get_item_permissions_check( $request ) {
298
		return true;
299
	}
300
301
	/**
302
	 * Prepare a CMB2 object for serialization
303
	 *
304
	 * @param  mixed           $data
305
	 * @param  WP_REST_Request $request
306
	 * @return array           Taxonomy data
307
	 */
308
	public function prepare_item_for_response( $data, $request ) {
309
310
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
311
		$data = $this->filter_response_by_context( $data, $context );
312
313
		return apply_filters( 'cmb2_rest_prepare', $data, $request );
314
	}
315
316
	/**
317
	 * Get CMB2 fields schema, conforming to JSON Schema
318
	 *
319
	 * @return array
320
	 */
321
	public function get_item_schema() {
322
		$schema = array(
323
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
324
			'title'                => 'CMB2',
325
			'type'                 => 'object',
326
			'properties'           => array(
327
				'description' => array(
328
					'description'  => 'A human-readable description of the object.',
329
					'type'         => 'string',
330
					'context'      => array( 'view' ),
331
					),
332
					'name'             => array(
333
						'description'  => 'The id for the object.',
334
						'type'         => 'integer',
335
						'context'      => array( 'view' ),
336
					),
337
				'name' => array(
338
					'description'  => 'The title for the object.',
339
					'type'         => 'string',
340
					'context'      => array( 'view' ),
341
				),
342
			),
343
		);
344
		return $this->add_additional_fields_schema( $schema );
345
	}
346
347
}
348