| @@ -43,8 +43,13 @@ | ||
| 43 | 43 | * @return void |
| 44 | 44 | * @since 0.0.1 |
| 45 | 45 | */ |
| 46 | 46 | public function handle_donation_submission() { |
| 47 | + // Throttle abuse on this public endpoint before doing any work. | |
| 48 | + if ( ! Helper::check_rate_limit( 'submit_donation' ) ) { | |
| 49 | + wp_send_json_error( __( 'Too many requests. Please wait a moment and try again.', 'suredonation' ), 429 ); | |
| 50 | + } | |
| 51 | + | |
| 47 | 52 | // First check if nonce exists before accessing any other POST data. |
| 48 | 53 | if ( ! isset( $_POST['suredonation_nonce'] ) ) { |
| 49 | 54 | wp_send_json_error( __( 'Security check failed', 'suredonation' ) ); |
| 50 | 55 | } |
| @@ -53,17 +58,27 @@ | ||
| 53 | 58 | $nonce = sanitize_text_field( wp_unslash( $_POST['suredonation_nonce'] ) ); |
| 54 | 59 | |
| 55 | 60 | // Now get values needed to determine nonce action. |
| 56 | 61 | $is_standalone = isset( $_POST['is_standalone'] ) && '1' === $_POST['is_standalone']; |
| 57 | - $campaign_id = isset( $_POST['campaign_id'] ) ? intval( $_POST['campaign_id'] ) : 0; | |
| 62 | + $campaign_id = isset( $_POST['campaign_id'] ) ? absint( $_POST['campaign_id'] ) : 0; | |
| 58 | 63 | |
| 64 | + // Standalone forms must not have a campaign — prevent bypass of campaign validation. | |
| 65 | + if ( $is_standalone ) { | |
| 66 | + $campaign_id = 0; | |
| 67 | + } | |
| 68 | + | |
| 59 | 69 | // Verify nonce - different nonce for standalone vs campaign-linked forms. |
| 60 | - $nonce_action = $is_standalone ? 'suredonation_donation_standalone' : 'suredonation_donation_' . $campaign_id; | |
| 70 | + $nonce_action = Helper::get_donation_nonce_action( $campaign_id ); | |
| 61 | 71 | |
| 62 | 72 | if ( ! wp_verify_nonce( $nonce, $nonce_action ) ) { |
| 63 | 73 | wp_send_json_error( __( 'Security check failed', 'suredonation' ) ); |
| 64 | 74 | } |
| 65 | 75 | |
| 76 | + // Reject bot submissions caught by the honeypot before processing. | |
| 77 | + if ( Helper::is_honeypot_spam() ) { | |
| 78 | + wp_send_json_error( __( 'Your submission was flagged as spam. Please try again.', 'suredonation' ) ); | |
| 79 | + } | |
| 80 | + | |
| 66 | 81 | // Validate campaign only if not standalone. |
| 67 | 82 | $campaign = null; |
| 68 | 83 | if ( ! $is_standalone ) { |
| 69 | 84 | if ( ! $campaign_id ) { |
| @@ -77,9 +92,9 @@ | ||
| 77 | 92 | } |
| 78 | 93 | |
| 79 | 94 | // Get form data. |
| 80 | 95 | $amount = isset( $_POST['amount'] ) ? floatval( $_POST['amount'] ) : 0; |
| 81 | - $cover_fees = isset( $_POST['cover_fees'] ) ? true : false; | |
| 96 | + $cover_fees = isset( $_POST['cover_fees'] ) && 'true' === $_POST['cover_fees']; | |
| 82 | 97 | $is_anonymous = isset( $_POST['is_anonymous'] ) ? true : false; |
| 83 | 98 | $donor_name = $is_anonymous ? __( 'Anonymous', 'suredonation' ) : sanitize_text_field( wp_unslash( $_POST['donor_name'] ?? '' ) ); |
| 84 | 99 | $donor_email = sanitize_email( wp_unslash( $_POST['donor_email'] ?? '' ) ); |
| 85 | 100 | $donor_phone = sanitize_text_field( wp_unslash( $_POST['donor_phone'] ?? '' ) ); |
| @@ -84,13 +99,30 @@ | ||
| 84 | 99 | $donor_email = sanitize_email( wp_unslash( $_POST['donor_email'] ?? '' ) ); |
| 85 | 100 | $donor_phone = sanitize_text_field( wp_unslash( $_POST['donor_phone'] ?? '' ) ); |
| 86 | 101 | $donor_comment = sanitize_textarea_field( wp_unslash( $_POST['donor_comment'] ?? '' ) ); |
| 87 | 102 | |
| 103 | + // Get form_id and block_id for amount validation. | |
| 104 | + $form_id = isset( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; | |
| 105 | + $block_id = isset( $_POST['block_id'] ) ? sanitize_text_field( wp_unslash( $_POST['block_id'] ) ) : ''; | |
| 106 | + | |
| 88 | 107 | // Validate required fields. |
| 89 | 108 | if ( $amount <= 0 ) { |
| 90 | 109 | wp_send_json_error( __( 'Invalid donation amount', 'suredonation' ) ); |
| 91 | 110 | } |
| 92 | 111 | |
| 112 | + // Require form_id and block_id for amount validation — reject if missing to prevent bypass. | |
| 113 | + if ( empty( $form_id ) || empty( $block_id ) ) { | |
| 114 | + wp_send_json_error( __( 'Invalid form configuration.', 'suredonation' ) ); | |
| 115 | + } | |
| 116 | + | |
| 117 | + // Validate field values + amount against block configuration. Pass the | |
| 118 | + // offline gateway so the Stripe-only minimum floor is not applied here. | |
| 119 | + $currency = Payment_Helper::get_currency(); | |
| 120 | + $validation_result = Payment_Helper::validate_submission( Payment_Helper::get_submitted_fields(), $amount, $currency, $form_id, $block_id, 'offline' ); | |
| 121 | + if ( ! $validation_result['valid'] ) { | |
| 122 | + wp_send_json_error( esc_html( $validation_result['message'] ) ); | |
| 123 | + } | |
| 124 | + | |
| 93 | 125 | if ( ! $is_anonymous ) { |
| 94 | 126 | if ( empty( $donor_name ) ) { |
| 95 | 127 | wp_send_json_error( __( 'Donor name is required', 'suredonation' ) ); |
| 96 | 128 | } |
| @@ -98,18 +130,24 @@ | ||
| 98 | 130 | wp_send_json_error( __( 'Valid email address is required', 'suredonation' ) ); |
| 99 | 131 | } |
| 100 | 132 | } |
| 101 | 133 | |
| 102 | - // Calculate fee and amounts. | |
| 103 | - $fee_percentage = 0.029; // 2.9% | |
| 104 | - $fee_fixed = 0.30; | |
| 105 | - $base_amount = $amount; | |
| 106 | - $fees_covered = 0; | |
| 134 | + // Server-side fee calculation — ignore client-supplied base_amount to prevent manipulation. | |
| 135 | + $base_amount = $amount; | |
| 136 | + $fees_covered = 0; | |
| 107 | 137 | |
| 108 | - if ( $cover_fees ) { | |
| 109 | - // Calculate the base amount from total (reverse calculation). | |
| 110 | - $base_amount = ( $amount - $fee_fixed ) / ( 1 + $fee_percentage ); | |
| 111 | - $fees_covered = $amount - $base_amount; | |
| 138 | + if ( $cover_fees && $base_amount > 0 ) { | |
| 139 | + $fee_config = Payment_Helper::get_cover_fees_config( $form_id, 'offline' ); | |
| 140 | + | |
| 141 | + if ( ! $fee_config['enabled'] ) { | |
| 142 | + $cover_fees = false; | |
| 143 | + } | |
| 144 | + | |
| 145 | + if ( $cover_fees ) { | |
| 146 | + $fees_covered = Payment_Helper::calculate_fee( $base_amount, $fee_config['fee_percentage'], $fee_config['fee_fixed'] ); | |
| 147 | + } else { | |
| 148 | + $fees_covered = 0; | |
| 149 | + } | |
| 112 | 150 | } |
| 113 | 151 | |
| 114 | 152 | // Get or create donor. |
| 115 | 153 | $donor_id = 0; |
| @@ -139,8 +177,9 @@ | ||
| 139 | 177 | 'donor_phone' => $donor_phone, |
| 140 | 178 | 'is_anonymous' => $is_anonymous ? 1 : 0, |
| 141 | 179 | 'donation_type' => 'one-time', |
| 142 | 180 | 'donor_comment' => $donor_comment, |
| 181 | + 'form_id' => $form_id, | |
| 143 | 182 | 'ip_address' => Helper::get_client_ip(), |
| 144 | 183 | 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', |
| 145 | 184 | 'referer_url' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', |
| 146 | 185 | ] |
| @@ -151,34 +190,36 @@ | ||
| 151 | 190 | } |
| 152 | 191 | |
| 153 | 192 | // Note: Donation status will be updated by payment gateway webhooks or manual confirmation. |
| 154 | 193 | |
| 155 | - // Send donation confirmation email. | |
| 194 | + // This donation is created as pending/manual, so send the "processing" | |
| 195 | + // (donation received) email rather than the completed-confirmation | |
| 196 | + // email. The confirmation email is reserved for when payment is | |
| 197 | + // actually confirmed, matching the gateway flows. | |
| 156 | 198 | $donation_data = [ |
| 157 | - 'id' => $donation_id, | |
| 158 | - 'donor_name' => $donor_name, | |
| 159 | - 'donor_email' => $donor_email, | |
| 160 | - 'amount' => $base_amount, | |
| 161 | - 'fees_covered' => $fees_covered, | |
| 162 | - 'currency' => Payment_Helper::get_currency(), | |
| 199 | + 'id' => $donation_id, | |
| 200 | + 'donor_name' => $donor_name, | |
| 201 | + 'donor_email' => $donor_email, | |
| 202 | + 'amount' => $base_amount, | |
| 203 | + 'fees_covered' => $fees_covered, | |
| 204 | + 'currency' => Payment_Helper::get_currency(), | |
| 205 | + 'gateway' => 'manual', | |
| 206 | + 'donation_type' => 'one-time', | |
| 163 | 207 | ]; |
| 164 | 208 | |
| 165 | - Email_Handler::send_donation_confirmation( $donation_id, $campaign_id, $donation_data ); | |
| 209 | + Email_Handler::send_donation_processing( $donation_id, $campaign_id, $donation_data, $form_id ); | |
| 166 | 210 | |
| 167 | - // Get thank you message. | |
| 168 | - $thank_you_message = ''; | |
| 169 | - if ( ! $is_standalone && $campaign_id ) { | |
| 170 | - $thank_you_message = Helper::get_campaign_meta_value( $campaign_id, 'thank_you_message', '' ); | |
| 211 | + // Build the confirmation/thank-you HTML from the form's confirmation message. | |
| 212 | + $confirmation_html = Helper::render_confirmation_message( $donation_id ); | |
| 213 | + if ( '' === $confirmation_html ) { | |
| 214 | + $confirmation_html = esc_html__( 'Your generous contribution will make a real difference. A confirmation email has been sent to you.', 'suredonation' ); | |
| 171 | 215 | } |
| 172 | - if ( empty( $thank_you_message ) ) { | |
| 173 | - $thank_you_message = esc_html__( 'Your generous contribution will make a real difference. A confirmation email has been sent to you.', 'suredonation' ); | |
| 174 | - } | |
| 175 | 216 | |
| 176 | 217 | // Send success response. |
| 177 | 218 | wp_send_json_success( |
| 178 | 219 | [ |
| 179 | 220 | 'donation_id' => $donation_id, |
| 180 | - 'message' => wp_kses_post( Helper::get_string_value( $thank_you_message ) ), | |
| 221 | + 'message' => $confirmation_html, | |
| 181 | 222 | ] |
| 182 | 223 | ); |
| 183 | 224 | } |
| 184 | 225 | } |