| @@ -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. |
| @@ -110,10 +122,11 @@ | ||
| 110 | 122 | break; |
| 111 | 123 | |
| 112 | 124 | case 'FILTER_CARTFLOWS_CHECKOUT_PRODUCTS': |
| 113 | 125 | if ( isset( $_POST[ $key ] ) && is_array( $_POST[ $key ] ) ) { |
| 114 | - $i = 0; | |
| 115 | - $q = 0; | |
| 126 | + $i = 0; | |
| 127 | + $q = 0; | |
| 128 | + $checkout_products = array(); | |
| 116 | 129 | |
| 117 | 130 | $post_data = wc_clean( $_POST[ $key ] ); |
| 118 | 131 | |
| 119 | 132 | foreach ( $post_data as $p_index => $p_data ) { |
| @@ -123,20 +136,31 @@ | ||
| 123 | 136 | foreach ( $p_data as $i_key => $i_value ) { |
| 124 | 137 | |
| 125 | 138 | if ( is_array( $i_value ) ) { |
| 126 | 139 | foreach ( $i_value as $q_key => $q_value ) { |
| 127 | - $meta_value[ $i ][ $i_key ][ $q ] = array_map( 'sanitize_text_field', $q_value ); | |
| 140 | + $checkout_products[ $i ][ $i_key ][ $q ] = array_map( 'sanitize_text_field', $q_value ); | |
| 128 | 141 | |
| 129 | 142 | $q++; |
| 130 | 143 | } |
| 131 | 144 | } else { |
| 132 | - $meta_value[ $i ][ $i_key ] = sanitize_text_field( $i_value ); | |
| 145 | + $checkout_products[ $i ][ $i_key ] = sanitize_text_field( $i_value ); | |
| 133 | 146 | } |
| 134 | 147 | } |
| 135 | 148 | |
| 136 | 149 | $i++; |
| 137 | 150 | } |
| 151 | + | |
| 152 | + if ( ! empty( $checkout_products ) ) { | |
| 153 | + $meta_value = $checkout_products; | |
| 154 | + } | |
| 138 | 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 | + } | |
| 139 | 163 | break; |
| 140 | 164 | |
| 141 | 165 | case 'FILTER_CARTFLOWS_CHECKOUT_FIELDS': |
| 142 | 166 | $count = 10; |
| @@ -148,9 +172,11 @@ | ||
| 148 | 172 | $post_data = $_POST[ $key ]; //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 149 | 173 | |
| 150 | 174 | if ( 'wcf_field_order_billing' == $key || 'wcf_field_order_shipping' == $key ) { |
| 151 | 175 | |
| 152 | - $type_of_fields = ltrim( $key, 'wcf_field_order_' ); | |
| 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 ); | |
| 153 | 179 | $billing_shipping_fields = \Cartflows_Helper::get_checkout_fields( $type_of_fields, $post_id ); |
| 154 | 180 | |
| 155 | 181 | foreach ( $post_data as $field_key_name => $value ) { |
| 156 | 182 | |
| @@ -194,8 +220,16 @@ | ||
| 194 | 220 | $meta_value = $ordered_fields; |
| 195 | 221 | } |
| 196 | 222 | } |
| 197 | 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 | + */ | |
| 198 | 232 | break; |
| 199 | 233 | |
| 200 | 234 | case 'FILTER_CARTFLOWS_OPTIN_FIELDS': |
| 201 | 235 | $count = 10; |
| @@ -250,8 +284,14 @@ | ||
| 250 | 284 | |
| 251 | 285 | $meta_value = $ordered_fields; |
| 252 | 286 | } |
| 253 | 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 | + */ | |
| 254 | 294 | break; |
| 255 | 295 | |
| 256 | 296 | default: |
| 257 | 297 | if ( 'FILTER_DEFAULT' === $sanitize_filter ) { |
| @@ -256,9 +296,15 @@ | ||
| 256 | 296 | default: |
| 257 | 297 | if ( 'FILTER_DEFAULT' === $sanitize_filter ) { |
| 258 | 298 | $meta_value = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : ''; |
| 259 | 299 | } else { |
| 260 | - $meta_value = apply_filters( 'cartflows_admin_save_meta_field_values', $meta_value, $post_id, $key, $sanitize_filter, $action ); | |
| 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 ); | |
| 261 | 307 | } |
| 262 | 308 | |
| 263 | 309 | break; |
| 264 | 310 | } |
| @@ -277,6 +323,8 @@ | ||
| 277 | 323 | // To delete the wcf-checkout-products if empty. |
| 278 | 324 | delete_post_meta( $post_id, $key ); |
| 279 | 325 | } |
| 280 | 326 | } |
| 327 | + | |
| 328 | + return $skipped_fields; | |
| 281 | 329 | } |
| 282 | 330 | } |