Passed
Pull Request — master (#394)
by Brian
05:43
created

object_supports_field()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 2
rs 10
cc 1
nc 1
nop 2
1
<?php
2
/**
3
 * GetPaid REST Posts controller class.
4
 *
5
 * Extends the GetPaid_REST_Controller class to provide functionalities for endpoints
6
 * that store 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_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
	 * Contains this controller's class name.
36
	 *
37
	 * @var string
38
	 */
39
	public $crud_class;
40
41
	/**
42
	 * Registers the routes for the objects of the controller.
43
	 *
44
	 * @since 1.0.19
45
	 *
46
	 * @see register_rest_route()
47
	 */
48
	public function register_namespace_routes( $namespace ) {
49
50
		register_rest_route(
51
			$namespace,
52
			'/' . $this->rest_base,
53
			array(
54
				array(
55
					'methods'             => WP_REST_Server::READABLE,
56
					'callback'            => array( $this, 'get_items' ),
57
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
58
					'args'                => $this->get_collection_params(),
59
				),
60
				array(
61
					'methods'             => WP_REST_Server::CREATABLE,
62
					'callback'            => array( $this, 'create_item' ),
63
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
64
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
65
				),
66
				'schema' => array( $this, 'get_public_item_schema' ),
67
			)
68
		);
69
70
		$get_item_args = array(
71
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
72
		);
73
74
		register_rest_route(
75
			$namespace,
76
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
77
			array(
78
				'args'   => array(
79
					'id' => array(
80
						'description' => __( 'Unique identifier for the object.', 'invoicing' ),
81
						'type'        => 'integer',
82
					),
83
				),
84
				array(
85
					'methods'             => WP_REST_Server::READABLE,
86
					'callback'            => array( $this, 'get_item' ),
87
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
88
					'args'                => $get_item_args,
89
				),
90
				array(
91
					'methods'             => WP_REST_Server::EDITABLE,
92
					'callback'            => array( $this, 'update_item' ),
93
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
94
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
95
				),
96
				array(
97
					'methods'             => WP_REST_Server::DELETABLE,
98
					'callback'            => array( $this, 'delete_item' ),
99
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
100
					'args'                => array(
101
						'force' => array(
102
							'type'        => 'boolean',
103
							'default'     => false,
104
							'description' => __( 'Whether to bypass Trash and force deletion.', 'invoicing' ),
105
						),
106
					),
107
				),
108
				'schema' => array( $this, 'get_public_item_schema' ),
109
			)
110
		);
111
112
		register_rest_route(
113
			$namespace,
114
			'/' . $this->rest_base . '/batch',
115
			array(
116
				array(
117
					'methods'             => WP_REST_Server::EDITABLE,
118
					'callback'            => array( $this, 'batch_items' ),
119
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
120
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
121
				),
122
				'schema' => array( $this, 'get_public_batch_schema' ),
123
			)
124
		);
125
126
	}
127
128
	/**
129
	 * Check permissions of items on REST API.
130
	 *
131
	 * @since 1.0.19
132
	 * @param string $context   Request context.
133
	 * @param int    $object_id Post ID.
134
	 * @return bool
135
	 */
136
	public function check_post_permissions( $context = 'read', $object_id = 0 ) {
137
138
		$contexts = array(
139
			'read'   => 'read_private_posts',
140
			'create' => 'publish_posts',
141
			'edit'   => 'edit_post',
142
			'delete' => 'delete_post',
143
			'batch'  => 'edit_others_posts',
144
		);
145
146
		if ( 'revision' === $this->post_type ) {
147
			$permission = false;
148
		} else {
149
			$cap              = $contexts[ $context ];
150
			$post_type_object = get_post_type_object( $this->post_type );
151
			$permission       = current_user_can( $post_type_object->cap->$cap, $object_id );
152
		}
153
154
		return apply_filters( 'getpaid_rest_check_permissions', $permission, $context, $object_id, $this->post_type );
155
	}
156
157
	/**
158
	 * Check if a given request has access to read items.
159
	 *
160
	 * @param  WP_REST_Request $request Full details about the request.
161
	 * @return WP_Error|boolean
162
	 */
163
	public function get_items_permissions_check( $request ) {
164
		return $this->check_post_permissions() ? true : new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot list resources.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
165
	}
166
167
	/**
168
	 * Check if a given request has access to create an item.
169
	 *
170
	 * @param  WP_REST_Request $request Full details about the request.
171
	 * @return WP_Error|boolean
172
	 */
173
	public function create_item_permissions_check( $request ) {
174
		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() ) );
175
	}
