PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / api / reviews.php

reviews.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/api/reviews.php

368 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Product reviews REST controller (admin moderation + manual creation).
4 *
5 * Reviews are WordPress comments of type `storeengine_product` with a
6 * `storeengine_rating` meta and optional `storeengine_review_media` (attachment
7 * IDs). This controller powers the admin Products → Reviews screen.
8 *
9 * @package StoreEngine\API
10 */
11
12 namespace StoreEngine\API;
13
14 use StoreEngine\Utils\Helper;
15 use WP_Comment_Query;
16 use WP_Error;
17 use WP_REST_Controller;
18 use WP_REST_Request;
19 use WP_REST_Response;
20 use WP_REST_Server;
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 class Reviews extends WP_REST_Controller {
27
28 public function __construct() {
29 $this->namespace = STOREENGINE_PLUGIN_SLUG . '/v1';
30 $this->rest_base = 'reviews';
31 }
32
33 public static function init() {
34 $self = new self();
35 add_action( 'rest_api_init', [ $self, 'register_routes' ] );
36 }
37
38 public function register_routes() {
39 register_rest_route( $this->namespace, '/' . $this->rest_base, [
40 [
41 'methods' => WP_REST_Server::READABLE,
42 'callback' => [ $this, 'get_items' ],
43 'permission_callback' => [ $this, 'permissions_check' ],
44 'args' => $this->get_collection_params(),
45 ],
46 [
47 'methods' => WP_REST_Server::CREATABLE,
48 'callback' => [ $this, 'create_item' ],
49 'permission_callback' => [ $this, 'permissions_check' ],
50 'args' => $this->get_editable_args(),
51 ],
52 ] );
53
54 register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', [
55 [
56 'methods' => WP_REST_Server::READABLE,
57 'callback' => [ $this, 'get_item' ],
58 'permission_callback' => [ $this, 'permissions_check' ],
59 ],
60 [
61 'methods' => WP_REST_Server::EDITABLE,
62 'callback' => [ $this, 'update_item' ],
63 'permission_callback' => [ $this, 'permissions_check' ],
64 'args' => $this->get_editable_args(),
65 ],
66 [
67 'methods' => WP_REST_Server::DELETABLE,
68 'callback' => [ $this, 'delete_item' ],
69 'permission_callback' => [ $this, 'permissions_check' ],
70 'args' => [
71 'force' => [
72 'type' => 'boolean',
73 'default' => false,
74 ],
75 ],
76 ],
77 ] );
78 }
79
80 public function permissions_check( $request ) {
81 return Helper::check_rest_user_cap( 'manage_options' );
82 }
83
84 public function get_collection_params(): array {
85 return [
86 'page' => [ 'type' => 'integer', 'default' => 1 ],
87 'per_page' => [ 'type' => 'integer', 'default' => 20 ],
88 'search' => [ 'type' => 'string' ],
89 'status' => [ 'type' => 'string', 'default' => 'all', 'enum' => [ 'all', 'approve', 'hold', 'spam', 'trash' ] ],
90 'product' => [ 'type' => 'integer', 'default' => 0 ],
91 'rating' => [ 'type' => 'integer', 'default' => 0 ],
92 'orderby' => [ 'type' => 'string', 'default' => 'comment_date_gmt' ],
93 'order' => [ 'type' => 'string', 'default' => 'DESC', 'enum' => [ 'ASC', 'DESC' ] ],
94 ];
95 }
96
97 protected function get_editable_args(): array {
98 return [
99 'product_id' => [ 'type' => 'integer' ],
100 'author' => [ 'type' => 'string' ],
101 'email' => [ 'type' => 'string' ],
102 'content' => [ 'type' => 'string' ],
103 'rating' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 5 ],
104 'status' => [ 'type' => 'string', 'enum' => [ 'approve', 'hold', 'spam', 'trash' ] ],
105 'date' => [ 'type' => 'string' ],
106 'media' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
107 ];
108 }
109
110 public function get_items( $request ) {
111 $page = max( 1, (int) $request->get_param( 'page' ) );
112 $per_page = min( 100, max( 1, (int) $request->get_param( 'per_page' ) ) );
113 $status = $request->get_param( 'status' ) ?: 'all';
114
115 $args = [
116 'type' => 'storeengine_product',
117 'number' => $per_page,
118 'offset' => ( $page - 1 ) * $per_page,
119 'orderby' => $request->get_param( 'orderby' ) ?: 'comment_date_gmt',
120 'order' => $request->get_param( 'order' ) ?: 'DESC',
121 'status' => $this->map_status( $status ),
122 ];
123
124 if ( $request->get_param( 'search' ) ) {
125 $args['search'] = sanitize_text_field( $request->get_param( 'search' ) );
126 }
127
128 if ( $request->get_param( 'product' ) ) {
129 $args['post_id'] = absint( $request->get_param( 'product' ) );
130 }
131
132 if ( $request->get_param( 'rating' ) ) {
133 $args['meta_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
134 [
135 'key' => 'storeengine_rating',
136 'value' => absint( $request->get_param( 'rating' ) ),
137 ],
138 ];
139 }
140
141 $query = new WP_Comment_Query();
142 $comments = $query->query( $args );
143
144 // Total for pagination (same filters, count only).
145 $count_args = $args;
146 $count_args['number'] = 0;
147 $count_args['offset'] = 0;
148 $count_args['count'] = true;
149 $total = ( new WP_Comment_Query() )->query( $count_args );
150
151 $items = array_map( [ $this, 'prepare_review' ], $comments );
152
153 $response = new WP_REST_Response( $items );
154 $response->header( 'X-WP-Total', (int) $total );
155 $response->header( 'X-WP-TotalPages', (int) ceil( $total / $per_page ) );
156
157 return $response;
158 }
159
160 public function get_item( $request ) {
161 $comment = get_comment( absint( $request['id'] ) );
162
163 if ( ! $comment || 'storeengine_product' !== $comment->comment_type ) {
164 return new WP_Error( 'storeengine_review_not_found', __( 'Review not found.', 'storeengine' ), [ 'status' => 404 ] );
165 }
166
167 return rest_ensure_response( $this->prepare_review( $comment ) );
168 }
169
170 public function create_item( $request ) {
171 $product_id = absint( $request->get_param( 'product_id' ) );
172
173 if ( ! $product_id || 'storeengine_product' !== get_post_type( $product_id ) ) {
174 return new WP_Error( 'storeengine_invalid_product', __( 'A valid product is required.', 'storeengine' ), [ 'status' => 400 ] );
175 }
176
177 $rating = (int) $request->get_param( 'rating' );
178 $content = (string) $request->get_param( 'content' );
179
180 if ( $rating < 1 || $rating > 5 ) {
181 return new WP_Error( 'storeengine_invalid_rating', __( 'Rating must be between 1 and 5.', 'storeengine' ), [ 'status' => 400 ] );
182 }
183
184 $author = sanitize_text_field( (string) $request->get_param( 'author' ) );
185 $email = sanitize_email( (string) $request->get_param( 'email' ) );
186 $status = $request->get_param( 'status' ) ?: 'approve';
187 $date = $request->get_param( 'date' );
188
189 if ( '' === $author ) {
190 $author = __( 'Anonymous', 'storeengine' );
191 }
192
193 $commentdata = [
194 'comment_post_ID' => $product_id,
195 'comment_author' => $author,
196 'comment_author_email' => $email,
197 'comment_content' => wp_kses_post( $content ),
198 'comment_type' => 'storeengine_product',
199 'comment_approved' => 'approve' === $status ? 1 : ( 'hold' === $status ? 0 : $status ),
200 'comment_author_IP' => '',
201 'comment_agent' => 'StoreEngine',
202 'user_id' => 0,
203 ];
204
205 if ( $date ) {
206 $timestamp = strtotime( $date );
207 if ( $timestamp ) {
208 $commentdata['comment_date'] = gmdate( 'Y-m-d H:i:s', $timestamp + ( (int) ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ) ) );
209 $commentdata['comment_date_gmt'] = gmdate( 'Y-m-d H:i:s', $timestamp );
210 }
211 }
212
213 // wp_insert_comment doesn't run comment_post save hooks, so we set the
214 // rating/media meta ourselves below.
215 $comment_id = wp_insert_comment( wp_slash( $commentdata ) );
216
217 if ( ! $comment_id ) {
218 return new WP_Error( 'storeengine_review_create_failed', __( 'Could not create the review.', 'storeengine' ), [ 'status' => 500 ] );
219 }
220
221 update_comment_meta( $comment_id, 'storeengine_rating', $rating );
222 $this->save_media_meta( $comment_id, $product_id, $request->get_param( 'media' ) );
223
224 return rest_ensure_response( $this->prepare_review( get_comment( $comment_id ) ) );
225 }
226
227 public function update_item( $request ) {
228 $comment = get_comment( absint( $request['id'] ) );
229
230 if ( ! $comment || 'storeengine_product' !== $comment->comment_type ) {
231 return new WP_Error( 'storeengine_review_not_found', __( 'Review not found.', 'storeengine' ), [ 'status' => 404 ] );
232 }
233
234 $update = [ 'comment_ID' => $comment->comment_ID ];
235
236 if ( null !== $request->get_param( 'content' ) ) {
237 $update['comment_content'] = wp_kses_post( (string) $request->get_param( 'content' ) );
238 }
239
240 if ( null !== $request->get_param( 'author' ) ) {
241 $update['comment_author'] = sanitize_text_field( (string) $request->get_param( 'author' ) );
242 }
243
244 if ( null !== $request->get_param( 'email' ) ) {
245 $update['comment_author_email'] = sanitize_email( (string) $request->get_param( 'email' ) );
246 }
247
248 if ( count( $update ) > 1 ) {
249 wp_update_comment( wp_slash( $update ) );
250 }
251
252 $status = $request->get_param( 'status' );
253 if ( $status ) {
254 wp_set_comment_status( $comment->comment_ID, $status );
255 }
256
257 $rating = $request->get_param( 'rating' );
258 if ( null !== $rating && $rating >= 1 && $rating <= 5 ) {
259 update_comment_meta( $comment->comment_ID, 'storeengine_rating', (int) $rating );
260 }
261
262 if ( null !== $request->get_param( 'media' ) ) {
263 $this->save_media_meta( $comment->comment_ID, (int) $comment->comment_post_ID, $request->get_param( 'media' ) );
264 }
265
266 return rest_ensure_response( $this->prepare_review( get_comment( $comment->comment_ID ) ) );
267 }
268
269 public function delete_item( $request ) {
270 $comment = get_comment( absint( $request['id'] ) );
271
272 if ( ! $comment || 'storeengine_product' !== $comment->comment_type ) {
273 return new WP_Error( 'storeengine_review_not_found', __( 'Review not found.', 'storeengine' ), [ 'status' => 404 ] );
274 }
275
276 $force = (bool) $request->get_param( 'force' );
277 $result = wp_delete_comment( $comment->comment_ID, $force );
278
279 if ( ! $result ) {
280 return new WP_Error( 'storeengine_review_delete_failed', __( 'Could not delete the review.', 'storeengine' ), [ 'status' => 500 ] );
281 }
282
283 return rest_ensure_response( [ 'deleted' => true, 'id' => (int) $comment->comment_ID ] );
284 }
285
286 protected function save_media_meta( int $comment_id, int $product_id, $media ) {
287 if ( ! is_array( $media ) ) {
288 return;
289 }
290
291 $ids = array_values( array_filter( array_map( 'absint', $media ) ) );
292
293 if ( empty( $ids ) ) {
294 delete_comment_meta( $comment_id, 'storeengine_review_media' );
295 return;
296 }
297
298 foreach ( $ids as $attachment_id ) {
299 update_post_meta( $attachment_id, '_storeengine_review_media', $product_id );
300 }
301
302 update_comment_meta( $comment_id, 'storeengine_review_media', $ids );
303 }
304
305 protected function map_status( string $status ) {
306 switch ( $status ) {
307 case 'approve':
308 return 'approve';
309 case 'hold':
310 return 'hold';
311 case 'spam':
312 return 'spam';
313 case 'trash':
314 return 'trash';
315 default:
316 return 'all';
317 }
318 }
319
320 protected function prepare_review( $comment ): array {
321 $product = get_post( $comment->comment_post_ID );
322 $rating = (int) get_comment_meta( $comment->comment_ID, 'storeengine_rating', true );
323 $media_ids = get_comment_meta( $comment->comment_ID, 'storeengine_review_media', true );
324 $media = [];
325
326 if ( is_array( $media_ids ) ) {
327 foreach ( $media_ids as $attachment_id ) {
328 $attachment_id = absint( $attachment_id );
329 $url = wp_get_attachment_url( $attachment_id );
330 if ( ! $url ) {
331 continue;
332 }
333 $mime = (string) get_post_mime_type( $attachment_id );
334 $media[] = [
335 'id' => $attachment_id,
336 'url' => $url,
337 'thumb' => wp_get_attachment_image_url( $attachment_id, 'thumbnail' ) ?: $url,
338 'type' => ( 0 === strpos( $mime, 'video/' ) ) ? 'video' : 'image',
339 ];
340 }
341 }
342
343 $status = 'hold';
344 if ( '1' === (string) $comment->comment_approved || 1 === $comment->comment_approved || 'approve' === $comment->comment_approved ) {
345 $status = 'approve';
346 } elseif ( 'spam' === $comment->comment_approved ) {
347 $status = 'spam';
348 } elseif ( 'trash' === $comment->comment_approved ) {
349 $status = 'trash';
350 }
351
352 return [
353 'id' => (int) $comment->comment_ID,
354 'product_id' => (int) $comment->comment_post_ID,
355 'product_title' => $product ? $product->post_title : '',
356 'author' => $comment->comment_author,
357 'email' => $comment->comment_author_email,
358 'avatar' => get_avatar_url( $comment->comment_author_email ),
359 'content' => $comment->comment_content,
360 'rating' => $rating,
361 'status' => $status,
362 'date' => $comment->comment_date,
363 'date_gmt' => $comment->comment_date_gmt,
364 'media' => $media,
365 ];
366 }
367 }
368