Completed
Push — master ( 3be021...a518b9 )
by Mike
04:08
created

ProductReviews::prepare_links()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

246
		$response = Pagination::add_pagination_headers( $response, $request, $total_reviews, /** @scrutinizer ignore-type */ $max_pages );
Loading history...
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 $total_items of Automattic\WooCommerce\R...dd_pagination_headers() does only seem to accept integer, 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

246
		$response = Pagination::add_pagination_headers( $response, $request, /** @scrutinizer ignore-type */ $total_reviews, $max_pages );
Loading history...
247
248
		return $response;
249
	}
250
251
	/**
252
	 * Create a single review.
253
	 *
254
	 * @param \WP_REST_Request $request Full details about the request.
255
	 * @return \WP_Error|\WP_REST_Response
256
	 */
257
	public function create_item( $request ) {
258
		if ( ! empty( $request['id'] ) ) {
259
			return new \WP_Error( 'woocommerce_rest_review_exists', __( 'Cannot create existing product review.', 'woocommerce' ), array( 'status' => 400 ) );
260
		}
261
262
		$product_id = (int) $request['product_id'];
263
264
		if ( 'product' !== get_post_type( $product_id ) ) {
265
			return new \WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
266
		}
267
268
		$prepared_review = $this->prepare_item_for_database( $request );
269
		if ( is_wp_error( $prepared_review ) ) {
270
			return $prepared_review;
271
		}
272
273
		$prepared_review['comment_type'] = 'review';
274
275
		/*
276
		 * Do not allow a comment to be created with missing or empty comment_content. See wp_handle_comment_submission().
277
		 */
278
		if ( empty( $prepared_review['comment_content'] ) ) {
279
			return new \WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) );
280
		}
281
282
		// Setting remaining values before wp_insert_comment so we can use wp_allow_comment().
283
		if ( ! isset( $prepared_review['comment_date_gmt'] ) ) {
284
			$prepared_review['comment_date_gmt'] = current_time( 'mysql', true );
285
		}
286
287
		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

287
		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...
288
			$prepared_review['comment_author_IP'] = wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // WPCS: input var ok.
289
		} else {
290
			$prepared_review['comment_author_IP'] = '127.0.0.1';
291
		}
292
293
		if ( ! empty( $request['author_user_agent'] ) ) {
294
			$prepared_review['comment_agent'] = $request['author_user_agent'];
295
		} elseif ( $request->get_header( 'user_agent' ) ) {
296
			$prepared_review['comment_agent'] = $request->get_header( 'user_agent' );
297
		} else {
298
			$prepared_review['comment_agent'] = '';
299
		}
300
301
		$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

301
		$check_comment_lengths = wp_check_comment_data_max_lengths( /** @scrutinizer ignore-type */ $prepared_review );
Loading history...
302
		if ( is_wp_error( $check_comment_lengths ) ) {
303
			$error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() );
304
			return new \WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) );
305
		}
306
307
		$prepared_review['comment_parent']     = 0;
308
		$prepared_review['comment_author_url'] = '';
309
		$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

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

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