Passed
Branch master (50908e)
by Stiofan
07:01
created

WPInv_REST_Items_Controller   F

Complexity

Total Complexity 118

Size/Duplication

Total Lines 1225
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 422
c 1
b 0
f 0
dl 0
loc 1225
rs 2
wmc 118

24 Methods

Rating   Name   Duplication   Size   Complexity  
A update_item() 0 54 5
A get_schema_links() 0 22 1
A create_item() 0 49 4
A register_routes() 0 11 1
A get_item_types() 0 2 1
A delete_item() 0 39 3
A get_collection_params() 0 87 1
A check_is_post_type_allowed() 0 2 1
A __construct() 0 7 1
A get_available_actions() 0 26 3
A get_item_permissions_check() 0 23 3
B get_item_schema() 0 169 2
F prepare_item_for_response() 0 107 20
A get_post() 0 15 3
A get_items_permissions_check() 0 7 3
A get_item() 0 26 2
C get_fields_for_response() 0 56 13
A prepare_links() 0 25 1
A delete_item_permissions_check() 0 22 3
A create_item_permissions_check() 0 22 5
F prepare_item_for_database() 0 88 30
A update_item_permissions_check() 0 17 4
A check_read_permission() 0 8 3
A get_items() 0 66 5

How to fix   Complexity   

Complex Class

Complex classes like WPInv_REST_Items_Controller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WPInv_REST_Items_Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * REST API Items controller
4
 *
5
 * Handles requests to the invoices endpoint.
6
 *
7
 * @package  Invoicing
8
 * @since    1.0.13
9
 */
10
11
if ( !defined( 'WPINC' ) ) {
12
    exit;
13
}
14
15
/**
16
 * REST API items controller class.
17
 *
18
 * @package Invoicing
19
 */
