| @@ -34,11 +34,48 @@ | ||
| 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' ] ); | |
| 38 | 42 | } |
| 39 | 43 | |
| 40 | 44 | /** |
| 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 | + /** | |
| 41 | 78 | * Handle donation form submission. |
| 42 | 79 | * |
| 43 | 80 | * @return void |
| 44 | 81 | * @since 0.0.1 |
| @@ -95,11 +132,10 @@ | ||
| 95 | 132 | $amount = isset( $_POST['amount'] ) ? floatval( $_POST['amount'] ) : 0; |
| 96 | 133 | $cover_fees = isset( $_POST['cover_fees'] ) && 'true' === $_POST['cover_fees']; |
| 97 | 134 | // The anonymous flag is display-only: the donor's real name is stored as |
| 98 | 135 | // usual below and only public surfaces mask it. |
| 99 | - $donor_name = sanitize_text_field( wp_unslash( $_POST['donor_name'] ?? '' ) ); | |
| 100 | - $donor_email = sanitize_email( wp_unslash( $_POST['donor_email'] ?? '' ) ); | |
| 101 | - $donor_comment = sanitize_textarea_field( wp_unslash( $_POST['donor_comment'] ?? '' ) ); | |
| 136 | + $donor_name = sanitize_text_field( wp_unslash( $_POST['donor_name'] ?? '' ) ); | |
| 137 | + $donor_email = sanitize_email( wp_unslash( $_POST['donor_email'] ?? '' ) ); | |
| 102 | 138 | |
| 103 | 139 | // Get form_id and block_id for amount validation. |
| 104 | 140 | $form_id = isset( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; |
| 105 | 141 | $is_anonymous = Payment_Helper::get_submitted_is_anonymous( $form_id ); |
| @@ -105,9 +141,13 @@ | ||
| 105 | 141 | $is_anonymous = Payment_Helper::get_submitted_is_anonymous( $form_id ); |
| 106 | 142 | // Derive the donor phone from the validated mapped field, not a separate |
| 107 | 143 | // unvalidated $_POST['donor_phone'] (see Payment_Helper::get_mapped_donor_phone). |
| 108 | 144 | $donor_phone = Payment_Helper::get_mapped_donor_phone( $form_id ); |
| 109 | - $block_id = isset( $_POST['block_id'] ) ? sanitize_text_field( wp_unslash( $_POST['block_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'] ) ) : ''; | |
| 110 | 150 | |
| 111 | 151 | // Validate required fields. |
| 112 | 152 | if ( $amount <= 0 ) { |
| 113 | 153 | wp_send_json_error( __( 'Invalid donation amount', 'suredonation' ) ); |
| @@ -120,9 +160,9 @@ | ||
| 120 | 160 | |
| 121 | 161 | // Validate field values + amount against block configuration. Pass the |
| 122 | 162 | // offline gateway so the Stripe-only minimum floor is not applied here. |
| 123 | 163 | $currency = Payment_Helper::get_currency(); |
| 124 | - $validation_result = Payment_Helper::validate_submission( Payment_Helper::get_submitted_fields(), $amount, $currency, $form_id, $block_id, 'offline' ); | |
| 164 | + $validation_result = Payment_Helper::validate_submission( Payment_Helper::get_submitted_fields(), $amount, $currency, $form_id, $block_id, 'offline', 'one-time' ); | |
| 125 | 165 | if ( ! $validation_result['valid'] ) { |
| 126 | 166 | wp_send_json_error( esc_html( $validation_result['message'] ) ); |
| 127 | 167 | } |
| 128 | 168 | |
| @@ -167,26 +207,27 @@ | ||
| 167 | 207 | |
| 168 | 208 | // Create donation in database. |
| 169 | 209 | $donation_id = Donations::add( |
| 170 | 210 | [ |
| 171 | - 'campaign_id' => $campaign_id, | |
| 172 | - 'donor_id' => $donor_id ? $donor_id : 0, | |
| 173 | - 'amount' => number_format( $base_amount, 2, '.', '' ), | |
| 174 | - 'fees_covered' => number_format( $fees_covered, 2, '.', '' ), | |
| 175 | - 'currency' => Payment_Helper::get_currency(), | |
| 176 | - 'gateway' => 'manual', | |
| 177 | - 'payment_status' => 'pending', | |
| 178 | - 'payment_mode' => $payment_mode, | |
| 179 | - 'donor_name' => $donor_name, | |
| 180 | - 'donor_email' => $donor_email, | |
| 181 | - 'donor_phone' => $donor_phone, | |
| 182 | - 'is_anonymous' => $is_anonymous ? 1 : 0, | |
| 183 | - 'donation_type' => 'one-time', | |
| 184 | - 'donor_comment' => $donor_comment, | |
| 185 | - 'form_id' => $form_id, | |
| 186 | - 'ip_address' => Helper::get_client_ip(), | |
| 187 | - 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', | |
| 188 | - 'referer_url' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', | |
| 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'] ) ) : '', | |
| 189 | 230 | ] |
| 190 | 231 | ); |
| 191 | 232 | |
| 192 | 233 | if ( ! $donation_id ) { |
| @@ -209,8 +250,13 @@ | ||
| 209 | 250 | 'amount' => $base_amount, |
| 210 | 251 | 'fees_covered' => $fees_covered, |
| 211 | 252 | 'currency' => Payment_Helper::get_currency(), |
| 212 | 253 | '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. | |
| 213 | 259 | 'donation_type' => 'one-time', |
| 214 | 260 | ]; |
| 215 | 261 | |
| 216 | 262 | Email_Handler::send_donation_processing( $donation_id, $campaign_id, $donation_data, $form_id ); |