Completed
Pull Request — trunk (#541)
by Justin
04:46
created

CMB2_REST::sanitize_field_value()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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