20
class WPInv_REST_Items_Controller extends WP_REST_Posts_Controller {
21
22
    /**
23
	 * Post type.
24
	 *
25
	 * @var string
26
	 */
27
	protected $post_type = 'wpi_item';
28
	
29
	/**
30
	 * Cached results of get_item_schema.
31
	 *
32
	 * @since 1.0.13
33
	 * @var array
34
	 */
35
	protected $schema;
36
37
    /**
38
	 * Constructor.
39
	 *
40
	 * @since 1.0.13
41
	 *
42
	 * @param string $namespace Api Namespace
43
	 */
44
	public function __construct( $namespace ) {
45
        
46
        // Set api namespace...
47
		$this->namespace = $namespace;
48
49
        // ... and the rest base
50
        $this->rest_base = 'items';
51
		
52
    }
53
	
54
	/**
55
	 * Registers the routes for the objects of the controller.
56
	 *
57
	 * @since 1.0.13
58
	 *
59
	 * @see register_rest_route()
60
	 */
61
	public function register_routes() {
62
63
		parent::register_routes();
64
65
		register_rest_route(
66
			$this->namespace,
67
			'/' . $this->rest_base . '/item-types',
68
			array(
69
				array(
70
					'methods'             => WP_REST_Server::READABLE,
71
					'callback'            => array( $this, 'get_item_types' ),
72
				),
73
			)
74
		);
75
76
	}
77
78
    /**
79
	 * Checks if a given request has access to read items.
80
     * 
81
	 *
82
	 * @since 1.0.13
83
	 *
84
	 * @param WP_REST_Request $request Full details about the request.
85
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
86
	 */
87
	public function get_items_permissions_check( $request ) {
88
	
89
		if ( current_user_can( 'manage_options' ) ||  current_user_can( 'manage_invoicing' ) ) {
90
			return true;
91
		}
92
93
		return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to view invoice items.', 'invoicing' ), array( 'status' => rest_authorization_required_code() ) );
94
95
    }
96
    
97
    /**
98
	 * Retrieves a collection of invoice items.
99
	 *
100
	 * @since 1.0.13
101
	 *
102
	 * @param WP_REST_Request $request Full details about the request.
103
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
104
	 */
105
	public function get_items( $request ) {
106
		
107
		// Retrieve the list of registered item query parameters.
108
        $registered = $this->get_collection_params();
109
        
110
        $args       = array();
111
112
        foreach( array_keys( $registered ) as $key ) {
113
114
            if( isset( $request[ $key] ) ) {
115
                $args[ $key ] = $request[ $key];
116
            }
117
118
        }
119
120
		/**
121
		 * Filters the wpinv_get_items arguments for items rest requests.
122
		 *
123
		 *
124
		 * @since 1.0.13
125
		 *
126
		 *
127
		 * @param array           $args    Key value array of query var to query value.
128
		 * @param WP_REST_Request $request The request used.
129
		 */
130
        $args       = apply_filters( "wpinv_rest_get_items_arguments", $args, $request, $this );
131
		
132
		// Special args
133
		$args[ 'return' ]   = 'objects';
134
		$args[ 'paginate' ] = true;
135
136
        // Run the query.
137
		$query = wpinv_get_all_items( $args );
138
		
139
		// Prepare the retrieved items
140
		$items = array();
141
		foreach( $query->items as $item ) {
142
143
			if ( ! $this->check_read_permission( $item ) ) {
144
				continue;
145
			}
146
147
			$data       = $this->prepare_item_for_response( $item, $request );
148
			$items[]    = $this->prepare_response_for_collection( $data );
149
150
		}
151
152
		// Prepare the response.
153
		$response = rest_ensure_response( $items );
154
		$response->header( 'X-WP-Total', (int) $query->total );
155
		$response->header( 'X-WP-TotalPages', (int) $query->max_num_pages );
156
157
		/**
158
		 * Filters the responses for item requests.
159
		 *
160
		 *
161
		 * @since 1.0.13
162
		 *
163
		 *
164
		 * @param arrWP_REST_Response $response    Response object.
165
		 * @param WP_REST_Request     $request The request used.
166
         * @param array               $args Array of args used to retrieve the items
167
		 */
168
        $response       = apply_filters( "wpinv_rest_items_response", $response, $request, $args );
169
170
        return rest_ensure_response( $response );
171
        
172
    }
173
174
    /**
175
	 * Get the post, if the ID is valid.
176
	 *
177
	 * @since 1.0.13
178
	 *
179
	 * @param int $item_id Supplied ID.
180
	 * @return WPInv_Item|WP_Error Item object if ID is valid, WP_Error otherwise.
181
	 */
182
	protected function get_post( $item_id ) {
183
		
184
		$error     = new WP_Error( 'rest_item_invalid_id', __( 'Invalid item ID.', 'invoicing' ), array( 'status' => 404 ) );
185
186
        // Ids start from 1
187
        if ( (int) $item_id <= 0 ) {
188
			return $error;
189
		}
190
191
		$item = wpinv_get_item_by( 'id', (int) $item_id );
192
		if ( empty( $item ) ) {
193
			return $error;
194
        }
195
196
        return $item;
197
198
    }
199
200
    /**
201
	 * Checks if a given request has access to read an invoice item.
202
	 *
203
	 * @since 1.0.13
204
	 *
205
	 * @param WP_REST_Request $request Full details about the request.
206
	 * @return bool|WP_Error True if the request has read access for the invoice item, WP_Error object otherwise.
207
	 */
208
	public function get_item_permissions_check( $request ) {
209
210
        // Retrieve the item object.
211
        $item = $this->get_post( $request['id'] );
212
        
213
        // Ensure it is valid.
214
		if ( is_wp_error( $item ) ) {
215
			return $item;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $item also could return the type WPInv_Item which is incompatible with the documented return type WP_Error|boolean.
Loading history...
216
		}
217
218
		$post_type = get_post_type_object( $this->post_type );
219
220
		if ( ! current_user_can(  $post_type->cap->read_post, $item->ID  ) ) {
0 ignored issues
show
Bug introduced by
The property ID does not seem to exist on WP_Error.
Loading history...
221
			return new WP_Error( 
222
                'rest_cannot_edit', 
223
                __( 'Sorry, you are not allowed to view this item.', 'invoicing' ), 
224
                array( 
225
                    'status' => rest_authorization_required_code(),
226
                )
227
            );
228
        }
229
230
		return $this->check_read_permission( $item );
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type WP_Error; however, parameter $item of WPInv_REST_Items_Control...check_read_permission() does only seem to accept WPInv_Item, 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

230
		return $this->check_read_permission( /** @scrutinizer ignore-type */ $item );
Loading history...
231
    }
232
    
233
    /**
234
	 * Checks if an item can be read.
235
	 * 
236
	 * An item can be read by site admins.
237
	 *
238
	 *
239
	 * @since 1.0.13
240
	 *
241
	 * @param WPInv_Item $item WPInv_Item object.
242
	 * @return bool Whether the post can be read.
243
	 */
244
	public function check_read_permission( $item ) {
245
246
		// An item can be read by an admin...
247
		if ( current_user_can( 'manage_options' ) ||  current_user_can( 'manage_invoicing' ) ) {
248
			return true;
249
		}
250
251
		return false;
252
    }
253
    
254
    /**
255
	 * Retrieves a single invoice item.
256
	 *
257
	 * @since 1.0.13
258
	 *
259
	 * @param WP_REST_Request $request Full details about the request.
260
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
261
	 */
262
	public function get_item( $request ) {
263
264
        // Fetch the item.
265
        $item = $this->get_post( $request['id'] );
266
        
267
        // Abort early if it does not exist
268
		if ( is_wp_error( $item ) ) {
269
			return $item;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $item also could return the type WPInv_Item which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
270
		}
271
272
		// Prepare the response
273
		$response = $this->prepare_item_for_response( $item, $request );
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type WP_Error; however, parameter $item of WPInv_REST_Items_Control...are_item_for_response() does only seem to accept WPInv_Item, 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

273
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $item, $request );
Loading history...
274
275
		/**
276
		 * Filters the responses for single invoice item requests.
277
		 *
278
		 *
279
		 * @since 1.0.13
280
		 * @var WP_HTTP_Response
281
		 *
282
		 * @param WP_HTTP_Response $response Response.
283
		 * @param WP_REST_Request  $request The request used.
284
		 */
285
        $response       = apply_filters( "wpinv_rest_get_item_response", $response, $request );
286
287
        return rest_ensure_response( $response );
288
289
    }
290
    
291
    /**
292
	 * Checks if a given request has access to create an invoice item.
293
	 *
294
	 * @since 1.0.13
295
	 *
296
	 * @param WP_REST_Request $request Full details about the request.
297
	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
298
	 */
299
	public function create_item_permissions_check( $request ) {
300
	
301
		if ( ! empty( $request['id'] ) ) {
302
			return new WP_Error( 'rest_item_exists', __( 'Cannot create existing item.', 'invoicing' ), array( 'status' => 400 ) );
303
		}
304
305
		if ( current_user_can( 'manage_options' ) ||  current_user_can( 'manage_invoicing' ) ) {
306
			return true;
307
		}
308
309
		$post_type = get_post_type_object( $this->post_type );
310
		if ( ! current_user_can( $post_type->cap->create_posts ) ) {
311
			return new WP_Error( 
312
                'rest_cannot_create', 
313
                __( 'Sorry, you are not allowed to create invoice items as this user.', 'invoicing' ), 
314
                array( 
315
                    'status' => rest_authorization_required_code(),
316
                )
317
            );
318
        }
319
320
		return true;
321
    }
