Completed
Pull Request — trunk (#541)
by Justin
08:25
created

CMB2_REST_Endpoints::get_rest_field()   D

Complexity

Conditions 15
Paths 122

Size

Total Lines 82
Code Lines 42

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 82
rs 4.6274
cc 15
eloc 42
nc 122
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
	 * Box object id
42
	 * @var   mixed
43
	 * @since 2.2.0
44
	 */
45
	public $object_id = null;
46
47
	/**
48
	 * Box object type
49
	 * @var   string
50
	 * @since 2.2.0
51
	 */
52
	public $object_type = '';
53
54
	/**
55
	 * Constructor
56
	 * @since 2.2.0
57
	 */
58
	public function __construct() {
59
		$this->namespace .= $this->version;
60
	}
61
62
	/**
63
	 * Register the routes for the objects of the controller.
64
	 *
65
	 * @since 2.2.0
66
	 */
67
	public function register_routes() {
68
69
		// Returns all boxes data.
70
		register_rest_route( $this->namespace, '/boxes/', array(
71
			array(
72
				'methods'         => WP_REST_Server::READABLE,
73
				'callback'        => array( $this, 'get_all_boxes' ),
74
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
75
			),
76
			'schema' => array( $this, 'get_item_schema' ),
77
		) );
78
79
		// Returns specific box's data.
80
		register_rest_route( $this->namespace, '/boxes/(?P<cmb_id>[\w-]+)', array(
81
			array(
82
				'methods'         => WP_REST_Server::READABLE,
83
				'callback'        => array( $this, 'get_box' ),
84
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
85
			),
86
			'schema' => array( $this, 'get_item_schema' ),
87
		) );
88
89
		// Returns specific box's fields.
90
		register_rest_route( $this->namespace, '/boxes/(?P<cmb_id>[\w-]+)/fields/', array(
91
			array(
92
				'methods'         => WP_REST_Server::READABLE,
93
				'callback'        => array( $this, 'get_box_fields' ),
94
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
95
			),
96
			'schema' => array( $this, 'get_item_schema' ),
97
		) );
98
99
		// Returns specific field data.
100
		register_rest_route( $this->namespace, '/boxes/(?P<cmb_id>[\w-]+)/fields/(?P<field_id>[\w-]+)', array(
101
			array(
102
				'methods'         => WP_REST_Server::READABLE,
103
				'callback'        => array( $this, 'get_field' ),
104
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
105
			),
106
			'schema' => array( $this, 'get_item_schema' ),
107
		) );
108
109
	}
110
111
	/**
112
	 * Get all public fields
113
	 *
114
	 * @since 2.2.0
115
	 *
116
	 * @param WP_REST_Request $request The API request object.
117
	 * @return array
118
	 */
119
	public function get_all_boxes( $request ) {
120
		$this->initiate_request( $request );
121
122
		$boxes = CMB2_Boxes::get_by_property( 'show_in_rest', false );
123
124
		if ( empty( $boxes ) ) {
125
			return $this->prepare_item( array( 'error' => __( 'No boxes found.', 'cmb2' ) ), $this->request );
126
		}
127
128
		$boxes_data = array();
129
		// Loop boxes and get specific field.
130
		foreach ( $boxes as $key => $cmb ) {
131
			$boxes_data[ $cmb->cmb_id ] = $this->get_rest_box( $cmb );
132
		}
133
134
		return $this->prepare_item( $boxes_data );
135
	}
136
137
	/**
138
	 * Get all public fields
139
	 *
140
	 * @since 2.2.0
141
	 *
142
	 * @param WP_REST_Request $request The API request object.
143
	 * @return array
144
	 */
145
	public function get_box( $request ) {
146
		$this->initiate_request( $request );
147
148
		$cmb_id = $this->request->get_param( 'cmb_id' );
149
150
		if ( $cmb_id && ( $cmb = cmb2_get_metabox( $cmb_id, $this->object_id, $this->object_type ) ) ) {
151
			return $this->prepare_item( $this->get_rest_box( $cmb ) );
152
		}
153
154
		return $this->prepare_item( array( 'error' => __( 'No box found by that id.', 'cmb2' ) ) );
155
	}
156
157
	/**
158
	 * Get all box fields
159
	 *
160
	 * @since 2.2.0
161
	 *
162
	 * @param WP_REST_Request $request The API request object.
163
	 * @return array
164
	 */
165
	public function get_box_fields( $request ) {
166
		$this->initiate_request( $request );
167
168
		$cmb_id = $this->request->get_param( 'cmb_id' );
169
170
		if ( $cmb_id && ( $cmb = cmb2_get_metabox( $cmb_id, $this->object_id, $this->object_type ) ) ) {
171
			$fields = array();
172
			foreach ( $cmb->prop( 'fields', array() ) as $field ) {
173
				$field = $this->get_rest_field( $cmb, $field['id'] );
174
175
				if ( ! is_wp_error( $field ) ) {
176
					$fields[ $field['id'] ] = $field;
177
				} else {
178
					$fields[ $field['id'] ] = array( 'error' => $field->get_error_message() );
179
				}
180
			}
181
182
			return $this->prepare_item( $fields );
183
		}
184
185
		return $this->prepare_item( array( 'error' => __( 'No box found by that id.', 'cmb2' ) ) );
186
	}
187
188
	/**
189
	 * Get a CMB2 box prepared for REST
190
	 *
191
	 * @since 2.2.0
192
	 *
193
	 * @param CMB2 $cmb
194
	 * @return array
195
	 */
196
	public function get_rest_box( $cmb ) {
0 ignored issues
show
Coding Style introduced by
get_rest_box 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...
197
		$cmb->object_type( $this->object_id );
198
		$cmb->object_id( $this->object_type );
199
200
		$boxes_data = $cmb->meta_box;
201
202
		if ( isset( $_GET['rendered'] ) ) {
203
			$boxes_data['form_open'] = $this->get_cb_results( array( $cmb, 'render_form_open' ) );
204
			$boxes_data['form_close'] = $this->get_cb_results( array( $cmb, 'render_form_close' ) );
205
206
			global $wp_scripts, $wp_styles;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
207
			$before_css = $wp_styles->queue;
208
			$before_js = $wp_scripts->queue;
209
210
			CMB2_JS::enqueue();
211
212
			$boxes_data['js_dependencies'] = array_values( array_diff( $wp_scripts->queue, $before_js ) );
213
			$boxes_data['css_dependencies'] = array_values( array_diff( $wp_styles->queue, $before_css ) );
214
		}
215
216
		// TODO: look into 'embed' parameter.
217
		unset( $boxes_data['fields'] );
218
		// Handle callable properties.
219
		unset( $boxes_data['show_on_cb'] );
220
221
		$base = $this->namespace . '/boxes/' . $cmb->cmb_id;
222
		$boxbase = $base . '/' . $cmb->cmb_id;
223
224
		$response = new WP_REST_Response( $boxes_data );
225
		$response->add_links( array(
226
			'self' => array(
227
				'href' => rest_url( trailingslashit( $boxbase ) ),
228
			),
229
			'collection' => array(
230
				'href' => rest_url( trailingslashit( $base ) ),
231
			),
232
			'fields' => array(
233
				'href' => rest_url( trailingslashit( $boxbase ) . 'fields/' ),
234
			),
235
		) );
236
237
		$boxes_data['_links'] = $response->get_links();
238
239
		return $boxes_data;
240
	}
241
242
	/**
243
	 * Get a specific field
244
	 *
245
	 * @since 2.2.0
246
	 *
247
	 * @param WP_REST_Request $request The API request object.
248
	 * @return array|WP_Error
249
	 */
250
	public function get_field( $request ) {
251
		$this->initiate_request( $request );
252
253
		$cmb = cmb2_get_metabox( $this->request->get_param( 'cmb_id' ), $this->object_id, $this->object_type  );
254
255
		if ( ! $cmb ) {
256
			return $this->prepare_item( array( 'error' => __( 'No box found by that id.', 'cmb2' ) ) );
257
		}
258
259
		$field = $this->get_rest_field( $cmb, $this->request->get_param( 'field_id' ) );
260
261
		if ( is_wp_error( $field ) ) {
262
			return $this->prepare_item( array( 'error' => $field->get_error_message() ) );
263
		}
264
265
		return $this->prepare_item( $field );
266
	}
267
268
	/**
269
	 * Get a specific field
270
	 *
271
	 * @since 2.2.0
272
	 *
273
	 * @param CMB2 $cmb
274
	 * @return array|WP_Error
275
	 */
276
	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...
277
278
		// TODO: more robust show_in_rest checking. use rest_read/rest_write properties.
279
		if ( ! $cmb->prop( 'show_in_rest' ) ) {
280
			return new WP_Error( 'cmb2_rest_error', __( "You don't have permission to view this field.", 'cmb2' ) );
281
		}
282
283
		$field = $cmb->get_field( $field_id );
284
285
		if ( ! $field ) {
286
			return new WP_Error( 'cmb2_rest_error', __( 'No field found by that id.', 'cmb2' ) );
287
		}
288
289
		// TODO: check for show_in_rest property.
290
		// $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...
291
		// 	? '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...
292
		// 	: 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...
293
294
295
		$field_data = array();
296
		$params_to_ignore = array( 'show_on_cb', 'show_in_rest', 'options' );
297
		$params_to_rename = array(
298
			'label_cb' => 'label',
299
			'options_cb' => 'options',
300
		);
301
302
		// TODO: Use request get object
303
		// Run this first so the js_dependencies arg is populated.
304
		$rendered = isset( $_GET['rendered'] ) && ( $cb = $field->maybe_callback( 'render_row_cb' ) )
305
			// Ok, callback is good, let's run it.
306
			? $this->get_cb_results( $cb, $field->args(), $field )
0 ignored issues
show
Bug introduced by
The variable $cb does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
307
			: false;
308
309
		foreach ( $field->args() as $key => $value ) {
310
			if ( in_array( $key, $params_to_ignore, true ) ) {
311
				continue;
312
			}
313
314
			if ( 'render_row_cb' === $key ) {
315
				continue;
316
			}
317
318
			if ( 'options_cb' === $key ) {
319
				$value = $field->options();
320
			} elseif ( in_array( $key, CMB2_Field::$callable_fields ) ) {
321
				$value = $field->get_param_callback_result( $key );
322
			}
323
324
			$key = isset( $params_to_rename[ $key ] ) ? $params_to_rename[ $key ] : $key;
325
326
			if ( empty( $value ) || is_scalar( $value ) || is_array( $value ) ) {
327
				$field_data[ $key ] = $value;
328
			} else {
329
				$field_data[ $key ] = __( 'Value Error', 'cmb2' );
330
			}
331
		}
332
333
		if ( isset( $_GET['rendered'] ) ) {
334
			$field_data['rendered'] = $rendered;
335
		}
336
337
		$field_data['value'] = $field->get_data();
338
339
		$base = $this->namespace . '/boxes/' . $cmb->cmb_id;
340
341
		$response = new WP_REST_Response( $field_data );
342
		$response->add_links( array(
343
			'self' => array(
344
				'href' => rest_url( trailingslashit( $base ) . 'fields/' . $field->_id() ),
345
			),
346
			'collection' => array(
347
				'href' => rest_url( trailingslashit( $base ) . 'fields/' ),
348
			),
349
			'box' => array(
350
				'href' => rest_url( trailingslashit( $base ) ),
351
			),
352
		) );
353
354
		$field_data['_links'] = $response->get_links();
355
356
		return $field_data;
357
	}
