Completed
Pull Request — trunk (#541)
by Justin
06:34
created

CMB2_REST::get_object_data()   C

Complexity

Conditions 8
Paths 12

Size

Total Lines 22
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 12
nop 1
dl 0
loc 22
ccs 0
cts 18
cp 0
crap 72
rs 6.6037
c 0
b 0
f 0
1
<?php
2
/**
3
 * Handles hooking CMB2 objects/fields into the WordPres REST API
4
 * which can allow fields to be read and/or updated.
5
 *
6
 * @since  2.2.4
7
 *
8
 * @category  WordPress_Plugin
9
 * @package   CMB2
10
 * @author    WebDevStudios
11
 * @license   GPL-2.0+
12
 * @link      http://webdevstudios.com
13
 */
14
class CMB2_REST extends CMB2_Hookup_Base {
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...
15
16
	/**
17
	 * The current CMB2 REST endpoint version
18
	 * @var string
19
	 * @since 2.2.4
20
	 */
21
	const VERSION = '1';
22
23
	/**
24
	 * The CMB2 REST base namespace (v should always be followed by $version)
25
	 * @var string
26
	 * @since 2.2.4
27
	 */
28
	const NAME_SPACE = 'cmb2/v1';
29
30
	/**
31
	 * @var   CMB2 object
32
	 * @since 2.2.4
33
	 */
34
	public $cmb;
35
36
	/**
37
	 * @var   CMB2_REST[] objects
38
	 * @since 2.2.4
39
	 */
40
	public static $boxes;
41
42
	/**
43
	 * Array of readable field objects.
44
	 * @var   CMB2_Field[]
45
	 * @since 2.2.4
46
	 */
47
	protected $read_fields = array();
48
49
	/**
50
	 * Array of editable field objects.
51
	 * @var   CMB2_Field[]
52
	 * @since 2.2.4
53
	 */
54
	protected $edit_fields = array();
55
56
	/**
57
	 * whether CMB2 object is readable via the rest api.
58
	 * @var boolean
59
	 */
60
	protected $rest_read = false;
61
62
	/**
63
	 * whether CMB2 object is editable via the rest api.
64
	 * @var boolean
65
	 */
66
	protected $rest_edit = false;
67
68
	/**
69
	 * Constructor
70
	 *
71
	 * @since 2.2.4
72
	 *
73
	 * @param CMB2 $cmb The CMB2 object to be registered for the API.
74
	 */
75
	public function __construct( CMB2 $cmb ) {
76
		$this->cmb = $cmb;
77
		self::$boxes[ $cmb->cmb_id ] = $this;
78
79
		$show_value = $this->cmb->prop( 'show_in_rest' );
80
81
		$this->rest_read = self::is_readable( $show_value );
82
		$this->rest_edit = self::is_editable( $show_value );
83
	}
84
85
	public function universal_hooks() {
86
		// hook up the CMB rest endpoint classes
87
		$this->once( 'rest_api_init', array( $this, 'init_routes' ), 0 );
88
89
		if ( function_exists( 'register_rest_field' ) ) {
90
			$this->once( 'rest_api_init', array( __CLASS__, 'register_appended_fields' ), 50 );
91
		}
92
93
		$this->declare_read_edit_fields();
94
95
		add_filter( 'is_protected_meta', array( $this, 'is_protected_meta' ), 10, 3 );
96
	}
97
98
	public function init_routes() {
99
		$wp_rest_server = rest_get_server();
100
101
		$boxes_controller = new CMB2_REST_Controller_Boxes( $wp_rest_server );
102
		$boxes_controller->register_routes();
103
104
		$fields_controller = new CMB2_REST_Controller_Fields( $wp_rest_server );
105
		$fields_controller->register_routes();
106
	}
107
108
	public static function register_appended_fields() {
109
110
		$types = array();
111
		foreach ( self::$boxes as $cmb_id => $cmb_rest ) {
112
			$types = array_merge( $types, $cmb_rest->cmb->prop( 'object_types' ) );
113
		}
114
		$types = array_unique( $types );
115
116
		// @todo separate registrations for each object_type, 'post', 'user', 'term', 'comment'.
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% 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...
117
		register_rest_field(
118
			$types,
119
			'cmb2',
120
			array(
121
				'get_callback' => array( __CLASS__, 'get_restable_field_values' ),
122
				'update_callback' => array( __CLASS__, 'update_restable_field_values' ),
123
				'schema' => null,
124
			)
125
		);
126
	}
127
128
	protected function declare_read_edit_fields() {
129
		foreach ( $this->cmb->prop( 'fields' ) as $field ) {
130
			$show_in_rest = isset( $field['show_in_rest'] ) ? $field['show_in_rest'] : null;
131
132
			if ( false === $show_in_rest ) {
133
				continue;
134
			}
135
136
			if ( $this->can_read( $show_in_rest ) ) {
137
				$this->read_fields[] = $field['id'];
138
			}
139
140
			if ( $this->can_edit( $show_in_rest ) ) {
141
				$this->edit_fields[] = $field['id'];
142
			}
143
144
		}
145
	}
146
147
	protected function can_read( $show_in_rest ) {
148
		// if 'null', then use default box value.
149
		if ( null === $show_in_rest ) {
150
			return $this->rest_read;
151
		}
152
153
		// Else check if the value represents readable.
154
		return self::is_readable( $show_in_rest );
155
	}
156
157
	protected function can_edit( $show_in_rest ) {
158
		// if 'null', then use default box value.
159
		if ( null === $show_in_rest ) {
160
			return $this->rest_edit;
161
		}
162
163
		// Else check if the value represents editable.
164
		return self::is_editable( $show_in_rest );
165
	}
166
167
	/**
168
	 * Handler for getting custom field data.
169
	 *
170
	 * @since  2.2.4
171
	 *
172
	 * @param  array           $object      The object data from the response
173
	 * @param  string          $field_name  Name of field
174
	 * @param  WP_REST_Request $request     Current request
175
	 * @param  string          $object_type The request object type
176
	 *
177
	 * @return mixed
178
	 */
179
	public static function get_restable_field_values( $object, $field_name, $request, $object_type ) {
0 ignored issues
show
Unused Code introduced by
The parameter $field_name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $object_type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
180
		$values = array();
181
		if ( ! isset( $object['id'] ) ) {
182
			return;
183
		}
184
185
		// @todo Security hardening... check for object type, check for show_in_rest values.
186
		foreach ( self::$boxes as $cmb_id => $rest_box ) {
187
			foreach ( $rest_box->read_fields as $field_id ) {
188
				$field = $rest_box->cmb->get_field( $field_id );
189
				$field->object_id( $object['id'] );
190
191
				// TODO: test other object types (users, comments, etc)
192
				// if ( isset( $object['type'] ) ) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% 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...
193
				// 	$field->object_type( $object['type'] );
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% 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...
194
				// }
195
196
				$values[ $cmb_id ][ $field->id( true ) ] = $field->get_data();
197
			}
198
		}
199
200
		return $values;
201
	}
202
203
	/**
204
	 * Handler for updating custom field data.
205
	 *
206
	 * @since  2.2.4
207
	 *
208
	 * @param  mixed           $value       The value of the field
0 ignored issues
show
Documentation introduced by
There is no parameter named $value. Did you maybe mean $values?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

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

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

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

Loading history...
209
	 * @param  object          $object      The object from the response
210
	 * @param  string          $field_name  Name of field
211
	 * @param  WP_REST_Request $request     Current request
212
	 * @param  string          $object_type The request object type
213
	 *
214
	 * @return bool|int
215
	 */
216
	public static function update_restable_field_values( $values, $object, $field_name, $request, $object_type ) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $object_type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
217
		if ( empty( $values ) || ! is_array( $values ) || 'cmb2' !== $field_name ) {
218
			return;
219
		}
220
221
		// @todo verify that $object_type matches this output.
222
		$data = self::get_object_data( $object );
223
		if ( ! $data ) {
224
			return;
225
		}
226
227
		$updated = array();
228
229
		// @todo Security hardening... check for object type, check for show_in_rest values.
230
		foreach ( self::$boxes as $cmb_id => $rest_box ) {
231
			if ( ! array_key_exists( $cmb_id, $values ) ) {
232
				continue;
233
			}
234
235
			$rest_box->cmb->object_id( $data['object_id'] );
236
			$rest_box->cmb->object_type( $data['object_type'] );
237
238
			// TODO: Test since refactor.
239
			$updated[ $cmb_id ] = $rest_box->sanitize_box_values( $values );
240
		}
241
242
		return $updated;
243
	}
244
245
	/**
246
	 * Loop through box fields and sanitize the values.
247
	 *
248
	 * @since  2.2.o
249
	 *
250
	 * @param  array   $values Array of values being provided.
251
	 * @return array           Array of updated/sanitized values.
252
	 */
253
	public function sanitize_box_values( array $values ) {
254
		$updated = array();
255
256
		$this->cmb->pre_process();
257
258
		foreach ( $this->edit_fields as $field_id ) {
259
			$updated[ $field_id ] = $this->sanitize_field_value( $values, $field_id );
0 ignored issues
show
Documentation introduced by
$field_id is of type object<CMB2_Field>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
260
		}
261
262
		$this->cmb->after_save();
263
264
		return $updated;
265
	}
266
267
	/**
268
	 * Handles returning a sanitized field value.
269
	 *
270
	 * @since  2.2.4
271
	 *
272
	 * @param  array   $values   Array of values being provided.
273
	 * @param  string  $field_id The id of the field to update.
274
	 *
275
	 * @return mixed             The results of saving/sanitizing a field value.
276
	 */
277
	protected function sanitize_field_value( array $values, $field_id ) {
278
		if ( ! array_key_exists( $field_id, $values[ $this->cmb->cmb_id ] ) ) {
279
			return;
280
		}
281
282
		$field = $this->cmb->get_field( $field_id );
283
284
		if ( 'title' == $field->type() ) {
285
			return;
286
		}
287
288
		$field->object_id( $this->cmb->object_id() );
289
		$field->object_type( $this->cmb->object_type() );
290
291
		if ( 'group' == $field->type() ) {
292
			return $this->sanitize_group_value( $values, $field );
0 ignored issues
show
Security Bug introduced by
It seems like $field defined by $this->cmb->get_field($field_id) on line 282 can also be of type false; however, CMB2_REST::sanitize_group_value() does only seem to accept object<CMB2_Field>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
293
		}
294
295
		return $field->save_field( $values[ $this->cmb->cmb_id ][ $field_id ] );
296
	}
297
298
	/**
299
	 * Handles returning a sanitized group field value.
300
	 *
301
	 * @since  2.2.4
302
	 *
303
	 * @param  array       $values Array of values being provided.
304
	 * @param  CMB2_Field  $field  CMB2_Field object.
305
	 *
306
	 * @return mixed               The results of saving/sanitizing the group field value.
307
	 */
308
	protected function sanitize_group_value( array $values, CMB2_Field $field ) {
309
		$fields = $field->fields();
310
		if ( empty( $fields ) ) {
311
			return;
312
		}
313
314
		$this->cmb->data_to_save[ $field->_id() ] = $values[ $this->cmb->cmb_id ][ $field->_id() ];
315
316
		return $this->cmb->save_group_field( $field );
0 ignored issues
show
Documentation introduced by
$field is of type object<CMB2_Field>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
317
	}
318
319
	/**
320
	 * Filter whether a meta key is protected.
321
	 *
322
	 * @since 2.2.4
323
	 *
324
	 * @param bool   $protected Whether the key is protected. Default false.
325
	 * @param string $meta_key  Meta key.
326
	 * @param string $meta_type Meta type.
327
	 */
328
	public function is_protected_meta( $protected, $meta_key, $meta_type ) {
0 ignored issues
show
Unused Code introduced by
The parameter $meta_type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
329
		if ( $this->field_can_edit( $meta_key ) ) {
330
			return false;
331
		}
332
333
		return $protected;
334
	}
335
336
	protected static function get_object_data( $object ) {
337
		$object_id = 0;
338
		if ( isset( $object->ID ) ) {
339
			$object_id   = intval( $object->ID );
340
			$object_type = isset( $object->user_login ) ? 'user' : 'post';
341
		} elseif ( isset( $object->comment_ID ) ) {
342
			$object_id   = intval( $object->comment_ID );
343
			$object_type = 'comment';
344
		} elseif ( is_array( $object ) && isset( $object['term_id'] ) ) {
345
			$object_id   = intval( $object['term_id'] );
346
			$object_type = 'term';
347
		} elseif ( isset( $object->term_id ) ) {
348
			$object_id   = intval( $object->term_id );
349
			$object_type = 'term';
350
		}
351
352
		if ( empty( $object_id ) ) {
353
			return false;
354
		}
355
356
		return compact( 'object_id', 'object_type' );
357
	}
358
359
	public function field_can_read( $field_id, $return_object = false ) {
360
		return $this->field_can( 'read_fields', $field_id, $return_object );
361
	}
362
363
	public function field_can_edit( $field_id, $return_object = false ) {
364
		return $this->field_can( 'edit_fields', $field_id, $return_object );
365
	}
366
367
	protected function field_can( $type = 'read_fields', $field_id, $return_object = false ) {
368
		if ( ! in_array( $field_id instanceof CMB2_Field ? $field_id->id() : $field_id, $this->{$type}, true ) ) {
369
			return false;
370
		}
371
372
		return $return_object ? $this->cmb->get_field( $field_id ) : true;
373
	}
374
375
	/**
376
	 * Get a CMB2_REST instance object from the registry by a CMB2 id.
377
	 *
378
	 * @since  2.2.4
379
	 *
380
	 * @param  string  $cmb_id CMB2 config id
381
	 *
382
	 * @return CMB2_REST|false The CMB2_REST object or false.
383
	 */
384
	public static function get_rest_box( $cmb_id ) {
385
		return isset( self::$boxes[ $cmb_id ] ) ? self::$boxes[ $cmb_id ] : false;
386
	}
387
388
	/**
389
	 * Remove a CMB2_REST instance object from the registry.
390
	 *
391
	 * @since  2.2.4
392
	 *
393
	 * @param string $cmb_id A CMB2 instance id.
394
	 */
395
	public static function remove( $cmb_id ) {
396
		if ( array_key_exists( $cmb_id, self::$boxes ) ) {
397
			unset( self::$boxes[ $cmb_id ] );
398
		}
399
	}
400
401
	/**
402
	 * Checks if given value is readable.
403
	 *
404
	 * Value is considered readable if it is not empty and if it does not match the editable blacklist.
405
	 *
406
	 * @since  2.2.4
407
	 *
408
	 * @param  mixed  $value Value to check.
409
	 *
410
	 * @return boolean       Whether value is considered readable.
411
	 */
412
	public static function is_readable( $value ) {
413
		return ! empty( $value ) && ! in_array( $value, array(
414
			WP_REST_Server::CREATABLE,
415
			WP_REST_Server::EDITABLE,
416
			WP_REST_Server::DELETABLE,
417
		), true );
418
	}
419
420
	/**
421
	 * Checks if given value is editable.
422
	 *
423
	 * Value is considered editable if matches the editable whitelist.
424
	 *
425
	 * @since  2.2.4
426
	 *
427
	 * @param  mixed  $value Value to check.
428
	 *
429
	 * @return boolean       Whether value is considered editable.
430
	 */
431
	public static function is_editable( $value ) {
432
		return in_array( $value, array(
433
			WP_REST_Server::EDITABLE,
434
			WP_REST_Server::ALLMETHODS,
435
		), true );
436
	}
437
438
	/**
439
	 * Magic getter for our object.
440
	 *
441
	 * @param string $field
442
	 * @throws Exception Throws an exception if the field is invalid.
443
	 *
444
	 * @return mixed
445
	 */
446
	public function __get( $field ) {
447
		switch ( $field ) {
448
			case 'read_fields':
449
			case 'edit_fields':
450
			case 'rest_read':
451
			case 'rest_edit':
452
				return $this->{$field};
453
			default:
454
				throw new Exception( 'Invalid ' . __CLASS__ . ' property: ' . $field );
455
		}
456
	}
457
458
}
459