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

ProductReviews::delete_item_permissions_check()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * REST API Product Reviews Controller
4
 *
5
 * Handles requests to /products/<product_id>/reviews.
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\Responses\ProductReviewResponse;
15
use WooCommerce\RestApi\Controllers\Version4\Utilities\Permissions;
16
17
/**
18
 * REST API Product Reviews controller class.
19
 */
20
class ProductReviews extends AbstractController {
21
22
	/**
23
	 * Route base.
24
	 *
25
	 * @var string
26
	 */
27
	protected $rest_base = 'products/reviews';
28
29
	/**
30
	 * Permission to check.
31
	 *
32
	 * @var string
33
	 */
34
	protected $resource_type = 'product_reviews';
35
36
	/**
37
	 * Register the routes for product reviews.
38
	 */
39
	public function register_routes() {
40
		register_rest_route(
41
			$this->namespace,
42
			'/' . $this->rest_base,
43
			array(
44
				array(
45
					'methods'             => \WP_REST_Server::READABLE,
46
					'callback'            => array( $this, 'get_items' ),
47
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
48
					'args'                => $this->get_collection_params(),
49
				),
50
				array(
51
					'methods'             => \WP_REST_Server::CREATABLE,
52
					'callback'            => array( $this, 'create_item' ),
53
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
54
					'args'                => array_merge(
55
						$this->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
56
						array(
57
							'product_id'     => array(
58
								'required'    => true,
59
								'description' => __( 'Unique identifier for the product.', 'woocommerce' ),
60
								'type'        => 'integer',
61
							),
62
							'review'         => array(
63
								'required'    => true,
64
								'type'        => 'string',
65
								'description' => __( 'Review content.', 'woocommerce' ),
66
							),
67
							'reviewer'       => array(
68
								'required'    => true,
69
								'type'        => 'string',
70
								'description' => __( 'Name of the reviewer.', 'woocommerce' ),
71
							),
72
							'reviewer_email' => array(
73
								'required'    => true,
74
								'type'        => 'string',
75
								'description' => __( 'Email of the reviewer.', 'woocommerce' ),
76
							),
77
						)
78
					),
79
				),
80
				'schema' => array( $this, 'get_public_item_schema' ),
81
			),
82
			true
83
		);
84
85
		register_rest_route(
86
			$this->namespace,
87
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
88
			array(
89
				'args'   => array(
90
					'id' => array(
91
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
92
						'type'        => 'integer',
93
					),
94
				),
95
				array(
96
					'methods'             => \WP_REST_Server::READABLE,
97
					'callback'            => array( $this, 'get_item' ),
98
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
99
					'args'                => array(
100
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
101
					),
102
				),
103
				array(
104
					'methods'             => \WP_REST_Server::EDITABLE,
105
					'callback'            => array( $this, 'update_item' ),
106
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
107
					'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
108
				),
109
				array(
110
					'methods'             => \WP_REST_Server::DELETABLE,
111
					'callback'            => array( $this, 'delete_item' ),
112
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
113
					'args'                => array(
114
						'force' => array(
115
							'default'     => false,
116
							'type'        => 'boolean',
117
							'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
118
						),
119
					),
120
				),
121
				'schema' => array( $this, 'get_public_item_schema' ),
122
			),
123
			true
124
		);
125
126
		$this->register_batch_route();
127
	}
128
129
	/**
130
	 * Get all reviews.
131
	 *
132
	 * @param \WP_REST_Request $request Full details about the request.
133
	 * @return array|\WP_Error
134
	 */
135
	public function get_items( $request ) {
136
		// Retrieve the list of registered collection query parameters.
137
		$registered = $this->get_collection_params();
138
139
		/*
140
		 * This array defines mappings between public API query parameters whose
141
		 * values are accepted as-passed, and their internal \WP_Query parameter
142
		 * name equivalents (some are the same). Only values which are also
143
		 * present in $registered will be set.
144
		 */
145
		$parameter_mappings = array(
146
			'reviewer'         => 'author__in',
147
			'reviewer_email'   => 'author_email',
148
			'reviewer_exclude' => 'author__not_in',
149
			'exclude'          => 'comment__not_in',
150
			'include'          => 'comment__in',
151
			'offset'           => 'offset',
152
			'order'            => 'order',
153
			'per_page'         => 'number',
154
			'product'          => 'post__in',
155
			'search'           => 'search',
156
			'status'           => 'status',
157
		);
158
159
		$prepared_args = array();
160
161
		/*
162
		 * For each known parameter which is both registered and present in the request,
163
		 * set the parameter's value on the query $prepared_args.
164
		 */
165
		foreach ( $parameter_mappings as $api_param => $wp_param ) {
166
			if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
167
				$prepared_args[ $wp_param ] = $request[ $api_param ];
168
			}
169
		}
170
171
		// Ensure certain parameter values default to empty strings.
172
		foreach ( array( 'author_email', 'search' ) as $param ) {
173
			if ( ! isset( $prepared_args[ $param ] ) ) {
174
				$prepared_args[ $param ] = '';
175
			}
176
		}
177
178
		if ( isset( $registered['orderby'] ) ) {
179
			$prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] );
