GetPaid_REST_Controller::check_batch_limit()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
rs 10
c 0
b 0
f 0
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( /** @scrutinizer ignore-unused */ $namespace ) {
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
	 * Fill batches.
114
	 *
115
	 * @param array array of request items.
116
	 * @return array
117
	 */
118
	protected function fill_batch_keys( $items ) {
119
120
		$items['create'] = empty( $items['create'] ) ? array() : $items['create'];
121
		$items['update'] = empty( $items['update'] ) ? array() : $items['update'];
122
		$items['delete'] = empty( $items['delete'] ) ? array() : wp_parse_id_list( $items['delete'] );
123
		return $items;
124
125
	}
126
127
	/**
128
	 * Check batch limit.
129
	 *
130
	 * @param array $items Request items.
131
	 * @return bool|WP_Error
132
	 */
133
	protected function check_batch_limit( $items ) {
134
		$limit = apply_filters( 'getpaid_rest_batch_items_limit', 100, $this->get_normalized_rest_base() );
135
		$total = count( $items['create'] ) + count( $items['update'] ) + count( $items['delete'] );
136
137
		if ( $total > $limit ) {
138
			/* translators: %s: items limit */
139
			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 ) );
140
		}
141
142
		return true;
143
	}
144
145
	/**
146
	 * Bulk create items.
147
	 *
148
	 * @param array $items Array of items to create.
149
	 * @param WP_REST_Request $request Full details about the request.
150
	 * @param WP_REST_Server $wp_rest_server
151
	 * @return array()
152
	 */
153
	protected function batch_create_items( $items, $request, $wp_rest_server ) {
154
155
		$query  = $request->get_query_params();
156
		$create = array();
157
158
		foreach ( $items as $item ) {
159
			$_item = new WP_REST_Request( 'POST' );
160
161
			// Default parameters.
162
			$defaults = array();
163
			$schema   = $this->get_public_item_schema();
164
			foreach ( $schema['properties'] as $arg => $options ) {
165
				if ( isset( $options['default'] ) ) {
166
					$defaults[ $arg ] = $options['default'];
167
				}
168
			}
169
			$_item->set_default_params( $defaults );
170
171
			// Set request parameters.
172
			$_item->set_body_params( $item );
173
174
			// Set query (GET) parameters.
175
			$_item->set_query_params( $query );
176
177
			// Create the item.
178
			$_response = $this->create_item( $_item );
179
180
			// If an error occured...
181
			if ( is_wp_error( $_response ) ) {
182
183
				$create[]   = array(
184
					'id'    => 0,
185
					'error' => array(
186
						'code'    => $_response->get_error_code(),
187
						'message' => $_response->get_error_message(),
188
						'data'    => $_response->get_error_data(),
189
					),
190
				);
191
192
				continue;
193
			}
194
195
			$create[] = $wp_rest_server->response_to_data( /** @scrutinizer ignore-type */ $_response, false );
196
197
		}
198
199
		return $create;
200
201
	}
202
203
	/**
204
	 * Bulk update items.
205
	 *
206
	 * @param array $items Array of items to update.
207
	 * @param WP_REST_Request $request Full details about the request.
208
	 * @param WP_REST_Server $wp_rest_server
209
	 * @return array()
210
	 */
211
	protected function batch_update_items( $items, $request, $wp_rest_server ) {
212
213
		$query  = $request->get_query_params();
214
		$update = array();
215
216
		foreach ( $items as $item ) {
217
218
			// Create a dummy request.
219
			$_item = new WP_REST_Request( 'PUT' );
220
221
			// Add body params.
222
			$_item->set_body_params( $item );
223
224
			// Set query (GET) parameters.
225
			$_item->set_query_params( $query );
226
227
			// Update the item.
228
			$_response = $this->update_item( $_item );
229
230
			// If an error occured...
231
			if ( is_wp_error( $_response ) ) {
232
233
				$update[] = array(
234
					'id'    => $item['id'],
235
					'error' => array(
236
						'code'    => $_response->get_error_code(),
237
						'message' => $_response->get_error_message(),
238
						'data'    => $_response->get_error_data(),
239
					),
240
				);
241
242
				continue;
243
244
			}
245
246
			$update[] = $wp_rest_server->response_to_data( /** @scrutinizer ignore-type */ $_response, false );
247
248
		}
249
250
		return $update;
251
252
	}
253
254
	/**
255
	 * Bulk delete items.
256
	 *
257
	 * @param array $items Array of items to delete.
258
	 * @param WP_REST_Server $wp_rest_server
259
	 * @return array()
260
	 */