176
177
	/**
178
	 * Check if a given request has access to read an item.
179
	 *
180
	 * @param  WP_REST_Request $request Full details about the request.
181
	 * @return WP_Error|boolean
182
	 */
183
	public function get_item_permissions_check( $request ) {
184
		$post = get_post( (int) $request['id'] );
185
186
		if ( $post && ! $this->check_post_permissions( 'read', $post->ID ) ) {
187
			return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
188
		}
189
190
		return true;
191
	}
192
193
	/**
194
	 * Check if a given request has access to update an item.
195
	 *
196
	 * @param  WP_REST_Request $request Full details about the request.
197
	 * @return WP_Error|boolean
198
	 */
199
	public function update_item_permissions_check( $request ) {
200
		$post = get_post( (int) $request['id'] );
201
202
		if ( $post && ! $this->check_post_permissions( 'edit', $post->ID ) ) {
203
			return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
204
		}
205
206
		return true;
207
	}
208
209
	/**
210
	 * Check if a given request has access to delete an item.
211
	 *
212
	 * @param  WP_REST_Request $request Full details about the request.
213
	 * @return bool|WP_Error
214
	 */
215
	public function delete_item_permissions_check( $request ) {
216
		$post = get_post( (int) $request['id'] );
217
218
		if ( $post && ! $this->check_post_permissions( 'delete', $post->ID ) ) {
219
			return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
220
		}
221
222
		return true;
223
	}
224
225
	/**
226
	 * Check if a given request has access batch create, update and delete items.
227
	 *
228
	 * @param  WP_REST_Request $request Full details about the request.
229
	 *
230
	 * @return boolean|WP_Error
231
	 */
232
	public function batch_items_permissions_check( $request ) {
233
		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() ) );
234
	}
235
236
	/**
237
	 * Saves a single object.
238
	 *
239
	 * @param GetPaid_Data $object Object to save.
240
	 * @return WP_Error|GetPaid_Data
241
	 */
242
	protected function save_object( $object ) {
243
		$object->save();
244
245
		if ( ! empty( $object->last_error ) ) {
246
			return new WP_Error( 'rest_cannot_save', $object->last_error, array( 'status' => 400 ) );
247
		}
248
249
		return new $this->crud_class( $object->get_id() );
250
	}
251
252
	/**
253
	 * Returns the item's object.
254
	 *
255
	 * Child classes must implement this method.
256
	 * @since 1.0.13
257
	 *
258
	 * @param int|WP_Post $object_id Supplied ID.
259
	 * @return GetPaid_Data|WP_Error GetPaid_Data object if ID is valid, WP_Error otherwise.
260
	 */
261
	protected function get_object( $object_id ) {
262
263
		// Do we have an object?
264
		if ( empty( $this->crud_class ) || ! class_exists( $this->crud_class ) ) {
265
			return new WP_Error( 'no_crud_class', __( 'You need to specify a CRUD class for this controller', 'invoicing' ) );
266
		}
267
268
		// Fetch the object.
269
		$object = new $this->crud_class( $object_id );
270
		if ( ! empty( $object->last_error ) ) {
271
			return new WP_Error( 'rest_object_invalid_id', $object->last_error, array( 'status' => 404 ) );
272
		}
273
274
		return $object->get_id() ? $object : new WP_Error( 'rest_object_invalid_id', __( 'Invalid ID.', 'invoicing' ), array( 'status' => 404 ) );
275
276
	}
277
278
	/**
279
	 * @deprecated
280
	 */
281
	public function get_post( $object_id ) {
282
		return $this->get_object( $object_id );
283
    }
284
285
	/**
286
	 * Get a single object.
287
	 *
288
	 * @param WP_REST_Request $request Full details about the request.
289
	 * @return WP_Error|WP_REST_Response
290
	 */
291
	public function get_item( $request ) {
292
293
		// Fetch the item.
294
		$object = $this->get_object( $request['id'] );
295
296
		if ( is_wp_error( $object ) ) {
297
			return $object;
298
		}
299
300
		// Generate a response.
301
		$data     = $this->prepare_item_for_response( $object, $request );
0 ignored issues
show
Bug introduced by
$object of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_Posts_Contr...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

301
		$data     = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $object, $request );
Loading history...
302
		$response = rest_ensure_response( $data );
303
304
		// (Maybe) add a link to the html pagee.