180
		}
181
182
		if ( isset( $prepared_args['status'] ) ) {
183
			$prepared_args['status'] = 'approved' === $prepared_args['status'] ? 'approve' : $prepared_args['status'];
184
		}
185
186
		$prepared_args['no_found_rows'] = false;
187
		$prepared_args['date_query']    = array();
188
189
		// Set before into date query. Date query must be specified as an array of an array.
190
		if ( isset( $registered['before'], $request['before'] ) ) {
191
			$prepared_args['date_query'][0]['before'] = $request['before'];
192
		}
193
194
		// Set after into date query. Date query must be specified as an array of an array.
195
		if ( isset( $registered['after'], $request['after'] ) ) {
196
			$prepared_args['date_query'][0]['after'] = $request['after'];
197
		}
198
199
		if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) {
200
			$prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
201
		}
202
203
		/**
204
		 * Filters arguments, before passing to \WP_Comment_Query, when querying reviews via the REST API.
205
		 *
206
		 * @since 3.5.0
207
		 * @link https://developer.wordpress.org/reference/classes/\WP_Comment_Query/
208
		 * @param array           $prepared_args Array of arguments for \WP_Comment_Query.
209
		 * @param \WP_REST_Request $request       The current request.
210
		 */
211
		$prepared_args = apply_filters( 'woocommerce_rest_product_review_query', $prepared_args, $request );
212
213
		// Make sure that returns only reviews.
214
		$prepared_args['type'] = 'review';
215
216
		// Query reviews.
217
		$query        = new \WP_Comment_Query();
218
		$query_result = $query->query( $prepared_args );
219
		$reviews      = array();
220
221
		foreach ( $query_result as $review ) {
222
			if ( ! Permissions::user_can_read( 'product_review', $review->comment_ID ) ) {
223
				continue;
224
			}
225
226
			$data      = $this->prepare_item_for_response( $review, $request );
227
			$reviews[] = $this->prepare_response_for_collection( $data );
228
		}
229
230
		$total_reviews = (int) $query->found_comments;
231
		$max_pages     = (int) $query->max_num_pages;
232
233
		if ( $total_reviews < 1 ) {
234
			// Out-of-bounds, run the query again without LIMIT for total count.
235
			unset( $prepared_args['number'], $prepared_args['offset'] );
236
237
			$query                  = new \WP_Comment_Query();
238
			$prepared_args['count'] = true;
239
240
			$total_reviews = $query->query( $prepared_args );
241
			$max_pages     = ceil( $total_reviews / $request['per_page'] );
242
		}
243
244
		$response = rest_ensure_response( $reviews );
245
		$response->header( 'X-WP-Total', $total_reviews );
0 ignored issues
show
Bug introduced by
It seems like $total_reviews can also be of type array and array and array and array<mixed,WP_Comment|array|null>; however, parameter $value of WP_HTTP_Response::header() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

245
		$response->header( 'X-WP-Total', /** @scrutinizer ignore-type */ $total_reviews );
Loading history...
246
		$response->header( 'X-WP-TotalPages', $max_pages );
247
248
		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
249
250
		if ( $request['page'] > 1 ) {
251
			$prev_page = $request['page'] - 1;
252
253
			if ( $prev_page > $max_pages ) {
254
				$prev_page = $max_pages;
255
			}
256
257
			$prev_link = add_query_arg( 'page', $prev_page, $base );
258
			$response->link_header( 'prev', $prev_link );
259
		}
260
261
		if ( $max_pages > $request['page'] ) {
262
			$next_page = $request['page'] + 1;
263
			$next_link = add_query_arg( 'page', $next_page, $base );
264
265
			$response->link_header( 'next', $next_link );
266
		}
267
268
		return $response;
269
	}
270
271
	/**
272
	 * Create a single review.
273
	 *
274
	 * @param \WP_REST_Request $request Full details about the request.
275
	 * @return \WP_Error|\WP_REST_Response
276
	 */