261
	protected function batch_delete_items( $items, $wp_rest_server ) {
262
263
		$delete = array();
264
265
		foreach ( array_filter( $items ) as $id ) {
266
267
			// Prepare the request.
268
			$_item = new WP_REST_Request( 'DELETE' );
269
			$_item->set_query_params(
270
				array(
271
					'id'    => $id,
272
					'force' => true,
273
				)
274
			);
275
276
			// Delete the item.
277
			$_response = $this->delete_item( $_item );
278
279
			if ( is_wp_error( $_response ) ) {
280
281
				$delete[] = array(
282
					'id'    => $id,
283
					'error' => array(
284
						'code'    => $_response->get_error_code(),
285
						'message' => $_response->get_error_message(),
286
						'data'    => $_response->get_error_data(),
287
					),
288
				);
289
290
				continue;
291
			}
292
293
			$delete[] = $wp_rest_server->response_to_data( /** @scrutinizer ignore-type */ $_response, false );
294
295
		}
296
297
		return $delete;
298
299
	}
300
301
	/**
302
	 * Bulk create, update and delete items.
303
	 *
304
	 * @param WP_REST_Request $request Full details about the request.
305
	 * @return WP_Error|array.
0 ignored issues
show
Documentation Bug introduced by
The doc comment WP_Error|array. at position 2 could not be parsed: Unknown type name 'array.' at position 2 in WP_Error|array..
Loading history...
306
	 */
307
	public function batch_items( $request ) {
308
		global $wp_rest_server;
309
310
		// Prepare the batch items.
311
		$items = $this->fill_batch_keys( array_filter( $request->get_params() ) );
312
313
		// Ensure that the batch has not exceeded the limit to prevent abuse.
314
		$limit = $this->check_batch_limit( $items );
315
		if ( is_wp_error( $limit ) ) {
316
			return $limit;
317
		}
318
319
		// Process the items.
320
		return array(
321
			'create' => $this->batch_create_items( $items['create'], $request, $wp_rest_server ),
322
			'update' => $this->batch_update_items( $items['update'], $request, $wp_rest_server ),
323
			'delete' => $this->batch_delete_items( $items['delete'], $wp_rest_server ),
324
		);
325
326
	}
327
328
	/**
329
	 * Add meta query.
330
	 *
331
	 * @since 1.0.19
332
	 * @param array $args       Query args.
333
	 * @param array $meta_query Meta query.
334
	 * @return array
335
	 */
336
	protected function add_meta_query( $args, $meta_query ) {
337
		if ( empty( $args['meta_query'] ) ) {
338
			$args['meta_query'] = array();
339
		}
340
341
		$args['meta_query'][] = $meta_query;
342
343
		return $args['meta_query'];
344
	}
345
346
	/**
347
	 * Get the batch schema, conforming to JSON Schema.
348
	 *
349
	 * @return array
350
	 */
351
	public function get_public_batch_schema() {
352
353
		return array(
354
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
355
			'title'      => 'batch',
356
			'type'       => 'object',
357
			'properties' => array(
358
				'create' => array(
359
					'description' => __( 'List of created resources.', 'invoicing' ),
360
					'type'        => 'array',
361
					'context'     => array( 'view', 'edit' ),
362
					'items'       => array(
363
						'type' => 'object',
364
					),
365
				),
366
				'update' => array(
367
					'description' => __( 'List of updated resources.', 'invoicing' ),
368
					'type'        => 'array',
369
					'context'     => array( 'view', 'edit' ),
370
					'items'       => array(
371
						'type' => 'object',
372
					),
373
				),
374
				'delete' => array(
375
					'description' => __( 'List of deleted resources.', 'invoicing' ),
376
					'type'        => 'array',
377
					'context'     => array( 'view', 'edit' ),
378
					'items'       => array(
379
						'type' => 'integer',
380
					),
381
				),
382
			),
383
		);
384
385
	}
386
387
	/**
388
	 * Returns the value of schema['properties']
389
	 *
390
	 * i.e Schema fields.
391
	 *
392
	 * @since 1.0.19
393
	 * @return array
394
	 */
395
	protected function get_schema_properties() {
396
397
		$schema     = $this->get_item_schema();
398
		$properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
399
400
		// For back-compat, include any field with an empty schema
401
		// because it won't be present in $this->get_item_schema().
402
		foreach ( $this->get_additional_fields() as $field_name => $field_options ) {
403
			if ( is_null( $field_options['schema'] ) ) {
404
				$properties[ $field_name ] = $field_options;
405
			}
406
		}
407
408
		return $properties;
409
	}
410
411
	/**
412
	 * Filters fields by context.
413
	 *
414
	 * @param array $fields Array of fields
415
	 * @param string|null context view, edit or embed
0 ignored issues
show
Bug introduced by
The type context was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
416
	 * @since 1.0.19
417
	 * @return array
418
	 */
419
	protected function filter_response_fields_by_context( $fields, $context ) {
420
421
		if ( empty( $context ) ) {
422
			return $fields;
423
		}
424
425
		foreach ( $fields as $name => $options ) {
426
			if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
427
				unset( $fields[ $name ] );
428
			}
429
		}
430
431
		return $fields;
432
433
	}
434
435
	/**
436
	 * Filters fields by an array of requested fields.
437
	 *
438
	 * @param array $fields Array of available fields
439
	 * @param array $requested array of requested fields.
440
	 * @since 1.0.19
441
	 * @return array
442
	 */