305
		if ( $this->public && ! is_wp_error( $response ) ) {
306
			$response->link_header( 'alternate', get_permalink( $object->get_id() ), array( 'type' => 'text/html' ) );
0 ignored issues
show
Bug introduced by
The method get_id() 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

306
			$response->link_header( 'alternate', get_permalink( $object->/** @scrutinizer ignore-call */ 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...
Bug introduced by
It seems like get_permalink($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

306
			$response->link_header( 'alternate', /** @scrutinizer ignore-type */ get_permalink( $object->get_id() ), array( 'type' => 'text/html' ) );
Loading history...
307
		}
308
309
		return $response;
310
	}
311
312
	/**
313
	 * Create a single object.
314
	 *
315
	 * @param WP_REST_Request $request Full details about the request.
316
	 * @return WP_Error|WP_REST_Response
317
	 */
318
	public function create_item( $request ) {
319
320
		// Can not create an existing item.
321
		if ( ! empty( $request['id'] ) ) {
322
			/* translators: %s: post type */
323
			return new WP_Error( "getpaid_rest_{$this->post_type}_exists", __( 'Cannot create existing resource.', 'invoicing' ), array( 'status' => 400 ) );
324
		}
325
326
		// Generate a GetPaid_Data object from the request.
327
		$object = $this->prepare_item_for_database( $request );
328
		if ( is_wp_error( $object ) ) {
329
			return $object;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $object also could return the type GetPaid_Data which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
330
		}
331
332
		// Save the object.
333
		$object = $this->save_object( $object );
0 ignored issues
show
Bug introduced by
It seems like $object can also be of type WP_Error; however, parameter $object of GetPaid_REST_Posts_Controller::save_object() does only seem to accept GetPaid_Data, 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

333
		$object = $this->save_object( /** @scrutinizer ignore-type */ $object );
Loading history...
334
		if ( is_wp_error( $object ) ) {
335
			return $object;
336
		}
337
338
		// Save special fields.
339
		$save_special = $this->update_additional_fields_for_object( $object, $request );
340
		if ( is_wp_error( $save_special ) ) {
341
			$object->delete( true );
0 ignored issues
show
Bug introduced by
The method delete() 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

341
			$object->/** @scrutinizer ignore-call */ 
342
            delete( true );

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...
342
			return $save_special;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $save_special also could return the type boolean which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
343
		}
344
345
		/**
346
		 * Fires after a single item is created or updated via the REST API.
347
		 *
348
		 * @param WP_Post         $post      Post object.
349
		 * @param WP_REST_Request $request   Request object.
350
		 * @param boolean         $creating  True when creating item, false when updating.
351
		 */
352
		do_action( "getpaid_rest_insert_{$this->post_type}", $object, $request, true );
353
354
		$request->set_param( 'context', 'edit' );
355
		$response = $this->prepare_item_for_response( $object, $request );
0 ignored issues
show
Bug introduced by
$object of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_Posts_Contr...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

355
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $object, $request );
Loading history...
356
		$response = rest_ensure_response( $response );
357
		$response->set_status( 201 );
358
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) );
359
360
		return $response;
361
	}
362
363
	/**
364
	 * Update a single object.
365
	 *
366
	 * @param WP_REST_Request $request Full details about the request.
367
	 * @return WP_Error|WP_REST_Response
368
	 */
369
	public function update_item( $request ) {
370
371
		// Fetch the item.
372
		$object = $this->get_object( $request['id'] );
373
		if ( is_wp_error( $object ) ) {
374
			return $object;
375
		}
376
377
		// Prepare the item for saving.
378
		$object = $this->prepare_item_for_database( $request );
379
		if ( is_wp_error( $object ) ) {
380
			return $object;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $object also could return the type GetPaid_Data which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
381
		}
382
383
		// Save the item.
384
		$object = $this->save_object( $object );
0 ignored issues
show
Bug introduced by
It seems like $object can also be of type WP_Error; however, parameter $object of GetPaid_REST_Posts_Controller::save_object() does only seem to accept GetPaid_Data, 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

384
		$object = $this->save_object( /** @scrutinizer ignore-type */ $object );
Loading history...
385
		if ( is_wp_error( $object ) ) {
386
			return $object;
387
		}
388
389
		// Save special fields (those added via hooks).
390
		$save_special = $this->update_additional_fields_for_object( $object, $request );
391
		if ( is_wp_error( $save_special ) ) {
392
			return $save_special;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $save_special also could return the type boolean which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
393
		}
394
395
		/**
396
		 * Fires after a single item is created or updated via the REST API.
397
		 *
398
		 * @param GetPaid_Data    $object    GetPaid_Data object.
399
		 * @param WP_REST_Request $request   Request object.
400
		 * @param boolean         $creating  True when creating item, false when updating.
401
		 */
402
		do_action( "getpaid_rest_insert_{$this->post_type}", $object, $request, false );
403
404
		$request->set_param( 'context', 'edit' );
405
		$response = $this->prepare_item_for_response( $object, $request );
0 ignored issues
show
Bug introduced by
$object of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_Posts_Contr...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

405
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $object, $request );
Loading history...
406
		return rest_ensure_response( $response );
