Issues (850)

Security Analysis    4 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection (1)
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection (2)
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting (1)
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

api/class-getpaid-rest-posts-controller.php (5 issues)

Labels
1
<?php
2
/**
3
 * GetPaid REST Posts controller class.
4
 *
5
 * Extends the GetPaid_REST_CRUD_Controller class to provide functionalities for endpoints
6
 * that store CRUD data using CPTs
7
 *
8
 * @version 1.0.19
9
 */
10
11
defined( 'ABSPATH' ) || exit;
12
13
/**
14
 * GetPaid REST Posts controller class.
15
 *
16
 * @package Invoicing
17
 */
18
class GetPaid_REST_Posts_Controller extends GetPaid_REST_CRUD_Controller {
19
20
    /**
21
	 * Post type.
22
	 *
23
	 * @var string
24
	 */
25
	protected $post_type;
26
27
	/**
28
	 * Controls visibility on frontend.
29
	 *
30
	 * @var string
31
	 */
32
	public $public = false;
33
34
	/**
35
	 * Registers the routes for the objects of the controller.
36
	 *
37
	 * @since 1.0.19
38
	 *
39
	 * @see register_rest_route()
40
	 */
41
	public function register_namespace_routes( $namespace ) {
42
43
		parent::register_namespace_routes( $namespace );
44
45
		register_rest_route(
46
			$namespace,
47
			'/' . $this->rest_base . '/batch',
48
			array(
49
				array(
50
					'methods'             => WP_REST_Server::EDITABLE,
51
					'callback'            => array( $this, 'batch_items' ),
52
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
53
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
54
				),
55
				'schema' => array( $this, 'get_public_batch_schema' ),
56
			)
57
		);
58
59
	}
60
61
	/**
62
	 * Check permissions of items on REST API.
63
	 *
64
	 * @since 1.0.19
65
	 * @param string $context   Request context.
66
	 * @param int    $object_id Post ID.
67
	 * @return bool
68
	 */
69
	public function check_post_permissions( $context = 'read', $object_id = 0 ) {
70
71
		$contexts = array(
72
			'read'   => 'read_private_posts',
73
			'create' => 'publish_posts',
74
			'edit'   => 'edit_post',
75
			'delete' => 'delete_post',
76
			'batch'  => 'edit_others_posts',
77
		);
78
79
		$cap              = $contexts[ $context ];
80
		$post_type_object = get_post_type_object( $this->post_type );
81
		$permission       = current_user_can( $post_type_object->cap->$cap, $object_id );
82
83
		return apply_filters( 'getpaid_rest_check_permissions', $permission, $context, $object_id, $this->post_type );
84
	}
85
86
	/**
87
	 * Check if a given request has access to read items.
88
	 *
89
	 * @param  WP_REST_Request $request Full details about the request.
90
	 * @return WP_Error|boolean
91
	 */
92
	public function get_items_permissions_check( $request ) {
93
		return $this->check_post_permissions() ? true : new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot list resources.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
94
	}
95
96
	/**
97
	 * Check if a given request has access to create an item.
98
	 *
99
	 * @param  WP_REST_Request $request Full details about the request.
100
	 * @return WP_Error|boolean
101
	 */
102
	public function create_item_permissions_check( $request ) {
103
		return $this->check_post_permissions( 'create' ) ? true : new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
104
	}
105
106
	/**
107
	 * Check if a given request has access to read an item.
108
	 *
109
	 * @param  WP_REST_Request $request Full details about the request.
110
	 * @return WP_Error|boolean
111
	 */
112
	public function get_item_permissions_check( $request ) {
113
		$post = get_post( (int) $request['id'] );
114
115
		if ( $post && ! $this->check_post_permissions( 'read', $post->ID ) ) {
116
			return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
117
		}
118
119
		return true;
120
	}
121
122
	/**
123
	 * Check if a given request has access to update an item.
124
	 *
125
	 * @param  WP_REST_Request $request Full details about the request.
126
	 * @return WP_Error|boolean
127
	 */
128
	public function update_item_permissions_check( $request ) {
129
		$post = get_post( (int) $request['id'] );
130
131
		if ( $post && ! $this->check_post_permissions( 'edit', $post->ID ) ) {
132
			return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
133
		}
134
135
		return true;
136
	}
137
138
	/**
139
	 * Check if a given request has access to delete an item.
140
	 *
141
	 * @param  WP_REST_Request $request Full details about the request.
142
	 * @return bool|WP_Error
143
	 */
144
	public function delete_item_permissions_check( $request ) {
145
		$post = get_post( (int) $request['id'] );
146
147
		if ( $post && ! $this->check_post_permissions( 'delete', $post->ID ) ) {
148
			return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
149
		}
150
151
		return true;
152
	}
153
154
	/**
155
	 * Check if a given request has access batch create, update and delete items.
156
	 *
157
	 * @param  WP_REST_Request $request Full details about the request.
158
	 *
159
	 * @return boolean|WP_Error
160
	 */
161
	public function batch_items_permissions_check( $request ) {
162
		return $this->check_post_permissions( 'batch' ) ? true : new WP_Error( 'rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
163
	}
164
165
	/**
166
	 * @deprecated
167
	 */
168
	public function get_post( $object_id ) {
169
		return $this->get_object( $object_id );
170
    }
171
172
	/**
173
	 * Get a single object.
174
	 *
175
	 * @param WP_REST_Request $request Full details about the request.
176
	 * @return WP_Error|WP_REST_Response
177
	 */
178
	public function get_item( $request ) {
179
180
		// Fetch item.
181
		$response = parent::get_item( $request );
182
183
		// (Maybe) add a link to the html pagee.
184
		if ( $this->public && ! is_wp_error( $response ) ) {
185
			$response->link_header( 'alternate', get_permalink( $this->data_object->get_id() ), array( 'type' => 'text/html' ) );
0 ignored issues
show
The method link_header() does not exist on WP_Error. ( Ignorable by Annotation )

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

185
			$response->/** @scrutinizer ignore-call */ 
186
              link_header( 'alternate', get_permalink( $this->data_object->get_id() ), array( 'type' => 'text/html' ) );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
It seems like get_permalink($this->data_object->get_id()) can also be of type false; however, parameter $link of WP_REST_Response::link_header() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

185
			$response->link_header( 'alternate', /** @scrutinizer ignore-type */ get_permalink( $this->data_object->get_id() ), array( 'type' => 'text/html' ) );
Loading history...
186
		}
187
188
		return $response;
189
	}
190
191
	/**
192
	 * Create a single object.
193
	 *
194
	 * @param WP_REST_Request $request Full details about the request.
195
	 * @return WP_Error|WP_REST_Response
196
	 */
197
	public function create_item( $request ) {
198
199
		// Create item.
200
		$response = parent::create_item( $request );
201
202
		// Fire a hook after an item is created.
203
		if ( ! is_wp_error( $response ) ) {
204
205
			/**
206
			 * Fires after a single item is created or updated via the REST API.
207
			 *
208
			 * @param WP_Post         $post      Post object.
209
			 * @param WP_REST_Request $request   Request object.
210
			 * @param boolean         $creating  True when creating item, false when updating.
211
			 */
212
			do_action( "getpaid_rest_insert_{$this->post_type}", $this->data_object, $request, true );
213
214
		}
215
216
		return $response;
217
218
	}
219
220
	/**
221
	 * Update a single object.
222
	 *
223
	 * @param WP_REST_Request $request Full details about the request.
224
	 * @return WP_Error|WP_REST_Response
225
	 */
226
	public function update_item( $request ) {
227
228
		// Create item.
229
		$response = parent::update_item( $request );
230
231
		// Fire a hook after an item is created.
232
		if ( ! is_wp_error( $response ) ) {
233
234
			/**
235
			 * Fires after a single item is created or updated via the REST API.
236
			 *
237
			 * @param WP_Post         $post      Post object.
238
			 * @param WP_REST_Request $request   Request object.
239
			 * @param boolean         $creating  True when creating item, false when updating.
240
			 */
241
			do_action( "getpaid_rest_insert_{$this->post_type}", $this->data_object, $request, false );
242
243
		}
244
245
		return $response;
246
247
	}
248
249
	/**
250
	 * Get a collection of objects.
251
	 *
252
	 * @param WP_REST_Request $request Full details about the request.
253
	 * @return WP_Error|WP_REST_Response
254
	 */
255
	public function get_items( $request ) {
256
257
		$args                         = array();
258
		$args['offset']               = $request['offset'];
259
		$args['order']                = $request['order'];
260
		$args['orderby']              = $request['orderby'];
261
		$args['paged']                = $request['page'];
262
		$args['post__in']             = $request['include'];
263
		$args['post__not_in']         = $request['exclude'];
264
		$args['posts_per_page']       = $request['per_page'];
265
		$args['name']                 = $request['slug'];
266
		$args['post_parent__in']      = $request['parent'];
267
		$args['post_parent__not_in']  = $request['parent_exclude'];
268
		$args['s']                    = $request['search'];
269
		$args['post_status']          = wpinv_parse_list( $request['status'] );
270
271
		$args['date_query'] = array();
272
273
		// Set before into date query. Date query must be specified as an array of an array.
274
		if ( isset( $request['before'] ) ) {
275
			$args['date_query'][0]['before'] = $request['before'];
276
		}
277
278
		// Set after into date query. Date query must be specified as an array of an array.
279
		if ( isset( $request['after'] ) ) {
280
			$args['date_query'][0]['after'] = $request['after'];
281
		}
282
283
		// Force the post_type & fields arguments, since they're not a user input variable.
284
		$args['post_type'] = $this->post_type;
285
		$args['fields']    = 'ids';
286
287
		// Filter the query arguments for a request.
288
		$args       = apply_filters( "getpaid_rest_{$this->post_type}_query", $args, $request );
289
		$query_args = $this->prepare_items_query( $args, $request );
290
291
		$posts_query = new WP_Query();
292
		$query_result = $posts_query->query( $query_args );
293
294
		$posts = array();
295
		foreach ( $query_result as $post_id ) {
296
			if ( ! $this->check_post_permissions( 'read', $post_id ) ) {
297
				continue;
298
			}
299
300
			$data    = $this->prepare_item_for_response( $this->get_object( $post_id ), $request );
0 ignored issues
show
$this->get_object($post_id) of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_CRUD_Contro...are_item_for_response(). ( Ignorable by Annotation )

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

300
			$data    = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $this->get_object( $post_id ), $request );
Loading history...
301
			$posts[] = $this->prepare_response_for_collection( $data );
302
		}
303
304
		$page        = (int) $query_args['paged'];
305
		$total_posts = $posts_query->found_posts;
306
307
		if ( $total_posts < 1 ) {
308
			// Out-of-bounds, run the query again without LIMIT for total count.
309
			unset( $query_args['paged'] );
310
			$count_query = new WP_Query();
311
			$count_query->query( $query_args );
312
			$total_posts = $count_query->found_posts;
313
		}
314
315
		$max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] );