277
	public function create_item( $request ) {
278
		if ( ! empty( $request['id'] ) ) {
279
			return new \WP_Error( 'woocommerce_rest_review_exists', __( 'Cannot create existing product review.', 'woocommerce' ), array( 'status' => 400 ) );
280
		}
281
282
		$product_id = (int) $request['product_id'];
283
284
		if ( 'product' !== get_post_type( $product_id ) ) {
285
			return new \WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
286
		}
287
288
		$prepared_review = $this->prepare_item_for_database( $request );
289
		if ( is_wp_error( $prepared_review ) ) {
290
			return $prepared_review;
291
		}
292
293
		$prepared_review['comment_type'] = 'review';
294
295
		/*
296
		 * Do not allow a comment to be created with missing or empty comment_content. See wp_handle_comment_submission().
297
		 */
298
		if ( empty( $prepared_review['comment_content'] ) ) {
299
			return new \WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) );
300
		}
301
302
		// Setting remaining values before wp_insert_comment so we can use wp_allow_comment().
303
		if ( ! isset( $prepared_review['comment_date_gmt'] ) ) {
304
			$prepared_review['comment_date_gmt'] = current_time( 'mysql', true );
305
		}
306
307
		if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) { // WPCS: input var ok, sanitization ok.
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_SERVER['REMOTE_ADDR']) can also be of type array; however, parameter $ip of rest_is_ip_address() 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

307
		if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( /** @scrutinizer ignore-type */ wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) { // WPCS: input var ok, sanitization ok.
Loading history...
308
			$prepared_review['comment_author_IP'] = wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // WPCS: input var ok.
309
		} else {
310
			$prepared_review['comment_author_IP'] = '127.0.0.1';
311
		}
312
313
		if ( ! empty( $request['author_user_agent'] ) ) {
314
			$prepared_review['comment_agent'] = $request['author_user_agent'];
315
		} elseif ( $request->get_header( 'user_agent' ) ) {
316
			$prepared_review['comment_agent'] = $request->get_header( 'user_agent' );
317
		} else {
318
			$prepared_review['comment_agent'] = '';
319
		}
320
321
		$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_review );
0 ignored issues
show
Bug introduced by
It seems like $prepared_review can also be of type WP_Error; however, parameter $comment_data of wp_check_comment_data_max_lengths() 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

321
		$check_comment_lengths = wp_check_comment_data_max_lengths( /** @scrutinizer ignore-type */ $prepared_review );
Loading history...
322
		if ( is_wp_error( $check_comment_lengths ) ) {
323
			$error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() );
324
			return new \WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) );
325
		}
326
327
		$prepared_review['comment_parent']     = 0;
328
		$prepared_review['comment_author_url'] = '';
329
		$prepared_review['comment_approved']   = wp_allow_comment( $prepared_review, true );
0 ignored issues
show
Bug introduced by
It seems like $prepared_review can also be of type WP_Error; however, parameter $commentdata of wp_allow_comment() 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

329
		$prepared_review['comment_approved']   = wp_allow_comment( /** @scrutinizer ignore-type */ $prepared_review, true );
Loading history...
330
331
		if ( is_wp_error( $prepared_review['comment_approved'] ) ) {
332
			$error_code    = $prepared_review['comment_approved']->get_error_code();
333
			$error_message = $prepared_review['comment_approved']->get_error_message();
334
335
			if ( 'comment_duplicate' === $error_code ) {
336
				return new \WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 409 ) );
337
			}
338
339
			if ( 'comment_flood' === $error_code ) {
340
				return new \WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 400 ) );
341
			}
342
343
			return $prepared_review['comment_approved'];
344
		}
345
346
		/**
347
		 * Filters a review before it is inserted via the REST API.
348
		 *
349
		 * Allows modification of the review right before it is inserted via wp_insert_comment().
350
		 * Returning a \WP_Error value from the filter will shortcircuit insertion and allow
351
		 * skipping further processing.
352
		 *
353
		 * @since 3.5.0
354
		 * @param array|\WP_Error  $prepared_review The prepared review data for wp_insert_comment().
355
		 * @param \WP_REST_Request $request          Request used to insert the review.
356
		 */
357
		$prepared_review = apply_filters( 'woocommerce_rest_pre_insert_product_review', $prepared_review, $request );
358
		if ( is_wp_error( $prepared_review ) ) {
359
			return $prepared_review;
360
		}
361
362
		$review_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_review ) ) );