322
    
323
    /**
324
	 * Creates a single invoice item.
325
	 *
326
	 * @since 1.0.13
327
	 *
328
	 * @param WP_REST_Request $request Full details about the request.
329
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
330
	 */
331
	public function create_item( $request ) {
332
333
		if ( ! empty( $request['id'] ) ) {
334
			return new WP_Error( 'rest_item_exists', __( 'Cannot create existing invoice item.', 'invoicing' ), array( 'status' => 400 ) );
335
		}
336
337
		$request->set_param( 'context', 'edit' );
338
339
		// Prepare the updated data.
340
		$item_data = $this->prepare_item_for_database( $request );
341
342
		if ( is_wp_error( $item_data ) ) {
343
			return $item_data;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $item_data also could return the type array which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
344
		}
345
346
		// Try creating the item.
347
        $item = wpinv_create_item( $item_data, true );
348
349
		if ( is_wp_error( $item ) ) {
350
            return $item;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $item also could return the type WPInv_Item|boolean|integer which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
351
		}
352
353
		// Prepare the response
354
		$response = $this->prepare_item_for_response( $item, $request );
355
356
		/**
357
		 * Fires after a single invoice item is created or updated via the REST API.
358
		 *
359
		 * @since 1.0.13
360
		 *
361
		 * @param WPinv_Item   $item  Inserted or updated item object.
362
		 * @param WP_REST_Request $request  Request object.
363
		 * @param bool            $creating True when creating a post, false when updating.
364
		 */
365
		do_action( "wpinv_rest_insert_item", $item, $request, true );
366
367
		/**
368
		 * Filters the responses for creating single item requests.
369
		 *
370
		 *
371
		 * @since 1.0.13
372
		 *
373
		 *
374
		 * @param array           $item_data Invoice properties.
375
		 * @param WP_REST_Request $request The request used.
376
		 */
377
        $response       = apply_filters( "wpinv_rest_create_item_response", $response, $request );
378
379
        return rest_ensure_response( $response );
380
	}
381
382
	/**
383
	 * Checks if a given request has access to update an item.
384
	 *
385
	 * @since 1.0.13
386
	 *
387
	 * @param WP_REST_Request $request Full details about the request.
388
	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
389
	 */
390
	public function update_item_permissions_check( $request ) {
391
392
		// Retrieve the item.
393
		$item = $this->get_post( $request['id'] );
394
		if ( is_wp_error( $item ) ) {
395
			return $item;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $item also could return the type WPInv_Item which is incompatible with the documented return type WP_Error|true.
Loading history...
396
		}
397
398
		if ( current_user_can( 'manage_options' ) ||  current_user_can( 'manage_invoicing' ) ) {
399
			return true;
400
		}
401
402
		return new WP_Error( 
403
			'rest_cannot_edit', 
404
			__( 'Sorry, you are not allowed to update this item.', 'invoicing' ), 
405
			array( 
406
				'status' => rest_authorization_required_code(),
407
			)
408
		);
409
410
	}
411
412
	/**
413
	 * Updates a single item.
414
	 *
415
	 * @since 1.0.13
416
	 *
417
	 * @param WP_REST_Request $request Full details about the request.
418
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
419
	 */
420
	public function update_item( $request ) {
421
		
422
		// Ensure the item exists.
423
        $valid_check = $this->get_post( $request['id'] );
424
        
425
        // Abort early if it does not exist
426
		if ( is_wp_error( $valid_check ) ) {
427
			return $valid_check;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $valid_check also could return the type WPInv_Item which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
428
		}
429
430
		$request->set_param( 'context', 'edit' );
431
432
		// Prepare the updated data.
433
		$data_to_update = $this->prepare_item_for_database( $request );
434
435
		if ( is_wp_error( $data_to_update ) ) {
436
			return $data_to_update;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $data_to_update also could return the type array which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
437
		}
438
439
		// Abort if no item data is provided
440
        if( empty( $data_to_update ) ) {
441
            return new WP_Error( 'missing_data', __( 'An update request cannot be empty.', 'invoicing' ) );
442
        }
443
444
		// Include the item ID
445
		$data_to_update['ID'] = $request['id'];
446
447
		// Update the item
448
		$updated_item = wpinv_update_item( $data_to_update, true );
449
450
		// Incase the update operation failed...
451
		if ( is_wp_error( $updated_item ) ) {
452
			return $updated_item;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $updated_item also could return the type WPInv_Item|boolean|integer which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
453
		}
454
455
		// Prepare the response
456
		$response = $this->prepare_item_for_response( $updated_item, $request );
457
458
		/** This action is documented in includes/class-wpinv-rest-item-controller.php */
459
		do_action( "wpinv_rest_insert_item", $updated_item, $request, false );
460
461
		/**
462
		 * Filters the responses for updating single item requests.
463
		 *
464
		 *
465
		 * @since 1.0.13
466
		 *
467
		 *
468
		 * @param array           $item_data Item properties.
469
		 * @param WP_REST_Request $request The request used.
470
		 */
471
        $response       = apply_filters( "wpinv_rest_update_item_response", $response,  $data_to_update, $request );
472
473
        return rest_ensure_response( $response );
474
	}
475
476
	/**
477
	 * Checks if a given request has access to delete an item.
478
	 *
479
	 * @since 1.0.13
480
	 *
481
	 * @param WP_REST_Request $request Full details about the request.
482
	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
483
	 */