316
317
		$response = rest_ensure_response( $posts );
318
		$response->header( 'X-WP-Total', (int) $total_posts );
319
		$response->header( 'X-WP-TotalPages', (int) $max_pages );
320
321
		$request_params = $request->get_query_params();
322
		$base = add_query_arg( $request_params, rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
323
324
		if ( $page > 1 ) {
325
			$prev_page = $page - 1;
326
			if ( $prev_page > $max_pages ) {
327
				$prev_page = $max_pages;
328
			}
329
			$prev_link = add_query_arg( 'page', $prev_page, $base );
330
			$response->link_header( 'prev', $prev_link );
331
		}
332
		if ( $max_pages > $page ) {
333
			$next_page = $page + 1;
334
			$next_link = add_query_arg( 'page', $next_page, $base );
335
			$response->link_header( 'next', $next_link );
336
		}
337
338
		return $response;
339
	}
340
341
	/**
342
	 * Delete a single item.
343
	 *
344
	 * @param WP_REST_Request $request Full details about the request.
345
	 * @return WP_REST_Response|WP_Error
346
	 */
347
	public function delete_item( $request ) {
348
349
		// Fetch the item.
350
		$item = $this->get_object( $request['id'] );
351
		if ( is_wp_error( $item ) ) {
352
			return $item;
353
		}
354
355
		$supports_trash = EMPTY_TRASH_DAYS > 0;
356
		$force          = $supports_trash && (bool) $request['force'];
357
358
		if ( ! $this->check_post_permissions( 'delete', $item->ID ) ) {
0 ignored issues
show
The property ID does not seem to exist on WP_Error.
Loading history...
359
			return new WP_Error( 'cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
360
		}
361
362
		$request->set_param( 'context', 'edit' );
363
		$response = $this->prepare_item_for_response( $item, $request );
0 ignored issues
show
$item of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_CRUD_Contro...are_item_for_response(). ( Ignorable by Annotation )

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

363
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $item, $request );
Loading history...
364
365
		if ( ! wp_delete_post( $item->ID, $force ) ) {
366
			return new WP_Error( 'rest_cannot_delete', sprintf( __( 'The resource cannot be deleted.', 'invoicing' ), $this->post_type ), array( 'status' => 500 ) );
367
		}
368
369
		return $response;
370
	}
