Passed
Push — master ( 5bd17a...71a32c )
by Mike
04:53
created

OrderNotes   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 429
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 208
dl 0
loc 429
rs 9.36
c 0
b 0
f 0
wmc 38

11 Methods

Rating   Name   Duplication   Size   Complexity  
A register_routes() 0 74 1
A get_item() 0 18 6
A create_item() 0 38 5
B get_items() 0 45 6
A get_item_permissions_check() 0 8 3
A prepare_links() 0 16 1
B delete_item() 0 47 9
A delete_item_permissions_check() 0 8 3
A get_collection_params() 0 13 1
A get_item_schema() 0 51 1
A get_data_for_response() 0 8 2
1
<?php
2
/**
3
 * REST API Order Notes controller
4
 *
5
 * Handles requests to the /orders/<order_id>/notes endpoint.
6
 *
7
 * @package WooCommerce/RestApi
8
 */
9
10
namespace WooCommerce\RestApi\Controllers\Version4;
11
12
defined( 'ABSPATH' ) || exit;
13
14
use WooCommerce\RestApi\Controllers\Version4\Utilities\Permissions;
15
16
/**
17
 * REST API Order Notes controller class.
18
 */
19
class OrderNotes extends AbstractController {
20
21
	/**
22
	 * Route base.
23
	 *
24
	 * @var string
25
	 */
26
	protected $rest_base = 'orders/(?P<order_id>[\d]+)/notes';
27
28
	/**
29
	 * Post type.
30
	 *
31
	 * @var string
32
	 */
33
	protected $post_type = 'shop_order';
34
35
	/**
36
	 * Register the routes for order notes.
37
	 */
38
	public function register_routes() {
39
		register_rest_route(
40
			$this->namespace,
41
			'/' . $this->rest_base,
42
			array(
43
				'args' => array(
44
					'order_id'  => array(
45
						'description' => __( 'The order ID.', 'woocommerce' ),
46
						'type'        => 'integer',
47
					),
48
				),
49
				array(
50
					'methods'             => \WP_REST_Server::READABLE,
51
					'callback'            => array( $this, 'get_items' ),
52
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
53
					'args'                => $this->get_collection_params(),
54
				),
55
				array(
56
					'methods'             => \WP_REST_Server::CREATABLE,
57
					'callback'            => array( $this, 'create_item' ),
58
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
59
					'args'                => array_merge(
60
						$this->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
61
						array(
62
							'note' => array(
63
								'type'        => 'string',
64
								'description' => __( 'Order note content.', 'woocommerce' ),
65
								'required'    => true,
66
							),
67
						)
68
					),
69
				),
70
				'schema' => array( $this, 'get_public_item_schema' ),
71
			),
72
			true
73
		);
74
75
		register_rest_route(
76
			$this->namespace,
77
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
78
			array(
79
				'args' => array(
80
					'id' => array(
81
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
82
						'type'        => 'integer',
83
					),
84
					'order_id'  => array(
85
						'description' => __( 'The order ID.', 'woocommerce' ),
86
						'type'        => 'integer',
87
					),
88
				),
89
				array(
90
					'methods'             => \WP_REST_Server::READABLE,
91
					'callback'            => array( $this, 'get_item' ),
92
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
93
					'args'                => array(
94
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
95
					),
96
				),
97
				array(
98
					'methods'             => \WP_REST_Server::DELETABLE,
99
					'callback'            => array( $this, 'delete_item' ),
100
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
101
					'args'                => array(
102
						'force' => array(
103
							'default'     => false,
104
							'type'        => 'boolean',
105
							'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
106
						),
107
					),
108
				),
109
				'schema' => array( $this, 'get_public_item_schema' ),
110
			),
111
			true
112
		);
113
	}
114
115
	/**
116
	 * Check if a given request has access to read a order note.
117
	 *
118
	 * @param  \WP_REST_Request $request Full details about the request.
119
	 * @return \WP_Error|boolean
120
	 */
121
	public function get_item_permissions_check( $request ) {
122
		$order = wc_get_order( (int) $request['order_id'] );
123
124
		if ( $order && ! Permissions::user_can_read( $this->post_type, $order->get_id() ) ) {
125
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
126
		}
127
128
		return true;
129
	}
130
131
	/**
132
	 * Check if a given request has access delete a order note.
133
	 *
134
	 * @param  \WP_REST_Request $request Full details about the request.
135
	 *
136
	 * @return bool|\WP_Error
137
	 */
138
	public function delete_item_permissions_check( $request ) {
139
		$order = wc_get_order( (int) $request['order_id'] );
140
141
		if ( $order && ! Permissions::user_can_delete( $this->post_type, $order->get_id() ) ) {
142
			return new \WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
143
		}
144
145
		return true;
146
	}
147
148
	/**
149
	 * Get order notes from an order.
150
	 *
151
	 * @param \WP_REST_Request $request Request data.
152
	 *
153
	 * @return array|\WP_Error
154
	 */
155
	public function get_items( $request ) {
156
		$order = wc_get_order( (int) $request['order_id'] );
157
158
		if ( ! $order || $this->post_type !== $order->get_type() ) {
159
			return new \WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
160
		}
161
162
		$args = array(
163
			'post_id' => $order->get_id(),
164
			'approve' => 'approve',
165
			'type'    => 'order_note',
166
		);
167
168
		// Allow filter by order note type.
169
		if ( 'customer' === $request['type'] ) {
170
			$args['meta_query'] = array( // WPCS: slow query ok.
171
				array(
172
					'key'     => 'is_customer_note',
173
					'value'   => 1,
174
					'compare' => '=',
175
				),
176
			);
177
		} elseif ( 'internal' === $request['type'] ) {
178
			$args['meta_query'] = array( // WPCS: slow query ok.
179
				array(
180
					'key'     => 'is_customer_note',
181
					'compare' => 'NOT EXISTS',
182
				),
183
			);
184
		}
185
186
		remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10 );
187
188
		$notes = get_comments( $args );
189
190
		add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
191
192
		$data = array();
193
		foreach ( $notes as $note ) {
194
			$order_note = $this->prepare_item_for_response( $note, $request );
195
			$order_note = $this->prepare_response_for_collection( $order_note );
196
			$data[]     = $order_note;
197
		}
198
199
		return rest_ensure_response( $data );
200
	}