484
	public function delete_item_permissions_check( $request ) {
485
486
		// Retrieve the item.
487
		$item = $this->get_post( $request['id'] );
488
		if ( is_wp_error( $item ) ) {
489
			return $item;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $item also could return the type WPInv_Item which is incompatible with the documented return type WP_Error|true.
Loading history...
490
		}
491
492
		// 
493
494
		// Ensure the current user can delete the item
495
		if (! wpinv_can_delete_item( $request['id'] ) ) {
496
			return new WP_Error( 
497
                'rest_cannot_delete', 
498
                __( 'Sorry, you are not allowed to delete this item.', 'invoicing' ), 
499
                array( 
500
                    'status' => rest_authorization_required_code(),
501
                )
502
            );
503
		}
504
505
		return true;
506
	}
507
508
	/**
509
	 * Deletes a single item.
510
	 *
511
	 * @since 1.0.13
512
	 *
513
	 * @param WP_REST_Request $request Full details about the request.
514
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
515
	 */
516
	public function delete_item( $request ) {
517
		
518
		// Retrieve the item.
519
		$item = $this->get_post( $request['id'] );
520
		if ( is_wp_error( $item ) ) {
521
			return $item;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $item also could return the type WPInv_Item which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
522
		}
523
524
		$request->set_param( 'context', 'edit' );
525
526
		// Prepare the item id
527
		$id    = $item->ID;
0 ignored issues
show
Bug introduced by
The property ID does not seem to exist on WP_Error.
Loading history...
528
529
		// Prepare the response
530
		$response = $this->prepare_item_for_response( $item, $request );
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type WP_Error; however, parameter $item of WPInv_REST_Items_Control...are_item_for_response() does only seem to accept WPInv_Item, 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

530
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $item, $request );
Loading history...
531
532
		// Check if the user wants to bypass the trash...
533
		$force_delete = (bool) $request['force'];
534
535
		// Try deleting the item.
536
		$deleted = wp_delete_post( $id, $force_delete );
537
538
		// Abort early if we can't delete the item.
539
		if ( ! $deleted ) {
540
			return new WP_Error( 'rest_cannot_delete', __( 'The item cannot be deleted.', 'invoicing' ), array( 'status' => 500 ) );
541
		}
542
543
		/**
544
		 * Fires immediately after a single item is deleted or trashed via the REST API.
545
		 *
546
		 *
547
		 * @since 1.0.13
548
		 *
549
		 * @param WPInv_Item    $item  The deleted or trashed item.
550
		 * @param WP_REST_Request  $request  The request sent to the API.
551
		 */
552
		do_action( "wpinv_rest_delete_item", $item, $request );
553
554
		return $response;
555
556
	}
557
    
558
    
559
    /**
560
	 * Retrieves the query params for the items collection.
561
	 *
562
	 * @since 1.0.13
563
	 *
564
	 * @return array Collection parameters.
565
	 */
566
	public function get_collection_params() {
567
        
568
        $query_params               = array(
569
570
            // Invoice status.
571
            'status'                => array(
572
                'default'           => 'publish',
573
                'description'       => __( 'Limit result set to items assigned one or more statuses.', 'invoicing' ),
574
                'type'              => 'array',
575
                'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
576
            ),
577
            
578
            // Item types
579
            'type'                  => array(
580
				'description'       => __( 'Type of items to fetch.', 'invoicing' ),
581
				'type'              => 'array',
582
				'default'           => wpinv_item_types(),
583
				'items'             => array(
584
                    'enum'          => wpinv_item_types(),
585
                    'type'          => 'string',
586
                ),
587
			),
588
			
589
			// Number of results per page
590
            'limit'                 => array(
591
				'description'       => __( 'Number of items to fetch.', 'invoicing' ),
592
				'type'              => 'integer',
593
				'default'           => (int) get_option( 'posts_per_page' ),
594
            ),
595
596
            // Pagination
597
            'page'     => array(
598
				'description'       => __( 'Current page to fetch.', 'invoicing' ),
599
				'type'              => 'integer',
600
				'default'           => 1,
601
            ),
602
603
            // Exclude certain items
604
            'exclude'  => array(
605
                'description' => __( 'Ensure result set excludes specific IDs.', 'invoicing' ),
606
                'type'        => 'array',
607
                'items'       => array(
608
                    'type' => 'integer',
609
                ),
610
                'default'     => array(),
611
            ),
612
613
            // Order items by
614
            'orderby'  => array(
615
                'description' => __( 'Sort items by object attribute.', 'invoicing' ),
616
                'type'        => 'string',
617
                'default'     => 'date',
618
                'enum'        => array(
619
                    'author',
620
                    'date',
621
                    'ID',
622
                    'modified',
623
					'title',
624
					'relevance',
625
					'rand'
626
                ),
627
            ),
628
629
            // How to order
630
            'order'    => array(
631
                'description' => __( 'Order sort attribute ascending or descending.', 'invoicing' ),
632
                'type'        => 'string',
633
                'default'     => 'DESC',
634
                'enum'        => array( 'ASC', 'DESC' ),
635
			),
636
			
637
			// Search term
638
            'search'                => array(
639
				'description'       => __( 'Return items that match the search term.', 'invoicing' ),
640
				'type'              => 'string',
641
            ),
642
        );
643
644
		/**
645
		 * Filter collection parameters for the items controller.
646
		 *
647
		 *
648
		 * @since 1.0.13
649
		 *
650
		 * @param array        $query_params JSON Schema-formatted collection parameters.
651
		 */
652
		return apply_filters( "wpinv_rest_items_collection_params", $query_params );
653
    }