371
372
	/**
373
	 * Prepare links for the request.
374
	 *
375
	 * @param GetPaid_Data    $object GetPaid_Data object.
376
	 * @return array Links for the given object.
377
	 */
378
	protected function prepare_links( $object ) {
379
380
		$links = parent::prepare_links( $object );
381
382
		if ( is_callable( array( $object, 'get_user_id' ) ) ) {
383
			$links['user'] = array(
384
				'href'       => rest_url( 'wp/v2/users/' . call_user_func( array( $object, 'get_user_id' ) ) ),
385
				'embeddable' => true,
386
			);
387
		}
388
389
		if ( is_callable( array( $object, 'get_owner' ) ) ) {
390
			$links['owner']  = array(
391
				'href'       => rest_url( 'wp/v2/users/' . call_user_func( array( $object, 'get_owner' ) ) ),
392
				'embeddable' => true,
393
			);
394
		}
395
396
		if ( is_callable( array( $object, 'get_parent_id' ) ) && call_user_func( array( $object, 'get_parent_id' ) ) ) {
397
			$links['parent']  = array(
398
				'href'       => rest_url( "$this->namespace/$this->rest_base/" . call_user_func( array( $object, 'get_parent_id' ) ) ),
399
				'embeddable' => true,
400
			);
401
		}
402
403
		return $links;
404
	}