443
	protected function filter_response_fields_by_array( $fields, $requested ) {
444
445
		// Trim off any whitespace from the list array.
446
		$requested = array_map( 'trim', $requested );
447
448
		// Always persist 'id', because it can be needed for add_additional_fields_to_object().
449
		if ( in_array( 'id', $fields, true ) ) {
450
			$requested[] = 'id';
451
		}
452
453
		// Get rid of duplicate fields.
454
		$requested = array_unique( $requested );
455
456
		// Return the list of all included fields which are available.
457
		return array_reduce(
458
			$requested,
459
			function( $response_fields, $field ) use ( $fields ) {
460
461
				if ( in_array( $field, $fields, true ) ) {
462
					$response_fields[] = $field;
463
					return $response_fields;
464
				}
465
466
				// Check for nested fields if $field is not a direct match.
467
				$nested_fields = explode( '.', $field );
468
469
				// A nested field is included so long as its top-level property is
470
				// present in the schema.
471
				if ( in_array( $nested_fields[0], $fields, true ) ) {
472
					$response_fields[] = $field;
473
				}
474
475
				return $response_fields;
476
			},
477
			array()
478
		);
479
480
	}
481
482
	/**
483
	 * Gets an array of fields to be included on the response.
484
	 *
485
	 * Included fields are based on item schema and `_fields=` request argument.
486
	 * Copied from WordPress 5.3 to support old versions.
487
	 *
488
	 * @since 1.0.19
489
	 * @param WP_REST_Request $request Full details about the request.
490
	 * @return array Fields to be included in the response.
491
	 */
492
	public function get_fields_for_response( $request ) {
493
494
		// Retrieve fields in the schema.
495
		$properties = $this->get_schema_properties();
496
497
		// Exclude fields that specify a different context than the request context.
498
		$properties = $this->filter_response_fields_by_context( $properties, $request['context'] );
499
500
		// We only need the field keys.
501
		$fields = array_keys( $properties );
502
503
		// Is the user filtering the response fields??
504
		if ( empty( $request['_fields'] ) ) {
505
			return $fields;
506
		}
507
508
		return $this->filter_response_fields_by_array( $fields, wpinv_parse_list( $request['_fields'] ) );
509
510
	}
511
512
	/**
513
	 * Limits an object to the requested fields.
514
	 *
515
	 * Included fields are based on the `_fields` request argument.
516
	 *
517
	 * @since 1.0.19
518
	 * @param array $data Fields to include in the response.
519
	 * @param array $fields Requested fields.
520
	 * @return array Fields to be included in the response.
521
	 */
522
	public function limit_object_to_requested_fields( $data, $fields, $prefix = '' ) {
523
524
		// Is the user filtering the response fields??
525
		if ( empty( $fields ) ) {
526
			return $data;
527
		}
528
529
		foreach ( $data as $key => $value ) {
530
531
			// Numeric arrays.
532
			if ( is_numeric( $key ) && is_array( $value ) ) {
533
				$data[ $key ] = $this->limit_object_to_requested_fields( $value, $fields, $prefix );
534
				continue;
535
			}
536
537
			// Generate a new prefix.
538
			$new_prefix = empty( $prefix ) ? $key : "$prefix.$key";
539
540
			// Check if it was requested.
541
			if ( ! empty( $key ) && ! $this->is_field_included( $new_prefix, $fields ) ) {
542
				unset( $data[ $key ] );
543
				continue;
544
			}
545
546
			if ( $key != 'meta_data' && is_array( $value ) ) {
547
				$data[ $key ] = $this->limit_object_to_requested_fields( $value, $fields, $new_prefix );
548
			}
549
}
550
551
		return $data;
552
	}
553
554
	/**
555
	 * Given an array of fields to include in a response, some of which may be
556
	 * `nested.fields`, determine whether the provided field should be included
557
	 * in the response body.
558
	 *
559
	 * Copied from WordPress 5.3 to support old versions.
560
	 *
561
	 * @since 1.0.19
562
	 *
563
	 * @param string $field  A field to test for inclusion in the response body.
564
	 * @param array  $fields An array of string fields supported by the endpoint.
565
	 * @return bool Whether to include the field or not.
566
	 * @see rest_is_field_included()
567
	 */
568
	public function is_field_included( $field, $fields ) {
569
		if ( in_array( $field, $fields, true ) ) {
570
			return true;
571
		}
572
573
		foreach ( $fields as $accepted_field ) {
574
			// Check to see if $field is the parent of any item in $fields.
575
			// A field "parent" should be accepted if "parent.child" is accepted.
576
			if ( strpos( $accepted_field, "$field." ) === 0 ) {
577
				return true;
578
			}
579
			// Conversely, if "parent" is accepted, all "parent.child" fields
580
			// should also be accepted.
581
			if ( strpos( $field, "$accepted_field." ) === 0 ) {
582
				return true;
583
			}
584
		}
585
586
		return false;
587
	}
588
589
}
590