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-legacy-core/ajax/meta-data.php +80 -52 3.2.1 → 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();
@@ -163,9 +204,13 @@
163 204 wp_send_json( $products_found );
164 205 }
165 206
166 207 /**
167 - * Function to search coupons
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
168 213 */
169 214 public function json_search_coupons() {
170 215
171 216 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
@@ -173,58 +218,41 @@
173 218 }
174 219
175 220 check_ajax_referer( 'cartflows_json_search_coupons', 'security' );
176 221
177 - global $wpdb;
178 -
179 222 if ( ! isset( $_POST['term'] ) ) {
180 223 return;
181 224 }
182 225
183 226 $term = ! empty( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : '';
184 - if ( empty( $term ) ) {
227 +
228 + if ( '' === trim( $term ) ) {
185 229 die();
186 230 }
187 231
188 - $posts = wp_cache_get( 'wcf_search_coupons', 'wcf_funnel_Cart' );
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 );
189 234
190 - if ( false === $posts ) {
191 - $posts = $wpdb->get_results(
192 - $wpdb->prepare(
193 - "SELECT *
194 - FROM {$wpdb->prefix}posts
195 - WHERE post_type = %s
196 - AND post_title LIKE %s
197 - AND post_status = %s",
198 - 'shop_coupon',
199 - $wpdb->esc_like( $term ) . '%',
200 - 'publish'
201 - )
202 - ); // db call ok.
203 - wp_cache_set( 'wcf_search_coupons', $posts, 'wcf_funnel_Cart' );
204 - }
235 + wp_send_json( $coupons_found );
236 + }
205 237
206 - $coupons_found = array();
207 - $all_discount_types = wc_get_coupon_types();
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 ) {
208 245
209 - if ( $posts ) {
210 - foreach ( $posts as $post ) {
246 + if ( is_string( $raw ) ) {
247 + $raw = explode( ',', $raw );
248 + }
211 249
212 - $discount_type = get_post_meta( $post->ID, 'discount_type', true );
213 -
214 - if ( ! empty( $all_discount_types[ $discount_type ] ) ) {
215 - array_push(
216 - $coupons_found,
217 - array(
218 - 'value' => get_the_title( $post->ID ),
219 - 'label' => get_the_title( $post->ID ) . ' (Type: ' . $all_discount_types[ $discount_type ] . ')',
220 - )
221 - );
222 - }
223 - }
250 + if ( ! is_array( $raw ) ) {
251 + return array();
224 252 }
225 253
226 - wp_send_json( $coupons_found );
254 + return array_values( array_filter( array_map( 'absint', $raw ) ) );
227 255 }
228 256
229 257 /**
230 258 * Function to sanitize the product type data attribute.