405
406
	/**
407
	 * Determine the allowed query_vars for a get_items() response and
408
	 * prepare for WP_Query.
409
	 *
410
	 * @param array           $prepared_args Prepared arguments.
411
	 * @param WP_REST_Request $request Request object.
412
	 * @return array          $query_args
413
	 */
414
	protected function prepare_items_query( $prepared_args = array(), $request = null ) {
415
416
		$valid_vars = array_flip( $this->get_allowed_query_vars() );
417
		$query_args = array();
418
		foreach ( $valid_vars as $var => $index ) {
419
			if ( isset( $prepared_args[ $var ] ) ) {
420
				$query_args[ $var ] = apply_filters( "getpaid_rest_query_var-{$var}", $prepared_args[ $var ], $index );
421
			}
422
		}
423
424
		$query_args['ignore_sticky_posts'] = true;
425
426
		if ( 'include' === $query_args['orderby'] ) {
427
			$query_args['orderby'] = 'post__in';
428
		} elseif ( 'id' === $query_args['orderby'] ) {
429
			$query_args['orderby'] = 'ID'; // ID must be capitalized.
430
		} elseif ( 'slug' === $query_args['orderby'] ) {
431
			$query_args['orderby'] = 'name';
432
		}
433
434
		return apply_filters( 'getpaid_rest_prepare_items_query', $query_args, $request, $this );
435
436
	}