654
    
655
    /**
656
	 * Checks if a given post type can be viewed or managed.
657
	 *
658
	 * @since 1.0.13
659
	 *
660
	 * @param object|string $post_type Post type name or object.
661
	 * @return bool Whether the post type is allowed in REST.
662
	 */
663
	protected function check_is_post_type_allowed( $post_type ) {
664
		return true;
665
	}
666
667
	/**
668
	 * Prepares a single item for create or update.
669
	 *
670
	 * @since 1.0.13
671
	 *
672
	 * @param WP_REST_Request $request Request object.
673
	 * @return array|WP_Error Invoice Properties or WP_Error.
674
	 */
675
	protected function prepare_item_for_database( $request ) {
676
		$prepared_item = new stdClass();
677
678
		// Post ID.
679
		if ( isset( $request['id'] ) ) {
680
			$existing_item = $this->get_post( $request['id'] );
681
			if ( is_wp_error( $existing_item ) ) {
682
				return $existing_item;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $existing_item also could return the type WPInv_Item which is incompatible with the documented return type WP_Error|array.
Loading history...
683
			}
684
685
			$prepared_item->ID 		  = $existing_item->ID;
0 ignored issues
show
Bug introduced by
The property ID does not seem to exist on WP_Error.
Loading history...
686
		}
687
688
		$schema = $this->get_item_schema();
689
690
		// item title.
691
		if ( ! empty( $schema['properties']['name'] ) && isset( $request['name'] ) ) {
692
			$prepared_item->title = sanitize_text_field( $request['name'] );
693
		}
694
695
		// item summary.
696
		if ( ! empty( $schema['properties']['summary'] ) && isset( $request['summary'] ) ) {
697
			$prepared_item->excerpt = wp_kses_post( $request['summary'] );
698
		}
699
700
		// item price.
701
		if ( ! empty( $schema['properties']['price'] ) && isset( $request['price'] ) ) {
702
			$prepared_item->price = floatval( $request['price'] );
703
		}
704
705
		// minimum price (for dynamc items).
706
		if ( ! empty( $schema['properties']['minimum_price'] ) && isset( $request['minimum_price'] ) ) {
707
			$prepared_item->minimum_price = floatval( $request['minimum_price'] );
708
		}
709
710
		// item status.
711
		if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) {
712
			$prepared_item->status = 'publish' === $request['status'] ? 'publish' : 'pending';
713
		}
714
715
		// item type.
716
		if ( ! empty( $schema['properties']['type'] ) && isset( $request['type'] ) ) {
717
			$prepared_item->type = in_array( $request['type'], wpinv_item_types() ) ? trim( strtolower( $request['type'] ) ) : 'custom';
718
		}
719
720
		// VAT rule.
721
		if ( ! empty( $schema['properties']['vat_rule'] ) && isset( $request['vat_rule'] ) ) {
722
			$prepared_item->vat_rule = 'digital' === $request['vat_rule'] ? 'digital' : 'physical';
723
		}
724
725
		// Simple strings.
726
		foreach( array( 'custom_id', 'custom_name', 'custom_singular_name' ) as $property ) {
727
728
			if ( ! empty( $schema['properties'][$property] ) && isset( $request[$property] ) ) {
729
				$prepared_item->$property = sanitize_text_field( $request[$property] );
730
			}
731
732
		}
733
734
		// Simple integers.
735
		foreach( array( 'is_recurring', 'recurring_interval', 'recurring_limit', 'free_trial', 'trial_interval', 'dynamic_pricing', 'editable' ) as $property ) {
736
737
			if ( ! empty( $schema['properties'][$property] ) && isset( $request[$property] ) ) {
738
				$prepared_item->$property = intval( $request[$property] );
739
			}
740
741
		}
742
743
		// Time periods.
744
		foreach( array( 'recurring_period',  'trial_period' ) as $property ) {
745
746
			if ( ! empty( $schema['properties'][$property] ) && isset( $request[$property] ) ) {
747
				$prepared_item->$property = in_array( $request[$property], array( 'D', 'W', 'M', 'Y' ) ) ? trim( strtoupper( $request[$property] ) ) : 'D';
748
			}
749
750
		}
751
752
		$item_data = (array) wp_unslash( $prepared_item );
0 ignored issues
show
Bug introduced by
$prepared_item of type stdClass is incompatible with the type array|string expected by parameter $value of wp_unslash(). ( Ignorable by Annotation )

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

752
		$item_data = (array) wp_unslash( /** @scrutinizer ignore-type */ $prepared_item );
Loading history...
753
754
		/**
755
		 * Filters an item before it is inserted via the REST API.
756
		 *
757
		 * @since 1.0.13
758
		 *
759
		 * @param array        $item_data An array of item data
760
		 * @param WP_REST_Request $request       Request object.
761
		 */
762
		return apply_filters( "wpinv_rest_pre_insert_item", $item_data, $request );
763
764
	}
765
766
	/**
767
	 * Prepares a single item output for response.
768
	 *
769
	 * @since 1.0.13
770
	 *
771
	 * @param WPInv_Item   $item    item object.
772
	 * @param WP_REST_Request $request Request object.
773
	 * @return WP_REST_Response Response object.
774
	 */
