Completed
Pull Request — trunk (#541)
by Justin
03:11
created

CMB2_REST::field_can()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 3
nop 3
dl 0
loc 7
rs 9.4285
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.0
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.0
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.0
27
	 */
28
	const BASE = 'cmb2/v1';
29
30
	/**
31
	 * @var   CMB2 object
32
	 * @since 2.2.0
33
	 */
34
	public $cmb;
35
36
	/**
37
	 * @var   CMB2_REST[] objects
38
	 * @since 2.2.0
39
	 */
40
	public static $boxes;
41
42
	/**
43
	 * Array of readable field objects.
44
	 * @var   CMB2_Field[]
45
	 * @since 2.2.0
46
	 */
47
	protected $read_fields = array();
48
49
	/**
50
	 * Array of writeable field objects.
51
	 * @var   CMB2_Field[]
52
	 * @since 2.2.0
53
	 */
54
	protected $write_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 readable via the rest api
64
	 * @var boolean
65
	 */
66
	protected $rest_write = false;
67
68
	/**
69
	 * Constructor
70
	 * @since 2.2.0
71
	 * @param CMB2 $cmb The CMB2 object to be registered for the API.
72
	 */
73
	public function __construct( CMB2 $cmb ) {
74
		$this->cmb = $cmb;
75
		self::$boxes[ $cmb->cmb_id ] = $this;
76
77
		$show_value = $this->cmb->prop( 'show_in_rest' );
78
		$this->rest_read  = 'write_only' !== $show_value;
79
		$this->rest_write = in_array( $show_value, array( 'read_and_write', 'write_only' ), true );
80
	}
81
82
	public function universal_hooks() {
83
		// hook up the CMB rest endpoint classes
84
		$this->once( 'rest_api_init', array( $this, 'init_routes' ), 0 );
85
		$this->once( 'rest_api_init', array( __CLASS__, 'register_appended_fields' ), 50 );
86
87
		$this->declare_read_write_fields();
88
89
		add_filter( 'is_protected_meta', array( $this, 'is_protected_meta' ), 10, 3 );
90
	}
91
92
	public function init_routes() {
93
		global $wp_rest_server;
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...
94
95
		$boxes_controller = new CMB2_REST_Controller_Boxes( $wp_rest_server );
96
		$boxes_controller->register_routes();
97
98
		$fields_controller = new CMB2_REST_Controller_Fields( $wp_rest_server );
99
		$fields_controller->register_routes();
100
	}
101
102
	public static function register_appended_fields() {
103
104
		$types = array();
105
		foreach ( self::$boxes as $cmb_id => $cmb_rest ) {
106
			$types = array_merge( $types, $cmb_rest->cmb->prop( 'object_types' ) );
107
		}
108
		$types = array_unique( $types );
109
110
		register_api_field(
111
			$types,
112
			'cmb2',
113
			array(
114
				'get_callback' => array( __CLASS__, 'get_restable_field_values' ),
115
				'update_callback' => array( __CLASS__, 'update_restable_field_values' ),
116
				'schema' => null,
117
			)
118
		);
119
	}
120
121
	protected function declare_read_write_fields() {
122
		foreach ( $this->cmb->prop( 'fields' ) as $field ) {
123
			$show_in_rest = isset( $field['show_in_rest'] ) ? $field['show_in_rest'] : null;
124
125
			if ( false === $show_in_rest ) {
126
				continue;
127
			}
128
129
			if ( $this->can_read( $show_in_rest ) ) {
130
				$this->read_fields[] = $field['id'];
131
			}
132
133
			if ( $this->can_write( $show_in_rest ) ) {
134
				$this->write_fields[] = $field['id'];
135
			}
136
137
		}
138
	}
139
140
	protected function can_read( $show_in_rest ) {
141
		return $this->rest_read
142
			? 'write_only' !== $show_in_rest
143
			: in_array( $show_in_rest, array( 'read_and_write', 'read_only' ), true );
144
	}
145
146
	protected function can_write( $show_in_rest ) {
147
		return $this->rest_write
148
			? 'read_only' !== $show_in_rest
149
			: in_array( $show_in_rest, array( 'read_and_write', 'write_only' ), true );
150
	}
151
152
	/**
153
	 * Handler for getting custom field data.
154
	 * @since  2.2.0
155
	 * @param  array           $object   The object from the response
156
	 * @param  string          $field_id Name of field
157
	 * @param  WP_REST_Request $request  Current request
158
	 * @return mixed
159
	 */
160
	public static function get_restable_field_values( $object, $field_id, $request ) {
0 ignored issues
show
Unused Code introduced by
The parameter $field_id 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...
161
		$values = array();
162
		if ( ! isset( $object['id'] ) ) {
163
			return;
164
		}
165
166
		foreach ( self::$boxes as $cmb_id => $rest_box ) {
167
			foreach ( $rest_box->read_fields as $field_id ) {
168
				$field = $rest_box->cmb->get_field( $field_id );
169
				$field->object_id( $object['id'] );
170
171
				// TODO: test other object types (users, comments, etc)
172
				if ( isset( $object->type ) ) {
173
					$field->object_type( $object->type );
174
				}
175
176
				$values[ $cmb_id ][ $field->id( true ) ] = $field->get_data();
177
			}
178
		}
179
180
		return $values;
181
	}
182
183
	/**
184
	 * Handler for updating custom field data.
185
	 * @since  2.2.0
186
	 * @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...
187
	 * @param  object   $object   The object from the response
188
	 * @param  string   $field_id Name of field
189
	 * @return bool|int
190
	 */
191
	public static function update_restable_field_values( $values, $object, $field_id ) {
192
		if ( empty( $values ) || ! is_array( $values ) || 'cmb2' !== $field_id ) {
193
			return;
194
		}
195
196
		$data = self::get_object_data( $object );
197
		if ( ! $data ) {
198
			return;
199
		}
200
201
		$updated = array();
202
203
		foreach ( self::$boxes as $cmb_id => $rest_box ) {
204
			if ( ! array_key_exists( $cmb_id, $values ) ) {
205
				continue;
206
			}
207
208
			$rest_box->cmb->object_id( $data['object_id'] );
209
			$rest_box->cmb->object_type( $data['object_type'] );
210
211
			// TODO: Test since refactor.
212
			$updated[ $cmb_id ] = $rest_box->sanitize_box_values( $values );
213
		}
214
215
		return $updated;
216
	}
217
218
	/**
219
	 * Loop through box fields and sanitize the values.
220
	 * @since  2.2.o
221
	 * @param  array   $values Array of values being provided.
222
	 * @return array           Array of updated/sanitized values.
223
	 */
224
	public function sanitize_box_values( array $values ) {
225
		$updated = array();
226
227
		$this->cmb->pre_process();
228
229
		foreach ( $this->write_fields as $field_id ) {
230
			$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...
231
		}
232
233
		$this->cmb->after_save();
234
235
		return $updated;
236
	}
237
238
	/**
239
	 * Handles returning a sanitized field value.
240
	 * @since  2.2.0
241
	 * @param  array   $values   Array of values being provided.
242
	 * @param  string  $field_id The id of the field to update.
243
	 * @return mixed             The results of saving/sanitizing a field value.
244
	 */
245
	protected function sanitize_field_value( array $values, $field_id ) {
246
		if ( ! array_key_exists( $field_id, $values[ $this->cmb->cmb_id ] ) ) {
247
			return;
248
		}
249
250
		$field = $this->cmb->get_field( $field_id );
251
252
		if ( 'title' == $field->type() ) {
253
			return;
254
		}
255
256
		$field->object_id   = $this->cmb->object_id();
257
		$field->object_type = $this->cmb->object_type();
258
259
		if ( 'group' == $field->type() ) {
260
			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 250 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...
261
		}
262
263
		return $field->save_field( $values[ $this->cmb->cmb_id ][ $field_id ] );
264
	}
265
266
	/**
267
	 * Handles returning a sanitized group field value.
268
	 * @since  2.2.0
269
	 * @param  array       $values Array of values being provided.
270
	 * @param  CMB2_Field  $field  CMB2_Field object.
271
	 * @return mixed               The results of saving/sanitizing the group field value.
272
	 */
273
	protected function sanitize_group_value( array $values, CMB2_Field $field ) {
274
		$fields = $field->fields();
275
		if ( empty( $fields ) ) {
276
			return;
277
		}
278
279
		$this->cmb->data_to_save[ $field->_id() ] = $values[ $this->cmb->cmb_id ][ $field->_id() ];
280
281
		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...
282
	}
283
284
	/**
285
	 * Filter whether a meta key is protected.
286
	 * @since 2.2.0
287
	 * @param bool   $protected Whether the key is protected. Default false.
288
	 * @param string $meta_key  Meta key.
289
	 * @param string $meta_type Meta type.
290
	 */
291
	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...
292
		if ( $this->field_can_write( $meta_key ) ) {
293
			return false;
294
		}
295
296
		return $protected;
297
	}
