Passed
Push — master ( 3bda5e...50908e )
by
unknown
03:36 queued 12s
created

check_read_permission()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 1
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
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 View Code Duplication
	public function get_items( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
	protected function get_post( $item_id ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
	public function get_item_permissions_check( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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;
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  ) ) {
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 );
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'] );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->get_post($request['id']); of type WPInv_Item|WP_Error adds the type WPInv_Item to the return on line 269 which is incompatible with the return type documented by WPInv_REST_Items_Controller::get_item of type WP_REST_Response|WP_Error.
Loading history...
266
        
267
        // Abort early if it does not exist
268
		if ( is_wp_error( $item ) ) {
269
			return $item;
270
		}
271
272
		// Prepare the response
273
		$response = $this->prepare_item_for_response( $item, $request );
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 View Code Duplication
	public function create_item( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->prepare_item_for_database($request); of type array|WP_Error adds the type array to the return on line 343 which is incompatible with the return type documented by WPInv_REST_Items_Controller::create_item of type WP_REST_Response|WP_Error.
Loading history...
341
342
		if ( is_wp_error( $item_data ) ) {
343
			return $item_data;
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;
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 View Code Duplication
	public function update_item_permissions_check( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
391
392
		// Retrieve the item.
393
		$item = $this->get_post( $request['id'] );
394
		if ( is_wp_error( $item ) ) {
395
			return $item;
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 View Code Duplication
	public function update_item( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
421
		
422
		// Ensure the item exists.
423
        $valid_check = $this->get_post( $request['id'] );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->get_post($request['id']); of type WPInv_Item|WP_Error adds the type WPInv_Item to the return on line 427 which is incompatible with the return type documented by WPInv_REST_Items_Controller::update_item of type WP_REST_Response|WP_Error.
Loading history...
424
        
425
        // Abort early if it does not exist
426
		if ( is_wp_error( $valid_check ) ) {
427
			return $valid_check;
428
		}
429
430
		$request->set_param( 'context', 'edit' );
431
432
		// Prepare the updated data.
433
		$data_to_update = $this->prepare_item_for_database( $request );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->prepare_item_for_database($request); of type array|WP_Error adds the type array to the return on line 436 which is incompatible with the return type documented by WPInv_REST_Items_Controller::update_item of type WP_REST_Response|WP_Error.
Loading history...
434
435
		if ( is_wp_error( $data_to_update ) ) {
436
			return $data_to_update;
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;
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 View Code Duplication
	public function delete_item_permissions_check( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
485
486
		// Retrieve the item.
487
		$item = $this->get_post( $request['id'] );
488
		if ( is_wp_error( $item ) ) {
489
			return $item;
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 View Code Duplication
	public function delete_item( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
517
		
518
		// Retrieve the item.
519
		$item = $this->get_post( $request['id'] );
520
		if ( is_wp_error( $item ) ) {
521
			return $item;
522
		}
523
524
		$request->set_param( 'context', 'edit' );
525
526
		// Prepare the item id
527
		$id    = $item->ID;
528
529
		// Prepare the response
530
		$response = $this->prepare_item_for_response( $item, $request );
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 View Code Duplication
		if ( isset( $request['id'] ) ) {
680
			$existing_item = $this->get_post( $request['id'] );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->get_post($request['id']); of type WPInv_Item|WP_Error adds the type WPInv_Item to the return on line 682 which is incompatible with the return type documented by WPInv_REST_Items_Control...epare_item_for_database of type array|WP_Error.
Loading history...
681
			if ( is_wp_error( $existing_item ) ) {
682
				return $existing_item;
683
			}
684
685
			$prepared_item->ID 		  = $existing_item->ID;
686
		}
687
688
		$schema = $this->get_item_schema();
689
690
		// item title.
691 View Code Duplication
		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 View Code Duplication
		if ( ! empty( $schema['properties']['price'] ) && isset( $request['price'] ) ) {
702
			$prepared_item->price = floatval( $request['price'] );
703
		}
704
705
		// minimum price (for dynamc items).
706 View Code Duplication
		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 View Code Duplication
			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 View Code Duplication
			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 );
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 View Code Duplication
		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 View Code Duplication
			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 View Code Duplication
				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 View Code Duplication
		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 );
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 View Code Duplication
	public function get_fields_for_response( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
Documentation introduced by
The property post_type does not exist on object<WPInv_Item>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

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
}