363
364
		if ( ! $review_id ) {
365
			return new \WP_Error( 'woocommerce_rest_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) );
366
		}
367
368
		if ( isset( $request['status'] ) ) {
369
			$this->handle_status_param( $request['status'], $review_id );
370
		}
371
372
		update_comment_meta( $review_id, 'rating', ! empty( $request['rating'] ) ? $request['rating'] : '0' );
373
374
		$review = get_comment( $review_id );
375
376
		/**
377
		 * Fires after a comment is created or updated via the REST API.
378
		 *
379
		 * @param WP_Comment      $review   Inserted or updated comment object.
380
		 * @param \WP_REST_Request $request  Request object.
381
		 * @param bool            $creating True when creating a comment, false when updating.
382
		 */
383
		do_action( 'woocommerce_rest_insert_product_review', $review, $request, true );
384
385
		$fields_update = $this->update_additional_fields_for_object( $review, $request );
386
		if ( is_wp_error( $fields_update ) ) {
387
			return $fields_update;
388
		}
389
390
		$context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
391
		$request->set_param( 'context', $context );
392
393
		$response = $this->prepare_item_for_response( $review, $request );
394
395
		$response->set_status( 201 );
396
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $review_id ) ) );
397
398
		return $response;
399
	}
400
401
	/**
402
	 * Get a single product review.
403
	 *
404
	 * @param \WP_REST_Request $request Full details about the request.
405
	 * @return \WP_Error|\WP_REST_Response
406
	 */
407
	public function get_item( $request ) {
408
		$review = $this->get_review( $request['id'] );
409
		if ( is_wp_error( $review ) ) {
410
			return $review;
411
		}
412
413
		return $this->prepare_item_for_response( $review, $request );
414
	}
415
416
	/**
417
	 * Updates a review.
418
	 *
419
	 * @param \WP_REST_Request $request Full details about the request.
420
	 * @return \WP_Error|\WP_REST_Response Response object on success, or error object on failure.
421
	 */
422
	public function update_item( $request ) {
423
		$review = $this->get_review( $request['id'] );
424
		if ( is_wp_error( $review ) ) {
425
			return $review;
426
		}
427
428
		$id = (int) $review->comment_ID;
0 ignored issues
show
Bug introduced by
The property comment_ID does not seem to exist on WP_Error.
Loading history...
429
430
		if ( isset( $request['type'] ) && 'review' !== get_comment_type( $id ) ) {
431
			return new \WP_Error( 'woocommerce_rest_review_invalid_type', __( 'Sorry, you are not allowed to change the comment type.', 'woocommerce' ), array( 'status' => 404 ) );
432
		}
433
434
		$prepared_args = $this->prepare_item_for_database( $request );
435
		if ( is_wp_error( $prepared_args ) ) {
436
			return $prepared_args;
437
		}
438
439
		if ( ! empty( $prepared_args['comment_post_ID'] ) ) {
440
			if ( 'product' !== get_post_type( (int) $prepared_args['comment_post_ID'] ) ) {
441
				return new \WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
442
			}
443
		}
444
445
		if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
446
			// Only the comment status is being changed.
447
			$change = $this->handle_status_param( $request['status'], $id );
448
449
			if ( ! $change ) {
450
				return new \WP_Error( 'woocommerce_rest_review_failed_edit', __( 'Updating review status failed.', 'woocommerce' ), array( 'status' => 500 ) );
451
			}
452
		} elseif ( ! empty( $prepared_args ) ) {
453
			if ( is_wp_error( $prepared_args ) ) {
454
				return $prepared_args;
455
			}
456
457
			if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) {
458
				return new \WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) );
459
			}
460
461
			$prepared_args['comment_ID'] = $id;
462
463
			$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args );
464
			if ( is_wp_error( $check_comment_lengths ) ) {
465
				$error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() );
466
				return new \WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) );
467
			}
468
469
			$updated = wp_update_comment( wp_slash( (array) $prepared_args ) );
470
471
			if ( false === $updated ) {
0 ignored issues
show
introduced by
The condition false === $updated is always false.
Loading history...
472
				return new \WP_Error( 'woocommerce_rest_comment_failed_edit', __( 'Updating review failed.', 'woocommerce' ), array( 'status' => 500 ) );
473
			}
474
475
			if ( isset( $request['status'] ) ) {
476
				$this->handle_status_param( $request['status'], $id );
477
			}
478
		}
479
480
		if ( ! empty( $request['rating'] ) ) {
481
			update_comment_meta( $id, 'rating', $request['rating'] );
482
		}
483
484
		$review = get_comment( $id );
485
486
		/** This action is documented in includes/api/class-wc-rest-product-reviews-controller.php */
487
		do_action( 'woocommerce_rest_insert_product_review', $review, $request, false );
488
489
		$fields_update = $this->update_additional_fields_for_object( $review, $request );
0 ignored issues
show
Bug introduced by
It seems like $review 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

489
		$fields_update = $this->update_additional_fields_for_object( /** @scrutinizer ignore-type */ $review, $request );
Loading history...
490
491
		if ( is_wp_error( $fields_update ) ) {
492
			return $fields_update;
493
		}
494
495
		$request->set_param( 'context', 'edit' );