775
	public function prepare_item_for_response( $item, $request ) {
776
777
		$GLOBALS['post'] = get_post( $item->get_ID() );
778
779
		setup_postdata( $item->get_ID() );
780
781
		// Fetch the fields to include in this response.
782
		$fields = $this->get_fields_for_response( $request );
783
784
		// Base fields for every item.
785
		$data = array();
786
787
		// Set up ID
788
		if ( rest_is_field_included( 'id', $fields ) ) {
789
			$data['id'] = $item->get_ID();
790
		}
791
792
793
		// Item properties
794
		$item_properties = array(
795
			'name', 'summary', 'price', 'status', 'type',
796
			'vat_rule', 'vat_class',
797
			'custom_id', 'custom_name', 'custom_singular_name', 
798
			'editable'
799
		);
800
801
		foreach( $item_properties as $property ) {
802
803
			if ( rest_is_field_included( $property, $fields ) && method_exists( $item, 'get_' . $property ) ) {
804
				$data[$property] = call_user_func( array( $item, 'get_' . $property ) );
805
			}
806
807
		}
808
809
		// Dynamic pricing.
810
		if( $item->supports_dynamic_pricing() ) {
811
812
			if( rest_is_field_included( 'dynamic_pricing', $fields ) ) {
813
				$data['dynamic_pricing'] = $item->get_is_dynamic_pricing();
814
			}
815
816
			if( rest_is_field_included( 'minimum_price', $fields ) ) {
817
				$data['minimum_price'] = $item->get_minimum_price();
818
			}
819
		}
820
821
		// Subscriptions.
822
		if( rest_is_field_included( 'is_recurring', $fields ) ) {
823
			$data['is_recurring'] = $item->get_is_recurring();
824
		}
825
826
		if( $item->is_recurring() ) {
827
828
			$recurring_fields = array( 'is_recurring', 'recurring_period', 'recurring_interval', 'recurring_limit', 'free_trial' );
829
			foreach( $recurring_fields as $field ) {
830
831
				if ( rest_is_field_included( $field, $fields ) && method_exists( $item, 'get_' . $field ) ) {
832
					$data[$field] = call_user_func( array( $item, 'get_' . $field ) );
833
				}
834
	
835
			}
836
837
			if( $item->has_free_trial() ) {
838
839
				$trial_fields = array( 'trial_period', 'trial_interval' );
840
				foreach( $trial_fields as $field ) {
841
842
					if ( rest_is_field_included( $field, $fields ) && method_exists( $item, 'get_' . $field ) ) {
843
						$data[$field] = call_user_func( array( $item, 'get_' . $field ) );
844
					}
845
	
846
				}
847
848
			}
849
850
		}
851
852
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
853
		$data    = $this->add_additional_fields_to_object( $data, $request );
854
		$data    = $this->filter_response_by_context( $data, $context );
855
856
		// Wrap the data in a response object.
857
		$response = rest_ensure_response( $data );
858
859
		$links = $this->prepare_links( $item );
860
		$response->add_links( $links );
861
862
		if ( ! empty( $links['self']['href'] ) ) {
863
			$actions = $this->get_available_actions( $item, $request );
864
865
			$self = $links['self']['href'];
866
867
			foreach ( $actions as $rel ) {
868
				$response->add_link( $rel, $self );
869
			}
870
		}
871
872
		/**
873
		 * Filters the item data for a response.
874
		 *
875
		 * @since 1.0.13
876
		 *
877
		 * @param WP_REST_Response $response The response object.
878
		 * @param WPInv_Item    $item  The item object.
879
		 * @param WP_REST_Request  $request  Request object.
880
		 */
881
		return apply_filters( "wpinv_rest_prepare_item", $response, $item, $request );
0 ignored issues
show
Bug Best Practice introduced by
The expression return apply_filters('wp...ponse, $item, $request) also could return the type array which is incompatible with the documented return type WP_REST_Response.
Loading history...
882
	}
883
884
	/**
885
	 * Gets an array of fields to be included on the response.
886
	 *
887
	 * Included fields are based on item schema and `_fields=` request argument.
888
	 *
889
	 * @since 1.0.13
890
	 *
891
	 * @param WP_REST_Request $request Full details about the request.
892
	 * @return array Fields to be included in the response.
893
	 */
894
	public function get_fields_for_response( $request ) {
895
		$schema     = $this->get_item_schema();
896
		$properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
897
898
		$additional_fields = $this->get_additional_fields();
899
		foreach ( $additional_fields as $field_name => $field_options ) {
900
			// For back-compat, include any field with an empty schema
901
			// because it won't be present in $this->get_item_schema().
902
			if ( is_null( $field_options['schema'] ) ) {
903
				$properties[ $field_name ] = $field_options;
904
			}
905
		}
906
907
		// Exclude fields that specify a different context than the request context.
908
		$context = $request['context'];
909
		if ( $context ) {
910
			foreach ( $properties as $name => $options ) {
911
				if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
912
					unset( $properties[ $name ] );
913
				}
914
			}
915
		}
916
917
		$fields = array_keys( $properties );
918
919
		if ( ! isset( $request['_fields'] ) ) {
920
			return $fields;
921
		}
922
		$requested_fields = wpinv_parse_list( $request['_fields'] );
923
		if ( 0 === count( $requested_fields ) ) {
924
			return $fields;
925
		}
926
		// Trim off outside whitespace from the comma delimited list.
927
		$requested_fields = array_map( 'trim', $requested_fields );
928
		// Always persist 'id', because it can be needed for add_additional_fields_to_object().
929
		if ( in_array( 'id', $fields, true ) ) {
930
			$requested_fields[] = 'id';
931
		}
932
		// Return the list of all requested fields which appear in the schema.
933
		return array_reduce(
934
			$requested_fields,
935
			function( $response_fields, $field ) use ( $fields ) {
936
				if ( in_array( $field, $fields, true ) ) {
937
					$response_fields[] = $field;
938
					return $response_fields;
939
				}
940
				// Check for nested fields if $field is not a direct match.
941
				$nested_fields = explode( '.', $field );
942
				// A nested field is included so long as its top-level property is
943
				// present in the schema.
944
				if ( in_array( $nested_fields[0], $fields, true ) ) {
945
					$response_fields[] = $field;
946
				}
947
				return $response_fields;
948
			},
949
			array()
950
		);
951
	}
