PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.1.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.1.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
suredonation / inc / ajax / donation-handler.php

donation-handler.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.1.1, at inc/ajax/donation-handler.php

231 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AJAX Donation Handler
4 *
5 * @package SureDonation
6 */
7
8 namespace SureDonation\Inc\Ajax;
9
10 use SureDonation\Inc\Database\Tables\Donations;
11 use SureDonation\Inc\Database\Tables\Donors;
12 use SureDonation\Inc\Emails\Email_Handler;
13 use SureDonation\Inc\Helper;
14 use SureDonation\Inc\Payments\Payment_Helper;
15 use SureDonation\Inc\Traits\Get_Instance;
16
17 // Exit if accessed directly.
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /**
23 * Donation_Handler class.
24 *
25 * @since 0.0.1
26 */
27 class Donation_Handler {
28 use Get_Instance;
29
30 /**
31 * Constructor.
32 *
33 * @since 0.0.1
34 */
35 public function __construct() {
36 add_action( 'wp_ajax_suredonation_submit_donation', [ $this, 'handle_donation_submission' ] );
37 add_action( 'wp_ajax_nopriv_suredonation_submit_donation', [ $this, 'handle_donation_submission' ] );
38 }
39
40 /**
41 * Handle donation form submission.
42 *
43 * @return void
44 * @since 0.0.1
45 */
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
52 // First check if nonce exists before accessing any other POST data.
53 if ( ! isset( $_POST['suredonation_nonce'] ) ) {
54 wp_send_json_error( __( 'Security check failed', 'suredonation' ) );
55 }
56
57 // Sanitize nonce value.
58 $nonce = sanitize_text_field( wp_unslash( $_POST['suredonation_nonce'] ) );
59
60 // Now get values needed to determine nonce action.
61 $is_standalone = isset( $_POST['is_standalone'] ) && '1' === $_POST['is_standalone'];
62 $campaign_id = isset( $_POST['campaign_id'] ) ? absint( $_POST['campaign_id'] ) : 0;
63
64 // Standalone forms must not have a campaign — prevent bypass of campaign validation.
65 if ( $is_standalone ) {
66 $campaign_id = 0;
67 }
68
69 // Verify nonce - different nonce for standalone vs campaign-linked forms.
70 $nonce_action = Helper::get_donation_nonce_action( $campaign_id );
71
72 if ( ! wp_verify_nonce( $nonce, $nonce_action ) ) {
73 wp_send_json_error( __( 'Security check failed', 'suredonation' ) );
74 }
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
81 // Validate campaign only if not standalone.
82 $campaign = null;
83 if ( ! $is_standalone ) {
84 if ( ! $campaign_id ) {
85 wp_send_json_error( __( 'Invalid campaign', 'suredonation' ) );
86 }
87
88 $campaign = get_post( $campaign_id );
89 if ( ! $campaign || SUREDONATION_POST_TYPE !== $campaign->post_type ) {
90 wp_send_json_error( __( 'Invalid campaign', 'suredonation' ) );
91 }
92 }
93
94 // Get form data.
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_comment = sanitize_textarea_field( wp_unslash( $_POST['donor_comment'] ?? '' ) );
101
102 // Get form_id and block_id for amount validation.
103 $form_id = isset( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0;
104 // Derive the donor phone from the validated mapped field, not a separate
105 // unvalidated $_POST['donor_phone'] (see Payment_Helper::get_mapped_donor_phone).
106 $donor_phone = Payment_Helper::get_mapped_donor_phone( $form_id );
107 $block_id = isset( $_POST['block_id'] ) ? sanitize_text_field( wp_unslash( $_POST['block_id'] ) ) : '';
108
109 // Validate required fields.
110 if ( $amount <= 0 ) {
111 wp_send_json_error( __( 'Invalid donation amount', 'suredonation' ) );
112 }
113
114 // Require form_id and block_id for amount validation — reject if missing to prevent bypass.
115 if ( empty( $form_id ) || empty( $block_id ) ) {
116 wp_send_json_error( __( 'Invalid form configuration.', 'suredonation' ) );
117 }
118
119 // Validate field values + amount against block configuration. Pass the
120 // offline gateway so the Stripe-only minimum floor is not applied here.
121 $currency = Payment_Helper::get_currency();
122 $validation_result = Payment_Helper::validate_submission( Payment_Helper::get_submitted_fields(), $amount, $currency, $form_id, $block_id, 'offline' );
123 if ( ! $validation_result['valid'] ) {
124 wp_send_json_error( esc_html( $validation_result['message'] ) );
125 }
126
127 if ( ! $is_anonymous ) {
128 if ( empty( $donor_name ) ) {
129 wp_send_json_error( __( 'Donor name is required', 'suredonation' ) );
130 }
131 if ( empty( $donor_email ) || ! is_email( $donor_email ) ) {
132 wp_send_json_error( __( 'Valid email address is required', 'suredonation' ) );
133 }
134 }
135
136 // Server-side fee calculation — ignore client-supplied base_amount to prevent manipulation.
137 $base_amount = $amount;
138 $fees_covered = 0;
139
140 if ( $cover_fees && $base_amount > 0 ) {
141 $fee_config = Payment_Helper::get_cover_fees_config( $form_id, 'offline' );
142
143 if ( ! $fee_config['enabled'] ) {
144 $cover_fees = false;
145 }
146
147 if ( $cover_fees ) {
148 $fees_covered = Payment_Helper::calculate_fee( $base_amount, $fee_config['fee_percentage'], $fee_config['fee_fixed'] );
149 } else {
150 $fees_covered = 0;
151 }
152 }
153
154 // Get or create donor.
155 $donor_id = 0;
156 if ( ! empty( $donor_email ) ) {
157 $donor_id = Donors::get_or_create( $donor_email, $donor_name, $donor_phone );
158 }
159
160 // Get payment mode.
161 $payment_mode = 'live';
162 if ( class_exists( 'SureDonation\Inc\Payments\Payment_Helper' ) ) {
163 $payment_mode = Payment_Helper::get_payment_mode();
164 }
165
166 // Create donation in database.
167 $donation_id = Donations::add(
168 [
169 'campaign_id' => $campaign_id,
170 'donor_id' => $donor_id ? $donor_id : 0,
171 'amount' => number_format( $base_amount, 2, '.', '' ),
172 'fees_covered' => number_format( $fees_covered, 2, '.', '' ),
173 'currency' => Payment_Helper::get_currency(),
174 'gateway' => 'manual',
175 'payment_status' => 'pending',
176 'payment_mode' => $payment_mode,
177 'donor_name' => $donor_name,
178 'donor_email' => $donor_email,
179 'donor_phone' => $donor_phone,
180 'is_anonymous' => $is_anonymous ? 1 : 0,
181 'donation_type' => 'one-time',
182 'donor_comment' => $donor_comment,
183 'form_id' => $form_id,
184 'ip_address' => Helper::get_client_ip(),
185 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
186 'referer_url' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '',
187 ]
188 );
189
190 if ( ! $donation_id ) {
191 wp_send_json_error( __( 'Failed to create donation', 'suredonation' ) );
192 }
193
194 // Persist the submitted field values for the entry record.
195 Donations::set_submitted_fields( $donation_id, Payment_Helper::get_submitted_field_data() );
196
197 // Note: Donation status will be updated by payment gateway webhooks or manual confirmation.
198
199 // This donation is created as pending/manual, so send the "processing"
200 // (donation received) email rather than the completed-confirmation
201 // email. The confirmation email is reserved for when payment is
202 // actually confirmed, matching the gateway flows.
203 $donation_data = [
204 'id' => $donation_id,
205 'donor_name' => $donor_name,
206 'donor_email' => $donor_email,
207 'amount' => $base_amount,
208 'fees_covered' => $fees_covered,
209 'currency' => Payment_Helper::get_currency(),
210 'gateway' => 'manual',
211 'donation_type' => 'one-time',
212 ];
213
214 Email_Handler::send_donation_processing( $donation_id, $campaign_id, $donation_data, $form_id );
215
216 // Build the confirmation/thank-you HTML from the form's confirmation message.
217 $confirmation_html = Helper::render_confirmation_message( $donation_id );
218 if ( '' === $confirmation_html ) {
219 $confirmation_html = esc_html__( 'Your generous contribution will make a real difference. A confirmation email has been sent to you.', 'suredonation' );
220 }
221
222 // Send success response.
223 wp_send_json_success(
224 [
225 'donation_id' => $donation_id,
226 'message' => $confirmation_html,
227 ]
228 );
229 }
230 }
231