407
	}
408
409
	/**
410
	 * Get a collection of objects.
411
	 *
412
	 * @param WP_REST_Request $request Full details about the request.
413
	 * @return WP_Error|WP_REST_Response
414
	 */
415
	public function get_items( $request ) {
416
		$args                         = array();
417
		$args['offset']               = $request['offset'];
418
		$args['order']                = $request['order'];
419
		$args['orderby']              = $request['orderby'];
420
		$args['paged']                = $request['page'];
421
		$args['post__in']             = $request['include'];
422
		$args['post__not_in']         = $request['exclude'];
423
		$args['posts_per_page']       = $request['per_page'];
424
		$args['name']                 = $request['slug'];
425
		$args['post_parent__in']      = $request['parent'];
426
		$args['post_parent__not_in']  = $request['parent_exclude'];
427
		$args['s']                    = $request['search'];
428
		$args['post_status']          = wpinv_parse_list( $request['status'] );
429
430
		$args['date_query'] = array();
431
		// Set before into date query. Date query must be specified as an array of an array.
432
		if ( isset( $request['before'] ) ) {
433
			$args['date_query'][0]['before'] = $request['before'];
434
		}
435
436
		// Set after into date query. Date query must be specified as an array of an array.
437
		if ( isset( $request['after'] ) ) {
438
			$args['date_query'][0]['after'] = $request['after'];
439
		}
440
441
		// Force the post_type & fields arguments, since they're not a user input variable.
442
		$args['post_type'] = $this->post_type;
443
		$args['fields']    = 'ids';
444
445
		// Filter the query arguments for a request.
446
		$args       = apply_filters( "getpaid_rest_{$this->post_type}_query", $args, $request );
447
		$query_args = $this->prepare_items_query( $args, $request );
448
449
		$posts_query = new WP_Query();
450
		$query_result = $posts_query->query( $query_args );
451
452
		$posts = array();
453
		foreach ( $query_result as $post_id ) {
454
			if ( ! $this->check_post_permissions( 'read', $post_id ) ) {
455
				continue;
456
			}
457
458
			$data    = $this->prepare_item_for_response( $this->get_object( $post_id ), $request );
0 ignored issues
show
Bug introduced by
$this->get_object($post_id) of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_Posts_Contr...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

458
			$data    = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $this->get_object( $post_id ), $request );
Loading history...
459
			$posts[] = $this->prepare_response_for_collection( $data );
460
		}
461
462
		$page        = (int) $query_args['paged'];
463
		$total_posts = $posts_query->found_posts;
464
465
		if ( $total_posts < 1 ) {
466
			// Out-of-bounds, run the query again without LIMIT for total count.
467
			unset( $query_args['paged'] );
468
			$count_query = new WP_Query();
469
			$count_query->query( $query_args );
470
			$total_posts = $count_query->found_posts;
471
		}
472
473
		$max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] );
474
475
		$response = rest_ensure_response( $posts );
476
		$response->header( 'X-WP-Total', (int) $total_posts );
477
		$response->header( 'X-WP-TotalPages', (int) $max_pages );
478
479
		$request_params = $request->get_query_params();
480
		$base = add_query_arg( $request_params, rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
481
482
		if ( $page > 1 ) {
483
			$prev_page = $page - 1;
484
			if ( $prev_page > $max_pages ) {
485
				$prev_page = $max_pages;
486
			}
487
			$prev_link = add_query_arg( 'page', $prev_page, $base );
488
			$response->link_header( 'prev', $prev_link );
489
		}
490
		if ( $max_pages > $page ) {
491
			$next_page = $page + 1;
492
			$next_link = add_query_arg( 'page', $next_page, $base );
493
			$response->link_header( 'next', $next_link );
494
		}
495
496
		return $response;
497
	}
498
499
	/**
500
	 * Delete a single item.
501
	 *
502
	 * @param WP_REST_Request $request Full details about the request.
503
	 * @return WP_REST_Response|WP_Error
504
	 */