952
953
	/**
954
	 * Retrieves the item's schema, conforming to JSON Schema.
955
	 *
956
	 * @since 1.0.13
957
	 *
958
	 * @return array Item schema data.
959
	 */
960
	public function get_item_schema() {
961
962
		// Maybe retrieve the schema from cache.
963
		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...
964
			return $this->add_additional_fields_schema( $this->schema );
965
		}
966
967
		$schema = array(
968
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
969
			'title'      => $this->post_type,
970
			'type'       => 'object',
971
972
			// Base properties for every Item.
973
			'properties' 		  => array(
974
975
				'id'           => array(
976
					'description' => __( 'Unique identifier for the item.', 'invoicing' ),
977
					'type'        => 'integer',
978
					'context'     => array( 'view', 'edit', 'embed' ),
979
					'readonly'    => true,
980
				),
981
982
				'name'			  => array(
983
					'description' => __( 'The name for the item.', 'invoicing' ),
984
					'type'        => 'string',
985
					'context'     => array( 'view', 'edit', 'embed' ),
986
				),
987
988
				'summary'        => array(
989
					'description' => __( 'A summary for the item.', 'invoicing' ),
990
					'type'        => 'string',
991
					'context'     => array( 'view', 'edit', 'embed' ),
992
				),
993
994
				'price'        => array(
995
					'description' => __( 'The price for the item.', 'invoicing' ),
996
					'type'        => 'number',
997
					'context'     => array( 'view', 'edit', 'embed' ),
998
				),
999
1000
				'status'       => array(
1001
					'description' => __( 'A named status for the item.', 'invoicing' ),
1002
					'type'        => 'string',
1003
					'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
1004
					'context'     => array( 'view', 'edit' ),
1005
				),
1006
1007
				'type'       => array(
1008
					'description' => __( 'The item type.', 'invoicing' ),
1009
					'type'        => 'string',
1010
					'enum'        => wpinv_item_types(),
1011
					'context'     => array( 'view', 'edit', 'embed' ),
1012
				),
1013
1014
				'vat_rule'       => array(
1015
					'description' => __( 'VAT rule applied to the item.', 'invoicing' ),
1016
					'type'        => 'string',
1017
					'enum'        => array( 'digital', 'physical' ),
1018
					'context'     => array( 'view', 'edit' ),
1019
				),
1020
1021
				'vat_class'       => array(
1022
					'description' => __( 'VAT class for the item.', 'invoicing' ),
1023
					'type'        => 'string',
1024
					'context'     => array( 'view', 'edit' ),
1025
					'readonly'    => true,
1026
				),
1027
1028
				'custom_id'       => array(
1029
					'description' => __( 'Custom id for the item.', 'invoicing' ),
1030
					'type'        => 'string',
1031
					'context'     => array( 'view', 'edit', 'embed' ),
1032
				),
1033
				
1034
				'custom_name'       => array(
1035
					'description' => __( 'Custom name for the item.', 'invoicing' ),
1036
					'type'        => 'string',
1037
					'context'     => array( 'view', 'edit', 'embed' ),
1038
				),
1039
1040
				'custom_singular_name'       => array(
1041
					'description' => __( 'Custom singular name for the item.', 'invoicing' ),
1042
					'type'        => 'string',
1043
					'context'     => array( 'view', 'edit', 'embed' ),
1044
				),
1045
1046
				'dynamic_pricing'        => array(
1047
					'description' => __( 'Whether the item allows a user to set their own price.', 'invoicing' ),
1048
					'type'        => 'integer',
1049
					'context'     => array( 'view', 'edit', 'embed' ),
1050
				),
1051
1052
				'minimum_price'        => array(
1053
					'description' => __( 'For dynamic prices, this is the minimum price that a user can set.', 'invoicing' ),
1054
					'type'        => 'number',
1055
					'context'     => array( 'view', 'edit', 'embed' ),
1056
				),
1057
1058
				'is_recurring'        => array(
1059
					'description' => __( 'Whether the item is a subscription item.', 'invoicing' ),
1060
					'type'        => 'integer',
1061
					'context'     => array( 'view', 'edit', 'embed' ),
1062
				),
1063
1064
				'recurring_period'        => array(
1065
					'description' => __( 'The recurring period for a recurring item.', 'invoicing' ),
1066
					'type'        => 'string',
1067
					'context'     => array( 'view', 'edit', 'embed' ),
1068
					'enum'        => array( 'D', 'W', 'M', 'Y' ),
1069
				),
1070
1071
				'recurring_interval'        => array(
1072
					'description' => __( 'The recurring interval for a subscription item.', 'invoicing' ),
1073
					'type'        => 'integer',
1074
					'context'     => array( 'view', 'edit', 'embed' ),
1075
				),
1076
1077
				'recurring_limit'        => array(
1078
					'description' => __( 'The maximum number of renewals for a subscription item.', 'invoicing' ),
1079
					'type'        => 'integer',
1080
					'context'     => array( 'view', 'edit', 'embed' ),
1081
				),
1082
1083
				'free_trial'        => array(
1084
					'description' => __( 'Whether the item has a free trial period.', 'invoicing' ),
1085
					'type'        => 'integer',
1086
					'context'     => array( 'view', 'edit', 'embed' ),
1087
				),
1088
1089
				'trial_period'        => array(
1090
					'description' => __( 'The trial period of a recurring item.', 'invoicing' ),
1091
					'type'        => 'string',
1092
					'context'     => array( 'view', 'edit', 'embed' ),
1093
					'enum'        => array( 'D', 'W', 'M', 'Y' ),
1094
				),
1095
1096
				'trial_interval'        => array(
1097
					'description' => __( 'The trial interval for a subscription item.', 'invoicing' ),
1098
					'type'        => 'integer',
1099
					'context'     => array( 'view', 'edit', 'embed' ),
1100
				),
1101
1102
				'editable'        => array(
1103
					'description' => __( 'Whether or not the item is editable.', 'invoicing' ),
1104
					'type'        => 'integer',
1105
					'context'     => array( 'view', 'edit' ),
1106
				),
1107
1108
			),
1109
		);
