PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.3.0
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.3.0
3.3.0 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 trunk 1.0.4 1.1.0 1.1.0.1 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 All 162 releases
cartflows / admin-core / ajax / meta-data.php

meta-data.php in CartFlows – Funnel Builder & Checkout Plugin for WooCommerce 3.3.0, at admin-core/ajax/meta-data.php

404 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CartFlows Flows ajax actions.
4 *
5 * @package CartFlows
6 */
7
8 namespace CartflowsAdmin\AdminCore\Ajax;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 use CartflowsAdmin\AdminCore\Ajax\AjaxBase;
16 use CartflowsAdmin\AdminCore\Inc\AdminHelper;
17
18 /**
19 * Class Steps.
20 */
21 class MetaData extends AjaxBase {
22
23 /**
24 * Instance
25 *
26 * @access private
27 * @var object Class object.
28 * @since 1.0.0
29 */
30 private static $instance;
31
32 /**
33 * Rows the pickers display, before `cartflows_product_search_result_limit`
34 * adjusts it for the product endpoint.
35 *
36 * @since x.x.x
37 * @var int
38 */
39 const SEARCH_RESULT_LIMIT = 20;
40
41 /**
42 * Floor for the rows read before the supported-product-type filter narrows them.
43 *
44 * @since x.x.x
45 * @var int
46 */
47 const SEARCH_CANDIDATE_LIMIT = 100;
48
49 /**
50 * Ceiling for `cartflows_product_search_result_limit`.
51 *
52 * @since x.x.x
53 * @var int
54 */
55 const SEARCH_RESULT_LIMIT_MAX = 200;
56
57 /**
58 * Initiator
59 *
60 * @since 1.0.0
61 * @return object initialized object of class.
62 */
63 public static function get_instance() {
64 if ( ! isset( self::$instance ) ) {
65 self::$instance = new self();
66 }
67 return self::$instance;
68 }
69
70 /**
71 * Register ajax events.
72 *
73 * @since 1.0.0
74 * @return void
75 */
76 public function register_ajax_events() {
77
78 $ajax_events = array(
79 'json_search_products',
80 'json_search_coupons',
81 );
82
83 $this->init_ajax_events( $ajax_events );
84 }
85
86 /**
87 * AJAX handler to search products for the admin pickers.
88 *
89 * Searches titles, SKUs and GTINs only, ranked exact match first. It
90 * deliberately does not search descriptions.
91 *
92 * @see \Cartflows_Product_Search::search_product_ids()
93 *
94 * @return void
95 */
96 public function json_search_products() {
97
98 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
99 return;
100 }
101
102 check_ajax_referer( 'cartflows_json_search_products', 'security' );
103
104 if ( ! isset( $_POST['term'] ) ) {
105 return;
106 }
107
108 $term = ! empty( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : '';
109
110 // sanitize_text_field() trims, so a whitespace-only term arrives empty here and would otherwise match every product.
111 if ( '' === trim( $term ) ) {
112 wp_send_json( array() );
113 }
114
115 // CartFlows supported product types.
116 $supported_product_types = apply_filters( 'cartflows_supported_product_types_for_search', array( 'simple', 'variable', 'variation', 'subscription', 'variable-subscription', 'subscription_variation', 'course' ) );
117
118 // Allowed product types.
119 if ( isset( $_POST['allowed_products'] ) && ! empty( $_POST['allowed_products'] ) ) {
120
121 $allowed_product_types = sanitize_text_field( ( wp_unslash( $_POST['allowed_products'] ) ) );
122
123 $allowed_product_types = $this->sanitize_data_attributes( $allowed_product_types );
124
125 $supported_product_types = $allowed_product_types;
126 }
127
128 // Include product types.
129 if ( isset( $_POST['include_products'] ) && ! empty( $_POST['include_products'] ) ) {
130
131 $include_product_types = sanitize_text_field( ( wp_unslash( $_POST['include_products'] ) ) );
132
133 $include_product_types = $this->sanitize_data_attributes( $include_product_types );
134
135 $supported_product_types = array_merge( $supported_product_types, $include_product_types );
136 }
137
138 // Exclude product types.
139 if ( isset( $_POST['exclude_products'] ) && ! empty( $_POST['exclude_products'] ) ) {
140
141 $excluded_product_types = sanitize_text_field( ( wp_unslash( $_POST['exclude_products'] ) ) );
142
143 $excluded_product_types = $this->sanitize_data_attributes( $excluded_product_types );
144
145 $supported_product_types = array_diff( $supported_product_types, $excluded_product_types );
146 }
147
148 // Resolved before the search so raising the filter actually widens the candidate set it is sliced from.
149 $result_limit = (int) apply_filters( 'cartflows_product_search_result_limit', self::SEARCH_RESULT_LIMIT );
150
151 // Clamped at both ends: the candidate set is a multiple of this, and an unbounded filter value would exhaust memory here.
152 $result_limit = min( self::SEARCH_RESULT_LIMIT_MAX, max( 1, $result_limit ) );
153 $candidate_limit = max( self::SEARCH_CANDIDATE_LIMIT, $result_limit * 5 );
154
155 // Excluded here rather than in the browser: filtering after the cap hides products that were never sent.
156 $excluded_product_ids = isset( $_POST['exclude_product_ids'] ) ? $this->sanitize_id_list( sanitize_text_field( wp_unslash( $_POST['exclude_product_ids'] ) ) ) : array();
157
158 // Read a wide candidate set: the supported-type filter below would otherwise shrink an already capped list.
159 $ids = \Cartflows_Product_Search::search_product_ids( $term, $candidate_limit );
160
161 // One prime beats the post and meta reads wc_get_product() and get_type() would each make per candidate.
162 if ( ! empty( $ids ) ) {
163 _prime_post_caches( $ids, true, true );
164 }
165
166 // Hydrated lazily and capped pair-aware: the candidate set is far wider than the rows that get rendered.
167 $product_objects = \Cartflows_Product_Search::collect_supported_products( $ids, $supported_product_types, $result_limit, $excluded_product_ids );
168
169 $products_found = array();
170
171 foreach ( $product_objects as $product_object ) {
172 $formatted_name = $product_object->get_name();
173 $managing_stock = $product_object->managing_stock();
174 $is_in_stock = $product_object->is_in_stock();
175 $product_type = $product_object->get_type();
176
177 $availibility_text = $this->get_stock_availability_text( $product_object );
178 $product_price_range = $this->get_formatted_product_price_range( $product_object );
179
180
181 if ( $managing_stock && ! empty( $_GET['display_stock'] ) ) {
182 $stock_amount = $product_object->get_stock_quantity();
183 /* Translators: %d stock amount */
184 $formatted_name .= ' &ndash; ' . sprintf( __( 'Stock: %d', 'cartflows' ), wc_format_stock_quantity_for_display( $stock_amount, $product_object ) );
185 }
186
187 array_push(
188 $products_found,
189 array(
190 'value' => $product_object->get_id(),
191 'label' => $formatted_name,
192 'original_price' => \Cartflows_Helper::get_product_original_price( $product_object ),
193 'product_name' => $product_object->get_name(),
194 'product_desc' => $product_object->get_short_description(),
195 'product_image' => get_the_post_thumbnail_url( $product_object->get_id() ),
196 'product_type' => $product_type,
197 'type_label' => $this->get_product_type_label( $product_object ),
198 'stock_status' => $availibility_text,
199 'in_stock' => $is_in_stock,
200 'price_range' => $product_price_range,
201 )
202 );
203 }
204
205 wp_send_json( $products_found );
206 }
207
208 /**
209 * AJAX handler to search coupons for the admin pickers.
210 *
211 * Matches anywhere in the coupon code, ranked exact then prefix then the rest.
212 *
213 * @return void
214 */
215 public function json_search_coupons() {
216
217 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
218 return;
219 }
220
221 check_ajax_referer( 'cartflows_json_search_coupons', 'security' );
222
223 if ( ! isset( $_POST['term'] ) ) {
224 return;
225 }
226
227 $term = ! empty( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : '';
228
229 if ( '' === trim( $term ) ) {
230 die();
231 }
232
233 $ids = \Cartflows_Coupon_Search::search_coupon_ids( $term, self::SEARCH_CANDIDATE_LIMIT );
234 $coupons_found = \Cartflows_Coupon_Search::format_for_picker( $ids, self::SEARCH_RESULT_LIMIT );
235
236 wp_send_json( $coupons_found );
237 }
238
239 /**
240 * Sanitize a comma separated list of product IDs.
241 *
242 * @param mixed $raw Raw request value.
243 * @return array<int, int> Positive integer IDs.
244 */
245 public function sanitize_id_list( $raw ) {
246
247 if ( is_string( $raw ) ) {
248 $raw = explode( ',', $raw );
249 }
250
251 if ( ! is_array( $raw ) ) {
252 return array();
253 }
254
255 return array_values( array_filter( array_map( 'absint', $raw ) ) );
256 }
257
258 /**
259 * Function to sanitize the product type data attribute.
260 *
261 * @param array $product_types product types.
262 */
263 public function sanitize_data_attributes( $product_types = array() ) {
264
265 if ( ! is_array( $product_types ) ) {
266 $product_types = explode( ',', $product_types );
267 }
268
269 // Sanitize the excluded types against valid product types.
270 foreach ( $product_types as $index => $value ) {
271 $product_types[ $index ] = strtolower( trim( $value ) );
272 }
273 return $product_types;
274 }
275
276 /**
277 * Function to get the stock availability text for a given product.
278 *
279 * This function checks the stock status of a product and returns a string indicating its availability.
280 * The availability text is translated to ensure it is user-friendly and consistent with the plugin's language.
281 *
282 * @param \WC_Product $product The product object to check the stock availability for.
283 * @return string The stock availability text for the product.
284 */
285 public function get_stock_availability_text( $product ) {
286
287 $availability_text = '';
288
289 if ( ! is_a( $product, 'WC_Product' ) ) {
290 return ''; // Return empty if product is not valid.
291 }
292
293 $availability = $product->get_availability();
294
295 if ( ! empty( $availability['class'] ) ) {
296
297 switch ( $availability['class'] ) {
298 case 'available-on-backorder':
299 $availability_text = __( 'On backorder', 'cartflows' );
300 break;
301 case 'in-stock':
302 $availability_text = __( 'In stock', 'cartflows' );
303 break;
304 case 'out-of-stock':
305 $availability_text = __( 'Out of stock', 'cartflows' );
306 break;
307 default:
308 break;
309 }
310 }
311
312 return $availability_text;
313 }
314
315 /**
316 * Get a short, human readable label for product types that behave differently in a funnel.
317 *
318 * Returned as its own field rather than appended to the product name, because the name is
319 * also used for the selected-product chip. Only types that need distinguishing are labelled;
320 * everything else returns an empty string so no tag is rendered.
321 *
322 * @since 3.1.4
323 * @param \WC_Product $product The product object.
324 * @return string Label to show beside the product, or an empty string.
325 */
326 public function get_product_type_label( $product ) {
327
328 if ( ! is_a( $product, 'WC_Product' ) ) {
329 return '';
330 }
331
332 $labels = apply_filters(
333 'cartflows_product_type_labels_for_search',
334 array(
335 'bundle' => __( 'Bundle', 'cartflows' ),
336 )
337 );
338
339 $product_type = $product->get_type();
340
341 return isset( $labels[ $product_type ] ) ? $labels[ $product_type ] : '';
342 }
343
344 /**
345 * Function to generate a formatted price range for a product.
346 *
347 * This function determines the price range for a product based on its type. For variable products, it calculates the price range from the minimum and maximum prices of its variations. For other product types, it uses the product's price or regular price if available.
348 * The function returns a formatted string representing the price range.
349 *
350 * @param \WC_Product $product The product object to generate the price range for.
351 * @return string The formatted price range for the product.
352 */
353 public function get_formatted_product_price_range( $product ) {
354 $original_price = 0;
355
356 $product_type = $product->get_type();
357
358 if ( 'variable' === $product_type ) {
359 // Code for variation product.
360 $variation_price_range = $product->get_variation_prices( true );
361
362 $price = array();
363 $min_price = isset( $variation_price_range['price'] ) ? current( $variation_price_range['price'] ) : null;
364 $max_price = isset( $variation_price_range['price'] ) ? end( $variation_price_range['price'] ) : null;
365 $min_reg_price = isset( $variation_price_range['regular_price'] ) ? current( $variation_price_range['regular_price'] ) : null;
366 $max_reg_price = isset( $variation_price_range['regular_price'] ) ? end( $variation_price_range['regular_price'] ) : null;
367
368 if ( $min_price !== $max_price && ! is_null( $min_price ) && ! is_null( $max_price ) ) {
369 // Build the range from each bound directly so WooCommerce's screen-reader "Price range: ... through ..." text is not appended.
370 $original_price = html_entity_decode( wp_strip_all_tags( wc_price( $min_price ) ) ) . ' – ' . html_entity_decode( wp_strip_all_tags( wc_price( $max_price ) ) );
371 }
372
373 if ( $min_reg_price !== $max_reg_price && ! is_null( $min_reg_price ) && ! is_null( $max_reg_price ) ) {
374 if ( ! empty( $original_price ) ) {
375 $original_price = html_entity_decode( wp_strip_all_tags( wc_price( $min_reg_price ) ) ) . ' – ' . html_entity_decode( wp_strip_all_tags( wc_price( $max_reg_price ) ) );
376 }
377 }
378 } elseif ( 'bundle' === $product_type && method_exists( $product, 'get_bundle_price' ) ) {
379 /*
380 * A bundle's own price is only its container base price, which is 0 whenever the
381 * bundled items are priced individually. Use the bundle's min/max price instead so
382 * the selector shows what the customer would actually pay, as it does for variable
383 * products. Guarded on the method so this stays inert without Product Bundles.
384 */
385 $min_price = $product->get_bundle_price( 'min', false );
386 $max_price = $product->get_bundle_price( 'max', false );
387
388 if ( '' !== $min_price && null !== $min_price ) {
389 $original_price = html_entity_decode( wp_strip_all_tags( wc_price( $min_price ) ) );
390
391 if ( '' !== $max_price && null !== $max_price && (float) $max_price !== (float) $min_price ) {
392 $original_price .= ' – ' . html_entity_decode( wp_strip_all_tags( wc_price( $max_price ) ) );
393 }
394 }
395 } else {
396 $price = $product->get_price();
397 $original_price = ! empty( $price ) ? $price : $product->get_regular_price();
398 $original_price = html_entity_decode( wp_strip_all_tags( wc_price( $original_price ) ) );
399 }
400
401 return $original_price;
402 }
403 }
404