Passed
Push — master ( f7c939...5bd17a )
by Mike
03:08
created

ProductReviews::prepare_item_for_response()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
nc 2
nop 2
dl 0
loc 20
rs 9.9332
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::check_resource( $this->resource_type, 'read', $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
		$response = rest_ensure_response( $response );
395
396
		$response->set_status( 201 );
397
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $review_id ) ) );
398
399
		return $response;
400
	}
401
402
	/**
403
	 * Get a single product review.
404
	 *
405
	 * @param \WP_REST_Request $request Full details about the request.
406
	 * @return \WP_Error|\WP_REST_Response
407
	 */
408
	public function get_item( $request ) {
409
		$review = $this->get_review( $request['id'] );
410
		if ( is_wp_error( $review ) ) {
411
			return $review;
412
		}
413
414
		$data     = $this->prepare_item_for_response( $review, $request );
0 ignored issues
show
Bug introduced by
$review of type WP_Error is incompatible with the type WP_Comment expected by parameter $review of WooCommerce\RestApi\Cont...are_item_for_response(). ( Ignorable by Annotation )

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

414
		$data     = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $review, $request );
Loading history...
415
		$response = rest_ensure_response( $data );
416
417
		return $response;
418
	}
419
420
	/**
421
	 * Updates a review.
422
	 *
423
	 * @param \WP_REST_Request $request Full details about the request.
424
	 * @return \WP_Error|\WP_REST_Response Response object on success, or error object on failure.
425
	 */
426
	public function update_item( $request ) {
427
		$review = $this->get_review( $request['id'] );
428
		if ( is_wp_error( $review ) ) {
429
			return $review;
430
		}
431
432
		$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...
433
434
		if ( isset( $request['type'] ) && 'review' !== get_comment_type( $id ) ) {
435
			return new \WP_Error( 'woocommerce_rest_review_invalid_type', __( 'Sorry, you are not allowed to change the comment type.', 'woocommerce' ), array( 'status' => 404 ) );
436
		}
437
438
		$prepared_args = $this->prepare_item_for_database( $request );
439
		if ( is_wp_error( $prepared_args ) ) {
440
			return $prepared_args;
441
		}
442
443
		if ( ! empty( $prepared_args['comment_post_ID'] ) ) {
444
			if ( 'product' !== get_post_type( (int) $prepared_args['comment_post_ID'] ) ) {
445
				return new \WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
446
			}
447
		}
448
449
		if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
450
			// Only the comment status is being changed.
451
			$change = $this->handle_status_param( $request['status'], $id );
452
453
			if ( ! $change ) {
454
				return new \WP_Error( 'woocommerce_rest_review_failed_edit', __( 'Updating review status failed.', 'woocommerce' ), array( 'status' => 500 ) );
455
			}
456
		} elseif ( ! empty( $prepared_args ) ) {
457
			if ( is_wp_error( $prepared_args ) ) {
458
				return $prepared_args;
459
			}
460
461
			if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) {
462
				return new \WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) );
463
			}
464
465
			$prepared_args['comment_ID'] = $id;
466
467
			$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args );
468
			if ( is_wp_error( $check_comment_lengths ) ) {
469
				$error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() );
470
				return new \WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) );
471
			}
472
473
			$updated = wp_update_comment( wp_slash( (array) $prepared_args ) );
474
475
			if ( false === $updated ) {
0 ignored issues
show
introduced by
The condition false === $updated is always false.
Loading history...
476
				return new \WP_Error( 'woocommerce_rest_comment_failed_edit', __( 'Updating review failed.', 'woocommerce' ), array( 'status' => 500 ) );
477
			}
478
479
			if ( isset( $request['status'] ) ) {
480
				$this->handle_status_param( $request['status'], $id );
481
			}
482
		}
483
484
		if ( ! empty( $request['rating'] ) ) {
485
			update_comment_meta( $id, 'rating', $request['rating'] );
486
		}
487
488
		$review = get_comment( $id );
489
490
		/** This action is documented in includes/api/class-wc-rest-product-reviews-controller.php */
491
		do_action( 'woocommerce_rest_insert_product_review', $review, $request, false );
492
493
		$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

493
		$fields_update = $this->update_additional_fields_for_object( /** @scrutinizer ignore-type */ $review, $request );
Loading history...
494
495
		if ( is_wp_error( $fields_update ) ) {
496
			return $fields_update;
497
		}
498
499
		$request->set_param( 'context', 'edit' );
500
501
		$response = $this->prepare_item_for_response( $review, $request );
0 ignored issues
show
Bug introduced by
It seems like $review can also be of type array; however, parameter $review of WooCommerce\RestApi\Cont...are_item_for_response() does only seem to accept WP_Comment, 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

501
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $review, $request );
Loading history...
502
503
		return rest_ensure_response( $response );
504
	}
505
506
	/**
507
	 * Deletes a review.
508
	 *
509
	 * @param \WP_REST_Request $request Full details about the request.
510
	 * @return \WP_Error|\WP_REST_Response Response object on success, or error object on failure.
511
	 */
512
	public function delete_item( $request ) {
513
		$review = $this->get_review( $request['id'] );
514
		if ( is_wp_error( $review ) ) {
515
			return $review;
516
		}
517
518
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
519
520
		/**
521
		 * Filters whether a review can be trashed.
522
		 *
523
		 * Return false to disable trash support for the post.
524
		 *
525
		 * @since 3.5.0
526
		 * @param bool       $supports_trash Whether the post type support trashing.
527
		 * @param WP_Comment $review         The review object being considered for trashing support.
528
		 */
529
		$supports_trash = apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review );
530
531
		$request->set_param( 'context', 'edit' );
532
533
		if ( $force ) {
534
			$previous = $this->prepare_item_for_response( $review, $request );
0 ignored issues
show
Bug introduced by
$review of type WP_Error is incompatible with the type WP_Comment expected by parameter $review of WooCommerce\RestApi\Cont...are_item_for_response(). ( Ignorable by Annotation )

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

534
			$previous = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $review, $request );
Loading history...
535
			$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...
536
			$response = new \WP_REST_Response();
537
			$response->set_data(
538
				array(
539
					'deleted'  => true,
540
					'previous' => $previous->get_data(),
541
				)
542
			);
543
		} else {
544
			// If this type doesn't support trashing, error out.
545
			if ( ! $supports_trash ) {
546
				/* translators: %s: force=true */
547
				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 ) );
548
			}
549
550
			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...
551
				return new \WP_Error( 'woocommerce_rest_already_trashed', __( 'The object has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) );
552
			}
553
554
			$result   = wp_trash_comment( $review->comment_ID );
555
			$review   = get_comment( $review->comment_ID );
556
			$response = $this->prepare_item_for_response( $review, $request );
0 ignored issues
show
Bug introduced by
It seems like $review can also be of type array; however, parameter $review of WooCommerce\RestApi\Cont...are_item_for_response() does only seem to accept WP_Comment, 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

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