437
438
	/**
439
	 * Get all the WP Query vars that are allowed for the API request.
440
	 *
441
	 * @return array
442
	 */
443
	protected function get_allowed_query_vars() {
444
		global $wp;
445
446
		/**
447
		 * Filter the publicly allowed query vars.
448
		 *
449
		 * Allows adjusting of the default query vars that are made public.
450
		 *
451
		 * @param array  Array of allowed WP_Query query vars.
452
		 */
453
		$valid_vars = apply_filters( 'query_vars', $wp->public_query_vars );
454
455
		$post_type_obj = get_post_type_object( $this->post_type );
456
		if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
457
			$private = apply_filters( 'getpaid_rest_private_query_vars', $wp->private_query_vars );
458
			$valid_vars = array_merge( $valid_vars, $private );
459
		}
460
461
		// Define our own in addition to WP's normal vars.
462
		$rest_valid = array(
463
			'post_status',
464
			'date_query',
465
			'ignore_sticky_posts',
466
			'offset',
467
			'post__in',
468
			'post__not_in',
469
			'post_parent',
470
			'post_parent__in',
471
			'post_parent__not_in',
472
			'posts_per_page',
473
			'meta_query',
474
			'tax_query',
475
			'meta_key',
476
			'meta_value',
477
			'meta_compare',
478
			'meta_value_num',
479
		);
480
		$valid_vars = array_merge( $valid_vars, $rest_valid );
481
482
		// Filter allowed query vars for the REST API.
483
		$valid_vars = apply_filters( 'getpaid_rest_query_vars', $valid_vars, $this );
484
485
		return $valid_vars;
486
	}
487
488
	/**
489
	 * Get the query params for collections of attachments.
490
	 *
491
	 * @return array
492
	 */
493
	public function get_collection_params() {
494
495
		return array_merge(
496
			parent::get_collection_params(),
497
			array(
498
				'status'  => array(
499
					'default'           => $this->get_post_statuses(),
500
					'description'       => __( 'Limit result set to resources assigned one or more statuses.', 'invoicing' ),
501
					'type'              => array( 'array', 'string' ),
502
					'items'             => array(
503
						'enum' => $this->get_post_statuses(),
504
						'type' => 'string',
505
					),
506
					'validate_callback' => 'rest_validate_request_arg',
507
					'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
508
				),
509
				'after'   => array(
510
					'description'       => __( 'Limit response to resources created after a given ISO8601 compliant date.', 'invoicing' ),
511
					'type'              => 'string',
512
					'format'            => 'string',
513
					'validate_callback' => 'rest_validate_request_arg',
514
					'sanitize_callback' => 'sanitize_text_field',
515
				),
516
				'before'  => array(
517
					'description'       => __( 'Limit response to resources created before a given ISO8601 compliant date.', 'invoicing' ),
518
					'type'              => 'string',
519
					'format'            => 'string',
520
					'validate_callback' => 'rest_validate_request_arg',
521
					'sanitize_callback' => 'sanitize_text_field',
522
				),
523
				'exclude' => array(
524
					'description'       => __( 'Ensure result set excludes specific IDs.', 'invoicing' ),
525
					'type'              => 'array',
526
					'items'             => array(
527
						'type' => 'integer',
528
					),
529
					'default'           => array(),
530
					'sanitize_callback' => 'wp_parse_id_list',
531
					'validate_callback' => 'rest_validate_request_arg',
532
				),
533
				'include' => array(
534
					'description'       => __( 'Limit result set to specific ids.', 'invoicing' ),
535
					'type'              => 'array',
536
					'items'             => array(
537
						'type' => 'integer',
538
					),
539
					'default'           => array(),
540
					'sanitize_callback' => 'wp_parse_id_list',
541
					'validate_callback' => 'rest_validate_request_arg',
542
				),
543
				'offset'  => array(
544
					'description'       => __( 'Offset the result set by a specific number of items.', 'invoicing' ),
545
					'type'              => 'integer',
546
					'sanitize_callback' => 'absint',
547
					'validate_callback' => 'rest_validate_request_arg',
548
				),
549
				'order'   => array(
550
					'description'       => __( 'Order sort attribute ascending or descending.', 'invoicing' ),
551
					'type'              => 'string',
552
					'default'           => 'desc',
553
					'enum'              => array( 'asc', 'desc' ),
554
					'validate_callback' => 'rest_validate_request_arg',
555
				),
556
				'orderby' => array(
557
					'description'       => __( 'Sort collection by object attribute.', 'invoicing' ),
558
					'type'              => 'string',
559
					'default'           => 'date',
560
					'enum'              => array(
561
						'date',
562
						'id',
563
						'include',
564
						'title',
565
						'slug',
566
						'modified',
567
					),
568
					'validate_callback' => 'rest_validate_request_arg',
569
				),
570
			)
571
		);
572
	}