358
359
	/**
360
	 * Check if a given request has access to a field or box.
361
	 * By default, no special permissions needed, but filtering return value.
362
	 *
363
	 * @since 2.2.0
364
	 *
365
	 * @param  WP_REST_Request $request Full details about the request.
366
	 * @return bool
367
	 */
368
	public function get_item_permissions_check( $request ) {
369
		$this->initiate_request( $request );
370
371
		/**
372
		 * By default, no special permissions needed.
373
		 *
374
		 * @since 2.2.0
375
		 *
376
		 * @param object $request        The WP_REST_Request object
377
		 * @param object $cmb2_endpoints This endpoints object
378
		 */
379
		return apply_filters( 'cmb2_request_permissions_check', true, $this->request );
380
	}
381
382
	/**
383
	 * Prepare a CMB2 object for serialization
384
	 *
385
	 * @since 2.2.0
386
	 *
387
	 * @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...
388
	 * @return array $data
389
	 */
390
	public function prepare_item( $post ) {
391
		return $this->prepare_item_for_response( $post, $this->request );
392
	}
393
394
	/**
395
	 * Output buffers a callback and returns the results.
396
	 *
397
	 * @since  2.2.0
398
	 *
399
	 * @param  mixed $cb Callable function/method.
400
	 * @return mixed     Results of output buffer after calling function/method.
401
	 */
