| @@ -23,12 +23,14 @@ | ||
| 23 | 23 | * @param int $post_id post id. |
| 24 | 24 | * @param array $post_meta options to store. |
| 25 | 25 | * @param string $action action to check nonce. |
| 26 | 26 | * |
| 27 | - * @return void | |
| 27 | + * @return array<int, string> Meta keys skipped because the user cannot author scripts. | |
| 28 | 28 | */ |
| 29 | 29 | public static function save_meta_fields( $post_id, $post_meta, $action = '' ) { |
| 30 | 30 | |
| 31 | + $skipped_fields = array(); | |
| 32 | + | |
| 31 | 33 | if ( ! check_ajax_referer( $action, 'security', false ) ) { |
| 32 | 34 | $response_data = array( 'message' => __( 'Nonce validation failed', 'cartflows' ) ); |
| 33 | 35 | wp_send_json_error( $response_data ); |
| 34 | 36 | } |
| @@ -33,9 +35,9 @@ | ||
| 33 | 35 | wp_send_json_error( $response_data ); |
| 34 | 36 | } |
| 35 | 37 | |
| 36 | 38 | if ( ! ( $post_id && is_array( $post_meta ) ) ) { |
| 37 | - return; | |
| 39 | + return $skipped_fields; | |
| 38 | 40 | } |
| 39 | 41 | |
| 40 | 42 | $allowed_html = array( |
| 41 | 43 | 'a' => array( |
| @@ -52,9 +54,14 @@ | ||
| 52 | 54 | if ( ! isset( $_POST[ $key ] ) ) { |
| 53 | 55 | continue; |
| 54 | 56 | } |
| 55 | 57 | |
| 56 | - $meta_value = false; | |
| 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; | |
| 57 | 64 | |
| 58 | 65 | // Sanitize values. |
| 59 | 66 | $sanitize_filter = ( isset( $data['sanitize'] ) ) ? $data['sanitize'] : 'FILTER_DEFAULT'; |
| 60 | 67 | |
| @@ -73,8 +80,11 @@ | ||
| 73 | 80 | |
| 74 | 81 | case 'FILTER_CARTFLOWS_ARRAY': |
| 75 | 82 | if ( isset( $_POST[ $key ] ) && is_array( $_POST[ $key ] ) ) { |
| 76 | 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; | |
| 77 | 87 | } |
| 78 | 88 | break; |
| 79 | 89 | |
| 80 | 90 | case 'FILTER_SANITIZE_COLOR': |
| @@ -96,8 +106,10 @@ | ||
| 96 | 106 | // Custom JS/CSS sinks are output raw on the front end. Restrict authoring to |
| 97 | 107 | // users with `unfiltered_html` so per-plugin caps cannot grant script write |
| 98 | 108 | // access to lower roles (e.g. Editors via the role manager). |
| 99 | 109 | if ( ! current_user_can( 'unfiltered_html' ) ) { |
| 110 | + // Record it — skipping silently let the caller report a clean save. | |
| 111 | + $skipped_fields[] = $key; | |
| 100 | 112 | continue 2; |
| 101 | 113 | } |
| 102 | 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. |
| 103 | 115 | // We first decode any existing HTML entities to prevent double-encoding on multiple saves, then encode once. |
| @@ -135,8 +147,15 @@ | ||
| 135 | 147 | |
| 136 | 148 | $i++; |
| 137 | 149 | } |
| 138 | 150 | } |
| 151 | + | |
| 152 | + if ( null === $meta_value ) { | |
| 153 | + // Delete-on-empty is deliberate here: clearing every product removes the | |
| 154 | + // meta. The repeater posts an empty scalar, not an array, once the last | |
| 155 | + // product is removed, so this has to sit outside the is_array() guard. | |
| 156 | + $meta_value = false; | |
| 157 | + } | |
| 139 | 158 | break; |
| 140 | 159 | |
| 141 | 160 | case 'FILTER_CARTFLOWS_CHECKOUT_FIELDS': |
| 142 | 161 | $count = 10; |
| @@ -148,9 +167,11 @@ | ||
| 148 | 167 | $post_data = $_POST[ $key ]; //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 149 | 168 | |
| 150 | 169 | if ( 'wcf_field_order_billing' == $key || 'wcf_field_order_shipping' == $key ) { |
| 151 | 170 | |
| 152 | - $type_of_fields = ltrim( $key, 'wcf_field_order_' ); | |
| 171 | + // ltrim() takes a character set, not a prefix — it only produced the | |
| 172 | + // right answer here because 'b' and 's' happen to be absent from it. | |
| 173 | + $type_of_fields = str_replace( 'wcf_field_order_', '', $key ); | |
| 153 | 174 | $billing_shipping_fields = \Cartflows_Helper::get_checkout_fields( $type_of_fields, $post_id ); |
| 154 | 175 | |
| 155 | 176 | foreach ( $post_data as $field_key_name => $value ) { |
| 156 | 177 | |
| @@ -194,8 +215,16 @@ | ||
| 194 | 215 | $meta_value = $ordered_fields; |
| 195 | 216 | } |
| 196 | 217 | } |
| 197 | 218 | |
| 219 | + /* | |
| 220 | + * Deliberately no `null === $meta_value` normalisation here. Unlike the | |
| 221 | + * checkout products and FILTER_CARTFLOWS_ARRAY cases, no shipped field posts | |
| 222 | + * a scalar or an unhandled key to this filter — the only keys that use it are | |
| 223 | + * handled above, and the field-order editor always posts per-field arrays. If | |
| 224 | + * that ever changes, leaving the saved ordering alone is the safe outcome; | |
| 225 | + * deleting it would be the silent data loss this sentinel exists to prevent. | |
| 226 | + */ | |
| 198 | 227 | break; |
| 199 | 228 | |
| 200 | 229 | case 'FILTER_CARTFLOWS_OPTIN_FIELDS': |
| 201 | 230 | $count = 10; |
| @@ -250,8 +279,14 @@ | ||
| 250 | 279 | |
| 251 | 280 | $meta_value = $ordered_fields; |
| 252 | 281 | } |
| 253 | 282 | } |
| 283 | + | |
| 284 | + /* | |
| 285 | + * Deliberately no `null === $meta_value` normalisation here either — same | |
| 286 | + * reasoning as FILTER_CARTFLOWS_CHECKOUT_FIELDS above. `wcf-optin-fields-billing` | |
| 287 | + * is the only key that uses this filter and it is handled. | |
| 288 | + */ | |
| 254 | 289 | break; |
| 255 | 290 | |
| 256 | 291 | default: |
| 257 | 292 | if ( 'FILTER_DEFAULT' === $sanitize_filter ) { |
| @@ -256,9 +291,15 @@ | ||
| 256 | 291 | default: |
| 257 | 292 | if ( 'FILTER_DEFAULT' === $sanitize_filter ) { |
| 258 | 293 | $meta_value = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : ''; |
| 259 | 294 | } else { |
| 260 | - $meta_value = apply_filters( 'cartflows_admin_save_meta_field_values', $meta_value, $post_id, $key, $sanitize_filter, $action ); | |
| 295 | + /* | |
| 296 | + * CartFlows Pro hooks this filter and several of its cases leave the | |
| 297 | + * incoming value untouched when the field was not posted, relying on | |
| 298 | + * it being false so the meta is deleted. Pass false rather than the | |
| 299 | + * new null sentinel so that contract is unchanged. | |
| 300 | + */ | |
| 301 | + $meta_value = apply_filters( 'cartflows_admin_save_meta_field_values', false, $post_id, $key, $sanitize_filter, $action ); | |
| 261 | 302 | } |
| 262 | 303 | |
| 263 | 304 | break; |
| 264 | 305 | } |
| @@ -273,6 +314,8 @@ | ||
| 273 | 314 | // To delete the wcf-checkout-products if empty. |
| 274 | 315 | delete_post_meta( $post_id, $key ); |
| 275 | 316 | } |
| 276 | 317 | } |
| 318 | + | |
| 319 | + return $skipped_fields; | |
| 277 | 320 | } |
| 278 | 321 | } |