496
497
		return $this->prepare_item_for_response( $review, $request );
498
	}
499
500
	/**
501
	 * Deletes a review.
502
	 *
503
	 * @param \WP_REST_Request $request Full details about the request.
504
	 * @return \WP_Error|\WP_REST_Response Response object on success, or error object on failure.
505
	 */
506
	public function delete_item( $request ) {
507
		$review = $this->get_review( $request['id'] );
508
		if ( is_wp_error( $review ) ) {
509
			return $review;
510
		}
511
512
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
513
514
		/**
515
		 * Filters whether a review can be trashed.
516
		 *
517
		 * Return false to disable trash support for the post.
518
		 *
519
		 * @since 3.5.0
520
		 * @param bool       $supports_trash Whether the post type support trashing.
521
		 * @param WP_Comment $review         The review object being considered for trashing support.
522
		 */
523
		$supports_trash = apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review );
524
525
		$request->set_param( 'context', 'edit' );
526
527
		if ( $force ) {
528
			$previous = $this->prepare_item_for_response( $review, $request );
529
			$result   = wp_delete_comment( $review->comment_ID, true );
0 ignored issues
show
Bug introduced by
The property comment_ID does not seem to exist on WP_Error.
Loading history...
530
			$response = new \WP_REST_Response();
531
			$response->set_data(
532
				array(
533
					'deleted'  => true,
534
					'previous' => $previous->get_data(),
535
				)
536
			);
537
		} else {
538
			// If this type doesn't support trashing, error out.
539
			if ( ! $supports_trash ) {
540
				/* translators: %s: force=true */
541
				return new \WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( "The object does not support trashing. Set '%s' to delete.", 'woocommerce' ), 'force=true' ), array( 'status' => 501 ) );
542
			}
543
544
			if ( 'trash' === $review->comment_approved ) {
0 ignored issues
show
Bug introduced by
The property comment_approved does not seem to exist on WP_Error.
Loading history...
545
				return new \WP_Error( 'woocommerce_rest_already_trashed', __( 'The object has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) );
546
			}
547
548
			$result   = wp_trash_comment( $review->comment_ID );
549
			$review   = get_comment( $review->comment_ID );
550
			$response = $this->prepare_item_for_response( $review, $request );
551
		}
552
553
		if ( ! $result ) {
554
			return new \WP_Error( 'woocommerce_rest_cannot_delete', __( 'The object cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
555
		}
556
557
		/**
558
		 * Fires after a review is deleted via the REST API.
559
		 *
560
		 * @param WP_Comment       $review   The deleted review data.
561
		 * @param \WP_REST_Response $response The response returned from the API.
562
		 * @param \WP_REST_Request  $request  The request sent to the API.
563
		 */
564
		do_action( 'woocommerce_rest_delete_review', $review, $response, $request );
565
566
		return $response;
567
	}
568
569
	/**
570
	 * Get data for this object in the format of this endpoint's schema.
571
	 *
572
	 * @param \WP_Comment      $object Object to prepare.
573
	 * @param \WP_REST_Request $request Request object.
574
	 * @return array Array of data in the correct format.
575
	 */
576
	protected function get_data_for_response( $object, $request ) {
577
		$formatter = new ProductReviewResponse();
578
579
		return $formatter->prepare_response( $object, $this->get_request_context( $request ) );
580
	}
581
582
	/**
583
	 * Prepare a single product review to be inserted into the database.
584
	 *
585
	 * @param  \WP_REST_Request $request Request object.
586
	 * @return array|\WP_Error  $prepared_review
587
	 */
588
	protected function prepare_item_for_database( $request ) {
589
		if ( isset( $request['id'] ) ) {
590
			$prepared_review['comment_ID'] = (int) $request['id'];
591
		}
592
593
		if ( isset( $request['review'] ) ) {
594
			$prepared_review['comment_content'] = $request['review'];
595
		}
596
597
		if ( isset( $request['product_id'] ) ) {
598
			$prepared_review['comment_post_ID'] = (int) $request['product_id'];
599
		}
600
601
		if ( isset( $request['reviewer'] ) ) {
602
			$prepared_review['comment_author'] = $request['reviewer'];
603
		}
604
605
		if ( isset( $request['reviewer_email'] ) ) {
606
			$prepared_review['comment_author_email'] = $request['reviewer_email'];
607
		}
608
609
		if ( ! empty( $request['date_created'] ) ) {
610
			$date_data = rest_get_date_with_gmt( $request['date_created'] );
611
612
			if ( ! empty( $date_data ) ) {
613
				list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data;
614
			}
615
		} elseif ( ! empty( $request['date_created_gmt'] ) ) {
616
			$date_data = rest_get_date_with_gmt( $request['date_created_gmt'], true );
617
618
			if ( ! empty( $date_data ) ) {
619
				list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data;
620
			}
621
		}
622
623
		/**
624
		 * Filters a review after it is prepared for the database.
625
		 *
626
		 * Allows modification of the review right after it is prepared for the database.
627
		 *
628
		 * @since 3.5.0
629
		 * @param array           $prepared_review The prepared review data for `wp_insert_comment`.
630
		 * @param \WP_REST_Request $request         The current request.
631
		 */
632
		return apply_filters( 'woocommerce_rest_preprocess_product_review', $prepared_review, $request );
633
	}
634
635
	/**
636
	 * Prepare links for the request.
637
	 *
638
	 * @param mixed            $item Object to prepare.
639
	 * @param \WP_REST_Request $request Request object.
640
	 * @return array
641
	 */
642
	protected function prepare_links( $item, $request ) {
643
		$links = array(
644
			'self'       => array(
645
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $item->comment_ID ) ),
646
			),
647
			'collection' => array(
648
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
649
			),
650
		);
651
		if ( 0 !== (int) $item->comment_post_ID ) {
652
			$links['up'] = array(
653
				'href'       => rest_url( sprintf( '/%s/products/%d', $this->namespace, $item->comment_post_ID ) ),
654
				'embeddable' => true,
655
			);
656
		}
657
		if ( 0 !== (int) $item->user_id ) {
658
			$links['reviewer'] = array(
659
				'href'       => rest_url( 'wp/v2/users/' . $item->user_id ),
660
				'embeddable' => true,
661
			);
662
		}
663
		return $links;
664
	}
