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 / ajax / product.php

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

781 lines 26.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Ajax;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 use StoreEngine\Classes\AbstractAjaxHandler;
10 use StoreEngine\Classes\AbstractRequestHandler;
11 use StoreEngine\Classes\Analytics;
12 use StoreEngine\Classes\Exceptions\StoreEngineException;
13 use StoreEngine\Classes\Integration;
14 use StoreEngine\Classes\Price;
15 use StoreEngine\Integrations;
16 use StoreEngine\Utils\Formatting;
17 use StoreEngine\Utils\Helper;
18 use StoreEngine\Utils\Pixel;
19 use StoreEngine\Utils\Template as Template;
20 use WP_Error;
21 use WP_Query;
22
23 class Product extends AbstractAjaxHandler {
24
25 public function __construct() {
26 $price_fields = apply_filters( 'storeengine/ajax/product/price_fields', [
27 'product_id' => 'id',
28 'price_name' => 'string',
29 'price_type' => 'string',
30 'price' => 'float',
31 'compare_price' => 'float',
32 'setup_fee' => 'boolean',
33 'setup_fee_name' => 'string',
34 'setup_fee_price' => 'float',
35 'setup_fee_type' => 'string',
36 'trial' => 'boolean',
37 'trial_days' => 'integer',
38 'expire' => 'boolean',
39 'expire_days' => 'integer',
40 'payment_duration' => 'integer',
41 'payment_duration_type' => 'string',
42 'upgradeable' => 'boolean',
43 'order' => 'integer',
44 ] );
45
46 // Actions.
47 $this->actions = [
48 'search_product_callback' => [
49 'callback' => [ $this, 'search_product_callback' ],
50 'capability' => 'manage_options',
51 'fields' => [
52 'search' => 'string',
53 'id' => 'id',
54 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Request field-type map key, not a query argument; the exclusion is a bounded admin-supplied id list.
55 'exclude' => AbstractRequestHandler::IDS,
56 'is_digital' => AbstractRequestHandler::BOOLEAN,
57 'is_versioned' => AbstractRequestHandler::BOOLEAN,
58 ]
59 ],
60 'get_product_list' => [
61 'callback' => [ $this, 'get_product_list' ],
62 'capability' => 'manage_options',
63 'fields' => [
64 'search' => 'string',
65 'integration_id' => 'int',
66 'provider' => 'string',
67 ],
68 ],
69 // Price CRUD — vendors need to manage prices on their own
70 // products. The primitive cap `edit_storeengine_products` is held
71 // by administrators, shop managers, AND the vendor role; each
72 // handler additionally enforces post-author ownership for the
73 // vendor case via user_can_manage_product_prices().
74 'get_product_prices' => [
75 'capability' => 'edit_storeengine_products',
76 'callback' => [ $this, 'get_product_prices' ],
77 'fields' => [
78 'product_id' => 'id',
79 ],
80 ],
81 'create_product_price' => [
82 'capability' => 'edit_storeengine_products',
83 'callback' => [ $this, 'create_product_price' ],
84 'fields' => $price_fields,
85 ],
86 'update_product_price' => [
87 'capability' => 'edit_storeengine_products',
88 'callback' => [ $this, 'update_product_price' ],
89 'fields' => array_merge( [ 'id' => 'id' ], $price_fields ),
90 ],
91 'toggle_price_visibility' => [
92 'capability' => 'edit_storeengine_products',
93 'callback' => [ $this, 'toggle_price_visibility' ],
94 'fields' => [
95 'id' => 'id',
96 'is_hidden' => 'boolean',
97 ],
98 ],
99 'delete_product_price' => [
100 'capability' => 'edit_storeengine_products',
101 'callback' => [ $this, 'delete_product_price' ],
102 'fields' => [ 'id' => 'id' ],
103 ],
104 'top_products' => [
105 'capability' => 'manage_options',
106 'callback' => [ $this, 'top_products' ],
107 ],
108 'get_product_integration_items' => [
109 'capability' => 'manage_options',
110 'callback' => [ $this, 'get_product_integration_items' ],
111 'fields' => [
112 'provider' => 'string',
113 'product_id' => 'id',
114 'search' => 'string',
115 ],
116 ],
117 'create_product_integration' => [
118 'capability' => 'manage_options',
119 'callback' => [ $this, 'create_product_integration' ],
120 'fields' => [
121 'provider' => 'string',
122 'price_id' => 'id',
123 'integration_id' => 'id',
124 'course_ids' => [
125 [
126 'value' => 'id',
127 'label' => 'text',
128 ]
129 ],
130 'product_id' => 'id',
131 ],
132 ],
133 'delete_product_integration' => [
134 'capability' => 'manage_options',
135 'callback' => [ $this, 'delete_product_integration' ],
136 'fields' => [
137 'id' => 'id',
138 'with_course_bundle' => 'string',
139 ],
140 ],
141 'get_product_slug' => [
142 'capability' => 'manage_options',
143 'callback' => [ $this, 'get_product_slug' ],
144 'fields' => [
145 'ID' => 'id',
146 ],
147 ],
148 'update_product_slug' => [
149 'capability' => 'manage_options',
150 'callback' => [ $this, 'update_product_slug' ],
151 'fields' => [
152 'ID' => 'id',
153 'new_slug' => 'string',
154 'new_title' => 'string',
155 ],
156 ],
157 'get_page_templates' => [
158 'capability' => 'manage_options',
159 'callback' => [ $this, 'get_page_templates' ],
160 ],
161 'archive_product_filter' => [
162 'allow_visitor_action' => true,
163 'callback' => [ $this, 'archive_product_filter' ],
164 'fields' => [
165 'search' => 'string',
166 'category' => 'strings',
167 'tags' => 'strings',
168 'orderby' => 'string',
169 'paged' => 'integer',
170 'count' => 'integer',
171 ],
172 ],
173 ];
174 }
175
176 protected function search_product_callback( array $payload ) {
177 if ( ! empty( $payload['id'] ) ) {
178 $products = [ Helper::get_product( $payload['id'] ) ];
179 } else {
180 $args = [
181 's' => $payload['search'] ?? '',
182 'posts_per_page' => 15,
183 'orderby' => 'post_title',
184 'order' => 'ASC',
185 'post_status' => 'publish',
186 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Bounded admin-only (manage_options) product search; excludes a small, caller-supplied id set.
187 'post__not_in' => $payload['exclude'] ?? [],
188 ];
189
190 $meta = [];
191 if ( array_key_exists( 'is_digital', $payload ) ) {
192 $meta[] = [
193 'key' => '_storeengine_product_shipping_type',
194 'value' => $payload['is_digital'] ? 'digital' : 'physical',
195 ];
196 }
197
198 if ( $meta ) {
199 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Admin-only product search filtered by product shipping-type meta; required for the digital/physical filter.
200 $args['meta_query'] = array_merge( [ 'relation' => 'AND'], $meta );
201 }
202
203 $products = Helper::get_products( $args );
204 }
205
206 $data = [];
207
208 foreach ( $products as $product ) {
209 if ( ! $product ) {
210 continue;
211 }
212
213 $prices = [];
214
215 add_filter( 'storeengine/product/get_price_types', [ $this, 'allow_bundle_supported_price_types' ] );
216
217 foreach ( $product->get_prices() as $price ) {
218 $prices[] = [
219 'label' => $price->get_name(),
220 'value' => $price->get_id(),
221 'price' => $price->get_price(),
222 'compare' => $price->get_compare_price(),
223 ];
224 }
225
226 remove_filter( 'storeengine/product/get_price_types', [ $this, 'allow_bundle_supported_price_types' ] );
227
228 $data[] = [
229 'label' => $product->get_name(),
230 'value' => $product->get_id(),
231 'prices' => $prices
232 ];
233 }
234
235 wp_send_json_success( $data );
236 }
237
238 public function allow_bundle_supported_price_types( array $types ): array {
239 if ( in_array( 'subscription', $types, true ) ) {
240 $types = array_flip( $types );
241 unset( $types['subscription'] );
242 $types = array_flip( $types );
243 }
244
245 return $types;
246 }
247
248 public function get_product_list( array $payload ) {
249 if ( ! isset( $payload['integration_id'] ) ) {
250 wp_send_json_error( [
251 'message' => __( 'integration_id is required', 'storeengine' ),
252 ] );
253 }
254
255 $search = $payload['search'] ?? '';
256 $provider = $payload['provider'] ?? 'storeengine/membership-addon';
257 wp_send_json_success( Helper::get_product_list( $payload['integration_id'], $provider, $search ) );
258 }
259
260 protected function get_page_templates() {
261 wp_send_json_success( wp_get_theme()->get_page_templates() );
262 }
263
264 /**
265 * Author-scope guard for the price AJAX endpoints.
266 *
267 * The endpoint cap is `edit_storeengine_products` — held by admins,
268 * shop managers, AND vendors. Admins/shop managers also hold the
269 * "others" cap and can touch anything; vendors only their own
270 * products. Mirrors `ProductPermissions::restrict_others_products()`
271 * which gates the REST product-CRUD endpoints.
272 */
273 protected function user_can_manage_product_prices( int $product_id ): bool {
274 if ( ! $product_id ) {
275 return false;
276 }
277 if ( current_user_can( 'edit_others_storeengine_products' ) || current_user_can( 'manage_options' ) ) {
278 return true;
279 }
280 $post = get_post( $product_id );
281 if ( ! $post || Helper::PRODUCT_POST_TYPE !== $post->post_type ) {
282 return false;
283 }
284 return (int) $post->post_author === get_current_user_id();
285 }
286
287 protected function get_product_prices( $payload ) {
288 if ( empty( $payload['product_id'] ) ) {
289 wp_send_json_error( esc_html__( 'Product ID required.', 'storeengine' ) );
290 }
291
292 if ( ! $this->user_can_manage_product_prices( (int) $payload['product_id'] ) ) {
293 wp_send_json_error( esc_html__( 'You can only manage prices on your own products.', 'storeengine' ), 403 );
294 }
295
296 wp_send_json_success( Helper::get_prices_array_by_product_id( $payload['product_id'] ) );
297 }
298
299 protected function create_product_price( $payload ) {
300 $data = $this->validate_and_prepare_price_data( $payload );
301
302 if ( is_wp_error( $data ) ) {
303 wp_send_json_error( $data->get_error_message() );
304 }
305
306 if ( ! $this->user_can_manage_product_prices( (int) ( $data['product_id'] ?? 0 ) ) ) {
307 wp_send_json_error( esc_html__( 'You can only add prices to your own products.', 'storeengine' ), 403 );
308 }
309
310 $price = new Price();
311 $this->populate_price_props( $price, $data );
312 $saved = $price->save();
313
314 if ( is_wp_error( $saved ) ) {
315 wp_send_json_error( sprintf(
316 /* translators: %s: Error message. */
317 esc_html__( 'Failed to save new price. Error: %s', 'storeengine' ),
318 esc_html( $saved->get_error_message() )
319 ) );
320 }
321
322 wp_send_json_success( $this->prepare_price_data_response( $price ) );
323 }
324
325 protected function update_product_price( $payload ) {
326 $data = $this->validate_and_prepare_price_data( $payload, 'edit' );
327
328 if ( is_wp_error( $data ) ) {
329 wp_send_json_error( $data->get_error_message() );
330 }
331
332 try {
333 $price = new Price( absint( $payload['id'] ) );
334 if ( ! $this->user_can_manage_product_prices( (int) $price->get_product_id() ) ) {
335 wp_send_json_error( esc_html__( 'You can only edit prices on your own products.', 'storeengine' ), 403 );
336 }
337 $this->populate_price_props( $price, $data );
338 $saved = $price->save();
339
340 if ( is_wp_error( $saved ) ) {
341 wp_send_json_error( sprintf(
342 /* translators: %s: Error message. */
343 esc_html__( 'Failed to update price. Error: %s', 'storeengine' ),
344 esc_html( $saved->get_error_message() )
345 ) );
346 }
347
348 wp_send_json_success( $this->prepare_price_data_response( $price ) );
349 } catch ( StoreEngineException $e ) {
350 wp_send_json_error( $e->getMessage() );
351 }
352 }
353
354 protected function toggle_price_visibility( $payload ) {
355 if ( empty( $payload['id'] ) ) {
356 wp_send_json_error( esc_html__( 'Price id is required!', 'storeengine' ) );
357 }
358
359 try {
360 $price = new Price( $payload['id'] );
361 if ( ! $this->user_can_manage_product_prices( (int) $price->get_product_id() ) ) {
362 wp_send_json_error( esc_html__( 'You can only toggle prices on your own products.', 'storeengine' ), 403 );
363 }
364
365 $is_hidden = ! array_key_exists( 'is_hidden', $payload ) ? ! $price->get_is_hidden() : (bool) $payload['is_hidden'];
366
367 $price->set_is_hidden( $is_hidden );
368 $saved = $price->save();
369
370 if ( is_wp_error( $saved ) ) {
371 wp_send_json_error( sprintf(
372 /* translators: %s: Error message. */
373 esc_html__( 'Failed to save price visibility. Error: %s', 'storeengine' ),
374 esc_html( $saved->get_error_message() )
375 ) );
376 }
377
378 wp_send_json_success( $this->prepare_price_data_response( $price ) );
379 } catch ( StoreEngineException $e ) {
380 wp_send_json_error( $e->getMessage() );
381 }
382 }
383
384 protected function prepare_price_data_response( Price $price ): array {
385 return [
386 'id' => $price->get_id(),
387 'price_name' => $price->get_name(),
388 'price_type' => $price->get_price_type(),
389 'price' => $price->get_price(),
390 'compare_price' => $price->get_compare_price(),
391 'product_id' => $price->get_product_id(),
392 'order' => $price->get_order(),
393 'settings' => $price->get_settings(),
394 // Include the checkout link so a freshly created/updated price has a
395 // working Checkout/Copy link immediately, without a page reload
396 // (mirrors the field on the product-fetch response).
397 'checkout_link' => add_query_arg( [
398 'product_id' => $price->get_product_id(),
399 'price_id' => $price->get_id(),
400 ], Helper::get_checkout_url() ),
401 ];
402 }
403
404 protected function populate_price_props( Price $price, array $data ) {
405 $price->set_props( [
406 'price_name' => $data['price_name'],
407 'price_type' => $data['price_type'],
408 'price' => $data['price'],
409 'compare_price' => $data['compare_price'] > 0 ? $data['compare_price'] : null,
410 'product_id' => absint( $data['product_id'] ),
411 'order' => absint( $data['order'] ),
412 'setup_fee' => (bool) ( $data['settings']['setup_fee'] ?: false ),
413 'setup_fee_name' => (string) ( $data['settings']['setup_fee_name'] ?: '' ),
414 'setup_fee_price' => ( $data['settings']['setup_fee_price'] ?: 0 ),
415 'setup_fee_type' => (string) ( $data['settings']['setup_fee_type'] ?: 'fixed' ),
416 'trial' => (bool) ( $data['settings']['trial'] ?: false ),
417 'trial_days' => absint( $data['settings']['trial_days'] ?: 0 ),
418 'expire' => (bool) ( $data['settings']['expire'] ?: false ),
419 'expire_days' => absint( $data['settings']['expire_days'] ?: 0 ),
420 'payment_duration' => absint( $data['settings']['payment_duration'] ?: 1 ),
421 'payment_duration_type' => (string) ( $data['settings']['payment_duration_type'] ?: 'monthly' ),
422 'upgradeable' => (bool) ( $data['settings']['upgradeable'] ?: false ),
423 ] );
424
425 do_action_ref_array( 'storeengine/ajax/product/set_price_props', [ &$price, $data ] );
426 }
427
428 protected function validate_and_prepare_price_data( array $payload, string $context = 'create' ) {
429 if ( 'edit' === $context && empty( $payload['id'] ) ) {
430 return new WP_Error( 'invalid-price-id', __( 'Product Price ID is missing.', 'storeengine' ) );
431 }
432
433 if ( empty( $payload['product_id'] ) ) {
434 return new WP_Error( 'invalid-product-id', __( 'Product ID is missing.', 'storeengine' ) );
435 }
436
437 if ( empty( $payload['price_name'] ) ) {
438 return new WP_Error( 'price_name', __( 'Price name is required.', 'storeengine' ) );
439 }
440
441 if ( empty( $payload['price_type'] ) ) {
442 return new WP_Error( 'price_type', __( 'Price type is required.', 'storeengine' ) );
443 }
444
445 if ( ! empty( $payload['price'] ) && (float) $payload['price'] <= 0 ) {
446 return new WP_Error( 'invalid-price', __( 'Price must be greater than 0.', 'storeengine' ) );
447 }
448
449 if ( ! empty( $payload['compare_price'] ) ) {
450 if ( $payload['compare_price'] <= 0 ) {
451 return new WP_Error( 'invalid-compare-price', __( 'Compare price must be greater than 0.', 'storeengine' ) );
452 }
453
454 $compare_price = abs( floatval( $payload['compare_price'] ) );
455 $price = abs( floatval( $payload['price'] ?? 0 ) );
456
457 if ( $compare_price <= $price ) {
458 return new WP_Error( 'invalid-compare-price', __( 'Compare price must be greater than price.', 'storeengine' ) );
459 }
460 }
461
462 $validate = apply_filters( 'storeengine/ajax/product/validate_price_data', true, $payload, $context );
463
464 if ( is_wp_error( $validate ) && $validate->has_errors() ) {
465 return $validate;
466 }
467
468 $settings = apply_filters(
469 'storeengine/ajax/product/prepare_price_settings',
470 [
471 'setup_fee' => $payload['setup_fee'] ?? false,
472 'setup_fee_name' => $payload['setup_fee_name'] ?? '',
473 'setup_fee_price' => $payload['setup_fee_price'] ?? '',
474 'setup_fee_type' => $payload['setup_fee_type'] ?? '',
475 'trial' => $payload['trial'] ?? false,
476 'trial_days' => $payload['trial_days'] ?? 7,
477 'expire' => $payload['expire'] ?? false,
478 'expire_days' => $payload['expire_days'] ?? 3,
479 'payment_duration' => $payload['payment_duration'] ?? 0,
480 'payment_duration_type' => $payload['payment_duration_type'] ?? '',
481 'upgradeable' => $payload['upgradeable'] ?? false,
482 ],
483 $payload,
484 $context
485 );
486
487 return [
488 'price_name' => $payload['price_name'],
489 'price_type' => $payload['price_type'],
490 'price' => $payload['price'],
491 'compare_price' => $payload['compare_price'] ?? null,
492 'product_id' => $payload['product_id'],
493 'settings' => $settings,
494 'order' => $payload['order'] ?? 0,
495 ];
496 }
497
498 protected function delete_product_price( $payload ) {
499 if ( empty( $payload['id'] ) ) {
500 wp_send_json_error( esc_html__( 'Price Id missing!', 'storeengine' ) );
501 }
502
503 try {
504 $price = new Price( $payload['id'] );
505
506 if ( ! $this->user_can_manage_product_prices( (int) $price->get_product_id() ) ) {
507 wp_send_json_error( esc_html__( 'You can only delete prices on your own products.', 'storeengine' ), 403 );
508 }
509
510 if ( $price->delete() ) {
511 do_action( 'storeengine/price_deleted', $payload['id'] );
512 wp_send_json_success( true );
513 }
514
515 wp_send_json_error( esc_html__( 'Something went wrong! Failed to delete product price!', 'storeengine' ) );
516 } catch ( StoreEngineException $e ) {
517 wp_send_json_error( $e->getMessage() );
518 }
519 }
520
521 public function top_products() {
522 wp_send_json_success( ( new Analytics() )->get_top_products() );
523 }
524
525 public function get_product_integration_items( $payload ) {
526 if ( empty( $payload['provider'] ) ) {
527 wp_send_json_error( esc_html__( 'Provider missing!', 'storeengine' ) );
528 }
529
530 try {
531 $integration = Integrations::get_instance()->get_integration( $payload['provider'] );
532 wp_send_json_success( [
533 'label' => $integration->get_items_label(),
534 'items' => $integration->get_items( [
535 'product_id' => $payload['product_id'] ?? 0,
536 'search' => $payload['search'] ?? '',
537 ] ),
538 ] );
539 } catch ( StoreEngineException $exception ) {
540 wp_send_json_error( $exception->getMessage() );
541 }
542 }
543
544 public function create_product_integration( $payload ) {
545 if ( empty( $payload['provider'] ) ) {
546 wp_send_json_error( esc_html__( 'Provider is required', 'storeengine' ) );
547 }
548 if ( empty( $payload['product_id'] ) ) {
549 wp_send_json_error( esc_html__( 'Product ID is required', 'storeengine' ) );
550 }
551 if ( empty( $payload['price_id'] ) ) {
552 wp_send_json_error( esc_html__( 'Price ID is required', 'storeengine' ) );
553 }
554 if ( 'storeengine/course-bundle' !== $payload['provider'] && empty( $payload['integration_id'] ) ) {
555 wp_send_json_error( esc_html__( 'Integration ID is required', 'storeengine' ) );
556 }
557
558 if ( 'storeengine/course-bundle' === $payload['provider'] ) {
559 $course_ids = $payload['course_ids'] ?? [];
560 if ( empty( $course_ids ) ) {
561 wp_send_json_error( esc_html__( 'Please select at least one course.', 'storeengine' ) );
562 }
563 $has_bundle = get_post_meta( $payload['product_id'], '_academy_course_bundle_id', true );
564 if ( ! empty( $has_bundle ) ) {
565 wp_send_json_error( esc_html__( 'Each product can only be assigned to a single course bundle.', 'storeengine' ) );
566 }
567
568 $bundle_id = wp_insert_post( [
569 'post_title' => get_the_title( $payload['product_id'] ),
570 'post_type' => 'alms_course_bundle',
571 'post_status' => 'publish',
572 'meta_input' => [
573 'academy_course_bundle_courses_ids' => $course_ids,
574 'academy_course_bundle_type' => 'paid',
575 ],
576 ] );
577
578 if ( is_wp_error( $bundle_id ) ) {
579 wp_send_json_error( $bundle_id->get_error_message() );
580 }
581
582 $payload['integration_id'] = $bundle_id;
583 }
584
585 global $wpdb;
586 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
587 $has_integration = $wpdb->get_var(
588 $wpdb->prepare( "
589 SELECT id, product_id, price_id
590 FROM
591 {$wpdb->prefix}storeengine_integrations
592 WHERE
593 integration_id = %d
594 AND provider = %s AND price_id = %d
595 ", $payload['integration_id'], $payload['provider'], $payload['price_id'] )
596 );
597 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
598
599 if ( $has_integration ) {
600 wp_send_json_error( esc_html__( 'Same integration already exists!', 'storeengine' ) );
601 }
602
603 $integration = new Integration();
604 $integration->set_provider( $payload['provider'] );
605 $integration->set_product_id( $payload['product_id'] );
606 $integration->set_price_id( $payload['price_id'] );
607 $integration->set_integration_id( $payload['integration_id'] );
608 $integration->save();
609
610 if ( 0 === $integration->get_id() ) {
611 wp_send_json_error( esc_html__( 'Failed to create integration!', 'storeengine' ) );
612 }
613
614 $data = [
615 'id' => $integration->get_id(),
616 'product_id' => $integration->get_product_id(),
617 'price_id' => $integration->get_price_id(),
618 'provider' => $integration->get_provider(),
619 'integration_id' => $integration->get_integration_id(),
620 ];
621
622 if ( 'storeengine/course-bundle' === $payload['provider'] ) {
623 $data['course_ids'] = $payload['course_ids'] ?? [];
624 }
625
626 wp_send_json_success( $data );
627 }
628
629 public function delete_product_integration( $payload ) {
630 if ( empty( $payload['id'] ) ) {
631 wp_send_json_error( esc_html__( 'Integration ID is required', 'storeengine' ) );
632 }
633
634 $integration = ( new Integration( $payload['id'] ) )->get();
635 if ( ! $integration ) {
636 wp_send_json_error( esc_html__( 'Integration not found!', 'storeengine' ) );
637 }
638
639 if ( 'storeengine/course-bundle' === $integration->get_provider() ) {
640 delete_post_meta( $integration->get_product_id(), '_academy_course_bundle_id' );
641 delete_post_meta( $integration->get_integration_id(), 'academy_course_bundle_product_id' );
642
643 $with_course_bundle = $payload['with_course_bundle'] && Formatting::string_to_bool( $payload['with_course_bundle'] );
644 if ( $with_course_bundle ) {
645 wp_delete_post( $integration->get_integration_id(), true );
646 }
647 }
648
649 if ( ! $integration->delete() ) {
650 wp_send_json_error( esc_html__( 'Failed to delete integration!', 'storeengine' ) );
651 }
652
653 wp_send_json_success();
654 }
655
656 public function get_product_slug( $payload ) {
657 if ( empty( $payload['ID'] ) ) {
658 wp_send_json_error( esc_html__( 'Product ID is required!', 'storeengine' ) );
659 }
660
661 wp_send_json_success( Helper::get_sample_permalink_args( $payload['ID'] ) );
662 }
663
664 public function update_product_slug( $payload ) {
665 if ( empty( $payload['ID'] ) ) {
666 wp_send_json_error( esc_html__( 'Product ID is required!', 'storeengine' ) );
667 }
668
669 if ( empty( $payload['new_slug'] ) ) {
670 wp_send_json_error( esc_html__( 'New slug is required!', 'storeengine' ) );
671 }
672
673 $payload['new_slug'] = sanitize_title( $payload['new_slug'] );
674
675 if ( ! $payload['new_slug'] ) {
676 wp_send_json_error( esc_html__( 'New slug is invalid!', 'storeengine' ) );
677 }
678
679 $post = get_post( $payload['ID'] );
680
681 if ( ! $post ) {
682 wp_send_json_error( __( 'Product not found', 'storeengine' ), 404 );
683 }
684
685 if ( $post->post_name === $payload['new_slug'] ) {
686 wp_send_json_error( esc_html__( 'Product slug is not changed!', 'storeengine' ) );
687 }
688
689 $update = wp_update_post( [ 'ID' => $post->ID, 'post_name' => $payload['new_slug'] ], true );
690
691 if ( is_wp_error( $update ) ) {
692 wp_send_json_error( $update->get_error_message() );
693 }
694
695 wp_send_json_success( Helper::get_sample_permalink_args( $payload['ID'], $payload['new_title'] ?? null, $payload['new_slug'] ) );
696 }
697
698 public function archive_product_filter( $payload ) {
699 $search = $payload['search'] ?? '';
700 $category = $payload['category'] ?? [];
701 $tags = $payload['tags'] ?? [];
702 $orderby = $payload['orderby'] ?? '';
703 $paged = ! empty( $payload['paged'] ) ? $payload['paged'] : 1;
704 $per_row = Helper::get_settings( 'product_archive_products_per_row' );
705 $posts_per_page = ! empty( $payload['count'] ) ? absint( $payload['count'] ) : (int) Helper::get_settings( 'product_archive_products_per_page', 12 );
706
707 $args = Helper::prepare_product_search_query_args( [
708 'search' => $search,
709 'category' => $category,
710 'tags' => $tags,
711 'paged' => max( 1, $paged ),
712 'orderby' => $orderby,
713 'posts_per_page' => $posts_per_page,
714 ] );
715 $args['meta_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
716 'relation' => 'OR',
717 [
718 'key' => '_storeengine_product_hide',
719 'value' => true,
720 'compare' => 'NOT EXISTS',
721 ],
722 [
723 'key' => '_storeengine_product_hide',
724 'value' => true,
725 'compare' => '!=',
726 ],
727 ];
728
729 $grid_class = Helper::get_responsive_column( $per_row );
730
731 // Make archive query.
732 // Setup globals for archive ajax.
733 $GLOBALS['page'] = max( 1, $paged );
734 $GLOBALS['paged'] = max( 1, $paged );
735 $GLOBALS['wp_the_query'] = new WP_Query( apply_filters( 'storeengine/product_filter_args', $args ) );
736 $GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];
737 $products_query = $GLOBALS['wp_the_query'];
738
739 ob_start();
740 ?>
741 <div class="storeengine-row">
742 <?php
743 if ( $products_query->have_posts() ) {
744 // Load posts loop.
745 while ( $products_query->have_posts() ) {
746 $products_query->the_post();
747 /**
748 * Hook: storeengine/templates/product_loop.
749 */
750 do_action( 'storeengine/templates/product_loop' );
751 Template::get_template( 'content-product.php', [ 'grid_class' => $grid_class ] );
752 }
753 Template::get_template( 'archive/pagination.php', [
754 'paged' => $paged,
755 'max_num_pages' => $products_query->max_num_pages,
756 ] );
757 } else {
758 Template::get_template( 'archive/product-none.php' );
759 }
760 ?>
761 </div>
762 <?php
763 $markup = ob_get_clean();
764
765 wp_send_json_success(
766 apply_filters(
767 'storeengine/ajax/product_archive_response',
768 [
769 'markup' => apply_filters( 'storeengine/product_filter_markup', $markup ),
770 'found_posts' => $products_query->found_posts,
771 'pixel_data' => [
772 'page_title' => html_entity_decode( wp_get_document_title() ),
773 'currency' => Formatting::get_currency(),
774 'products' => Pixel::get_product_archive_data(),
775 ],
776 ]
777 )
778 );
779 }
780 }
781