573
574
	/**
575
	 * Retrieves the items's schema, conforming to JSON Schema.
576
	 *
577
	 * @since 1.0.19
578
	 *
579
	 * @return array Item schema data.
580
	 */
581
	public function get_item_schema() {
582
583
		// Maybe retrieve the schema from cache.
584
		if ( ! empty( $this->schema ) ) {
585
			return $this->add_additional_fields_schema( $this->schema );
586
		}
587
588
		$type   = str_replace( 'wpi_', '', $this->post_type );
589
		$schema = array(
590
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
591
			'title'      => $this->post_type,
592
			'type'       => 'object',
593
			'properties' => wpinv_get_data( "$type-schema" ),
594
		);
595
596
		// Filters the invoice schema for the REST API.
597
        $schema = apply_filters( "wpinv_rest_{$type}_schema", $schema );
598
599
		// Cache the invoice schema.
600
		$this->schema = $schema;
601
602
		return $this->add_additional_fields_schema( $this->schema );
603
	}
604
605
	/**
606
	 * Sanitizes and validates the list of post statuses.
607
	 *
608
	 * @since 1.0.13
609
	 *
610
	 * @param string|array    $statuses  One or more post statuses.
611
	 * @param WP_REST_Request $request   Full details about the request.
612
	 * @param string          $parameter Additional parameter to pass to validation.
613
	 * @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
614
	 */
615
	public function sanitize_post_statuses( $statuses, $request, $parameter ) {
616
		return array_intersect( wp_parse_slug_list( $statuses ), $this->get_post_statuses() );
617
	}
618
619
	/**
620
	 * Retrieves a valid list of post statuses.
621
	 *
622
	 * @since 1.0.19
623
	 *
624
	 * @return array A list of registered item statuses.
625
	 */
626
	public function get_post_statuses() {
627
		return get_post_stati();
628
	}
629
630
	/**
631
	 * Checks if a key should be included in a response.
632
	 *
633
	 * @since  1.0.19
634
	 * @param  GetPaid_Data $object  Data object.
635
	 * @param  string       $field_key The key to check for.
636
	 * @return bool
637
	 */
638
	public function object_supports_field( $object, $field_key ) {
639
		$supports = parent::object_supports_field( $object, $field_key );
640
		return apply_filters( "getpaid_rest_{$this->post_type}_object_supports_key", $supports, $object, $field_key );
641
	}
642
643
}
644