505
	public function delete_item( $request ) {
506
507
		// Fetch the item.
508
		$item = $this->get_object( $request['id'] );
509
		if ( is_wp_error( $item ) ) {
510
			return $item;
511
		}
512
513
		$supports_trash = EMPTY_TRASH_DAYS > 0;
514
		$force          = $supports_trash && (bool) $request['force'];
515
516
		if ( ! $this->check_post_permissions( 'delete', $item->ID ) ) {
0 ignored issues
show
Bug introduced by
The property ID does not seem to exist on WP_Error.
Loading history...
517
			return new WP_Error( "cannot_delete", __( 'Sorry, you are not allowed to delete this resource.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
518
		}
519
520
		$request->set_param( 'context', 'edit' );
521
		$response = $this->prepare_item_for_response( $item, $request );
0 ignored issues
show
Bug introduced by
$item of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_Posts_Contr...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

521
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $item, $request );
Loading history...
522
523
		if ( ! wp_delete_post( $item->ID, $force ) ) {
524
			return new WP_Error( 'rest_cannot_delete', sprintf( __( 'The resource cannot be deleted.', 'invoicing' ), $this->post_type ), array( 'status' => 500 ) );
525
		}
526
527
		return $response;
528
	}
529
530
	/**
531
	 * Prepare links for the request.
532
	 *
533
	 * @param GetPaid_Data    $object GetPaid_Data object.
534
	 * @return array Links for the given object.
535
	 */
536
	protected function prepare_links( $object ) {
537
538
		$links = array(
539
			'self'       => array(
540
				'href'   => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ),
541
			),
542
			'collection' => array(
543
				'href'   => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
544
			),
545
		);
546
547
		if ( is_callable( array( $object, 'get_user_id' ) ) ) {
548
			$links['user'] = array(
549
				'href'       => rest_url( 'wp/v2/users/' . call_user_func(  array( $object, 'get_user_id' )  ) ),
550
				'embeddable' => true,
551
			);
552
		}
553
554
		if ( is_callable( array( $object, 'get_owner' ) ) ) {
555
			$links['owner']  = array(
556
				'href'       => rest_url( 'wp/v2/users/' . call_user_func(  array( $object, 'get_owner' )  ) ),
557
				'embeddable' => true,
558
			);
559
		}
560
561
		if ( is_callable( array( $object, 'get_parent_id' ) ) ) {
562
			$links['parent']  = array(
563
				'href'       => rest_url( "$this->namespace/$this->rest_base/" . call_user_func(  array( $object, 'get_parent_id' )  ) ),
564
				'embeddable' => true,
565
			);
566
		}
567
568
		return $links;
569
	}
570
571
	/**
572
	 * Determine the allowed query_vars for a get_items() response and
573
	 * prepare for WP_Query.
574
	 *
575
	 * @param array           $prepared_args Prepared arguments.
576
	 * @param WP_REST_Request $request Request object.
577
	 * @return array          $query_args
578
	 */
579
	protected function prepare_items_query( $prepared_args = array(), $request = null ) {
580
581
		$valid_vars = array_flip( $this->get_allowed_query_vars() );
582
		$query_args = array();
583
		foreach ( $valid_vars as $var => $index ) {
584
			if ( isset( $prepared_args[ $var ] ) ) {
585
				$query_args[ $var ] = apply_filters( "getpaid_rest_query_var-{$var}", $prepared_args[ $var ] );
586
			}
587
		}
588
589
		$query_args['ignore_sticky_posts'] = true;
590
591
		if ( 'include' === $query_args['orderby'] ) {
592
			$query_args['orderby'] = 'post__in';
593
		} elseif ( 'id' === $query_args['orderby'] ) {
594
			$query_args['orderby'] = 'ID'; // ID must be capitalized.
595
		} elseif ( 'slug' === $query_args['orderby'] ) {
596
			$query_args['orderby'] = 'name';
597
		}
598
599
		return apply_filters( 'getpaid_rest_prepare_items_query', $query_args, $request, $this );
600
601
	}
602
603
	/**
604
	 * Get all the WP Query vars that are allowed for the API request.
605
	 *
606
	 * @return array
607
	 */
608
	protected function get_allowed_query_vars() {
609
		global $wp;
610
611
		/**
612
		 * Filter the publicly allowed query vars.
613
		 *
614
		 * Allows adjusting of the default query vars that are made public.
615
		 *
616
		 * @param array  Array of allowed WP_Query query vars.
617
		 */
618
		$valid_vars = apply_filters( 'query_vars', $wp->public_query_vars );
619
620
		$post_type_obj = get_post_type_object( $this->post_type );
621
		if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
622
			$private = apply_filters( 'getpaid_rest_private_query_vars', $wp->private_query_vars );
623
			$valid_vars = array_merge( $valid_vars, $private );
624
		}
625
626
		// Define our own in addition to WP's normal vars.
627
		$rest_valid = array(
628
			'post_status',
629
			'date_query',
630
			'ignore_sticky_posts',
631
			'offset',
632
			'post__in',
633
			'post__not_in',
634
			'post_parent',
635
			'post_parent__in',
636
			'post_parent__not_in',
637
			'posts_per_page',
638
			'meta_query',
639
			'tax_query',
640
			'meta_key',
641
			'meta_value',
642
			'meta_compare',
643
			'meta_value_num',
644
		);