298
299
	protected static function get_object_data( $object ) {
300
		$object_id = 0;
301
		if ( isset( $object->ID ) ) {
302
			$object_id   = intval( $object->ID );
303
			$object_type = isset( $object->user_login ) ? 'user' : 'post';
304
		} elseif ( isset( $object->comment_ID ) ) {
305
			$object_id   = intval( $object->comment_ID );
306
			$object_type = 'comment';
307
		} elseif ( is_array( $object ) && isset( $object['term_id'] ) ) {
308
			$object_id   = intval( $object['term_id'] );
309
			$object_type = 'term';
310
		} elseif ( isset( $object->term_id ) ) {
311
			$object_id   = intval( $object->term_id );
312
			$object_type = 'term';
313
		}
314
315
		if ( empty( $object_id ) ) {
316
			return false;
317
		}
318
319
		return compact( 'object_id', 'object_type' );
320
	}
321
322
	public function field_can_read( $field_id, $return_object = false ) {
323
		return $this->field_can( 'read_fields', $field_id, $return_object );
324
	}
325
326
	public function field_can_write( $field_id, $return_object = false ) {
327
		return $this->field_can( 'write_fields', $field_id, $return_object );
328
	}
329
330
	protected function field_can( $type = 'read_fields', $field_id, $return_object = false ) {
331
		if ( ! in_array( $field_id, $this->{$type}, true ) ) {
332
			return false;
333
		}
334
335
		return $return_object ? $this->cmb->get_field( $field_id ) : true;
336
	}
337
338
	/**
339
	 * Get an instance of this class by a CMB2 id
340
	 *
341
	 * @since  2.2.0
342
	 *
343
	 * @param  string  $cmb_id CMB2 config id
344
	 *
345
	 * @return CMB2_REST|false The CMB2_REST object or false.
346
	 */
347
	public static function get_rest_box( $cmb_id ) {
348
		return isset( self::$boxes[ $cmb_id ] ) ? self::$boxes[ $cmb_id ] : false;
349
	}
350
351
	/**
352
	 * Magic getter for our object.
353
	 * @param string $field
354
	 * @throws Exception Throws an exception if the field is invalid.
355
	 * @return mixed
356
	 */
357
	public function __get( $field ) {
358
		switch ( $field ) {
359
			case 'read_fields':
360
			case 'write_fields':
361
			case 'rest_read':
362
			case 'rest_write':
363
				return $this->{$field};
364
			default:
365
				throw new Exception( 'Invalid ' . __CLASS__ . ' property: ' . $field );
366
		}
367
	}
368
369
}
370