201
202
	/**
203
	 * Create a single order note.
204
	 *
205
	 * @param \WP_REST_Request $request Full details about the request.
206
	 * @return \WP_Error|\WP_REST_Response
207
	 */
208
	public function create_item( $request ) {
209
		if ( ! empty( $request['id'] ) ) {
210
			/* translators: %s: post type */
211
			return new \WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
212
		}
213
214
		$order = wc_get_order( (int) $request['order_id'] );
215
216
		if ( ! $order || $this->post_type !== $order->get_type() ) {
217
			return new \WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
218
		}
219
220
		// Create the note.
221
		$note_id = $order->add_order_note( $request['note'], $request['customer_note'], $request['added_by_user'] );
222
223
		if ( ! $note_id ) {
224
			return new \WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) );
225
		}
226
227
		$note = get_comment( $note_id );
228
		$this->update_additional_fields_for_object( $note, $request );
0 ignored issues
show
Bug introduced by
It seems like $note can also be of type WP_Comment; however, parameter $object of WP_REST_Controller::upda...nal_fields_for_object() does only seem to accept array, 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

228
		$this->update_additional_fields_for_object( /** @scrutinizer ignore-type */ $note, $request );
Loading history...
229
230
		/**
231
		 * Fires after a order note is created or updated via the REST API.
232
		 *
233
		 * @param \WP_Comment      $note      New order note object.
234
		 * @param \WP_REST_Request $request   Request object.
235
		 * @param boolean         $creating  True when creating item, false when updating.
236
		 */
237
		do_action( 'woocommerce_rest_insert_order_note', $note, $request, true );
238
239
		$request->set_param( 'context', 'edit' );
240
		$response = $this->prepare_item_for_response( $note, $request );
241
		$response = rest_ensure_response( $response );
242
		$response->set_status( 201 );
243
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P<order_id>[\d]+)', $order->get_id(), $this->rest_base ), $note_id ) ) );
244
245
		return $response;
246
	}
247
248
	/**
249
	 * Get a single order note.
250
	 *
251
	 * @param \WP_REST_Request $request Full details about the request.
252
	 * @return \WP_Error|\WP_REST_Response
253
	 */
254
	public function get_item( $request ) {
255
		$id    = (int) $request['id'];
256
		$order = wc_get_order( (int) $request['order_id'] );
257
258
		if ( ! $order || $this->post_type !== $order->get_type() ) {
259
			return new \WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
260
		}
261
262
		$note = get_comment( $id );
263
264
		if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) {
265
			return new \WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
266
		}
267
268
		$order_note = $this->prepare_item_for_response( $note, $request );
269
		$response   = rest_ensure_response( $order_note );
270
271
		return $response;
272
	}
273
274
	/**
275
	 * Delete a single order note.
276
	 *
277
	 * @param \WP_REST_Request $request Full details about the request.
278
	 * @return \WP_REST_Response|\WP_Error
279
	 */
280
	public function delete_item( $request ) {
281
		$id    = (int) $request['id'];
282
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
283
284
		// We don't support trashing for this type, error out.
285
		if ( ! $force ) {
286
			return new \WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
287
		}
288
289
		$order = wc_get_order( (int) $request['order_id'] );
290
291
		if ( ! $order || $this->post_type !== $order->get_type() ) {
292
			return new \WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
293
		}
294
295
		$note = get_comment( $id );
296
297
		if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) {
298
			return new \WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
299
		}
300
301
		$request->set_param( 'context', 'edit' );
302
		$previous = $this->prepare_item_for_response( $note, $request );
303
		$result   = wc_delete_order_note( $note->comment_ID );