645
		$valid_vars = array_merge( $valid_vars, $rest_valid );
646
647
		// Filter allowed query vars for the REST API.
648
		$valid_vars = apply_filters( 'getpaid_rest_query_vars', $valid_vars, $this );
649
650
		return $valid_vars;
651
	}
652
653
	/**
654
	 * Get the query params for collections of attachments.
655
	 *
656
	 * @return array
657
	 */
658
	public function get_collection_params() {
659
		$params = parent::get_collection_params();
660
661
		$params['context']['default'] = 'view';
662
663
		$params['status'] = array(
664
			'default'           => $this->get_post_statuses(),
665
			'description'       => __( 'Limit result set to resources assigned one or more statuses.', 'invoicing' ),
666
			'type'              => array( 'array', 'string' ),
667
			'items'             => array(
668
				'enum'          => $this->get_post_statuses(),
669
				'type'          => 'string',
670
			),
671
			'validate_callback' => 'rest_validate_request_arg',
672
			'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
673
		);
674
675
		$params['after'] = array(
676
			'description'        => __( 'Limit response to resources created after a given ISO8601 compliant date.', 'invoicing' ),
677
			'type'               => 'string',
678
			'format'             => 'string',
679
			'validate_callback'  => 'rest_validate_request_arg',
680
			'sanitize_callback'  => 'sanitize_text_field',
681
		);
682
		$params['before'] = array(
683
			'description'        => __( 'Limit response to resources created before a given ISO8601 compliant date.', 'invoicing' ),
684
			'type'               => 'string',
685
			'format'             => 'string',
686
			'validate_callback'  => 'rest_validate_request_arg',
687
			'sanitize_callback'  => 'sanitize_text_field',
688
		);
689
		$params['exclude'] = array(
690
			'description'       => __( 'Ensure result set excludes specific IDs.', 'invoicing' ),
691
			'type'              => 'array',
692
			'items'             => array(
693
				'type'          => 'integer',
694
			),
695
			'default'           => array(),
696
			'sanitize_callback' => 'wp_parse_id_list',
697
			'validate_callback' => 'rest_validate_request_arg',
698
		);
699
		$params['include'] = array(
700
			'description'       => __( 'Limit result set to specific ids.', 'invoicing' ),
701
			'type'              => 'array',
702
			'items'             => array(
703
				'type'          => 'integer',
704
			),
705
			'default'           => array(),
706
			'sanitize_callback' => 'wp_parse_id_list',
707
			'validate_callback' => 'rest_validate_request_arg',
708
		);
709
		$params['offset'] = array(
710
			'description'        => __( 'Offset the result set by a specific number of items.', 'invoicing' ),
711
			'type'               => 'integer',
712
			'sanitize_callback'  => 'absint',
713
			'validate_callback'  => 'rest_validate_request_arg',
714
		);
715
		$params['order'] = array(
716
			'description'        => __( 'Order sort attribute ascending or descending.', 'invoicing' ),
717
			'type'               => 'string',
718
			'default'            => 'desc',
719
			'enum'               => array( 'asc', 'desc' ),
720
			'validate_callback'  => 'rest_validate_request_arg',
721
		);
722
		$params['orderby'] = array(
723
			'description'        => __( 'Sort collection by object attribute.', 'invoicing' ),
724
			'type'               => 'string',
725
			'default'            => 'date',
726
			'enum'               => array(
727
				'date',
728
				'id',
729
				'include',
730
				'title',
731
				'slug',
732
				'modified',
733
			),
734
			'validate_callback'  => 'rest_validate_request_arg',
735
		);
736
737
		$post_type_obj = get_post_type_object( $this->post_type );
738
739
		if ( isset( $post_type_obj->hierarchical ) && $post_type_obj->hierarchical ) {
740
			$params['parent'] = array(
741
				'description'       => __( 'Limit result set to those of particular parent IDs.', 'invoicing' ),
742
				'type'              => 'array',
743
				'items'             => array(
744
					'type'          => 'integer',
745
				),
746
				'sanitize_callback' => 'wp_parse_id_list',
747
				'default'           => array(),
748
			);
749
			$params['parent_exclude'] = array(
750
				'description'       => __( 'Limit result set to all items except those of a particular parent ID.', 'invoicing' ),
751
				'type'              => 'array',
752
				'items'             => array(
753
					'type'          => 'integer',
754
				),
755
				'sanitize_callback' => 'wp_parse_id_list',
756
				'default'           => array(),
757
			);
758
		}
759
760
		return $params;
761
	}
762
763
	/**
764
	 * Retrieves the items's schema, conforming to JSON Schema.
765
	 *
766
	 * @since 1.0.19
767
	 *
768
	 * @return array Item schema data.
769
	 */