402
	public function get_cb_results( $cb ) {
403
		$args = func_get_args();
404
		array_shift( $args ); // ignore $cb
405
		ob_start();
406
		call_user_func_array( $cb, $args );
407
408
		return ob_get_clean();
409
	}
410
411
	public function initiate_request( $request ) {
0 ignored issues
show
Coding Style introduced by
initiate_request uses the super-global variable $_REQUEST 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...
412
		$this->request = $request;
413
414
		if ( isset( $_REQUEST['object_id'] ) ) {
415
			$this->object_id = absint( $_REQUEST['object_id'] );
416
		}
417
418
		if ( isset( $_REQUEST['object_type'] ) ) {
419
			$this->object_type = absint( $_REQUEST['object_type'] );
420
		}
421
	}
422
423
	/**
424
	 * Prepare a CMB2 object for serialization
425
	 *
426
	 * @since 2.2.0
427
	 *
428
	 * @param  mixed           $data
429
	 * @param  WP_REST_Request $request Request object
430
	 * @return array $data
431
	 */
432
	public function prepare_item_for_response( $data, $request ) {
433
434
		$context = ! empty( $this->request['context'] ) ? $this->request['context'] : 'view';
435
		$data = $this->filter_response_by_context( $data, $context );
436
437
		/**
438
		 * Filter the prepared CMB2 item response.
439
		 *
440
		 * @since 2.2.0
441
		 *
442
		 * @param mixed  $data           Prepared data
443
		 * @param object $request        The WP_REST_Request object
444
		 * @param object $cmb2_endpoints This endpoints object
445
		 */
446
		return apply_filters( 'cmb2_rest_prepare', $data, $this->request, $this );
447
	}
448
449
	/**
450
	 * Get CMB2 fields schema, conforming to JSON Schema
451
	 *
452
	 * @since 2.2.0
453
	 *
454
	 * @return array
455
	 */
456
	public function get_item_schema() {
457
		$schema = array(
458
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
459
			'title'                => 'CMB2',
460
			'type'                 => 'object',
461
			'properties'           => array(
462
				'description' => array(
463
					'description'  => 'A human-readable description of the object.',
464
					'type'         => 'string',
465
					'context'      => array( 'view' ),
466
					),
467
					'name'             => array(
468
						'description'  => 'The id for the object.',
469
						'type'         => 'integer',
470
						'context'      => array( 'view' ),
471
					),
472
				'name' => array(
473
					'description'  => 'The title for the object.',
474
					'type'         => 'string',
475
					'context'      => array( 'view' ),
476
				),
477
			),
478
		);
479
		return $this->add_additional_fields_schema( $schema );
480
	}
481
482
}
483