304
305
		if ( ! $result ) {
306
			return new \WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), 'order_note' ), array( 'status' => 500 ) );
307
		}
308
309
		$response = new \WP_REST_Response();
310
		$response->set_data(
311
			array(
312
				'deleted'  => true,
313
				'previous' => $previous->get_data(),
314
			)
315
		);
316
317
		/**
318
		 * Fires after a order note is deleted or trashed via the REST API.
319
		 *
320
		 * @param \WP_Comment       $note     The deleted or trashed order note.
321
		 * @param \WP_REST_Response $response The response data.
322
		 * @param \WP_REST_Request  $request  The request sent to the API.
323
		 */
324
		do_action( 'woocommerce_rest_delete_order_note', $note, $response, $request );
325
326
		return $response;
327
	}
328
329
	/**
330
	 * Get data for this object in the format of this endpoint's schema.
331
	 *
332
	 * @param \WP_Comment      $object Object to prepare.
333
	 * @param \WP_REST_Request $request Request object.
334
	 * @return array Array of data in the correct format.
335
	 */
336
	protected function get_data_for_response( $object, $request ) {
337
		return array(
338
			'id'               => (int) $object->comment_ID,
339
			'author'           => __( 'WooCommerce', 'woocommerce' ) === $object->comment_author ? 'system' : $object->comment_author,
340
			'date_created'     => wc_rest_prepare_date_response( $object->comment_date ),
341
			'date_created_gmt' => wc_rest_prepare_date_response( $object->comment_date_gmt ),
342
			'note'             => $object->comment_content,
343
			'customer_note'    => (bool) get_comment_meta( $object->comment_ID, 'is_customer_note', true ),
344
		);
345
	}
346
347
	/**
348
	 * Prepare links for the request.
349
	 *
350
	 * @param mixed            $item Object to prepare.
351
	 * @param \WP_REST_Request $request Request object.
352
	 * @return array
353
	 */
354
	protected function prepare_links( $item, $request ) {
355
		$order_id = (int) $item->comment_post_ID;
356
		$base     = str_replace( '(?P<order_id>[\d]+)', $order_id, $this->rest_base );
357
		$links    = array(
358
			'self' => array(
359
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $item->comment_ID ) ),
360
			),
361
			'collection' => array(
362
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
363
			),
364
			'up' => array(
365
				'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ),
366
			),
367
		);
368
369
		return $links;
370
	}
371
372
	/**
373
	 * Get the Order Notes schema, conforming to JSON Schema.
374
	 *
375
	 * @return array
376
	 */
377
	public function get_item_schema() {
378
		$schema = array(
379
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
380
			'title'      => 'order_note',
381
			'type'       => 'object',
382
			'properties' => array(
383
				'id'               => array(
384
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
385
					'type'        => 'integer',
386
					'context'     => array( 'view', 'edit' ),
387
					'readonly'    => true,
388
				),
389
				'author'           => array(
390
					'description' => __( 'Order note author.', 'woocommerce' ),
391
					'type'        => 'string',
392
					'context'     => array( 'view', 'edit' ),
393
					'readonly'    => true,
394
				),
395
				'date_created'     => array(
396
					'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ),
397
					'type'        => 'date-time',
398
					'context'     => array( 'view', 'edit' ),
399
					'readonly'    => true,
400
				),
401
				'date_created_gmt' => array(
402
					'description' => __( 'The date the order note was created, as GMT.', 'woocommerce' ),
403
					'type'        => 'date-time',
404
					'context'     => array( 'view', 'edit' ),
405
					'readonly'    => true,
406
				),
407
				'note'             => array(
408
					'description' => __( 'Order note content.', 'woocommerce' ),
409
					'type'        => 'string',
410
					'context'     => array( 'view', 'edit' ),
411
				),
412
				'customer_note'    => array(
413
					'description' => __( 'If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only.', 'woocommerce' ),
414
					'type'        => 'boolean',
415
					'default'     => false,
416
					'context'     => array( 'view', 'edit' ),
417
				),
418
				'added_by_user'    => array(
419
					'description' => __( 'If true, this note will be attributed to the current user. If false, the note will be attributed to the system.', 'woocommerce' ),
420
					'type'        => 'boolean',
421
					'default'     => false,
422
					'context'     => array( 'edit' ),
423
				),
424
			),
425
		);
426
427
		return $this->add_additional_fields_schema( $schema );
428
	}
429
430
	/**
431
	 * Get the query params for collections.
432
	 *
433
	 * @return array
434
	 */
435
	public function get_collection_params() {
436
		$params            = array();
437
		$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
438
		$params['type']    = array(
439
			'default'           => 'any',
440
			'description'       => __( 'Limit result to customers or internal notes.', 'woocommerce' ),
441
			'type'              => 'string',
442
			'enum'              => array( 'any', 'customer', 'internal' ),
443
			'sanitize_callback' => 'sanitize_key',
444
			'validate_callback' => 'rest_validate_request_arg',
445
		);
446
447
		return $params;
448
	}
449
}
450