770
	public function get_item_schema() {
771
772
		// Maybe retrieve the schema from cache.
773
		if ( $this->schema ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->schema of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
774
			return $this->add_additional_fields_schema( $this->schema );
775
		}
776
777
		$type   = str_replace( 'wpi_', '', $this->post_type );
778
		$schema = array(
779
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
780
			'title'      => $this->post_type,
781
			'type'       => 'object',
782
			'properties' => wpinv_get_data( "$type-schema" ),
783
		);
784
785
		// Filters the invoice schema for the REST API.
786
        $schema = apply_filters( "wpinv_rest_{$type}_schema", $schema );
787
788
		// Cache the invoice schema.
789
		$this->schema = $schema;
790
791
		return $this->add_additional_fields_schema( $this->schema );
792
	}
793
794
	/**
795
	 * Only return writable props from schema.
796
	 *
797
	 * @param  array $schema Schema.
798
	 * @return bool
799
	 */
800
	public function filter_writable_props( $schema ) {
801
		return empty( $schema['readonly'] );
802
	}
803
804
	/**
805
	 * Sanitizes and validates the list of post statuses.
806
	 *
807
	 * @since 1.0.13
808
	 *
809
	 * @param string|array    $statuses  One or more post statuses.
810
	 * @param WP_REST_Request $request   Full details about the request.
811
	 * @param string          $parameter Additional parameter to pass to validation.
812
	 * @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
813
	 */
814
	public function sanitize_post_statuses( $statuses, $request, $parameter ) {
815
		return array_intersect( wp_parse_slug_list( $statuses ), $this->get_post_statuses() );
816
	}
817
818
	/**
819
	 * Retrieves a valid list of post statuses.
820
	 *
821
	 * @since 1.0.19
822
	 *
823
	 * @return array A list of registered item statuses.
824
	 */
825
	public function get_post_statuses() {
826
		return get_post_stati();
827
	}
828
829
	/**
830
	 * Prepare a single object for create or update.
831
	 *
832
	 * @since 1.0.19
833
	 * @param  WP_REST_Request $request Request object.
834
	 * @return GetPaid_Data|WP_Error Data object or WP_Error.
835
	 */
836
	protected function prepare_item_for_database( $request ) {
837
838
		// Do we have an object?
839
		if ( empty( $this->crud_class ) || ! class_exists( $this->crud_class ) ) {
840
			return new WP_Error( 'no_crud_class', __( 'You need to specify a CRUD class for this controller', 'invoicing' ) );
841
		}
842
843
		// Prepare the object.
844
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
845
		$object    = new $this->crud_class( $id );
846
847
		// Abort if an error exists.
848
		if ( ! empty( $object->last_error ) ) {
849
			return new WP_Error( 'invalid_item', $object->last_error );
850
		}
851
852
		$schema    = $this->get_item_schema();
853
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
854
855
		// Handle all writable props.
856
		foreach ( $data_keys as $key ) {
857
			$value = $request[ $key ];
858
859
			if ( ! is_null( $value ) ) {
860
				switch ( $key ) {
861
862
					case 'meta_data':
863
						if ( is_array( $value ) ) {
864
							foreach ( $value as $meta ) {
865
								$object->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
866
							}
867
						}
868
						break;
869
870
					default:
871
						if ( is_callable( array( $object, "set_{$key}" ) ) ) {
872
							$object->{"set_{$key}"}( $value );
873
						}
874
						break;
875
				}
876
			}
877
878
		}
879
880
		// Filters an object before it is inserted via the REST API..
881
		return apply_filters( "getpaid_rest_pre_insert_{$this->post_type}_object", $object, $request );
882
	}
883
884
	/**
885
	 * Retrieves data from a GetPaid class.
886
	 *
887
	 * @since  1.0.19
888
	 * @param  GetPaid_Meta_Data[]    $meta_data  meta data objects.
889
	 * @return array
890
	 */
891
	protected function prepare_object_meta_data( $meta_data ) {
892
		$meta = array();
893
894
		foreach( $meta_data as $object ) {
895
			$meta[] = $object->get_data();
896
		}
897
898
		return $meta;
899
	}
900
901
	/**
902
	 * Retrieves invoice items.
903
	 *
904
	 * @since  1.0.19
905
	 * @param  WPInv_Invoice $invoice  Invoice items.
906
	 * @return array
907
	 */
908
	protected function prepare_invoice_items( $invoice ) {
909
		$items = array();
910
911
		foreach( $invoice->get_items() as $item ) {
912
			$item_data = $item->prepare_data_for_saving();
913
914
			if ( 'amount' == $invoice->get_template() ) {
915
				$item_data['quantity'] = 1;
916
			}
917
918
			$items[] = $item_data;
919
		}
920
921
		return $items;
922
	}
