| @@ -34,48 +34,11 @@ | ||
| 34 | 34 | */ |
| 35 | 35 | public function __construct() { |
| 36 | 36 | add_action( 'wp_ajax_suredonation_submit_donation', [ $this, 'handle_donation_submission' ] ); |
| 37 | 37 | add_action( 'wp_ajax_nopriv_suredonation_submit_donation', [ $this, 'handle_donation_submission' ] ); |
| 38 | - | |
| 39 | - // Runtime gateway configuration, read by the form script when it initialises. | |
| 40 | - add_action( 'wp_ajax_suredonation_gateway_config', [ $this, 'get_gateway_config' ] ); | |
| 41 | - add_action( 'wp_ajax_nopriv_suredonation_gateway_config', [ $this, 'get_gateway_config' ] ); | |
| 42 | 38 | } |
| 43 | 39 | |
| 44 | 40 | /** |
| 45 | - * Serve the gateway configuration for a donation form. | |
| 46 | - * | |
| 47 | - * Public read, fetched by the form script when it initialises so the Stripe | |
| 48 | - * key, PayPal SDK URL, payment mode and currency reflect the settings as | |
| 49 | - * they are now — not as they were when a page cache stored the form. It | |
| 50 | - * goes through admin-ajax, which page caches leave alone by default and | |
| 51 | - * which keeps working on sites that restrict the REST API for visitors. | |
| 52 | - * | |
| 53 | - * @return void | |
| 54 | - * @since 1.5.1 | |
| 55 | - */ | |
| 56 | - public function get_gateway_config() { | |
| 57 | - // Throttle abuse as every other public endpoint does. The ceiling is | |
| 58 | - // far above the default because this fires once per form page view, | |
| 59 | - // not per donor action, and many visitors can legitimately share one | |
| 60 | - // address (an office or campus NAT). When it trips, the scripts fall | |
| 61 | - // back to the rendered configuration rather than failing. | |
| 62 | - if ( ! Helper::check_rate_limit( 'gateway_config', 120 ) ) { | |
| 63 | - wp_send_json_error( [ 'message' => __( 'Too many requests. Please wait a moment and try again.', 'suredonation' ) ], 429 ); | |
| 64 | - } | |
| 65 | - | |
| 66 | - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public read of non-secret data; nothing changes state, and a nonce would be cached with the page it is meant to protect. | |
| 67 | - $form_id = isset( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : 0; | |
| 68 | - | |
| 69 | - // Freshness is the whole point of this response. admin-ajax already | |
| 70 | - // sends these, but an edge cache with a blanket rule would not care, | |
| 71 | - // so the guarantee is made explicit rather than inherited. | |
| 72 | - nocache_headers(); | |
| 73 | - | |
| 74 | - wp_send_json_success( Payment_Helper::get_frontend_gateway_config( $form_id ) ); | |
| 75 | - } | |
| 76 | - | |
| 77 | - /** | |
| 78 | 41 | * Handle donation form submission. |
| 79 | 42 | * |
| 80 | 43 | * @return void |
| 81 | 44 | * @since 0.0.1 |
| @@ -128,26 +91,19 @@ | ||
| 128 | 91 | } |
| 129 | 92 | } |
| 130 | 93 | |
| 131 | 94 | // Get form data. |
| 132 | - $amount = isset( $_POST['amount'] ) ? floatval( $_POST['amount'] ) : 0; | |
| 133 | - $cover_fees = isset( $_POST['cover_fees'] ) && 'true' === $_POST['cover_fees']; | |
| 134 | - // The anonymous flag is display-only: the donor's real name is stored as | |
| 135 | - // usual below and only public surfaces mask it. | |
| 136 | - $donor_name = sanitize_text_field( wp_unslash( $_POST['donor_name'] ?? '' ) ); | |
| 137 | - $donor_email = sanitize_email( wp_unslash( $_POST['donor_email'] ?? '' ) ); | |
| 95 | + $amount = isset( $_POST['amount'] ) ? floatval( $_POST['amount'] ) : 0; | |
| 96 | + $cover_fees = isset( $_POST['cover_fees'] ) && 'true' === $_POST['cover_fees']; | |
| 97 | + $is_anonymous = isset( $_POST['is_anonymous'] ) ? true : false; | |
| 98 | + $donor_name = $is_anonymous ? __( 'Anonymous', 'suredonation' ) : sanitize_text_field( wp_unslash( $_POST['donor_name'] ?? '' ) ); | |
| 99 | + $donor_email = sanitize_email( wp_unslash( $_POST['donor_email'] ?? '' ) ); | |
| 100 | + $donor_phone = sanitize_text_field( wp_unslash( $_POST['donor_phone'] ?? '' ) ); | |
| 101 | + $donor_comment = sanitize_textarea_field( wp_unslash( $_POST['donor_comment'] ?? '' ) ); | |
| 138 | 102 | |
| 139 | 103 | // Get form_id and block_id for amount validation. |
| 140 | - $form_id = isset( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; | |
| 141 | - $is_anonymous = Payment_Helper::get_submitted_is_anonymous( $form_id ); | |
| 142 | - // Derive the donor phone from the validated mapped field, not a separate | |
| 143 | - // unvalidated $_POST['donor_phone'] (see Payment_Helper::get_mapped_donor_phone). | |
| 144 | - $donor_phone = Payment_Helper::get_mapped_donor_phone( $form_id ); | |
| 145 | - // Likewise derive the comment from the form's Donor Comment field rather | |
| 146 | - // than an unvalidated $_POST['donor_comment'] (see | |
| 147 | - // Payment_Helper::get_mapped_donor_comment). | |
| 148 | - $donor_comment = Payment_Helper::get_mapped_donor_comment( $form_id ); | |
| 149 | - $block_id = isset( $_POST['block_id'] ) ? sanitize_text_field( wp_unslash( $_POST['block_id'] ) ) : ''; | |
| 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'] ) ) : ''; | |
| 150 | 106 | |
| 151 | 107 | // Validate required fields. |
| 152 | 108 | if ( $amount <= 0 ) { |
| 153 | 109 | wp_send_json_error( __( 'Invalid donation amount', 'suredonation' ) ); |
| @@ -160,23 +116,21 @@ | ||
| 160 | 116 | |
| 161 | 117 | // Validate field values + amount against block configuration. Pass the |
| 162 | 118 | // offline gateway so the Stripe-only minimum floor is not applied here. |
| 163 | 119 | $currency = Payment_Helper::get_currency(); |
| 164 | - $validation_result = Payment_Helper::validate_submission( Payment_Helper::get_submitted_fields(), $amount, $currency, $form_id, $block_id, 'offline', 'one-time' ); | |
| 120 | + $validation_result = Payment_Helper::validate_submission( Payment_Helper::get_submitted_fields(), $amount, $currency, $form_id, $block_id, 'offline' ); | |
| 165 | 121 | if ( ! $validation_result['valid'] ) { |
| 166 | 122 | wp_send_json_error( esc_html( $validation_result['message'] ) ); |
| 167 | 123 | } |
| 168 | 124 | |
| 169 | - // Name and email are required whether or not the donation is anonymous — | |
| 170 | - // the flag only masks the name on public surfaces, so there still has to | |
| 171 | - // be a real name to mask (matches the gateway handlers, which validate | |
| 172 | - // these through validate_submission() regardless of the flag). | |
| 173 | - if ( empty( $donor_name ) ) { | |
| 174 | - wp_send_json_error( __( 'Donor name is required', 'suredonation' ) ); | |
| 125 | + if ( ! $is_anonymous ) { | |
| 126 | + if ( empty( $donor_name ) ) { | |
| 127 | + wp_send_json_error( __( 'Donor name is required', 'suredonation' ) ); | |
| 128 | + } | |
| 129 | + if ( empty( $donor_email ) || ! is_email( $donor_email ) ) { | |
| 130 | + wp_send_json_error( __( 'Valid email address is required', 'suredonation' ) ); | |
| 131 | + } | |
| 175 | 132 | } |
| 176 | - if ( empty( $donor_email ) || ! is_email( $donor_email ) ) { | |
| 177 | - wp_send_json_error( __( 'Valid email address is required', 'suredonation' ) ); | |
| 178 | - } | |
| 179 | 133 | |
| 180 | 134 | // Server-side fee calculation — ignore client-supplied base_amount to prevent manipulation. |
| 181 | 135 | $base_amount = $amount; |
| 182 | 136 | $fees_covered = 0; |
| @@ -194,11 +148,13 @@ | ||
| 194 | 148 | $fees_covered = 0; |
| 195 | 149 | } |
| 196 | 150 | } |
| 197 | 151 | |
| 198 | - // Get or create donor. The email is validated as non-empty above, so | |
| 199 | - // there is no guard here — anonymous or not, this path always has one. | |
| 200 | - $donor_id = Donors::get_or_create( $donor_email, $donor_name, $donor_phone ); | |
| 152 | + // Get or create donor. | |
| 153 | + $donor_id = 0; | |
| 154 | + if ( ! empty( $donor_email ) ) { | |
| 155 | + $donor_id = Donors::get_or_create( $donor_email, $donor_name, $donor_phone ); | |
| 156 | + } | |
| 201 | 157 | |
| 202 | 158 | // Get payment mode. |
| 203 | 159 | $payment_mode = 'live'; |
| 204 | 160 | if ( class_exists( 'SureDonation\Inc\Payments\Payment_Helper' ) ) { |
| @@ -207,27 +163,26 @@ | ||
| 207 | 163 | |
| 208 | 164 | // Create donation in database. |
| 209 | 165 | $donation_id = Donations::add( |
| 210 | 166 | [ |
| 211 | - 'campaign_id' => $campaign_id, | |
| 212 | - 'donor_id' => $donor_id ? $donor_id : 0, | |
| 213 | - 'amount' => number_format( $base_amount, 2, '.', '' ), | |
| 214 | - 'fees_covered' => number_format( $fees_covered, 2, '.', '' ), | |
| 215 | - 'currency' => Payment_Helper::get_currency(), | |
| 216 | - 'gateway' => 'manual', | |
| 217 | - 'payment_status' => 'pending', | |
| 218 | - 'payment_mode' => $payment_mode, | |
| 219 | - 'donor_name' => $donor_name, | |
| 220 | - 'donor_email' => $donor_email, | |
| 221 | - 'donor_phone' => $donor_phone, | |
| 222 | - 'is_anonymous' => $is_anonymous ? 1 : 0, | |
| 223 | - 'donation_type' => 'one-time', | |
| 224 | - 'donor_comment' => $donor_comment, | |
| 225 | - 'donor_comment_status' => Donations::initial_comment_status( $donor_comment ), | |
| 226 | - 'form_id' => $form_id, | |
| 227 | - 'ip_address' => Helper::get_client_ip(), | |
| 228 | - 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', | |
| 229 | - 'referer_url' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', | |
| 167 | + 'campaign_id' => $campaign_id, | |
| 168 | + 'donor_id' => $donor_id ? $donor_id : 0, | |
| 169 | + 'amount' => number_format( $base_amount, 2, '.', '' ), | |
| 170 | + 'fees_covered' => number_format( $fees_covered, 2, '.', '' ), | |
| 171 | + 'currency' => Payment_Helper::get_currency(), | |
| 172 | + 'gateway' => 'manual', | |
| 173 | + 'payment_status' => 'pending', | |
| 174 | + 'payment_mode' => $payment_mode, | |
| 175 | + 'donor_name' => $donor_name, | |
| 176 | + 'donor_email' => $donor_email, | |
| 177 | + 'donor_phone' => $donor_phone, | |
| 178 | + 'is_anonymous' => $is_anonymous ? 1 : 0, | |
| 179 | + 'donation_type' => 'one-time', | |
| 180 | + 'donor_comment' => $donor_comment, | |
| 181 | + 'form_id' => $form_id, | |
| 182 | + 'ip_address' => Helper::get_client_ip(), | |
| 183 | + 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', | |
| 184 | + 'referer_url' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', | |
| 230 | 185 | ] |
| 231 | 186 | ); |
| 232 | 187 | |
| 233 | 188 | if ( ! $donation_id ) { |
| @@ -233,11 +188,8 @@ | ||
| 233 | 188 | if ( ! $donation_id ) { |
| 234 | 189 | wp_send_json_error( __( 'Failed to create donation', 'suredonation' ) ); |
| 235 | 190 | } |
| 236 | 191 | |
| 237 | - // Persist the submitted field values for the entry record. | |
| 238 | - Donations::set_submitted_fields( $donation_id, Payment_Helper::get_submitted_field_data() ); | |
| 239 | - | |
| 240 | 192 | // Note: Donation status will be updated by payment gateway webhooks or manual confirmation. |
| 241 | 193 | |
| 242 | 194 | // This donation is created as pending/manual, so send the "processing" |
| 243 | 195 | // (donation received) email rather than the completed-confirmation |
| @@ -250,13 +202,8 @@ | ||
| 250 | 202 | 'amount' => $base_amount, |
| 251 | 203 | 'fees_covered' => $fees_covered, |
| 252 | 204 | 'currency' => Payment_Helper::get_currency(), |
| 253 | 205 | 'gateway' => 'manual', |
| 254 | - // One-time regardless of the block's configured type, and intentionally | |
| 255 | - // unguarded: this handler has no remaining caller in src/, writes a | |
| 256 | - // record rather than moving money, and gating it on payment type would | |
| 257 | - // reject manual entries on recurring forms. Whether it should still be | |
| 258 | - // registered at all is the better question, tracked separately. | |
| 259 | 206 | 'donation_type' => 'one-time', |
| 260 | 207 | ]; |
| 261 | 208 | |
| 262 | 209 | Email_Handler::send_donation_processing( $donation_id, $campaign_id, $donation_data, $form_id ); |