PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
1.6.1 1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
← All changes | inc/field-validation.php +132 -10 1.5.1 → 1.6.1 View file →
@@ -63,8 +63,9 @@
63 63 'suredonation/checkbox',
64 64 'suredonation/dropdown',
65 65 'suredonation/phone',
66 66 'suredonation/url',
67 + 'suredonation/donor-comment',
67 68 ];
68 69
69 70 /**
70 71 * Add block configuration for form fields.
@@ -199,8 +200,11 @@
199 200 break;
200 201 case 'suredonation/url':
201 202 $processed_config = self::process_url_block( $block['attrs'] );
202 203 break;
204 + case 'suredonation/donor-comment':
205 + $processed_config = self::process_donor_comment_block( $block['attrs'] );
206 + break;
203 207 }
204 208
205 209 /**
206 210 * Filter the stored validation config for a field block.
@@ -408,24 +412,115 @@
408 412 if ( ! ( $post instanceof \WP_Post ) || empty( $post->post_content ) ) {
409 413 return [];
410 414 }
411 415
412 - $payment_attrs = self::find_payment_block_attrs( parse_blocks( $post->post_content ) );
413 - if ( empty( $payment_attrs ) ) {
414 - return [];
416 + $blocks = parse_blocks( $post->post_content );
417 + $slugs = [];
418 +
419 + $payment_attrs = self::find_payment_block_attrs( $blocks );
420 + if ( ! empty( $payment_attrs ) ) {
421 + foreach ( [ 'customerNameField', 'customerEmailField', 'customerPhoneField', 'variableAmountField' ] as $attr ) {
422 + if ( isset( $payment_attrs[ $attr ] ) && is_string( $payment_attrs[ $attr ] ) ) {
423 + $slug = sanitize_text_field( $payment_attrs[ $attr ] );
424 + if ( '' !== $slug ) {
425 + $slugs[] = $slug;
426 + }
427 + }
428 + }
415 429 }
416 430
417 - $slugs = [];
418 - foreach ( [ 'customerNameField', 'customerEmailField', 'customerPhoneField', 'variableAmountField' ] as $attr ) {
419 - if ( isset( $payment_attrs[ $attr ] ) && is_string( $payment_attrs[ $attr ] ) ) {
420 - $slug = sanitize_text_field( $payment_attrs[ $attr ] );
421 - if ( '' !== $slug ) {
422 - $slugs[] = $slug;
431 + // The donor comment also lives in its own column, so it is excluded from
432 + // the additional set for the same reason. Unlike the fields above it is not
433 + // mapped on the payment block — the presence of the block is the mapping —
434 + // so it is resolved from the already-parsed tree rather than through
435 + // get_donor_comment_slug(), which would parse the form a second time.
436 + $comment_slug = self::find_slug_by_block_name( $blocks, 'suredonation/donor-comment' );
437 + if ( is_string( $comment_slug ) && '' !== $comment_slug ) {
438 + $slugs[] = sanitize_text_field( $comment_slug );
439 + }
440 +
441 + return array_values( array_unique( $slugs ) );
442 + }
443 +
444 + /**
445 + * Resolve the slug of the form's Donor Comment field.
446 + *
447 + * Unlike the donor phone — which is mapped through a picker on the payment
448 + * block — the Donor Comment field is its own block, so the block's presence
449 + * in the saved form *is* the mapping. Returning the slug lets the submission
450 + * handlers read the already-validated value out of the submitted field set
451 + * and store it in the dedicated donor_comment column, rather than trusting a
452 + * separate client-supplied key. A comment posted against a form that has no
453 + * Donor Comment block is therefore ignored, matching how
454 + * Payment_Helper::get_submitted_is_anonymous() derives the anonymity option
455 + * from the saved form.
456 + *
457 + * Only one field can feed the single column: when a form somehow contains
458 + * more than one block (the editor warns against it), the first in document
459 + * order wins.
460 + *
461 + * @since 1.6.0
462 + * @param int $form_id The donation form post ID.
463 + * @return string The Donor Comment field slug, or '' when the form has none.
464 + */
465 + public static function get_donor_comment_slug( $form_id ) {
466 + $form_id = (int) $form_id;
467 + if ( $form_id <= 0 || ! function_exists( 'parse_blocks' ) ) {
468 + return '';
469 + }
470 +
471 + // form_id is attacker-chosen on a public endpoint, so confirm it really is
472 + // a donation form before parsing its content — otherwise the request can
473 + // aim a full block parse at any post in the database.
474 + $post = get_post( $form_id );
475 + if ( ! ( $post instanceof \WP_Post )
476 + || \SureDonation\Inc\Post_Types\Donation_Form::POST_TYPE !== $post->post_type
477 + || empty( $post->post_content ) ) {
478 + return '';
479 + }
480 +
481 + $slug = self::find_slug_by_block_name( parse_blocks( $post->post_content ), 'suredonation/donor-comment' );
482 +
483 + return null === $slug ? '' : sanitize_text_field( $slug );
484 + }
485 +
486 + /**
487 + * Find the `slug` attribute of the first block with the given name.
488 + *
489 + * The inverse of find_block_name_by_slug(). Walks in document order, parents
490 + * before children, so "first match" is stable and matches what the editor
491 + * shows the author.
492 + *
493 + * @since 1.6.0
494 + * @param array<mixed> $blocks Array of parsed blocks.
495 + * @param string $block_name Block name to look for.
496 + * @return string|null The slug, or null when the block is absent or has no slug.
497 + */
498 + private static function find_slug_by_block_name( $blocks, $block_name ) {
499 + if ( ! is_array( $blocks ) ) {
500 + return null;
501 + }
502 +
503 + foreach ( $blocks as $block ) {
504 + if ( ! is_array( $block ) ) {
505 + continue;
506 + }
507 +
508 + if ( isset( $block['blockName'] ) && $block_name === $block['blockName']
509 + && isset( $block['attrs']['slug'] ) && is_string( $block['attrs']['slug'] )
510 + && '' !== $block['attrs']['slug'] ) {
511 + return $block['attrs']['slug'];
512 + }
513 +
514 + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
515 + $found = self::find_slug_by_block_name( $block['innerBlocks'], $block_name );
516 + if ( null !== $found ) {
517 + return $found;
423 518 }
424 519 }
425 520 }
426 521
427 - return array_values( array_unique( $slugs ) );
522 + return null;
428 523 }
429 524
430 525 /**
431 526 * Find the suredonation/payment block's attributes recursively.
@@ -898,8 +993,33 @@
898 993 return $url_config;
899 994 }
900 995
901 996 /**
997 + * Process donor comment block configuration.
998 + *
999 + * Stores required state, max length and the optional per-field custom
1000 + * required message for server-side enforcement. Mirrors the text input's
1001 + * rules — the field is a plain textarea with no format constraint.
1002 + *
1003 + * @param array<mixed> $attrs Block attributes.
1004 + * @return array<string, mixed> Processed donor comment block configuration.
1005 + * @since 1.6.0
1006 + */
1007 + private static function process_donor_comment_block( $attrs ) {
1008 + $comment_config = [
1009 + 'required' => ! empty( $attrs['required'] ),
1010 + 'max_length' => isset( $attrs['maxLength'] ) ? absint( Helper::get_string_value( $attrs['maxLength'] ) ) : 500,
1011 + ];
1012 +
1013 + $error_msg = isset( $attrs['errorMsg'] ) ? sanitize_text_field( Helper::get_string_value( $attrs['errorMsg'] ) ) : '';
1014 + if ( '' !== $error_msg ) {
1015 + $comment_config['error_msg'] = $error_msg;
1016 + }
1017 +
1018 + return $comment_config;
1019 + }
1020 +
1021 + /**
902 1022 * Process checkbox block configuration.
903 1023 *
904 1024 * A checkbox carries no format or range rules — only the required flag and
905 1025 * the optional per-field error message. `is_checkbox` is stored so the
@@ -1099,8 +1219,9 @@
1099 1219 'suredonation_checkbox_block_required_text' => __( 'This field is required.', 'suredonation' ),
1100 1220 'suredonation_dropdown_block_required_text' => __( 'This field is required.', 'suredonation' ),
1101 1221 'suredonation_phone_block_required_text' => __( 'This field is required.', 'suredonation' ),
1102 1222 'suredonation_url_block_required_text' => __( 'This field is required.', 'suredonation' ),
1223 + 'suredonation_donor_comment_block_required_text' => __( 'This field is required.', 'suredonation' ),
1103 1224 'suredonation_valid_email' => __( 'Please enter a valid email address.', 'suredonation' ),
1104 1225 'suredonation_valid_number' => __( 'Please enter a valid number.', 'suredonation' ),
1105 1226 'suredonation_valid_phone' => __( 'Please enter a valid phone number.', 'suredonation' ),
1106 1227 'suredonation_valid_url' => __( 'Please enter a valid URL.', 'suredonation' ),
@@ -1184,8 +1305,9 @@
1184 1305 }
1185 1306
1186 1307 switch ( $block_name ) {
1187 1308 case 'suredonation/input':
1309 + case 'suredonation/donor-comment':
1188 1310 $max_length = isset( $config['max_length'] ) && is_numeric( $config['max_length'] ) ? (int) $config['max_length'] : 0;
1189 1311 $length = function_exists( 'mb_strlen' ) ? mb_strlen( $value ) : strlen( $value );
1190 1312 if ( $max_length > 0 && $length > $max_length ) {
1191 1313 // str_replace (not sprintf) because the message is admin/translator