PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.1
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
sureforms / inc / field-validation.php

field-validation.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.12.1, at inc/field-validation.php

630 lines 26.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Field Validation Class
4 *
5 * Handles all field validation for SureForms
6 *
7 * @package SureForms
8 * @since 1.12.2
9 */
10
11 namespace SRFM\Inc;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly.
15 }
16
17 /**
18 * Field Validation Class
19 */
20 class Field_Validation {
21 /**
22 * Add block configuration for form fields.
23 *
24 * This function processes blocks in a form and stores their configuration as post meta.
25 * It applies filters to allow extensions to modify block configs and stores processed
26 * values for blocks that need special handling (like upload fields).
27 *
28 * @param array<mixed> $blocks Array of blocks to process.
29 * @param int $form_id Form post ID.
30 * @return void
31 * @since 1.12.2
32 */
33 public static function add_block_config( $blocks, $form_id ) {
34 // Initialize array to store processed block configurations.
35 $block_config = [];
36
37 // Loop through each block.
38 foreach ( $blocks as $block ) {
39 // Ensure $block is an array and has the required structure.
40 if ( ! is_array( $block ) ) {
41 continue;
42 }
43 if ( ! isset( $block['blockName'] ) || ! isset( $block['attrs'] ) || ! is_array( $block['attrs'] ) ) {
44 continue;
45 }
46 // Validate block id.
47 if ( ! array_key_exists( 'block_id', $block['attrs'] ) || empty( $block['attrs']['block_id'] ) || ! is_string( $block['attrs']['block_id'] ) ) {
48 continue;
49 }
50
51 $block_id = sanitize_text_field( $block['attrs']['block_id'] );
52 $block_name = $block['blockName'];
53
54 // Process specific block types.
55 $processed_config = null;
56
57 switch ( $block_name ) {
58 case 'srfm/payment':
59 $processed_config = self::process_payment_block( $block['attrs'], $blocks );
60 break;
61 case 'srfm/dropdown':
62 $processed_config = self::process_dropdown_block( $block['attrs'] );
63 break;
64 case 'srfm/multi-choice':
65 $processed_config = self::process_multichoice_block( $block['attrs'] );
66 break;
67 case 'srfm/number':
68 $processed_config = self::process_number_block( $block['attrs'] );
69 break;
70 case 'srfm/textarea':
71 $processed_config = self::process_textarea_block( $block['attrs'] );
72 break;
73 }
74
75 // If block was processed, store its configuration.
76 if ( null !== $processed_config && ! empty( $processed_config ) ) {
77 $processed_config['block_name'] = $block_name;
78 // Add the slug to the configuration.
79 if ( isset( $block['attrs']['slug'] ) && ! empty( $block['attrs']['slug'] ) ) {
80 $processed_config['slug'] = sanitize_text_field( $block['attrs']['slug'] );
81 }
82
83 $block_config[ $block_id ] = $processed_config;
84 continue;
85 }
86
87 // Allow extensions to process and modify block config.
88 $config = apply_filters( 'srfm_block_config', [ 'block' => $block ] );
89
90 // If block was processed by a filter, add its processed value.
91 if ( isset( $config['processed_value'] ) && ! empty( $config['processed_value'] ) ) {
92 $block_config[ $block_id ] = $config['processed_value'];
93 continue;
94 }
95 }
96
97 // Sync the meta on every save. When $block_config is empty (e.g. a textarea
98 // whose minLength was cleared, with no other blocks needing per-block
99 // validation), we must clear the stored meta — otherwise the previously
100 // saved values keep being used by the validator.
101 if ( ! empty( $block_config ) ) {
102 update_post_meta( $form_id, '_srfm_block_config', $block_config );
103 } else {
104 delete_post_meta( $form_id, '_srfm_block_config' );
105 }
106 }
107
108 /**
109 * Retrieve or migrate the block configuration for legacy forms.
110 *
111 * This function checks if the _srfm_block_config post meta exists for the given form ID.
112 * Example: get_post_meta( 123, '_srfm_block_config', true ) might return an array of block configs.
113 * If not found, it attempts to parse the form's post content and generate the block config.
114 * Example: If a legacy form with ID 123 has no _srfm_block_config, but its post_content contains blocks,
115 * the function will parse those blocks and call add_block_config() to generate and store the config.
116 *
117 * @param int $form_id The ID of the form post.
118 * @since 1.12.2
119 * @return array|null The block configuration array, or null if not found or invalid.
120 */
121 public static function get_or_migrate_block_config_for_legacy_form( $form_id ) {
122 // Validate that $form_id is a positive integer.
123 // Example: $form_id = 123 is valid; $form_id = -1 or 'abc' is not.
124 if ( ! is_int( $form_id ) || $form_id <= 0 ) {
125 return null;
126 }
127
128 // Retrieve the block config from post meta.
129 // Example: $block_config = [ 'block-1' => [ ... ], 'block-2' => [ ... ] ].
130 $block_config = get_post_meta( $form_id, '_srfm_block_config', true );
131 if ( ! empty( $block_config ) && is_array( $block_config ) ) {
132 // If it exists and is an array, return it directly (no migration needed).
133 // Example: Returning the existing $block_config array.
134 return $block_config;
135 }
136
137 // Get the post by ID and validate.
138 // Example: $post = get_post( 123 ); $post->post_content should contain block markup.
139 $post = get_post( $form_id );
140 if ( ! ( $post instanceof \WP_Post ) || empty( $post->post_content ) ) {
141 return null;
142 }
143
144 // Parse the blocks from the post content and attempt migration.
145 // Example: $blocks = parse_blocks( $post->post_content ); $blocks is an array of block arrays.
146 if ( function_exists( 'parse_blocks' ) ) {
147 $blocks = parse_blocks( $post->post_content );
148 if ( is_array( $blocks ) && ! empty( $blocks ) ) {
149 self::add_block_config( $blocks, $form_id );
150 }
151 }
152
153 // Retrieve the block config again after migration attempt.
154 // Example: After migration, $block_config should now be an array if successful.
155 $block_config = get_post_meta( $form_id, '_srfm_block_config', true );
156
157 return ! empty( $block_config ) && is_array( $block_config ) ? $block_config : null;
158 }
159
160 /**
161 * Prepare validation data for a given form.
162 *
163 * Retrieves the form block configuration from post meta and adds a 'name_with_id'
164 * key to each block, which is a unique identifier for the field (used for validation).
165 *
166 * @param int $current_form_id The ID of the form post.
167 * @since 1.12.2
168 * @return array|null The processed form configuration array, or null if not found.
169 */
170 public static function prepared_validation_data( $current_form_id ) {
171 // Retrieve the form block configuration from post meta.
172 $get_form_config = self::get_or_migrate_block_config_for_legacy_form( $current_form_id );
173
174 // If the configuration is an array, add a 'name_with_id' key to each block.
175 if ( is_array( $get_form_config ) ) {
176 foreach ( $get_form_config as $index => $block ) {
177 // Ensure both 'blockName' and 'block_id' exist before creating the identifier.
178 if ( isset( $block['blockName'] ) ) {
179 // 'name_with_id' is used as a unique field identifier for validation.
180 // Example: 'sureforms-input-abc123' for blockName 'sureforms/input' and block_id 'abc123'
181 $name_with_id = str_replace( '/', '-', $block['blockName'] ) . '-' . $index;
182
183 // Allow custom filter based on block type.
184 $name_with_id = apply_filters(
185 'srfm_block_config_name_with_id',
186 $name_with_id,
187 $block
188 );
189
190 $get_form_config[ $index ]['name_with_id'] = $name_with_id;
191 }
192 }
193 }
194
195 // Return the processed configuration array, or an empty array if not found.
196 return is_array( $get_form_config ) ? $get_form_config : [];
197 }
198
199 /**
200 * Validate form data for a given form.
201 *
202 * This function checks each field in the submitted form data (including uploaded files)
203 * and applies the 'srfm_validate_form_data' filter to validate each field according to
204 * its configuration. Only fields with keys containing '-lbl-' (SureForms fields) are processed.
205 * If a field fails validation, its error message is added to the $not_valid_fields array.
206 *
207 * @param array<mixed> $form_data The submitted form data (sanitized).
208 * @param int|mixed $current_form_id The ID of the form being validated.
209 * @since 1.12.2
210 * @return array An array of invalid fields and their error messages. Empty if all fields are valid.
211 */
212 public static function validate_form_data( $form_data, $current_form_id ) {
213 if ( ! is_array( $form_data ) || ! is_numeric( $current_form_id ) ) {
214 return [];
215 }
216
217 // Holds fields that are not valid. Example: [ 'srfm-email-c867d9d9-lbl-email' => 'This field is required.' ].
218 $not_valid_fields = [];
219
220 // Retrieve the processed form configuration for validation.
221 $get_form_config = self::prepared_validation_data( Helper::get_integer_value( $current_form_id ) );
222
223 $form_data = apply_filters( 'srfm_field_validation_data', $form_data );
224
225 // Iterate over each field in the form data.
226 foreach ( $form_data as $key => $value ) {
227 /**
228 * Only process SureForms fields.
229 * The '-lbl-' substring is mandatory in SureForms field keys.
230 * Example: $key = 'srfm-email-c867d9d9-lbl-email'
231 */
232 if ( false === strpos( $key, '-lbl-' ) ) {
233 continue;
234 }
235
236 $get_name_with_id = explode( '-lbl-', $key );
237 // Extract the part after the last '-' in the key, if it matches the pattern.
238 // Example: $get_name_with_id[0] = "srfm-email-c867d9d9".
239 // $extracted_id = "c867d9d9".
240 $extracted_id = '';
241 if ( is_string( $key ) && preg_match( '/-([a-zA-Z0-9]+)$/', $get_name_with_id[0], $matches ) ) {
242 $extracted_id = $matches[1];
243 // Now $extracted_id contains "c867d9d9" for "srfm-email-c867d9d9".
244 }
245
246 // $get_slug will be the slug after the first hyphen in the second part.
247 // Example: $get_name_with_id[1] = "email" or "field-email", $get_slug = "email".
248 $get_slug = isset( $get_name_with_id[1] ) ? preg_replace( '/^[^-]+-/', '', $get_name_with_id[1] ) : '';
249
250 // $get_field_name is the field name without the block id.
251 // Example: "srfm-email-c867d9d9" => "srfm-email".
252 $get_field_name = str_replace( '-' . $extracted_id, '', $get_name_with_id[0] );
253
254 // Apply the validation filter for the current field.
255 // Example: Passes all relevant field data to the filter for validation.
256 $field_validated = apply_filters(
257 'srfm_validate_form_data',
258 [
259 'field_key' => $key,
260 'field_value' => $value,
261 'form_id' => $current_form_id,
262 'form_config' => $get_form_config,
263 'block_id' => $extracted_id,
264 'block_slug' => $get_slug,
265 'name_with_id' => $get_name_with_id[0],
266 'field_name' => $get_field_name,
267 ]
268 );
269
270 // Check the result of the validation.
271 // Example: $field_validated = [ 'validated' => false, 'error' => 'This field is required.' ].
272 if ( isset( $field_validated['validated'] ) ) {
273 // If the field is valid, skip to the next field.
274 if ( true === $field_validated['validated'] ) {
275 continue;
276 }
277
278 // If the field is not valid, add the error message to the result array.
279 // Example: $not_valid_fields[ 'srfm-email-c867d9d9-lbl-email' ] = 'This field is required.'.
280 if ( false === $field_validated['validated'] ) {
281 $not_valid_fields[ $key ] = $field_validated['error'] ?? __( 'Field is not valid.', 'sureforms' );
282 }
283 }
284
285 // Textarea minimum character server-side validation.
286 if ( 'srfm-textarea' === $get_field_name && is_string( $value ) && '' !== $value ) {
287 $block_config = isset( $get_form_config[ $extracted_id ] ) && is_array( $get_form_config[ $extracted_id ] ) ? $get_form_config[ $extracted_id ] : [];
288 $min_length = isset( $block_config['min_length'] ) ? absint( $block_config['min_length'] ) : 0;
289 if ( $min_length > 0 && mb_strlen( $value ) < $min_length ) {
290 $dynamic_messages = Translatable::dynamic_validation_messages();
291 $min_chars_message = isset( $dynamic_messages['srfm_textarea_min_chars'] ) && is_string( $dynamic_messages['srfm_textarea_min_chars'] ) && '' !== $dynamic_messages['srfm_textarea_min_chars']
292 ? $dynamic_messages['srfm_textarea_min_chars']
293 /* translators: %s represents the minimum number of characters required */
294 : __( 'Please enter at least %s characters.', 'sureforms' );
295 $not_valid_fields[ $key ] = sprintf( $min_chars_message, $min_length );
296 }
297 }
298
299 // Email field RFC 5321 length limits (local part / domain), overridable via filter.
300 // Only the main email value is in form data (the confirm input has no `name`),
301 // so the server validates that value; the client mirrors this for both inputs.
302 // Split on the LAST @ per RFC 5321 so the local part may contain a quoted @.
303 $at_pos = is_string( $value ) && '' !== $value ? strrpos( $value, '@' ) : false;
304 if ( 'srfm-email' === $get_field_name && is_string( $value ) && false !== $at_pos ) {
305 $email_limits = self::get_email_char_limits();
306 $local_max = $email_limits['local'];
307 $domain_max = $email_limits['domain'];
308 $local_len = mb_strlen( substr( $value, 0, $at_pos ) );
309 $domain_len = mb_strlen( substr( $value, $at_pos + 1 ) );
310
311 $dynamic_messages = Translatable::dynamic_validation_messages();
312 if ( $local_max > 0 && $local_len > $local_max ) {
313 $local_message = isset( $dynamic_messages['srfm_email_local_max_length'] ) && is_string( $dynamic_messages['srfm_email_local_max_length'] ) && '' !== $dynamic_messages['srfm_email_local_max_length']
314 ? $dynamic_messages['srfm_email_local_max_length']
315 /* translators: %s: maximum characters allowed before the @ symbol. */
316 : __( 'The part before @ may not exceed %s characters.', 'sureforms' );
317 $not_valid_fields[ $key ] = sprintf( $local_message, $local_max );
318 } elseif ( $domain_max > 0 && $domain_len > $domain_max ) {
319 $domain_message = isset( $dynamic_messages['srfm_email_domain_max_length'] ) && is_string( $dynamic_messages['srfm_email_domain_max_length'] ) && '' !== $dynamic_messages['srfm_email_domain_max_length']
320 ? $dynamic_messages['srfm_email_domain_max_length']
321 /* translators: %s: maximum characters allowed after the @ symbol. */
322 : __( 'The part after @ may not exceed %s characters.', 'sureforms' );
323 $not_valid_fields[ $key ] = sprintf( $domain_message, $domain_max );
324 }
325 }
326 }
327
328 // Return the array of invalid fields and their error messages.
329 // Example: [ 'srfm-email-c867d9d9-lbl-email' => 'This field is required.' ].
330 return $not_valid_fields;
331 }
332
333 /**
334 * Resolve the Email field character limits (RFC 5321), split on the last @.
335 *
336 * Single source of truth shared by the server validation and the limits localized to the
337 * frontend script, so a filter override applies consistently to both.
338 *
339 * @return array{local:int,domain:int} Resolved limits. A value of 0 disables that check.
340 * @since 2.12.1
341 */
342 public static function get_email_char_limits() {
343 /**
344 * Filters the Email field character limits (RFC 5321).
345 *
346 * @param array $limits {
347 * Character limits for the email value, split on the last @.
348 *
349 * @type int $local Max characters before the @. 0 disables the check. Default 64.
350 * @type int $domain Max characters after the @. 0 disables the check. Default 255.
351 * }
352 * @since 2.12.1
353 */
354 $email_limits = apply_filters(
355 'srfm_email_field_char_limits',
356 [
357 'local' => 64,
358 'domain' => 255,
359 ]
360 );
361
362 return [
363 'local' => isset( $email_limits['local'] ) ? absint( $email_limits['local'] ) : 64,
364 'domain' => isset( $email_limits['domain'] ) ? absint( $email_limits['domain'] ) : 255,
365 ];
366 }
367
368 /**
369 * Process payment block configuration.
370 *
371 * @param array<mixed> $attrs Block attributes.
372 * @param array<mixed> $blocks All blocks.
373 * @return array Processed payment configuration.
374 * @since 2.3.0
375 */
376 private static function process_payment_block( $attrs, $blocks ) {
377 $payment_config = [];
378
379 // Extract payment type (single or subscription).
380 $payment_config['payment_type'] = isset( $attrs['paymentType'] ) && is_string( $attrs['paymentType'] ) ? sanitize_text_field( $attrs['paymentType'] ) : 'one-time';
381
382 // Persist subscription plan (interval + billing cycles) for any form that
383 // has a subscription path. The admin picks a single value for each in the
384 // editor; the server uses these stored values as the source of truth on
385 // submit so a tampered interval/cycles in form data cannot redirect Stripe
386 // to a different billing cadence.
387 if ( in_array( $payment_config['payment_type'], [ 'subscription', 'both' ], true ) && isset( $attrs['subscriptionPlan'] ) && is_array( $attrs['subscriptionPlan'] ) ) {
388 if ( isset( $attrs['subscriptionPlan']['interval'] ) && is_string( $attrs['subscriptionPlan']['interval'] ) ) {
389 $payment_config['subscription_interval'] = sanitize_text_field( $attrs['subscriptionPlan']['interval'] );
390 }
391 if ( isset( $attrs['subscriptionPlan']['billingCycles'] ) ) {
392 // billingCycles is either an integer count or the string 'ongoing'.
393 $cycles_raw = $attrs['subscriptionPlan']['billingCycles'];
394 $payment_config['subscription_billing_cycles'] = is_numeric( $cycles_raw ) ? intval( $cycles_raw ) : sanitize_text_field( (string) $cycles_raw );
395 }
396 }
397
398 // Extract amount type (fixed or minimum).
399 $payment_config['amount_type'] = isset( $attrs['amountType'] ) && is_string( $attrs['amountType'] ) ? sanitize_text_field( $attrs['amountType'] ) : 'fixed';
400
401 $payment_config['fixed_amount'] = isset( $attrs['fixedAmount'] ) ? floatval( $attrs['fixedAmount'] ) : 10;
402
403 $payment_config['minimum_amount'] = isset( $attrs['minimumAmount'] ) ? floatval( $attrs['minimumAmount'] ) : 0;
404
405 // Extract variable amount field reference.
406 if ( isset( $attrs['variableAmountField'] ) ) {
407 $variable_amount_slug = sanitize_text_field( $attrs['variableAmountField'] );
408 $payment_config['variable_amount_field'] = $variable_amount_slug;
409
410 // Find and add the block name from which the variable amount field comes from.
411 if ( ! empty( $variable_amount_slug ) && is_array( $blocks ) ) {
412 foreach ( $blocks as $block ) {
413 if ( isset( $block['attrs']['slug'] ) && $block['attrs']['slug'] === $variable_amount_slug ) {
414 $payment_config['variable_amount_field_block_name'] = $block['blockName'];
415 break;
416 }
417 }
418 }
419 }
420
421 // BOTH MODE: store per-type amount configs so server-side validation can
422 // use the correct config based on which flow the user actually chose.
423 if ( 'both' === $payment_config['payment_type'] ) {
424 $payment_config['one_time_amount_type'] = isset( $attrs['oneTimeAmountType'] ) && is_string( $attrs['oneTimeAmountType'] ) ? sanitize_text_field( $attrs['oneTimeAmountType'] ) : 'fixed';
425 $payment_config['one_time_fixed_amount'] = isset( $attrs['oneTimeFixedAmount'] ) ? floatval( $attrs['oneTimeFixedAmount'] ) : 10;
426 $payment_config['one_time_minimum_amount'] = isset( $attrs['oneTimeMinimumAmount'] ) ? floatval( $attrs['oneTimeMinimumAmount'] ) : 0;
427
428 if ( isset( $attrs['oneTimeVariableAmountField'] ) ) {
429 $ot_slug = sanitize_text_field( $attrs['oneTimeVariableAmountField'] );
430 $payment_config['one_time_variable_amount_field'] = $ot_slug;
431 if ( ! empty( $ot_slug ) && is_array( $blocks ) ) {
432 foreach ( $blocks as $block ) {
433 if ( isset( $block['attrs']['slug'] ) && $block['attrs']['slug'] === $ot_slug ) {
434 $payment_config['one_time_variable_amount_field_block_name'] = $block['blockName'];
435 break;
436 }
437 }
438 }
439 }
440
441 $payment_config['subscription_amount_type'] = isset( $attrs['subscriptionAmountType'] ) && is_string( $attrs['subscriptionAmountType'] ) ? sanitize_text_field( $attrs['subscriptionAmountType'] ) : 'fixed';
442 $payment_config['subscription_fixed_amount'] = isset( $attrs['subscriptionFixedAmount'] ) ? floatval( $attrs['subscriptionFixedAmount'] ) : 10;
443 $payment_config['subscription_minimum_amount'] = isset( $attrs['subscriptionMinimumAmount'] ) ? floatval( $attrs['subscriptionMinimumAmount'] ) : 0;
444
445 if ( isset( $attrs['subscriptionVariableAmountField'] ) ) {
446 $sub_slug = sanitize_text_field( $attrs['subscriptionVariableAmountField'] );
447 $payment_config['subscription_variable_amount_field'] = $sub_slug;
448 if ( ! empty( $sub_slug ) && is_array( $blocks ) ) {
449 foreach ( $blocks as $block ) {
450 if ( isset( $block['attrs']['slug'] ) && $block['attrs']['slug'] === $sub_slug ) {
451 $payment_config['subscription_variable_amount_field_block_name'] = $block['blockName'];
452 break;
453 }
454 }
455 }
456 }
457 }
458
459 return $payment_config;
460 }
461
462 /**
463 * Process dropdown block configuration.
464 *
465 * @param array<mixed> $attrs Block attributes.
466 * @return array Processed dropdown configuration.
467 * @since 2.3.0
468 */
469 private static function process_dropdown_block( $attrs ) {
470 $dropdown_config = [];
471
472 // Extract required field.
473 $dropdown_config['required'] = isset( $attrs['required'] ) && ! empty( $attrs['required'] ) ? true : false;
474
475 // Extract options with their full structure (label, icon, value).
476 if ( isset( $attrs['options'] ) && is_array( $attrs['options'] ) ) {
477 $sanitized_options = [];
478 foreach ( $attrs['options'] as $option ) {
479 if ( is_array( $option ) ) {
480 $sanitized_options[] = [
481 'label' => isset( $option['label'] ) ? sanitize_text_field( $option['label'] ) : '',
482 'icon' => isset( $option['icon'] ) ? sanitize_text_field( $option['icon'] ) : '',
483 'value' => isset( $option['value'] ) ? sanitize_text_field( $option['value'] ) : '',
484 ];
485 }
486 }
487 $dropdown_config['options'] = $sanitized_options;
488 }
489
490 // Extract showValues flag.
491 $dropdown_config['show_values'] = isset( $attrs['showValues'] ) ? rest_sanitize_boolean( $attrs['showValues'] ) : false;
492
493 // Extract multiSelect flag.
494 if ( isset( $attrs['multiSelect'] ) ) {
495 $dropdown_config['multi_select'] = rest_sanitize_boolean( $attrs['multiSelect'] );
496 }
497
498 // Extract minValue for multi-select validation.
499 if ( isset( $attrs['minValue'] ) ) {
500 $dropdown_config['min_value'] = absint( $attrs['minValue'] );
501 }
502
503 // Extract maxValue for multi-select validation.
504 if ( isset( $attrs['maxValue'] ) ) {
505 $dropdown_config['max_value'] = absint( $attrs['maxValue'] );
506 }
507
508 return $dropdown_config;
509 }
510
511 /**
512 * Process multi-choice block configuration.
513 *
514 * @param array<mixed> $attrs Block attributes.
515 * @return array Processed multi-choice configuration.
516 * @since 2.3.0
517 */
518 private static function process_multichoice_block( $attrs ) {
519 $multichoice_config = [];
520
521 // Extract required field.
522 $multichoice_config['required'] = isset( $attrs['required'] ) && ! empty( $attrs['required'] ) ? true : false;
523
524 // Extract singleSelection flag.
525 if ( isset( $attrs['singleSelection'] ) ) {
526 $multichoice_config['single_selection'] = rest_sanitize_boolean( $attrs['singleSelection'] );
527 }
528
529 // Extract minValue for validation.
530 if ( isset( $attrs['minValue'] ) ) {
531 $multichoice_config['min_value'] = absint( $attrs['minValue'] );
532 }
533
534 // Extract maxValue for validation.
535 if ( isset( $attrs['maxValue'] ) ) {
536 $multichoice_config['max_value'] = absint( $attrs['maxValue'] );
537 }
538
539 // Extract options with their full structure (label, icon, value).
540 if ( isset( $attrs['options'] ) && is_array( $attrs['options'] ) ) {
541 $sanitized_options = [];
542 foreach ( $attrs['options'] as $option ) {
543 if ( is_array( $option ) ) {
544 $sanitized_options[] = [
545 'label' => isset( $option['optionTitle'] ) ? trim( sanitize_text_field( $option['optionTitle'] ) ) : '',
546 'icon' => isset( $option['icon'] ) ? sanitize_text_field( $option['icon'] ) : '',
547 'value' => isset( $option['value'] ) ? sanitize_text_field( $option['value'] ) : '',
548 ];
549 }
550 }
551 $multichoice_config['options'] = $sanitized_options;
552 }
553
554 // Extract showValues flag.
555 if ( isset( $attrs['showValues'] ) ) {
556 $multichoice_config['show_values'] = rest_sanitize_boolean( $attrs['showValues'] );
557 }
558
559 return $multichoice_config;
560 }
561
562 /**
563 * Process textarea block configuration.
564 *
565 * @param array<mixed> $attrs Block attributes.
566 * @return array Processed textarea configuration.
567 * @since 2.8.2
568 */
569 private static function process_textarea_block( $attrs ) {
570 // Always emit a min_length key so a cleared/invalid value overwrites any
571 // previously stored config on save instead of falling back to stale data.
572 // Rich-text editors submit HTML markup which would skew mb_strlen counts,
573 // so they're treated as "no min-length validation".
574 if ( ! empty( $attrs['isRichText'] ) ) {
575 return [ 'min_length' => 0 ];
576 }
577
578 $min_length = isset( $attrs['minLength'] ) && is_numeric( $attrs['minLength'] ) ? absint( $attrs['minLength'] ) : 0;
579 $max_length = isset( $attrs['maxLength'] ) && is_numeric( $attrs['maxLength'] ) ? absint( $attrs['maxLength'] ) : 0;
580
581 // Misconfiguration guard — drop min when it exceeds max so the form stays submittable.
582 if ( $max_length > 0 && $min_length > $max_length ) {
583 $min_length = 0;
584 }
585
586 return [ 'min_length' => $min_length ];
587 }
588
589 /**
590 * Process number block configuration.
591 *
592 * @param array<mixed> $attrs Block attributes.
593 * @return array Processed number configuration.
594 * @since 2.4.0
595 */
596 private static function process_number_block( $attrs ) {
597 $number_config = [];
598
599 // Extract required field.
600 if ( isset( $attrs['required'] ) ) {
601 $number_config['required'] = ! empty( $attrs['required'] ) ? true : false;
602 }
603
604 // Extract format type (us-style or eu-style).
605 $number_config['format_type'] = isset( $attrs['formatType'] ) && is_string( $attrs['formatType'] ) ? sanitize_text_field( $attrs['formatType'] ) : 'us-style';
606
607 // Extract min value.
608 if ( isset( $attrs['min'] ) ) {
609 $number_config['min'] = floatval( $attrs['min'] );
610 }
611
612 // Extract max value.
613 if ( isset( $attrs['max'] ) ) {
614 $number_config['max'] = floatval( $attrs['max'] );
615 }
616
617 // Capture calculation config (Pro feature) so a calculation-driven number field used as a
618 // payment amount source can be re-derived server-side instead of trusting the submitted
619 // value. Harmless when the calculation feature is not in use.
620 if ( ! empty( $attrs['enableCalculation'] ) ) {
621 $number_config['enableCalculation'] = true;
622 $number_config['calculationFormula'] = isset( $attrs['calculationFormula'] ) && is_string( $attrs['calculationFormula'] ) ? $attrs['calculationFormula'] : '';
623 // Stored as null when unset so the validator rounds only when a precision is configured.
624 $number_config['calculationRound'] = isset( $attrs['calculationRound'] ) && is_numeric( $attrs['calculationRound'] ) ? absint( $attrs['calculationRound'] ) : null;
625 }
626
627 return $number_config;
628 }
629 }
630