Completed
Push — master ( b71328...08c185 )
by Brian
20s queued 15s
created

validate_setting_checkbox_field()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 10
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 10
rs 8.8333
cc 7
nc 6
nop 2
1
<?php
2
/**
3
 * GetPaid REST controller class.
4
 * 
5
 * Extends the WP_REST_Controller class to provide batch support for our REST
6
 * APIs and also to provide backwards support for our old namespaces.
7
 * 
8
 * @version 1.0.19
9
 */
10
11
defined( 'ABSPATH' ) || exit;
12
13
/**
14
 * Core class to access posts via the REST API.
15
 *
16
 * This is version 1.
17
 *
18
 * @since 1.0.19
19
 *
20
 * @see WP_REST_Controller
21
 */
22
class GetPaid_REST_Controller extends WP_REST_Controller {
23
24
	/**
25
     * The namespaces of this controller's route.
26
     *
27
     * @since 1.0.19
28
     * @var array
29
     */
30
	protected $namespaces;
31
32
	/**
33
     * The official namespace of this controller's route.
34
     *
35
     * @since 1.0.19
36
     * @var string
37
     */
38
	protected $namespace = 'getpaid/v1';
39
40
	/**
41
     * Cached results of get_item_schema.
42
     *
43
     * @since 1.0.19
44
     * @var array
45
     */
46
	protected $schema;
47
48
    /**
49
	 * Constructor.
50
	 *
51
	 * @since 1.0.19
52
	 *
53
	 */
54
	public function __construct() {
55
56
		// Offer several namespaces for backwards compatibility.
57
		$this->namespaces = apply_filters(
58
			'getpaid_rest_api_namespaces',
59
			array(
60
				'getpaid/v1',
61
				'invoicing/v1',
62
				'wpi/v1'
63
			)
64
		);
65
66
		// Register REST routes.
67
        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
68
69
	}
70
71
	/**
72
	 * Registers routes for each namespace.
73
	 *
74
	 * @since 1.0.19
75
	 *
76
	 */
77
	public function register_routes() {
78
79
		foreach ( $this->namespaces as $namespace ) {
80
			$this->register_namespace_routes( $namespace );
81
		}
82
83
	}
84
85
	/**
86
	 * Registers routes for a namespace.
87
	 *
88
	 * @since 1.0.19
89
	 *
90
	 * @param string $namespace
91
	 */
92
	public function register_namespace_routes( $namespace ) {
0 ignored issues
show
Unused Code introduced by
The parameter $namespace is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

92
	public function register_namespace_routes( /** @scrutinizer ignore-unused */ $namespace ) {

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

Loading history...
93
94
		getpaid_doing_it_wrong(
95
			__CLASS__ . '::' .__METHOD__,
96
			/* translators: %s: register_namespace_routes() */
97
			sprintf( __( "Method '%s' must be overridden." ), __METHOD__ ),
98
			'1.0.19'
99
		);
100
101
	}
102
103
	/**
104
	 * Get normalized rest base.
105
	 *
106
	 * @return string
107
	 */
108
	protected function get_normalized_rest_base() {
109
		return preg_replace( '/\(.*\)\//i', '', $this->rest_base );
110
	}
111
112
	/**
113
	 * Check batch limit.
114
	 *
115
	 * @param array $items Request items.
116
	 * @return bool|WP_Error
117
	 */
118
	protected function check_batch_limit( $items ) {
119
		$limit = apply_filters( 'getpaid_rest_batch_items_limit', 100, $this->get_normalized_rest_base() );
120
		$total = 0;
121
122
		if ( ! empty( $items['create'] ) ) {
123
			$total += count( $items['create'] );
124
		}
125
126
		if ( ! empty( $items['update'] ) ) {
127
			$total += count( $items['update'] );
128
		}
129
130
		if ( ! empty( $items['delete'] ) ) {
131
			$total += count( $items['delete'] );
132
		}
133
134
		if ( $total > $limit ) {
135
			/* translators: %s: items limit */
136
			return new WP_Error( 'getpaid_rest_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'invoicing' ), $limit ), array( 'status' => 413 ) );
137
		}
138
139
		return true;
140
	}
141
142
	/**
143
	 * Bulk create, update and delete items.
144
	 *
145
	 * @param WP_REST_Request $request Full details about the request.
146
	 * @return array Of WP_Error or WP_REST_Response.
147
	 */
148
	public function batch_items( $request ) {
149
		/**
150
		 * REST Server
151
		 *
152
		 * @var WP_REST_Server $wp_rest_server
153
		 */
154
		global $wp_rest_server;
155
156
		// Get the request params.
157
		$items    = array_filter( $request->get_params() );
158
		$query    = $request->get_query_params();
159
		$response = array();
160
161
		// Check batch limit.
162
		$limit = $this->check_batch_limit( $items );
163
		if ( is_wp_error( $limit ) ) {
164
			return $limit;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $limit returns the type WP_Error|true which is incompatible with the documented return type array.
Loading history...
165
		}
166
167
		if ( ! empty( $items['create'] ) ) {
168
			foreach ( $items['create'] as $item ) {
169
				$_item = new WP_REST_Request( 'POST' );
170
171
				// Default parameters.
172
				$defaults = array();
173
				$schema   = $this->get_public_item_schema();
174
				foreach ( $schema['properties'] as $arg => $options ) {
175
					if ( isset( $options['default'] ) ) {
176
						$defaults[ $arg ] = $options['default'];
177
					}
178
				}
179
				$_item->set_default_params( $defaults );
180
181
				// Set request parameters.
182
				$_item->set_body_params( $item );
183
184
				// Set query (GET) parameters.
185
				$_item->set_query_params( $query );
186
187
				$_response = $this->create_item( $_item );
188
189
				if ( is_wp_error( $_response ) ) {
190
					$response['create'][] = array(
191
						'id'    => 0,
192
						'error' => array(
193
							'code'    => $_response->get_error_code(),
194
							'message' => $_response->get_error_message(),
195
							'data'    => $_response->get_error_data(),
196
						),
197
					);
198
				} else {
199
					$response['create'][] = $wp_rest_server->response_to_data( $_response, '' );
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type boolean|string[] expected by parameter $embed of WP_REST_Server::response_to_data(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

199
					$response['create'][] = $wp_rest_server->response_to_data( $_response, /** @scrutinizer ignore-type */ '' );
Loading history...
Bug introduced by
$_response of type WP_Error is incompatible with the type WP_REST_Response expected by parameter $response of WP_REST_Server::response_to_data(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

199
					$response['create'][] = $wp_rest_server->response_to_data( /** @scrutinizer ignore-type */ $_response, '' );
Loading history...
200
				}
201
			}
202
		}
203
204
		if ( ! empty( $items['update'] ) ) {
205
			foreach ( $items['update'] as $item ) {
206
				$_item = new WP_REST_Request( 'PUT' );
207
				$_item->set_body_params( $item );
208
				$_response = $this->update_item( $_item );
209
210
				if ( is_wp_error( $_response ) ) {
211
					$response['update'][] = array(
212
						'id'    => $item['id'],
213
						'error' => array(
214
							'code'    => $_response->get_error_code(),
215
							'message' => $_response->get_error_message(),
216
							'data'    => $_response->get_error_data(),
217
						),
218
					);
219
				} else {
220
					$response['update'][] = $wp_rest_server->response_to_data( $_response, '' );
221
				}
222
			}
223
		}
224
225
		if ( ! empty( $items['delete'] ) ) {
226
			foreach ( $items['delete'] as $id ) {
227
				$id = (int) $id;
228
229
				if ( 0 === $id ) {
230
					continue;
231
				}
232
233
				$_item = new WP_REST_Request( 'DELETE' );
234
				$_item->set_query_params(
235
					array(
236
						'id'    => $id,
237
						'force' => true,
238
					)
239
				);
240
				$_response = $this->delete_item( $_item );
241
242
				if ( is_wp_error( $_response ) ) {
243
					$response['delete'][] = array(
244
						'id'    => $id,
245
						'error' => array(
246
							'code'    => $_response->get_error_code(),
247
							'message' => $_response->get_error_message(),
248
							'data'    => $_response->get_error_data(),
249
						),
250
					);
251
				} else {
252
					$response['delete'][] = $wp_rest_server->response_to_data( $_response, '' );
253
				}
254
			}
255
		}
256
257
		return $response;
258
	}
259
260
	/**
261
	 * Validate a text value for a text based setting.
262
	 *
263
	 * @since 1.0.19
264
	 * @param string $value Value.
265
	 * @param array  $setting Setting.
266
	 * @return string
267
	 */
268
	public function validate_setting_text_field( $value, $setting ) {
0 ignored issues
show
Unused Code introduced by
The parameter $setting is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

268
	public function validate_setting_text_field( $value, /** @scrutinizer ignore-unused */ $setting ) {

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

Loading history...
269
		$value = is_null( $value ) ? '' : $value;
0 ignored issues
show
introduced by
The condition is_null($value) is always false.
Loading history...
270
		return wp_kses_post( trim( stripslashes( $value ) ) );
271
	}
272
273
	/**
274
	 * Validate select based settings.
275
	 *
276
	 * @since 1.0.19
277
	 * @param string $value Value.
278
	 * @param array  $setting Setting.
279
	 * @return string|WP_Error
280
	 */
281
	public function validate_setting_select_field( $value, $setting ) {
282
		if ( array_key_exists( $value, $setting['options'] ) ) {
283
			return $value;
284
		} else {
285
			return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'invoicing' ), array( 'status' => 400 ) );
286
		}
287
	}
288
289
	/**
290
	 * Validate multiselect based settings.
291
	 *
292
	 * @since 1.0.19
293
	 * @param array $values Values.
294
	 * @param array $setting Setting.
295
	 * @return array|WP_Error
296
	 */
297
	public function validate_setting_multiselect_field( $values, $setting ) {
298
299
		if ( empty( $values ) ) {
300
			return array();
301
		}
302
303
		if ( ! is_array( $values ) ) {
0 ignored issues
show
introduced by
The condition is_array($values) is always true.
Loading history...
304
			return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'invoicing' ), array( 'status' => 400 ) );
305
		}
306
307
		$final_values = array();
308
		foreach ( $values as $value ) {
309
			if ( array_key_exists( $value, $setting['options'] ) ) {
310
				$final_values[] = $value;
311
			}
312
		}
313
314
		return $final_values;
315
	}
316
317
	/**
318
	 * Validate radio based settings.
319
	 *
320
	 * @since 1.0.19
321
	 * @param string $value Value.
322
	 * @param array  $setting Setting.
323
	 * @return string|WP_Error
324
	 */
325
	public function validate_setting_radio_field( $value, $setting ) {
326
		return $this->validate_setting_select_field( $value, $setting );
327
	}
328
329
	/**
330
	 * Validate checkbox based settings.
331
	 *
332
	 * @since 1.0.19
333
	 * @param string $value Value.
334
	 * @param array  $setting Setting.
335
	 * @return string|WP_Error
336
	 */
337
	public function validate_setting_checkbox_field( $value, $setting ) {
338
		if ( in_array( $value, array( 'yes', 'no' ) ) ) {
339
			return $value;
340
		} elseif ( is_bool( $value ) || is_numeric( $value ) ) {
341
			return empty( $value ) ? 'no' : 'yes';
342
		} elseif ( empty( $value ) ) {
343
			$value = isset( $setting['default'] ) ? $setting['default'] : 'no';
344
			return $value;
345
		} else {
346
			return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'invoicing' ), array( 'status' => 400 ) );
347
		}
348
	}