665
666
	/**
667
	 * Get the Product Review's schema, conforming to JSON Schema.
668
	 *
669
	 * @return array
670
	 */
671
	public function get_item_schema() {
672
		$schema = array(
673
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
674
			'title'      => 'product_review',
675
			'type'       => 'object',
676
			'properties' => array(
677
				'id'               => array(
678
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
679
					'type'        => 'integer',
680
					'context'     => array( 'view', 'edit' ),
681
					'readonly'    => true,
682
				),
683
				'date_created'     => array(
684
					'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ),
685
					'type'        => 'date-time',
686
					'context'     => array( 'view', 'edit' ),
687
					'readonly'    => true,
688
				),
689
				'date_created_gmt' => array(
690
					'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ),
691
					'type'        => 'date-time',
692
					'context'     => array( 'view', 'edit' ),
693
					'readonly'    => true,
694
				),
695
				'product_id'       => array(
696
					'description' => __( 'Unique identifier for the product that the review belongs to.', 'woocommerce' ),
697
					'type'        => 'integer',
698
					'context'     => array( 'view', 'edit' ),
699
				),
700
				'status'           => array(
701
					'description' => __( 'Status of the review.', 'woocommerce' ),
702
					'type'        => 'string',
703
					'default'     => 'approved',
704
					'enum'        => array( 'approved', 'hold', 'spam', 'unspam', 'trash', 'untrash' ),
705
					'context'     => array( 'view', 'edit' ),
706
				),
707
				'reviewer'         => array(
708
					'description' => __( 'Reviewer name.', 'woocommerce' ),
709
					'type'        => 'string',
710
					'context'     => array( 'view', 'edit' ),
711
				),
712
				'reviewer_email'   => array(
713
					'description' => __( 'Reviewer email.', 'woocommerce' ),
714
					'type'        => 'string',
715
					'format'      => 'email',
716
					'context'     => array( 'view', 'edit' ),
717
				),
718
				'review'           => array(
719
					'description' => __( 'The content of the review.', 'woocommerce' ),
720
					'type'        => 'string',
721
					'context'     => array( 'view', 'edit' ),
722
					'arg_options' => array(
723
						'sanitize_callback' => 'wp_filter_post_kses',
724
					),
725
				),
726
				'rating'           => array(
727
					'description' => __( 'Review rating (0 to 5).', 'woocommerce' ),
728
					'type'        => 'integer',
729
					'context'     => array( 'view', 'edit' ),
730
				),
731
				'verified'         => array(
732
					'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ),
733
					'type'        => 'boolean',
734
					'context'     => array( 'view', 'edit' ),
735
					'readonly'    => true,
736
				),
737
			),
738
		);
739
740
		if ( get_option( 'show_avatars' ) ) {
741
			$avatar_properties = array();
742
			$avatar_sizes      = rest_get_avatar_sizes();
743
744
			foreach ( $avatar_sizes as $size ) {
745
				$avatar_properties[ $size ] = array(
746
					/* translators: %d: avatar image size in pixels */
747
					'description' => sprintf( __( 'Avatar URL with image size of %d pixels.', 'woocommerce' ), $size ),
748
					'type'        => 'string',
749
					'format'      => 'uri',
750
					'context'     => array( 'embed', 'view', 'edit' ),
751
				);
752
			}
753
			$schema['properties']['reviewer_avatar_urls'] = array(
754
				'description' => __( 'Avatar URLs for the object reviewer.', 'woocommerce' ),
755
				'type'        => 'object',
756
				'context'     => array( 'view', 'edit' ),
757
				'readonly'    => true,
758
				'properties'  => $avatar_properties,
759
			);
760
		}
761
762
		return $this->add_additional_fields_schema( $schema );
763
	}
