PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
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
← All changes | inc/payments/payment-helper.php +278 -0 2.12.0 → 2.12.8 View file →
@@ -483,8 +483,9 @@
483 483 'payment_unavailable' => __( 'Payment is currently unavailable. Please contact the site administrator.', 'sureforms' ),
484 484 'payment_amount_not_configured' => __( 'Payment is currently unavailable. Please contact the site administrator to configure the payment amount.', 'sureforms' ),
485 485 'invalid_variable_amount' => __( 'Invalid payment amount', 'sureforms' ),
486 486 'amount_below_minimum' => __( 'Payment amount must be at least {symbol}{amount}.', 'sureforms' ),
487 + 'payment_required' => __( 'This form requires a payment. Please complete the payment and submit again.', 'sureforms' ),
487 488
488 489 // Field mapping validation.
489 490 'payment_name_not_mapped' => __( 'Payment is currently unavailable. Please contact the site administrator to configure the customer name field.', 'sureforms' ),
490 491 'payment_email_not_mapped' => __( 'Payment is currently unavailable. Please contact the site administrator to configure the customer email field.', 'sureforms' ),
@@ -861,8 +862,146 @@
861 862 return null;
862 863 }
863 864
864 865 /**
866 + * Get the payment methods a payment block can actually offer.
867 + *
868 + * The block's enabled methods intersected with the methods that are registered
869 + * and connected. Shared with Payment_Markup so the renderer and the submission
870 + * guard can never disagree about whether a payment field is usable.
871 + *
872 + * @param array<mixed> $attrs Payment block attributes.
873 + *
874 + * @since 2.12.3
875 + * @return array<string, mixed> Usable payment methods, keyed by method ID.
876 + */
877 + public static function get_registered_payment_methods( $attrs ) {
878 + $methods = [];
879 + $attrs = is_array( $attrs ) ? $attrs : [];
880 + $enabled_methods = isset( $attrs['paymentMethods'] ) && is_array( $attrs['paymentMethods'] ) ? $attrs['paymentMethods'] : [ 'stripe' ];
881 +
882 + // Filter to get method configurations - start with Stripe as default.
883 + $available_methods = apply_filters(
884 + 'srfm_payment_methods_registry',
885 + [
886 + 'stripe' => [
887 + 'id' => 'stripe',
888 + 'label' => __( 'Stripe', 'sureforms' ),
889 + 'description' => __( 'Pay with credit or debit card', 'sureforms' ),
890 + 'icon' => 'credit-card',
891 + 'enabled' => Stripe_Helper::is_stripe_connected(),
892 + 'container_class' => 'srfm-stripe-payment-element',
893 + ],
894 + ]
895 + );
896 +
897 + // Filter enabled methods.
898 + foreach ( $enabled_methods as $method_id ) {
899 + if ( is_array( $available_methods ) && isset( $available_methods[ $method_id ] ) && ! empty( $available_methods[ $method_id ]['enabled'] ) ) {
900 + $methods[ $method_id ] = $available_methods[ $method_id ];
901 + }
902 + }
903 +
904 + return $methods;
905 + }
906 +
907 + /**
908 + * Whether a payment block's configuration produces a usable payment field.
909 + *
910 + * Mirrors the conditions under which Payment_Markup::markup() returns early with
911 + * no markup: no usable payment method, or the customer field mappings the gateway
912 + * needs are missing. A block that renders nothing cannot be required on submit.
913 + *
914 + * @param array<mixed> $attrs Payment block attributes.
915 + *
916 + * @since 2.12.3
917 + * @return bool True when the block renders a payment field.
918 + */
919 + public static function is_payment_field_active( $attrs ) {
920 + if ( ! is_array( $attrs ) ) {
921 + return false;
922 + }
923 +
924 + if ( empty( self::get_registered_payment_methods( $attrs ) ) ) {
925 + return false;
926 + }
927 +
928 + // Customer field mappings, including the legacy subscriptionPlan fallbacks.
929 + $subscription_plan = isset( $attrs['subscriptionPlan'] ) && is_array( $attrs['subscriptionPlan'] ) ? $attrs['subscriptionPlan'] : [];
930 + $email_field = ! empty( $attrs['customerEmailField'] ) ? $attrs['customerEmailField'] : ( $subscription_plan['customer_email'] ?? '' );
931 +
932 + if ( empty( $email_field ) ) {
933 + return false;
934 + }
935 +
936 + $payment_type = ! empty( $attrs['paymentType'] ) && is_string( $attrs['paymentType'] ) ? $attrs['paymentType'] : 'one-time';
937 +
938 + // A subscription path also needs the customer name mapping.
939 + if ( in_array( $payment_type, [ 'subscription', 'both' ], true ) ) {
940 + $name_field = ! empty( $attrs['customerNameField'] ) ? $attrs['customerNameField'] : ( $subscription_plan['customer_name'] ?? '' );
941 +
942 + if ( empty( $name_field ) ) {
943 + return false;
944 + }
945 + }
946 +
947 + return true;
948 + }
949 +
950 + /**
951 + * Get the block IDs of payment fields a submission of this form must pay for.
952 + *
953 + * Derived from the stored form, never from what the client submitted. Blocks that
954 + * render nothing (see is_payment_field_active()) and blocks under conditional logic
955 + * are excluded: conditional logic is evaluated on the client, so a hidden payment
956 + * field legitimately submits no payment value and must not be required here.
957 + *
958 + * @param int $form_id Form ID.
959 + *
960 + * @since 2.12.3
961 + * @return array<string> Block IDs that require a verified payment.
962 + */
963 + public static function get_required_payment_block_ids( $form_id ) {
964 + // absint(), matching Submit_Token::verify()'s normalisation in
965 + // Form_Submit::submit_form_permissions_check(). get_integer_value() would keep a
966 + // negative id and bail below, so the guard would resolve a different form from
967 + // the one the submit token authorised.
968 + $form_id = absint( $form_id );
969 + $form = $form_id > 0 ? get_post( $form_id ) : null;
970 +
971 + if ( ! $form instanceof \WP_Post || '' === $form->post_content ) {
972 + return [];
973 + }
974 +
975 + $block_ids = self::collect_active_payment_block_ids( parse_blocks( $form->post_content ) );
976 +
977 + if ( empty( $block_ids ) ) {
978 + return [];
979 + }
980 +
981 + // Drop blocks that conditional logic can hide.
982 + foreach ( self::get_conditional_logic_block_ids( $form_id ) as $conditional_id ) {
983 + unset( $block_ids[ $conditional_id ] );
984 + }
985 +
986 + /**
987 + * Filters the payment block IDs that require a verified payment on submit.
988 + *
989 + * Lets add-ons that can evaluate their own visibility rules server-side add or
990 + * remove blocks — e.g. re-adding a conditionally shown payment field once the
991 + * rule is known to have matched.
992 + *
993 + * @since 2.12.3
994 + *
995 + * @param array<string> $block_ids Block IDs requiring a verified payment.
996 + * @param int $form_id Form ID.
997 + */
998 + $block_ids = apply_filters( 'srfm_required_payment_block_ids', array_keys( $block_ids ), $form_id );
999 +
1000 + return is_array( $block_ids ) ? $block_ids : [];
1001 + }
1002 +
1003 + /**
865 1004 * Resolve the WordPress user associated with a payment record.
866 1005 *
867 1006 * Resolution order:
868 1007 * 1. The linked entry's `user_id` (set when a logged-in user submitted the form).
@@ -937,8 +1076,147 @@
937 1076 * @param string $currency Currency code.
938 1077 * @return array|null Validation result array or null if validation passes.
939 1078 * @since 2.3.0
940 1079 */
1080 + /**
1081 + * Recursively collect the block IDs of payment blocks that render a payment field.
1082 + *
1083 + * Recurses into innerBlocks and expands core/block reusable/synced patterns, so a
1084 + * payment block that renders from inside a pattern is still required. The payment
1085 + * block sets "reusable": false, so reaching that state needs imported or
1086 + * hand-authored post_content rather than the editor — but a payment field that
1087 + * renders and is not required is exactly the hole this guard exists to close.
1088 + *
1089 + * @param array<mixed> $blocks Parsed blocks from parse_blocks().
1090 + * @param array<int, true> $visited_refs Reusable-block post IDs already expanded,
1091 + * keyed by ID — guards against reference cycles.
1092 + *
1093 + * @since 2.12.3
1094 + * @return array<string,true> Active payment block IDs, keyed by block ID.
1095 + */
1096 + private static function collect_active_payment_block_ids( $blocks, &$visited_refs = [] ) {
1097 + $block_ids = [];
1098 +
1099 + foreach ( $blocks as $block ) {
1100 + if ( ! is_array( $block ) ) {
1101 + continue;
1102 + }
1103 +
1104 + $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
1105 +
1106 + if ( 'srfm/payment' === ( $block['blockName'] ?? '' )
1107 + && ! empty( $attrs['block_id'] )
1108 + && is_scalar( $attrs['block_id'] )
1109 + && self::is_payment_field_active( $attrs )
1110 + ) {
1111 + $block_ids[ Helper::get_string_value( $attrs['block_id'] ) ] = true;
1112 + }
1113 +
1114 + if ( isset( $block['blockName'] ) && 'core/block' === $block['blockName'] && ! empty( $attrs['ref'] ) && is_scalar( $attrs['ref'] ) ) {
1115 + $ref = absint( $attrs['ref'] );
1116 +
1117 + if ( $ref && ! isset( $visited_refs[ $ref ] ) ) {
1118 + $visited_refs[ $ref ] = true;
1119 + $ref_post = get_post( $ref );
1120 +
1121 + if ( $ref_post instanceof \WP_Post && 'wp_block' === $ref_post->post_type && '' !== $ref_post->post_content ) {
1122 + $block_ids += self::collect_active_payment_block_ids( parse_blocks( $ref_post->post_content ), $visited_refs );
1123 + }
1124 + }
1125 + }
1126 +
1127 + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
1128 + $block_ids += self::collect_active_payment_block_ids( $block['innerBlocks'], $visited_refs );
1129 + }
1130 + }
1131 +
1132 + return $block_ids;
1133 + }
1134 +
1135 + /**
1136 + * Get the block IDs a form has conditional logic rules for.
1137 + *
1138 + * Field visibility rules live in the `_srfm_conditional_logic` post meta, keyed by
1139 + * block ID, and are evaluated on the client. A payment field under such a rule may
1140 + * legitimately be hidden at submit time.
1141 + *
1142 + * @param int $form_id Form ID.
1143 + *
1144 + * @since 2.12.3
1145 + * @return array<string> Block IDs carrying conditional logic rules.
1146 + */
1147 + private static function get_conditional_logic_block_ids( $form_id ) {
1148 + // Only exempt when conditional logic can actually hide anything. The rules are
1149 + // evaluated client-side by the add-on that registers this meta; without it
1150 + // nothing hides, so a stale rule (e.g. written by a form importer on a site that
1151 + // never had the add-on) must not buy a payment block an exemption.
1152 + if ( ! registered_meta_key_exists( 'post', '_srfm_conditional_logic', SRFM_FORMS_POST_TYPE ) ) {
1153 + return [];
1154 + }
1155 +
1156 + $conditional_logic = get_post_meta( $form_id, '_srfm_conditional_logic', true );
1157 +
1158 + if ( ! is_array( $conditional_logic ) ) {
1159 + return [];
1160 + }
1161 +
1162 + $block_ids = [];
1163 +
1164 + foreach ( $conditional_logic as $item ) {
1165 + if ( ! is_array( $item ) ) {
1166 + continue;
1167 + }
1168 +
1169 + foreach ( $item as $block_id => $rule ) {
1170 + if ( ! is_string( $block_id ) || '' === $block_id ) {
1171 + continue;
1172 + }
1173 +
1174 + // An actionable rule only — a rule with no conditions can never match, so
1175 + // the field's visibility is never altered and payment stays required.
1176 + if ( self::has_actionable_conditional_rule( $rule ) ) {
1177 + $block_ids[] = $block_id;
1178 + }
1179 + }
1180 + }
1181 +
1182 + return $block_ids;
1183 + }
1184 +
1185 + /**
1186 + * Whether a stored conditional-logic rule can actually change a field's visibility.
1187 + *
1188 + * Both `show` and `hide` actions can leave the field hidden for a given submission
1189 + * (show = hidden until the conditions match, hide = visible until they match), so the
1190 + * action itself is not the discriminator — the presence of at least one real condition
1191 + * is. Empty or malformed rules are left behind by editor cleanup and must not exempt
1192 + * a payment block.
1193 + *
1194 + * @param mixed $rule Stored rule for a single block.
1195 + *
1196 + * @since 2.12.3
1197 + * @return bool True when the rule carries at least one condition.
1198 + */
1199 + private static function has_actionable_conditional_rule( $rule ) {
1200 + if ( ! is_array( $rule ) || empty( $rule['action'] ) || empty( $rule['logic'] ) || ! is_array( $rule['logic'] ) ) {
1201 + return false;
1202 + }
1203 +
1204 + foreach ( $rule['logic'] as $conditions ) {
1205 + if ( ! is_array( $conditions ) ) {
1206 + continue;
1207 + }
1208 +
1209 + foreach ( $conditions as $condition ) {
1210 + if ( is_array( $condition ) && ! empty( $condition['field'] ) ) {
1211 + return true;
1212 + }
1213 + }
1214 + }
1215 +
1216 + return false;
1217 + }
1218 +
941 1219 /**
942 1220 * BOTH MODE: resolve the correct amount config keys from the payment block
943 1221 * config based on which flow (one-time or subscription) the user chose.
944 1222 *