349
350
	/**
351
	 * Validate textarea based settings.
352
	 *
353
	 * @since 1.0.19
354
	 * @param string $value Value.
355
	 * @param array  $setting Setting.
356
	 * @return string
357
	 */
358
	public function validate_setting_textarea_field( $value, $setting ) {
0 ignored issues
show
Unused Code introduced by
The parameter $setting is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

358
	public function validate_setting_textarea_field( $value, /** @scrutinizer ignore-unused */ $setting ) {

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

Loading history...
359
		$value = is_null( $value ) ? '' : $value;
0 ignored issues
show
introduced by
The condition is_null($value) is always false.
Loading history...
360
		return wp_kses(
361
			trim( stripslashes( $value ) ),
362
			array_merge(
363
				array(
364
					'iframe' => array(
365
						'src'   => true,
366
						'style' => true,
367
						'id'    => true,
368
						'class' => true,
369
					),
370
				),
371
				wp_kses_allowed_html( 'post' )
372
			)
373
		);
374
	}
375
376
	/**
377
	 * Add meta query.
378
	 *
379
	 * @since 1.0.19
380
	 * @param array $args       Query args.
381
	 * @param array $meta_query Meta query.
382
	 * @return array
383
	 */
384
	protected function add_meta_query( $args, $meta_query ) {
385
		if ( empty( $args['meta_query'] ) ) {
386
			$args['meta_query'] = array();
387
		}
388
389
		$args['meta_query'][] = $meta_query;
390
391
		return $args['meta_query'];
392
	}
393
394
	/**
395
	 * Get the batch schema, conforming to JSON Schema.
396
	 *
397
	 * @return array
398
	 */
399
	public function get_public_batch_schema() {
400
		$schema = array(
401
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
402
			'title'      => 'batch',
403
			'type'       => 'object',
404
			'properties' => array(
405
				'create' => array(
406
					'description' => __( 'List of created resources.', 'invoicing' ),
407
					'type'        => 'array',
408
					'context'     => array( 'view', 'edit' ),
409
					'items'       => array(
410
						'type'    => 'object',
411
					),
412
				),
413
				'update' => array(
414
					'description' => __( 'List of updated resources.', 'invoicing' ),
415
					'type'        => 'array',
416
					'context'     => array( 'view', 'edit' ),
417
					'items'       => array(
418
						'type'    => 'object',
419
					),
420
				),
421
				'delete' => array(
422
					'description' => __( 'List of deleted resources.', 'invoicing' ),
423
					'type'        => 'array',
424
					'context'     => array( 'view', 'edit' ),
425
					'items'       => array(
426
						'type'    => 'integer',
427
					),
428
				),
429
			),
430
		);
431
432
		return $schema;
433
	}
434
435
	/**
436
	 * Gets an array of fields to be included on the response.
437
	 *
438
	 * Included fields are based on item schema and `_fields=` request argument.
439
	 * Copied from WordPress 5.3 to support old versions.
440
	 *
441
	 * @since 1.0.19
442
	 * @param WP_REST_Request $request Full details about the request.
443
	 * @return array Fields to be included in the response.
444
	 */
445
	public function get_fields_for_response( $request ) {
446
		$schema     = $this->get_item_schema();
447
		$properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
448
449
		$additional_fields = $this->get_additional_fields();
450
		foreach ( $additional_fields as $field_name => $field_options ) {
451
			// For back-compat, include any field with an empty schema
452
			// because it won't be present in $this->get_item_schema().
453
			if ( is_null( $field_options['schema'] ) ) {
454
				$properties[ $field_name ] = $field_options;
455
			}
456
		}
457
458
		// Exclude fields that specify a different context than the request context.
459
		$context = $request['context'];
460
		if ( $context ) {
461
			foreach ( $properties as $name => $options ) {
462
				if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
463
					unset( $properties[ $name ] );
464
				}
465
			}
466
		}
467
468
		$fields = array_keys( $properties );
469
470
		if ( ! isset( $request['_fields'] ) ) {
471
			return $fields;
472
		}
473
		$requested_fields = wpinv_parse_list( $request['_fields'] );
474
		if ( 0 === count( $requested_fields ) ) {
475
			return $fields;
476
		}
477
		// Trim off outside whitespace from the comma delimited list.
478
		$requested_fields = array_map( 'trim', $requested_fields );
479
		// Always persist 'id', because it can be needed for add_additional_fields_to_object().
480
		if ( in_array( 'id', $fields, true ) ) {
481
			$requested_fields[] = 'id';
482
		}
483
		// Return the list of all requested fields which appear in the schema.
484
		return array_reduce(
485
			$requested_fields,
486
			function( $response_fields, $field ) use ( $fields ) {
487
				if ( in_array( $field, $fields, true ) ) {
488
					$response_fields[] = $field;
489
					return $response_fields;
490
				}
491
				// Check for nested fields if $field is not a direct match.
492
				$nested_fields = explode( '.', $field );
493
				// A nested field is included so long as its top-level property is
494
				// present in the schema.
495
				if ( in_array( $nested_fields[0], $fields, true ) ) {
496
					$response_fields[] = $field;
497
				}
498
				return $response_fields;
499
			},
500
			array()
501
		);
502
	}
503
504
	/**
505
	 * Given an array of fields to include in a response, some of which may be
506
	 * `nested.fields`, determine whether the provided field should be included
507
	 * in the response body.
508
	 *
509
	 * Copied from WordPress 5.3 to support old versions.
510
	 *
511
	 * @since 1.0.19
512
	 *
513
	 * @param string $field  A field to test for inclusion in the response body.
514
	 * @param array  $fields An array of string fields supported by the endpoint.
515
	 * @return bool Whether to include the field or not.
516
	 * @see rest_is_field_included()
517
	 */
518
	public function is_field_included( $field, $fields ) {
519
		if ( in_array( $field, $fields, true ) ) {
520
			return true;
521
		}
522
523
		foreach ( $fields as $accepted_field ) {
524
			// Check to see if $field is the parent of any item in $fields.
525
			// A field "parent" should be accepted if "parent.child" is accepted.
526
			if ( strpos( $accepted_field, "$field." ) === 0 ) {
527
				return true;
528
			}
529
			// Conversely, if "parent" is accepted, all "parent.child" fields
530
			// should also be accepted.
531
			if ( strpos( $field, "$accepted_field." ) === 0 ) {
532
				return true;
533
			}
534
		}
535
536
		return false;
537
	}
538
539
}
540