764
765
	/**
766
	 * Get the query params for collections.
767
	 *
768
	 * @return array
769
	 */
770
	public function get_collection_params() {
771
		$params = parent::get_collection_params();
772
773
		$params['context']['default'] = 'view';
774
775
		$params['after']            = array(
776
			'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
777
			'type'        => 'string',
778
			'format'      => 'date-time',
779
		);
780
		$params['before']           = array(
781
			'description' => __( 'Limit response to reviews published before a given ISO8601 compliant date.', 'woocommerce' ),
782
			'type'        => 'string',
783
			'format'      => 'date-time',
784
		);
785
		$params['exclude']          = array(
786
			'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
787
			'type'        => 'array',
788
			'items'       => array(
789
				'type' => 'integer',
790
			),
791
			'default'     => array(),
792
		);
793
		$params['include']          = array(
794
			'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ),
795
			'type'        => 'array',
796
			'items'       => array(
797
				'type' => 'integer',
798
			),
799
			'default'     => array(),
800
		);
801
		$params['offset']           = array(
802
			'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
803
			'type'        => 'integer',
804
		);
805
		$params['order']            = array(
806
			'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
807
			'type'        => 'string',
808
			'default'     => 'desc',
809
			'enum'        => array(
810
				'asc',
811
				'desc',
812
			),
813
		);
814
		$params['orderby']          = array(
815
			'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
816
			'type'        => 'string',
817
			'default'     => 'date_gmt',
818
			'enum'        => array(
819
				'date',
820
				'date_gmt',
821
				'id',
822
				'include',
823
				'product',
824
			),
825
		);
826
		$params['reviewer']         = array(
827
			'description' => __( 'Limit result set to reviews assigned to specific user IDs.', 'woocommerce' ),
828
			'type'        => 'array',
829
			'items'       => array(
830
				'type' => 'integer',
831
			),
832
		);
833
		$params['reviewer_exclude'] = array(
834
			'description' => __( 'Ensure result set excludes reviews assigned to specific user IDs.', 'woocommerce' ),
835
			'type'        => 'array',
836
			'items'       => array(
837
				'type' => 'integer',
838
			),
839
		);
840
		$params['reviewer_email']   = array(
841
			'default'     => null,
842
			'description' => __( 'Limit result set to that from a specific author email.', 'woocommerce' ),
843
			'format'      => 'email',
844
			'type'        => 'string',
845
		);
846
		$params['product']          = array(
847
			'default'     => array(),
848
			'description' => __( 'Limit result set to reviews assigned to specific product IDs.', 'woocommerce' ),
849
			'type'        => 'array',
850
			'items'       => array(
851
				'type' => 'integer',
852
			),
853
		);
854
		$params['status']           = array(
855
			'default'           => 'approved',
856
			'description'       => __( 'Limit result set to reviews assigned a specific status.', 'woocommerce' ),
857
			'sanitize_callback' => 'sanitize_key',
858
			'type'              => 'string',
859
			'enum'              => array(
860
				'all',
861
				'hold',
862
				'approved',
863
				'spam',
864
				'trash',
865
			),
866
		);
867
868
		/**
869
		 * Filter collection parameters for the reviews controller.
870
		 *
871
		 * This filter registers the collection parameter, but does not map the
872
		 * collection parameter to an internal \WP_Comment_Query parameter. Use the
873
		 * `wc_rest_review_query` filter to set \WP_Comment_Query parameters.
874
		 *
875
		 * @since 3.5.0
876
		 * @param array $params JSON Schema-formatted collection parameters.
877
		 */
878
		return apply_filters( 'woocommerce_rest_product_review_collection_params', $params );
879
	}
880
881
	/**
882
	 * Get the reivew, if the ID is valid.
883
	 *
884
	 * @since 3.5.0
885
	 * @param int $id Supplied ID.
886
	 * @return WP_Comment|\WP_Error Comment object if ID is valid, \WP_Error otherwise.
0 ignored issues
show
Bug introduced by
The type WooCommerce\RestApi\Cont...ers\Version4\WP_Comment was not found. Did you mean WP_Comment? If so, make sure to prefix the type with \.
Loading history...
887
	 */
