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
← All changes | admin-core/ajax/meta-data.php +80 -52 3.2.0 → 3.3.0 View file →
@@ -29,8 +29,33 @@
29 29 */
30 30 private static $instance;
31 31
32 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 + /**
33 58 * Initiator
34 59 *
35 60 * @since 1.0.0
36 61 * @return object initialized object of class.
@@ -58,9 +83,16 @@
58 83 $this->init_ajax_events( $ajax_events );
59 84 }
60 85
61 86 /**
62 - * Clone step with its meta.
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
63 95 */
64 96 public function json_search_products() {
65 97
66 98 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
@@ -68,10 +100,8 @@
68 100 }
69 101
70 102 check_ajax_referer( 'cartflows_json_search_products', 'security' );
71 103
72 - global $wpdb;
73 -
74 104 if ( ! isset( $_POST['term'] ) ) {
75 105 return;
76 106 }
77 107
@@ -76,8 +106,13 @@
76 106 }
77 107
78 108 $term = ! empty( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : '';
79 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 +
80 115 // CartFlows supported product types.
81 116 $supported_product_types = apply_filters( 'cartflows_supported_product_types_for_search', array( 'simple', 'variable', 'variation', 'subscription', 'variable-subscription', 'subscription_variation', 'course' ) );
82 117
83 118 // Allowed product types.
@@ -109,23 +144,29 @@
109 144
110 145 $supported_product_types = array_diff( $supported_product_types, $excluded_product_types );
111 146 }
112 147
113 - // Get all products data.
114 - $data = \WC_Data_Store::load( 'product' );
115 - $ids = $data->search_products( $term, '', true, false, 11 );
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 );
116 150
117 - // Get all product objects.
118 - $product_objects = array_filter( array_map( 'wc_get_product', $ids ), 'wc_products_array_filter_readable' );
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 );
119 154
120 - // Remove the product objects whose product type are not in supported array.
121 - $product_objects = array_filter(
122 - $product_objects,
123 - function ( $arr ) use ( $supported_product_types ) {
124 - return $arr && is_a( $arr, 'WC_Product' ) && in_array( $arr->get_type(), $supported_product_types, true );
125 - }
126 - );
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();
127 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 +
128 169 $products_found = array();
129 170
130 171 foreach ( $product_objects as $product_object ) {
131 172 $formatted_name = $product_object->get_name();
@@ -164,9 +205,13 @@
164 205 wp_send_json( $products_found );
165 206 }
166 207
167 208 /**
168 - * Function to search coupons
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
169 214 */
170 215 public function json_search_coupons() {
171 216
172 217 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
@@ -174,58 +219,41 @@
174 219 }
175 220
176 221 check_ajax_referer( 'cartflows_json_search_coupons', 'security' );
177 222
178 - global $wpdb;
179 -
180 223 if ( ! isset( $_POST['term'] ) ) {
181 224 return;
182 225 }
183 226
184 227 $term = ! empty( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : '';
185 - if ( empty( $term ) ) {
228 +
229 + if ( '' === trim( $term ) ) {
186 230 die();
187 231 }
188 232
189 - $posts = wp_cache_get( 'wcf_search_coupons', 'wcf_funnel_Cart' );
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 );
190 235
191 - if ( false === $posts ) {
192 - $posts = $wpdb->get_results(
193 - $wpdb->prepare(
194 - "SELECT *
195 - FROM {$wpdb->prefix}posts
196 - WHERE post_type = %s
197 - AND post_title LIKE %s
198 - AND post_status = %s",
199 - 'shop_coupon',
200 - $wpdb->esc_like( $term ) . '%',
201 - 'publish'
202 - )
203 - ); // db call ok.
204 - wp_cache_set( 'wcf_search_coupons', $posts, 'wcf_funnel_Cart' );
205 - }
236 + wp_send_json( $coupons_found );
237 + }
206 238
207 - $coupons_found = array();
208 - $all_discount_types = wc_get_coupon_types();
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 ) {
209 246
210 - if ( $posts ) {
211 - foreach ( $posts as $post ) {
247 + if ( is_string( $raw ) ) {
248 + $raw = explode( ',', $raw );
249 + }
212 250
213 - $discount_type = get_post_meta( $post->ID, 'discount_type', true );
214 -
215 - if ( ! empty( $all_discount_types[ $discount_type ] ) ) {
216 - array_push(
217 - $coupons_found,
218 - array(
219 - 'value' => get_the_title( $post->ID ),
220 - 'label' => get_the_title( $post->ID ) . ' (Type: ' . $all_discount_types[ $discount_type ] . ')',
221 - )
222 - );
223 - }
224 - }
251 + if ( ! is_array( $raw ) ) {
252 + return array();
225 253 }
226 254
227 - wp_send_json( $coupons_found );
255 + return array_values( array_filter( array_map( 'absint', $raw ) ) );
228 256 }
229 257
230 258 /**
231 259 * Function to sanitize the product type data attribute.