Completed
Pull Request — trunk (#541)
by Justin
21:06 queued 04:51
created

CMB2_REST::get_restable_field_values()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 4
eloc 10
c 5
b 0
f 0
nc 4
nop 3
dl 0
loc 22
rs 8.9197
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
		register_rest_field(
117
			$types,
118
			'cmb2',
119
			array(
120
				'get_callback' => array( __CLASS__, 'get_restable_field_values' ),
121
				'update_callback' => array( __CLASS__, 'update_restable_field_values' ),
122
				'schema' => null,
123
			)
124
		);
125
	}
126
127
	protected function declare_read_edit_fields() {
128
		foreach ( $this->cmb->prop( 'fields' ) as $field ) {
129
			$show_in_rest = isset( $field['show_in_rest'] ) ? $field['show_in_rest'] : null;
130
131
			if ( false === $show_in_rest ) {
132
				continue;
133
			}
134
135
			if ( $this->can_read( $show_in_rest ) ) {
136
				$this->read_fields[] = $field['id'];
137
			}
138
139
			if ( $this->can_edit( $show_in_rest ) ) {
140
				$this->edit_fields[] = $field['id'];
141
			}
142
143
		}
144
	}
145
146
	protected function can_read( $show_in_rest ) {
147
		// if 'null', then use default box value.
148
		if ( null === $show_in_rest ) {
149
			return $this->rest_read;
150
		}
151
152
		// Else check if the value represents readable.
153
		return self::is_readable( $show_in_rest );
154
	}
155
156
	protected function can_edit( $show_in_rest ) {
157
		// if 'null', then use default box value.
158
		if ( null === $show_in_rest ) {
159
			return $this->rest_edit;
160
		}
161
162
		// Else check if the value represents editable.
163
		return self::is_editable( $show_in_rest );
164
	}
165
166
	/**
167
	 * Handler for getting custom field data.
168
	 *
169
	 * @since  2.2.4
170
	 *
171
	 * @param  array           $data       The data from the response
172
	 * @param  string          $field_name Name of field
173
	 * @param  WP_REST_Request $request    Current request
174
	 *
175
	 * @return mixed
176
	 */
177
	public static function get_restable_field_values( $data, $field_name, $request ) {
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 $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...
178
		$values = array();
179
		if ( ! isset( $data['id'] ) ) {
180
			return;
181
		}
182
183
		foreach ( self::$boxes as $cmb_id => $rest_box ) {
184
			foreach ( $rest_box->read_fields as $field_id ) {
185
				$field = $rest_box->cmb->get_field( $field_id );
186
				$field->object_id( $data['id'] );
187
188
				// TODO: test other object types (users, comments, etc)
189
				// if ( isset( $data['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...
190
				// 	$field->object_type( $data['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...
191
				// }
192
193
				$values[ $cmb_id ][ $field->id( true ) ] = $field->get_data();
194
			}
195
		}
196
197
		return $values;
198
	}
199
200
	/**
201
	 * Handler for updating custom field data.
202
	 *
203
	 * @since  2.2.4
204
	 *
205
	 * @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...
206
	 * @param  object   $object     The object from the response
207
	 * @param  string   $field_name Name of field
208
	 *
209
	 * @return bool|int
210
	 */
211
	public static function update_restable_field_values( $values, $object, $field_name ) {
212
		if ( empty( $values ) || ! is_array( $values ) || 'cmb2' !== $field_name ) {
213
			return;
214
		}
215
216
		$data = self::get_object_data( $object );
217
		if ( ! $data ) {
218
			return;
219
		}
220
221
		$updated = array();
222
223
		foreach ( self::$boxes as $cmb_id => $rest_box ) {
224
			if ( ! array_key_exists( $cmb_id, $values ) ) {
225
				continue;
226
			}
227
228
			$rest_box->cmb->object_id( $data['object_id'] );
229
			$rest_box->cmb->object_type( $data['object_type'] );
230
231
			// TODO: Test since refactor.
232
			$updated[ $cmb_id ] = $rest_box->sanitize_box_values( $values );
233
		}
234
235
		return $updated;
236
	}
237
238
	/**
239
	 * Loop through box fields and sanitize the values.
240
	 *
241
	 * @since  2.2.o
242
	 *
243
	 * @param  array   $values Array of values being provided.
244
	 * @return array           Array of updated/sanitized values.
245
	 */
246
	public function sanitize_box_values( array $values ) {
247
		$updated = array();
248
249
		$this->cmb->pre_process();
250
251
		foreach ( $this->edit_fields as $field_id ) {
252
			$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...
253
		}
254
255
		$this->cmb->after_save();
256
257
		return $updated;
258
	}
259
260
	/**
261
	 * Handles returning a sanitized field value.
262
	 *
263
	 * @since  2.2.4
264
	 *
265
	 * @param  array   $values   Array of values being provided.
266
	 * @param  string  $field_id The id of the field to update.
267
	 *
268
	 * @return mixed             The results of saving/sanitizing a field value.
269
	 */
270
	protected function sanitize_field_value( array $values, $field_id ) {
271
		if ( ! array_key_exists( $field_id, $values[ $this->cmb->cmb_id ] ) ) {
272
			return;
273
		}
274
275
		$field = $this->cmb->get_field( $field_id );
276
277
		if ( 'title' == $field->type() ) {
278
			return;
279
		}
280
281
		$field->object_id( $this->cmb->object_id() );
282
		$field->object_type( $this->cmb->object_type() );
283
284
		if ( 'group' == $field->type() ) {
285
			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 275 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...
286
		}
287
288
		return $field->save_field( $values[ $this->cmb->cmb_id ][ $field_id ] );
289
	}