888
	protected function get_review( $id ) {
889
		$id    = (int) $id;
890
		$error = new \WP_Error( 'woocommerce_rest_review_invalid_id', __( 'Invalid review ID.', 'woocommerce' ), array( 'status' => 404 ) );
891
892
		if ( 0 >= $id ) {
893
			return $error;
894
		}
895
896
		$review = get_comment( $id );
897
		if ( empty( $review ) ) {
898
			return $error;
899
		}
900
901
		if ( ! empty( $review->comment_post_ID ) ) {
902
			$post = get_post( (int) $review->comment_post_ID );
903
904
			if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) {
905
				return new \WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
906
			}
907
		}
908
909
		return $review;
910
	}
911
912
	/**
913
	 * Prepends internal property prefix to query parameters to match our response fields.
914
	 *
915
	 * @since 3.5.0
916
	 * @param string $query_param Query parameter.
917
	 * @return string
918
	 */
919
	protected function normalize_query_param( $query_param ) {
920
		$prefix = 'comment_';
921
922
		switch ( $query_param ) {
923
			case 'id':
924
				$normalized = $prefix . 'ID';
925
				break;
926
			case 'product':
927
				$normalized = $prefix . 'post_ID';
928
				break;
929
			case 'include':
930
				$normalized = 'comment__in';
931
				break;
932
			default:
933
				$normalized = $prefix . $query_param;
934
				break;
935
		}
936
937
		return $normalized;
938
	}
939
940
941
942
	/**
943
	 * Sets the comment_status of a given review object when creating or updating a review.
944
	 *
945
	 * @since 3.5.0
946
	 * @param string|int $new_status New review status.
947
	 * @param int        $id         Review ID.
948
	 * @return bool Whether the status was changed.
949
	 */
950
	protected function handle_status_param( $new_status, $id ) {
951
		$old_status = wp_get_comment_status( $id );
952
953
		if ( $new_status === $old_status ) {
954
			return false;
955
		}
956
957
		switch ( $new_status ) {
958
			case 'approved':
959
			case 'approve':
960
			case '1':
961
				$changed = wp_set_comment_status( $id, 'approve' );
962
				break;
963
			case 'hold':
964
			case '0':
965
				$changed = wp_set_comment_status( $id, 'hold' );
966
				break;
967
			case 'spam':
968
				$changed = wp_spam_comment( $id );
969
				break;
970
			case 'unspam':
971
				$changed = wp_unspam_comment( $id );
972
				break;
973
			case 'trash':
974
				$changed = wp_trash_comment( $id );
975
				break;
976
			case 'untrash':
977
				$changed = wp_untrash_comment( $id );
978
				break;
979
			default:
980
				$changed = false;
981
				break;
982
		}
983
984
		return $changed;
985
	}
986
987
	/**
988
	 * Check if a given request has access to read a webhook.
989
	 *
990
	 * @param  \WP_REST_Request $request Full details about the request.
991
	 * @return \WP_Error|boolean
992
	 */
993
	public function get_item_permissions_check( $request ) {
994
		$id          = $request->get_param( 'id' );
995
		$check_valid = $this->get_review( $id );
996
997
		if ( is_wp_error( $check_valid ) ) {
998
			return $check_valid;
999
		}
1000
1001
		return parent::get_item_permissions_check( $request );
1002
	}
1003
1004
	/**
1005
	 * Check if a given request has access to delete an item.
1006
	 *
1007
	 * @param  \WP_REST_Request $request Full details about the request.
1008
	 * @return \WP_Error|boolean
1009
	 */
1010
	public function delete_item_permissions_check( $request ) {
1011
		$id          = $request->get_param( 'id' );
1012
		$check_valid = $this->get_review( $id );
1013
1014
		if ( is_wp_error( $check_valid ) ) {
1015
			return $check_valid;
1016
		}
1017
1018
		return parent::delete_item_permissions_check( $request );
1019
	}
1020
1021
	/**
1022
	 * Check if a given request has access to update an item.
1023
	 *
1024
	 * @param  \WP_REST_Request $request Full details about the request.
1025
	 * @return \WP_Error|boolean
1026
	 */
1027
	public function update_item_permissions_check( $request ) {
1028
		$id          = $request->get_param( 'id' );
1029
		$check_valid = $this->get_review( $id );
1030
1031
		if ( is_wp_error( $check_valid ) ) {
1032
			return $check_valid;
1033
		}
1034
1035
		return parent::update_item_permissions_check( $request );
1036
	}
1037
}
1038