1110
1111
		// Add helpful links to the item schem.
1112
		$schema['links'] = $this->get_schema_links();
1113
1114
		/**
1115
		 * Filters the item schema for the REST API.
1116
		 *
1117
		 * Enables adding extra properties to items.
1118
		 *
1119
		 * @since 1.0.13
1120
		 *
1121
		 * @param array   $schema    The item schema.
1122
		 */
1123
        $schema = apply_filters( "wpinv_rest_item_schema", $schema );
1124
1125
		//  Cache the item schema.
1126
		$this->schema = $schema;
1127
		
1128
		return $this->add_additional_fields_schema( $this->schema );
1129
	}
1130
1131
	/**
1132
	 * Retrieve Link Description Objects that should be added to the Schema for the invoices collection.
1133
	 *
1134
	 * @since 1.0.13
1135
	 *
1136
	 * @return array
1137
	 */
1138
	protected function get_schema_links() {
1139
1140
		$href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
1141
1142
		$links = array();
1143
1144
		$links[] = array(
1145
			'rel'          => 'https://api.w.org/action-publish',
1146
			'title'        => __( 'The current user can publish this item.' ),
1147
			'href'         => $href,
1148
			'targetSchema' => array(
1149
				'type'       => 'object',
1150
				'properties' => array(
1151
					'status' => array(
1152
						'type' => 'string',
1153
						'enum' => array( 'publish', 'future' ),
1154
					),
1155
				),
1156
			),
1157
		);
1158
1159
		return $links;
1160
	}
1161
1162
	/**
1163
	 * Prepares links for the request.
1164
	 *
1165
	 * @since 1.0.13
1166
	 *
1167
	 * @param WPInv_Item $item Item Object.
1168
	 * @return array Links for the given item.
1169
	 */
1170
	protected function prepare_links( $item ) {
1171
1172
		// Prepare the base REST API endpoint for items.
1173
		$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
1174
1175
		// Entity meta.
1176
		$links = array(
1177
			'self'       => array(
1178
				'href' => rest_url( trailingslashit( $base ) . $item->ID ),
1179
			),
1180
			'collection' => array(
1181
				'href' => rest_url( $base ),
1182
			),
1183
		);
1184
1185
		/**
1186
		 * Filters the returned item links for the REST API.
1187
		 *
1188
		 * Enables adding extra links to item API responses.
1189
		 *
1190
		 * @since 1.0.13
1191
		 *
1192
		 * @param array   $links    Rest links.
1193
		 */
1194
		return apply_filters( "wpinv_rest_item_links", $links );
1195
1196
	}
1197
1198
	/**
1199
	 * Get the link relations available for the post and current user.
1200
	 *
1201
	 * @since 1.0.13
1202
	 *
1203
	 * @param WPInv_Item   $item    Item object.
1204
	 * @param WP_REST_Request $request Request object.
1205
	 * @return array List of link relations.
1206
	 */
1207
	protected function get_available_actions( $item, $request ) {
1208
1209
		if ( 'edit' !== $request['context'] ) {
1210
			return array();
1211
		}
1212
1213
		$rels = array();
1214
1215
		// Retrieve the post type object.
1216
		$post_type = get_post_type_object( $item->post_type );
0 ignored issues
show
Bug introduced by
It seems like $item->post_type can also be of type WP_Error; however, parameter $post_type of get_post_type_object() 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

1216
		$post_type = get_post_type_object( /** @scrutinizer ignore-type */ $item->post_type );
Loading history...
Bug Best Practice introduced by
The property post_type does not exist on WPInv_Item. Since you implemented __get, consider adding a @property annotation.
Loading history...
1217
1218
		// Mark item as published.
1219
		if ( current_user_can( $post_type->cap->publish_posts ) ) {
1220
			$rels[] = 'https://api.w.org/action-publish';
1221
		}
1222
1223
		/**
1224
		 * Filters the available item link relations for the REST API.
1225
		 *
1226
		 * Enables adding extra link relation for the current user and request to item responses.
1227
		 *
1228
		 * @since 1.0.13
1229
		 *
1230
		 * @param array   $rels    Available link relations.
1231
		 */
1232
		return apply_filters( "wpinv_rest_item_link_relations", $rels );
1233
	}
1234
1235
	/**
1236
	 * Handles rest requests for item types.
1237
	 *
1238
	 * @since 1.0.13
1239
	 * 
1240
	 * 
1241
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
1242
	 */
1243
	public function get_item_types() {
1244
		return rest_ensure_response( wpinv_get_item_types() );
1245
	}
1246
1247
    
1248
}