923
924
	/**
925
	 * Retrieves data from a GetPaid class.
926
	 *
927
	 * @since  1.0.19
928
	 * @param  GetPaid_Data    $object  Data object.
929
	 * @param array            $fields Fields to include.
930
	 * @param string           $context either view or edit.
931
	 * @return array
932
	 */
933
	protected function prepare_object_data( $object, $fields, $context = 'view' ) {
934
935
		$data      = array();
936
		$schema    = $this->get_item_schema();
937
		$data_keys = array_keys( $schema['properties'] );
938
939
		// Handle all writable props.
940
		foreach ( $data_keys as $key ) {
941
942
			// Abort if it is not included.
943
			if ( ! empty( $fields ) && ! $this->is_field_included( $key, $fields ) ) {
944
				continue;
945
			}
946
947
			// Or this current object does not support the field.
948
			if ( ! $this->object_supports_field( $object, $key ) ) {
949
				continue;
950
			}
951
952
			// Handle meta data.
953
			if ( $key == 'meta_data' ) {
954
				$data['meta_data'] = $this->prepare_object_meta_data( $object->get_meta_data() );
955
				continue;
956
			}
957
958
			// Handle items.
959
			if ( $key == 'items' && is_a( $object, 'WPInv_Invoice' )  ) {
960
				$data['items'] = $this->prepare_invoice_items( $object );
961
				continue;
962
			}
963
964
			// Booleans.
965
			if ( is_callable( array( $object, $key ) ) ) {
966
				$data[ $key ] = $object->$key( $context );
967
				continue;
968
			}
969
970
			// Get object value.
971
			if ( is_callable( array( $object, "get_{$key}" ) ) ) {
972
				$value = $object->{"get_{$key}"}( $context );
973
974
				// If the value is an instance of GetPaid_Data...
975
				if ( is_a( $value, 'GetPaid_Data' ) ) {
976
					$value = $value->get_data( $context );
0 ignored issues
show
Unused Code introduced by
The call to GetPaid_Data::get_data() has too many arguments starting with $context. ( Ignorable by Annotation )

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

976
					/** @scrutinizer ignore-call */ 
977
     $value = $value->get_data( $context );

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
977
				}
978
979
				// For objects, retrieves it's properties.
980
				$data[ $key ] = is_object( $value ) ? get_object_vars( $value ) :  $value ;
981
				continue;
982
			}
983
984
			// The value does not exist on an object.
985
			$data[ $key ]     = apply_filters( "getpaid_{$this->post_type}_{$key}_object_data", null, $object );
986
987
		}
988
989
		return $data;
990
	}
991
992
	/**
993
	 * Checks if a key should be included in a response.
994
	 *
995
	 * @since  1.0.19
996
	 * @param  GetPaid_Data $object  Data object.
997
	 * @param  string       $field_key The key to check for.
998
	 * @return bool
999
	 */
1000
	public function object_supports_field( $object, $field_key ) {
1001
		return apply_filters( "getpaid_rest_{$this->post_type}_object_supports_key", true, $object, $field_key );
1002
	}
1003
1004
	/**
1005
	 * Prepare a single object output for response.
1006
	 *
1007
	 * @since  1.0.19
1008
	 * @param  GetPaid_Data    $object  Data object.
1009
	 * @param  WP_REST_Request $request Request object.
1010
	 * @return WP_REST_Response
1011
	 */
1012
	public function prepare_item_for_response( $object, $request ) {
1013
1014
		// Fetch the fields to include in this response.
1015
		$fields = $this->get_fields_for_response( $request );
1016
1017
		// Prepare object data.
1018
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
1019
		$data    = $this->prepare_object_data( $object, $fields, $context );
1020
		$data    = $this->add_additional_fields_to_object( $data, $request );
1021
		$data    = $this->filter_response_by_context( $data, $context );
1022
1023
		// Prepare the response.
1024
		$response = rest_ensure_response( $data );
1025
		$response->add_links( $this->prepare_links( $object, $request ) );
0 ignored issues
show
Unused Code introduced by
The call to GetPaid_REST_Posts_Controller::prepare_links() has too many arguments starting with $request. ( Ignorable by Annotation )

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

1025
		$response->add_links( $this->/** @scrutinizer ignore-call */ prepare_links( $object, $request ) );

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1026
1027
		// Filter item response.
1028
		return apply_filters( "getpaid_rest_prepare_{$this->post_type}_object", $response, $object, $request );
1029
	}
1030
1031
}
1032