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-legacy-core / ajax / meta-data.php

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

356 lines 11.2 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\AdminLegacyCore\Ajax;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 use CartflowsAdmin\AdminLegacyCore\Ajax\AjaxBase;
16 use CartflowsAdmin\AdminLegacyCore\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 'stock_status' => $availibility_text,
198 'in_stock' => $is_in_stock,
199 'price_range' => $product_price_range,
200 )
201 );
202 }
203
204 wp_send_json( $products_found );
205 }
206
207 /**
208 * AJAX handler to search coupons for the admin pickers.
209 *
210 * Matches anywhere in the coupon code, ranked exact then prefix then the rest.
211 *
212 * @return void
213 */
214 public function json_search_coupons() {
215
216 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
217 return;
218 }
219
220 check_ajax_referer( 'cartflows_json_search_coupons', 'security' );
221
222 if ( ! isset( $_POST['term'] ) ) {
223 return;
224 }
225
226 $term = ! empty( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : '';
227
228 if ( '' === trim( $term ) ) {
229 die();
230 }
231
232 $ids = \Cartflows_Coupon_Search::search_coupon_ids( $term, self::SEARCH_CANDIDATE_LIMIT );
233 $coupons_found = \Cartflows_Coupon_Search::format_for_picker( $ids, self::SEARCH_RESULT_LIMIT );
234
235 wp_send_json( $coupons_found );
236 }
237
238 /**
239 * Sanitize a comma separated list of product IDs.
240 *
241 * @param mixed $raw Raw request value.
242 * @return array<int, int> Positive integer IDs.
243 */
244 public function sanitize_id_list( $raw ) {
245
246 if ( is_string( $raw ) ) {
247 $raw = explode( ',', $raw );
248 }
249
250 if ( ! is_array( $raw ) ) {
251 return array();
252 }
253
254 return array_values( array_filter( array_map( 'absint', $raw ) ) );
255 }
256
257 /**
258 * Function to sanitize the product type data attribute.
259 *
260 * @param array $product_types product types.
261 */
262 public function sanitize_data_attributes( $product_types = array() ) {
263
264 if ( ! is_array( $product_types ) ) {
265 $product_types = explode( ',', $product_types );
266 }
267
268 // Sanitize the excluded types against valid product types.
269 foreach ( $product_types as $index => $value ) {
270 $product_types[ $index ] = strtolower( trim( $value ) );
271 }
272 return $product_types;
273 }
274
275 /**
276 * Function to get the stock availability text for a given product.
277 *
278 * This function checks the stock status of a product and returns a string indicating its availability.
279 * The availability text is translated to ensure it is user-friendly and consistent with the plugin's language.
280 *
281 * @param \WC_Product $product The product object to check the stock availability for.
282 * @return string The stock availability text for the product.
283 */
284 public function get_stock_availability_text( $product ) {
285
286 $availability_text = '';
287
288 if ( ! is_a( $product, 'WC_Product' ) ) {
289 return ''; // Return empty if product is not valid.
290 }
291
292 $availability = $product->get_availability();
293
294 if ( ! empty( $availability['class'] ) ) {
295
296 switch ( $availability['class'] ) {
297 case 'available-on-backorder':
298 $availability_text = __( 'On backorder', 'cartflows' );
299 break;
300 case 'in-stock':
301 $availability_text = __( 'In stock', 'cartflows' );
302 break;
303 case 'out-of-stock':
304 $availability_text = __( 'Out of stock', 'cartflows' );
305 break;
306 default:
307 break;
308 }
309 }
310
311 return $availability_text;
312 }
313
314 /**
315 * Function to generate a formatted price range for a product.
316 *
317 * 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.
318 * The function returns a formatted string representing the price range.
319 *
320 * @param \WC_Product $product The product object to generate the price range for.
321 * @return string The formatted price range for the product.
322 */
323 public function get_formatted_product_price_range( $product ) {
324 $original_price = 0;
325
326 $product_type = $product->get_type();
327
328 if ( 'variable' === $product_type ) {
329 // Code for variation product.
330 $variation_price_range = $product->get_variation_prices( true );
331
332 $price = array();
333 $min_price = isset( $variation_price_range['price'] ) ? current( $variation_price_range['price'] ) : null;
334 $max_price = isset( $variation_price_range['price'] ) ? end( $variation_price_range['price'] ) : null;
335 $min_reg_price = isset( $variation_price_range['regular_price'] ) ? current( $variation_price_range['regular_price'] ) : null;
336 $max_reg_price = isset( $variation_price_range['regular_price'] ) ? end( $variation_price_range['regular_price'] ) : null;
337
338 if ( $min_price !== $max_price && ! is_null( $min_price ) && ! is_null( $max_price ) ) {
339 $original_price = html_entity_decode( wp_strip_all_tags( wc_format_price_range( $min_price, $max_price ) ) );
340 }
341
342 if ( $min_reg_price !== $max_reg_price && ! is_null( $min_reg_price ) && ! is_null( $max_reg_price ) ) {
343 if ( ! empty( $original_price ) ) {
344 $original_price = html_entity_decode( wp_strip_all_tags( wc_format_price_range( $min_reg_price, $max_reg_price ) ) );
345 }
346 }
347 } else {
348 $price = $product->get_price();
349 $original_price = ! empty( $price ) ? $price : $product->get_regular_price();
350 $original_price = html_entity_decode( wp_strip_all_tags( wc_price( $original_price ) ) );
351 }
352
353 return $original_price;
354 }
355 }
356