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 / inc / meta-ops.php

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

331 lines 12.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CartFlows Step Meta Helper.
4 *
5 * @package CartFlows
6 */
7
8 namespace CartflowsAdmin\AdminCore\Inc;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class StepMeta.
17 */
18 class MetaOps {
19
20 /**
21 * Save Meta fields - Common Function.
22 *
23 * @param int $post_id post id.
24 * @param array $post_meta options to store.
25 * @param string $action action to check nonce.
26 *
27 * @return array<int, string> Meta keys skipped because the user cannot author scripts.
28 */
29 public static function save_meta_fields( $post_id, $post_meta, $action = '' ) {
30
31 $skipped_fields = array();
32
33 if ( ! check_ajax_referer( $action, 'security', false ) ) {
34 $response_data = array( 'message' => __( 'Nonce validation failed', 'cartflows' ) );
35 wp_send_json_error( $response_data );
36 }
37
38 if ( ! ( $post_id && is_array( $post_meta ) ) ) {
39 return $skipped_fields;
40 }
41
42 $allowed_html = array(
43 'a' => array(
44 'href' => array(),
45 ),
46 'br' => array(),
47 'strong' => array(),
48 'p' => array(),
49 'span' => array(),
50 );
51
52 foreach ( $post_meta as $key => $data ) {
53
54 if ( ! isset( $_POST[ $key ] ) ) {
55 continue;
56 }
57
58 /*
59 * null leaves the stored meta alone; false deletes it. Starting at null means a
60 * sanitize case that forgets to assign can no longer silently destroy saved data
61 * — every branch that genuinely wants a delete now says so explicitly.
62 */
63 $meta_value = null;
64
65 // Sanitize values.
66 $sanitize_filter = ( isset( $data['sanitize'] ) ) ? $data['sanitize'] : 'FILTER_DEFAULT';
67
68 switch ( $sanitize_filter ) {
69 case 'FILTER_SANITIZE_STRING':
70 $meta_value = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : '';
71 break;
72
73 case 'FILTER_SANITIZE_URL':
74 $meta_value = filter_input( INPUT_POST, $key, FILTER_SANITIZE_URL );
75 break;
76
77 case 'FILTER_SANITIZE_NUMBER_INT':
78 $meta_value = filter_input( INPUT_POST, $key, FILTER_SANITIZE_NUMBER_INT );
79 break;
80
81 case 'FILTER_CARTFLOWS_ARRAY':
82 if ( isset( $_POST[ $key ] ) && is_array( $_POST[ $key ] ) ) {
83 $meta_value = array_map( 'sanitize_text_field', wp_unslash( $_POST[ $key ] ) );
84 } else {
85 // A posted-but-not-array value has always cleared this meta; keep it.
86 $meta_value = false;
87 }
88 break;
89
90 case 'FILTER_SANITIZE_COLOR':
91 // Sanitizes a hex color with #.
92 $meta_value = sanitize_hex_color( $_POST[ $key ] );
93 break;
94
95 case 'FILTER_SANITIZE_FONT_FAMILY':
96 // FILTER_FLAG_NO_ENCODE_QUOTES - Do not encode the single and double quotes.
97 $meta_value = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : '';
98 break;
99
100 case 'FILTER_WP_KSES':
101 // It allow only tags that are defined in $allowed_html variable.
102 $meta_value = wp_kses( $_POST[ $key ], $allowed_html );
103 break;
104
105 case 'FILTER_SCRIPT':
106 // Custom JS/CSS sinks are output raw on the front end. Restrict authoring to
107 // users with `unfiltered_html` so per-plugin caps cannot grant script write
108 // access to lower roles (e.g. Editors via the role manager).
109 if ( ! current_user_can( 'unfiltered_html' ) ) {
110 // Record it — skipping silently let the caller report a clean save.
111 $skipped_fields[] = $key;
112 continue 2;
113 }
114 // Reason for ignoring phpcs rule: Here we are saving the custom JS/CSS script. Encoding it before saving to db. No escaping function working here.
115 // We first decode any existing HTML entities to prevent double-encoding on multiple saves, then encode once.
116 $meta_value = htmlentities( html_entity_decode( wp_unslash( $_POST[ $key ] ), ENT_QUOTES, 'UTF-8' ), ENT_QUOTES, 'UTF-8' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
117 break;
118
119 case 'FILTER_WP_KSES_POST':
120 // wp_kses_post() allow only the same tags that are allowed in WP Posts.
121 $meta_value = wp_kses_post( $_POST[ $key ] );
122 break;
123
124 case 'FILTER_CARTFLOWS_CHECKOUT_PRODUCTS':
125 if ( isset( $_POST[ $key ] ) && is_array( $_POST[ $key ] ) ) {
126 $i = 0;
127 $q = 0;
128 $checkout_products = array();
129
130 $post_data = wc_clean( $_POST[ $key ] );
131
132 foreach ( $post_data as $p_index => $p_data ) {
133 if ( ! array_key_exists( 'product', $p_data ) ) {
134 continue;
135 }
136 foreach ( $p_data as $i_key => $i_value ) {
137
138 if ( is_array( $i_value ) ) {
139 foreach ( $i_value as $q_key => $q_value ) {
140 $checkout_products[ $i ][ $i_key ][ $q ] = array_map( 'sanitize_text_field', $q_value );
141
142 $q++;
143 }
144 } else {
145 $checkout_products[ $i ][ $i_key ] = sanitize_text_field( $i_value );
146 }
147 }
148
149 $i++;
150 }
151
152 if ( ! empty( $checkout_products ) ) {
153 $meta_value = $checkout_products;
154 }
155 }
156
157 if ( null === $meta_value ) {
158 // Delete-on-empty is deliberate here: clearing every product removes the
159 // meta. The repeater posts an empty scalar, not an array, once the last
160 // product is removed, so this has to sit outside the is_array() guard.
161 $meta_value = false;
162 }
163 break;
164
165 case 'FILTER_CARTFLOWS_CHECKOUT_FIELDS':
166 $count = 10;
167 $ordered_fields = array();
168 $billing_shipping_fields = array();
169
170 if ( isset( $_POST[ $key ] ) && is_array( $_POST[ $key ] ) ) {
171 // Ignoring sanitization rule as we are here to sanitize user input.
172 $post_data = $_POST[ $key ]; //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
173
174 if ( 'wcf_field_order_billing' == $key || 'wcf_field_order_shipping' == $key ) {
175
176 // ltrim() takes a character set, not a prefix — it only produced the
177 // right answer here because 'b' and 's' happen to be absent from it.
178 $type_of_fields = str_replace( 'wcf_field_order_', '', $key );
179 $billing_shipping_fields = \Cartflows_Helper::get_checkout_fields( $type_of_fields, $post_id );
180
181 foreach ( $post_data as $field_key_name => $value ) {
182
183 if ( isset( $billing_shipping_fields[ $field_key_name ] ) ) {
184
185 $is_custom_field = isset( $billing_shipping_fields[ $field_key_name ]['custom'] ) ? $billing_shipping_fields[ $field_key_name ]['custom'] : false;
186
187 $ordered_fields[ $field_key_name ] = $billing_shipping_fields[ $field_key_name ];
188
189 $ordered_fields[ $field_key_name ]['priority'] = $count;
190 $count += 10;
191 $placeholder_text = ! empty( $value['placeholder'] ) ? wc_clean( stripslashes( $value['placeholder'] ) ) : '';
192
193 $ordered_fields[ $field_key_name ]['width'] = intval( $value['width'] );
194 $ordered_fields[ $field_key_name ]['label'] = wp_kses_post( trim( stripslashes( $value['label'] ) ) );
195 $ordered_fields[ $field_key_name ]['placeholder'] = $placeholder_text;
196 $ordered_fields[ $field_key_name ]['default'] = wp_kses_post( trim( stripslashes( $value['default'] ) ) );
197 $ordered_fields[ $field_key_name ]['required'] = 'yes' === $value['required'] ? true : false;
198 $ordered_fields[ $field_key_name ]['optimized'] = 'yes' === $value['optimized'] ? true : false;
199 $ordered_fields[ $field_key_name ]['enabled'] = 'yes' === $value['enabled'] ? true : false;
200 $ordered_fields[ $field_key_name ]['options'] = '';
201
202 if ( isset( $value['options'] ) && $value['options'] ) {
203 $options = explode( '|', $value['options'] );
204 $options = AdminHelper::sanitize_array_values( $options );
205 $ordered_fields[ $field_key_name ]['options'] = array_combine( array_keys( $options ), $options );
206
207 }
208
209 if ( ! empty( $value['custom_attributes'] ) ) {
210 $ordered_fields[ $field_key_name ]['custom_attributes'] = wc_clean( $value['custom_attributes'] );
211 }
212
213 if ( true === $is_custom_field ) {
214
215 $ordered_fields[ $field_key_name ]['show_in_email'] = isset( $value['show_in_email'] ) && 'yes' === $value['show_in_email'] ? true : false;
216 }
217 }
218 }
219
220 $meta_value = $ordered_fields;
221 }
222 }
223
224 /*
225 * Deliberately no `null === $meta_value` normalisation here. Unlike the
226 * checkout products and FILTER_CARTFLOWS_ARRAY cases, no shipped field posts
227 * a scalar or an unhandled key to this filter — the only keys that use it are
228 * handled above, and the field-order editor always posts per-field arrays. If
229 * that ever changes, leaving the saved ordering alone is the safe outcome;
230 * deleting it would be the silent data loss this sentinel exists to prevent.
231 */
232 break;
233
234 case 'FILTER_CARTFLOWS_OPTIN_FIELDS':
235 $count = 10;
236 $ordered_fields = array();
237 $billing_shipping_fields = array();
238
239 if ( isset( $_POST[ $key ] ) && is_array( $_POST[ $key ] ) ) {
240 // Ignoring sanitization rule as we are here to sanitize user input.
241 $post_data = $_POST[ $key ]; //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
242
243 if ( 'wcf-optin-fields-billing' === $key ) {
244
245 $type_of_fields = 'billing';
246 $billing_shipping_fields = \Cartflows_Helper::get_optin_fields( $type_of_fields, $post_id );
247
248 foreach ( $post_data as $field_key_name => $value ) {
249 if ( isset( $billing_shipping_fields[ $field_key_name ] ) ) {
250
251 $is_custom_field = isset( $billing_shipping_fields[ $field_key_name ]['custom'] ) ? $billing_shipping_fields[ $field_key_name ]['custom'] : false;
252
253 $ordered_fields[ $field_key_name ] = $billing_shipping_fields[ $field_key_name ];
254 $placeholder_text = ! empty( $value['placeholder'] ) ? wc_clean( stripslashes( $value['placeholder'] ) ) : '';
255 $ordered_fields[ $field_key_name ]['priority'] = $count;
256 $count += 10;
257
258 $ordered_fields[ $field_key_name ]['width'] = intval( $value['width'] );
259 $ordered_fields[ $field_key_name ]['label'] = wp_kses_post( trim( stripslashes( $value['label'] ) ) );
260 $ordered_fields[ $field_key_name ]['placeholder'] = $placeholder_text;
261 $ordered_fields[ $field_key_name ]['default'] = wp_kses_post( trim( stripslashes( $value['default'] ) ) );
262 $ordered_fields[ $field_key_name ]['required'] = 'yes' === $value['required'] ? true : false;
263 $ordered_fields[ $field_key_name ]['enabled'] = 'yes' === $value['enabled'] ? true : false;
264 $ordered_fields[ $field_key_name ]['options'] = '';
265
266 if ( isset( $value['options'] ) && $value['options'] ) {
267
268 $options = explode( '|', $value['options'] );
269 $options = AdminHelper::sanitize_array_values( $options );
270 $ordered_fields[ $field_key_name ]['options'] = array_combine( array_keys( $options ), $options );
271
272 }
273
274 if ( ! empty( $value['custom_attributes'] ) ) {
275 $ordered_fields[ $field_key_name ]['custom_attributes'] = wc_clean( $value['custom_attributes'] );
276 }
277
278 if ( true === $is_custom_field ) {
279
280 $ordered_fields[ $field_key_name ]['show_in_email'] = isset( $value['show_in_email'] ) && 'yes' === $value['show_in_email'] ? true : false;
281 }
282 }
283 }
284
285 $meta_value = $ordered_fields;
286 }
287 }
288
289 /*
290 * Deliberately no `null === $meta_value` normalisation here either — same
291 * reasoning as FILTER_CARTFLOWS_CHECKOUT_FIELDS above. `wcf-optin-fields-billing`
292 * is the only key that uses this filter and it is handled.
293 */
294 break;
295
296 default:
297 if ( 'FILTER_DEFAULT' === $sanitize_filter ) {
298 $meta_value = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : '';
299 } else {
300 /*
301 * CartFlows Pro hooks this filter and several of its cases leave the
302 * incoming value untouched when the field was not posted, relying on
303 * it being false so the meta is deleted. Pass false rather than the
304 * new null sentinel so that contract is unchanged.
305 */
306 $meta_value = apply_filters( 'cartflows_admin_save_meta_field_values', false, $post_id, $key, $sanitize_filter, $action );
307 }
308
309 break;
310 }
311
312 if ( is_null( $meta_value ) ) {
313 continue;
314 }
315
316 if ( false !== $meta_value ) {
317 update_post_meta( $post_id, $key, $meta_value );
318 // Track first checkout configured event.
319 if ( 'wcf-checkout-products' === $key ) {
320 \Cartflows_Helper::set_analytics_flag( 'first_checkout_configured', $post_id );
321 }
322 } else {
323 // To delete the wcf-checkout-products if empty.
324 delete_post_meta( $post_id, $key );
325 }
326 }
327
328 return $skipped_fields;
329 }
330 }
331