290
291
	/**
292
	 * Handles returning a sanitized group field value.
293
	 *
294
	 * @since  2.2.4
295
	 *
296
	 * @param  array       $values Array of values being provided.
297
	 * @param  CMB2_Field  $field  CMB2_Field object.
298
	 *
299
	 * @return mixed               The results of saving/sanitizing the group field value.
300
	 */
301
	protected function sanitize_group_value( array $values, CMB2_Field $field ) {
302
		$fields = $field->fields();
303
		if ( empty( $fields ) ) {
304
			return;
305
		}
306
307
		$this->cmb->data_to_save[ $field->_id() ] = $values[ $this->cmb->cmb_id ][ $field->_id() ];
308
309
		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...
310
	}
311
312
	/**
313
	 * Filter whether a meta key is protected.
314
	 *
315
	 * @since 2.2.4
316
	 *
317
	 * @param bool   $protected Whether the key is protected. Default false.
318
	 * @param string $meta_key  Meta key.
319
	 * @param string $meta_type Meta type.
320
	 */
321
	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...
322
		if ( $this->field_can_edit( $meta_key ) ) {
323
			return false;
324
		}
325
326
		return $protected;
327
	}
328
329
	protected static function get_object_data( $object ) {
330
		$object_id = 0;
331
		if ( isset( $object->ID ) ) {
332
			$object_id   = intval( $object->ID );
333
			$object_type = isset( $object->user_login ) ? 'user' : 'post';
334
		} elseif ( isset( $object->comment_ID ) ) {
335
			$object_id   = intval( $object->comment_ID );
336
			$object_type = 'comment';
337
		} elseif ( is_array( $object ) && isset( $object['term_id'] ) ) {
338
			$object_id   = intval( $object['term_id'] );
339
			$object_type = 'term';
340
		} elseif ( isset( $object->term_id ) ) {
341
			$object_id   = intval( $object->term_id );
342
			$object_type = 'term';
343
		}
344
345
		if ( empty( $object_id ) ) {
346
			return false;
347
		}
348
349
		return compact( 'object_id', 'object_type' );
350
	}
351
352
	public function field_can_read( $field_id, $return_object = false ) {
353
		return $this->field_can( 'read_fields', $field_id, $return_object );
354
	}
355
356
	public function field_can_edit( $field_id, $return_object = false ) {
357
		return $this->field_can( 'edit_fields', $field_id, $return_object );
358
	}
359
360
	protected function field_can( $type = 'read_fields', $field_id, $return_object = false ) {
361
		if ( ! in_array( $field_id instanceof CMB2_Field ? $field_id->id() : $field_id, $this->{$type}, true ) ) {
362
			return false;
363
		}
364
365
		return $return_object ? $this->cmb->get_field( $field_id ) : true;
366
	}
367
368
	/**
369
	 * Get an instance of this class by a CMB2 id
370
	 *
371
	 * @since  2.2.4
372
	 *
373
	 * @param  string  $cmb_id CMB2 config id
374
	 *
375
	 * @return CMB2_REST|false The CMB2_REST object or false.
376
	 */
377
	public static function get_rest_box( $cmb_id ) {
378
		return isset( self::$boxes[ $cmb_id ] ) ? self::$boxes[ $cmb_id ] : false;
379
	}
380
381
	/**
382
	 * Checks if given value is readable.
383
	 *
384
	 * Value is considered readable if it is not empty and if it does not match the editable blacklist.
385
	 *
386
	 * @since  2.2.4
387
	 *
388
	 * @param  mixed  $value Value to check.
389
	 *
390
	 * @return boolean       Whether value is considered readable.
391
	 */
392
	public static function is_readable( $value ) {
393
		return ! empty( $value ) && ! in_array( $value, array(
394
			WP_REST_Server::CREATABLE,
395
			WP_REST_Server::EDITABLE,
396
			WP_REST_Server::DELETABLE,
397
		), true );
398
	}
399
400
	/**
401
	 * Checks if given value is editable.
402
	 *
403
	 * Value is considered editable if matches the editable whitelist.
404
	 *
405
	 * @since  2.2.4
406
	 *
407
	 * @param  mixed  $value Value to check.
408
	 *
409
	 * @return boolean       Whether value is considered editable.
410
	 */
411
	public static function is_editable( $value ) {
412
		return in_array( $value, array(
413
			WP_REST_Server::EDITABLE,
414
			WP_REST_Server::ALLMETHODS,
415
		), true );
416
	}
417
418
	/**
419
	 * Magic getter for our object.
420
	 *
421
	 * @param string $field
422
	 * @throws Exception Throws an exception if the field is invalid.
423
	 *
424
	 * @return mixed
425
	 */
426
	public function __get( $field ) {
427
		switch ( $field ) {
428
			case 'read_fields':
429
			case 'edit_fields':
430
			case 'rest_read':
431
			case 'rest_edit':
432
				return $this->{$field};
433
			default:
434
				throw new Exception( 'Invalid ' . __CLASS__ . ' property: ' . $field );
435
		}
436
	}
437
438
}
439