PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.35
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.35
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / stripe / controllers / FrmStrpLiteLinkController.php

FrmStrpLiteLinkController.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.35, at stripe/controllers/FrmStrpLiteLinkController.php

602 lines 19.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 /**
7 * @since 6.5, introduced in v3.0 of the Stripe add on.
8 */
9 class FrmStrpLiteLinkController {
10
11 /**
12 * Process the form input and call handle_one_time_stripe_link_return_url if all of the required data is being submitted.
13 *
14 * @since 6.5, introduced in v3.0 of the Stripe add on.
15 *
16 * @return void
17 */
18 public static function handle_return_url() {
19 $intent_id = FrmAppHelper::simple_get( 'payment_intent' );
20 $client_secret = FrmAppHelper::simple_get( 'payment_intent_client_secret' );
21 $status = FrmAppHelper::simple_get( 'redirect_status' );
22
23 /**
24 * "succeeded" is used for cards and Link.
25 * "pending" happens for bank redirect types (iDEAL, Bancontact, SOFORT).
26 * No redirect status is valid as well (for Affirm payments).
27 */
28 if ( $intent_id && $client_secret && in_array( $status, array( 'pending', 'succeeded', 'failed', '' ), true ) ) {
29 self::handle_one_time_stripe_link_return_url( $intent_id, $client_secret );
30 die();
31 }
32
33 $setup_id = FrmAppHelper::simple_get( 'setup_intent' );
34 $client_secret = FrmAppHelper::simple_get( 'setup_intent_client_secret' );
35
36 if ( $setup_id && $client_secret && in_array( $status, array( 'succeeded', 'failed' ), true ) ) {
37 self::handle_recurring_stripe_link_return_url( $setup_id, $client_secret );
38 die();
39 }
40
41 wp_die();
42 }
43
44 /**
45 * Redirect the user after they return to the return URL based on the status of the payment intent information passed with the request.
46 * This will redirect and get handled by FrmStrpLiteAuth::maybe_show_message or possibly by redirected if there is a success URL set.
47 * If the setup intent is completed, the payment will be changed from pending as well.
48 *
49 * @since 6.5, introduced in v3.0 of the Stripe add on.
50 *
51 * @param string $intent_id
52 * @param string $client_secret
53 *
54 * @return void
55 */
56 private static function handle_one_time_stripe_link_return_url( $intent_id, $client_secret ) {
57 $redirect_helper = new FrmStrpLiteLinkRedirectHelper( $intent_id, $client_secret );
58 $frm_payment = new FrmTransLitePayment();
59 $payment = $frm_payment->get_one_by( $intent_id, 'receipt_id' );
60
61 if ( ! $payment ) {
62 $redirect_helper->handle_error( 'no_payment_record' );
63 die();
64 }
65
66 $intent = FrmStrpLiteAppHelper::call_stripe_helper_class( 'get_intent', $intent_id );
67
68 if ( ! is_object( $intent ) ) {
69 $redirect_helper->handle_error( 'intent_does_not_exist' );
70 die();
71 }
72
73 if ( $client_secret !== $intent->client_secret ) {
74 // Do an extra check against the client secret so the request isn't as easy to spoof.
75 $redirect_helper->handle_error( 'unable_to_verify' );
76 die();
77 }
78
79 $status = 'succeeded' === $intent->status ? 'complete' : 'authorized';
80 $new_payment_values = (array) $payment;
81 $new_payment_values['status'] = $status;
82
83 if ( 'complete' === $status ) {
84 $charge = reset( $intent->charges->data );
85 $new_payment_values['receipt_id'] = $charge->id;
86 }
87
88 $entry_id = $payment->item_id;
89 $entry = FrmEntry::getOne( $entry_id );
90
91 if ( ! $entry ) {
92 $redirect_helper->handle_error( 'no_entry_found' );
93 die();
94 }
95
96 $redirect_helper->set_entry_id( $entry->id );
97
98 $action = FrmStrpLiteActionsController::get_stripe_link_action( $entry->form_id );
99
100 if ( ! $action ) {
101 $redirect_helper->handle_error( 'no_stripe_link_action' );
102 die();
103 }
104
105 $currency = FrmTransLiteAppHelper::get_action_setting( 'currency', array( 'payment' => $payment ) );
106 $currency = FrmCurrencyHelper::get_currency( $currency );
107 $actual_amount = intval( $intent->amount );
108 $expected_amount = round( floatval( $payment->amount ), 2 );
109
110 if ( 0 !== $currency['decimals'] ) {
111 // Convert 10 to 1000 for example for Stripe.
112 // But avoid for this a 0-decimal currency like JPY.
113 $expected_amount *= 100;
114 }
115
116 $expected_amount = intval( round( $expected_amount ) );
117
118 if ( $expected_amount !== $actual_amount ) {
119 $redirect_helper->handle_error( 'amount_mismatch' );
120 die();
121 }
122
123 if ( 'succeeded' !== $intent->status ) {
124 if ( 'processing' === $intent->status ) {
125 FrmTransLitePaymentsController::change_payment_status( $payment, 'processing' );
126 $redirect_helper->handle_success( $entry, '' );
127 die();
128 }
129
130 FrmTransLitePaymentsController::change_payment_status( $payment, 'failed' );
131
132 $payment_failed = 'requires_payment_method' === $intent->status && 'payment_intent_authentication_failure' === $intent->last_payment_error->code;
133 $error_type = $payment_failed ? 'payment_failed' : 'did_not_complete';
134
135 $redirect_helper->handle_error( $error_type );
136 die();
137 }
138
139 $status = 'succeeded' === $intent->status ? 'complete' : 'authorized';
140 $new_payment_values = compact( 'status' );
141
142 if ( 'complete' === $status ) {
143 $charge = reset( $intent->charges->data );
144 $new_payment_values['receipt_id'] = $charge->id;
145 }
146
147 self::maybe_update_intent( $intent, $action, $entry );
148
149 // A webhook event may have already updated this payment, so check the status again before running triggers.
150 $needs_triggers = $status !== $payment->status && self::payment_status_still_needs_to_update( $payment->id, $status );
151 $updated = $frm_payment->update( $payment->id, $new_payment_values );
152
153 if ( $needs_triggers && $updated ) {
154 FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) );
155 }
156
157 $redirect_helper->handle_success( $entry, isset( $charge ) ? $charge->id : '' );
158 die();
159 }
160
161 /**
162 * Check that the payment status has not been updated by another request already.
163 * This is to avoid running the payment actions twice.
164 *
165 * @since 6.35
166 *
167 * @param int $payment_id The id of the payment to check.
168 * @param string $status The status the payment is about to be updated to.
169 *
170 * @return bool
171 */
172 private static function payment_status_still_needs_to_update( $payment_id, $status ) {
173 $frm_payment = new FrmTransLitePayment();
174 $payment = $frm_payment->get_one( $payment_id );
175
176 return $payment && $payment->status !== $status;
177 }
178
179 /**
180 * Try to add the description to a Stripe link payment after it was confirmed.
181 *
182 * @param object $intent
183 * @param stdClass|WP_Post $action
184 * @param stdClass $entry
185 *
186 * @return void
187 */
188 private static function maybe_update_intent( $intent, $action, $entry ) {
189 if ( empty( $action->post_content['description'] ) ) {
190 return;
191 }
192
193 $shortcode_atts = array(
194 'entry' => $entry,
195 'form' => $entry->form_id,
196 'value' => $action->post_content['description'],
197 );
198 $new_values = array( 'description' => FrmTransLiteAppHelper::process_shortcodes( $shortcode_atts ) );
199 FrmStrpLiteAppHelper::call_stripe_helper_class( 'update_intent', $intent->id, $new_values );
200 }
201
202 /**
203 * Handle return URL for a stripe link recurring payment which uses setup intents.
204 * This will redirect and get handled by FrmStrpLiteAuth::maybe_show_message or possibly by redirected if there is a success URL set.
205 * If the setup intent is completed, the subscription will be created as well.
206 *
207 * @since 6.5, introduced in v3.0 of the Stripe add on.
208 *
209 * @param string $setup_id
210 * @param string $client_secret
211 *
212 * @return void
213 */
214 private static function handle_recurring_stripe_link_return_url( $setup_id, $client_secret ) {
215 $redirect_helper = new FrmStrpLiteLinkRedirectHelper( $setup_id, $client_secret );
216 $frm_payment = new FrmTransLitePayment();
217 $payment = $frm_payment->get_one_by( $setup_id, 'receipt_id' );
218
219 if ( ! is_object( $payment ) ) {
220 $redirect_helper->handle_error( 'no_payment_record' );
221 die();
222 }
223
224 // Verify the setup intent.
225 $setup_intent = FrmStrpLiteAppHelper::call_stripe_helper_class( 'get_setup_intent', $setup_id );
226
227 if ( ! is_object( $setup_intent ) ) {
228 $redirect_helper->handle_error( 'intent_does_not_exist' );
229 die();
230 }
231
232 // Verify the client secret.
233 if ( $setup_intent->client_secret !== $client_secret ) {
234 $redirect_helper->handle_error( 'unable_to_verify' );
235 die();
236 }
237
238 // Verify the entry.
239 $entry = FrmEntry::getOne( $payment->item_id );
240
241 if ( ! is_object( $entry ) ) {
242 $redirect_helper->handle_error( 'no_entry_found' );
243 die();
244 }
245
246 $redirect_helper->set_entry_id( $entry->id );
247
248 // Verify it's an action with Stripe link enabled.
249 $action = FrmStrpLiteActionsController::get_stripe_link_action( $entry->form_id );
250
251 if ( ! is_object( $action ) ) {
252 $redirect_helper->handle_error( 'no_stripe_link_action' );
253 die();
254 }
255
256 $customer_id = $setup_intent->customer;
257 $payment_method_id = self::get_link_payment_method( $setup_intent );
258
259 if ( ! $payment_method_id ) {
260 FrmTransLitePaymentsController::change_payment_status( $payment, 'failed' );
261 $redirect_helper->handle_error( 'did_not_complete' );
262 die();
263 }
264
265 $amount = $payment->amount * 100;
266 $new_charge = array(
267 'customer' => $customer_id,
268 'default_payment_method' => $payment_method_id,
269 'plan' => FrmStrpLiteSubscriptionHelper::get_plan_from_atts(
270 array(
271 'action' => $action,
272 'amount' => $amount,
273 )
274 ),
275 'expand' => array( 'latest_invoice.charge' ),
276 );
277
278 if ( ! FrmStrpLitePaymentTypeHandler::should_use_automatic_payment_methods( $action ) ) {
279 $new_charge['payment_settings'] = array(
280 'payment_method_types' => FrmStrpLitePaymentTypeHandler::get_payment_method_types( $action ),
281 );
282 }
283
284 $atts = array(
285 'action' => $action,
286 'entry' => $entry,
287 );
288
289 $trial_end = FrmStrpLiteActionsController::get_trial_end_time( $atts );
290
291 if ( $trial_end ) {
292 $new_charge['trial_end'] = $trial_end;
293 }
294
295 $subscription = FrmStrpLiteAppHelper::call_stripe_helper_class( 'create_subscription', $new_charge );
296 $subscription = FrmStrpLiteSubscriptionHelper::maybe_create_missing_plan_and_create_subscription( $subscription, $new_charge, $action, $amount );
297
298 if ( ! is_object( $subscription ) ) {
299 $redirect_helper->handle_error( 'create_subscription_failed' );
300 die();
301 }
302
303 if ( 'succeeded' !== $setup_intent->status ) {
304 FrmTransLitePaymentsController::change_payment_status( $payment, 'failed' );
305 $redirect_helper->handle_error( 'payment_failed' );
306 die();
307 }
308
309 $customer_has_been_charged = ! empty( $subscription->latest_invoice->charge );
310 $atts['charge'] = FrmStrpLiteSubscriptionHelper::prepare_charge_object_for_subscription( $subscription, $amount );
311 $new_payment_values = (array) $payment;
312
313 if ( $customer_has_been_charged ) {
314 $charge = $subscription->latest_invoice->charge;
315 $new_payment_values['receipt_id'] = $charge->id;
316
317 if ( 'failed' === $charge->status ) {
318 FrmTransLitePaymentsController::change_payment_status( $payment, 'failed' );
319
320 $new_payment_values['receipt_id'] = $charge->id;
321 $frm_payment->update( $payment->id, $new_payment_values );
322
323 $redirect_helper->handle_error( 'payment_failed', $charge->id );
324 }
325
326 $new_payment_values['status'] = 'pending' === $charge->status ? 'processing' : 'complete';
327
328 $new_payment_values['expire_date'] = '0000-00-00';
329
330 foreach ( $subscription->latest_invoice->lines->data as $line ) {
331 $new_payment_values['expire_date'] = gmdate( 'Y-m-d', $line->period->end );
332 }
333 } elseif ( $trial_end ) {
334 $new_payment_values['amount'] = 0;
335 $new_payment_values['begin_date'] = gmdate( 'Y-m-d', time() );
336 $new_payment_values['expire_date'] = gmdate( 'Y-m-d', $trial_end );
337 }//end if
338
339 $new_payment_values['sub_id'] = FrmStrpLiteSubscriptionHelper::create_new_subscription( $atts );
340
341 $frm_payment->update( $payment->id, $new_payment_values );
342
343 if ( $customer_has_been_charged ) {
344 // Set the payment to complete.
345 $status = 'complete';
346 FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) );
347
348 // Update the next billing date.
349 $next_bill_date = gmdate( 'Y-m-d' );
350
351 foreach ( $subscription->latest_invoice->lines->data as $line ) {
352 $next_bill_date = gmdate( 'Y-m-d', $line->period->end );
353 }
354
355 $frm_sub = new FrmTransLiteSubscription();
356 $frm_sub->update(
357 $new_payment_values['sub_id'],
358 array( 'next_bill_date' => $next_bill_date )
359 );
360 }
361
362 $redirect_helper->handle_success( $entry, isset( $charge ) ? $charge->id : '' );
363 die();
364 }
365
366 /**
367 * Check for a link payment method associated with a customer for a Stripe link recurring payment/subscription.
368 * This gets created on Stripe's end after confirmSetup is called client-side in the Stripe add on.
369 * This is required in order to associate a payment method with the subscription that gets created.
370 *
371 * @since 6.5, introduced in v3.0 of the Stripe add on.
372 *
373 * @param object $setup_intent
374 *
375 * @return false|string
376 */
377 private static function get_link_payment_method( $setup_intent ) {
378 if ( is_object( $setup_intent->latest_attempt ) && ! empty( $setup_intent->latest_attempt->payment_method_details ) ) {
379 $payment_method_details = $setup_intent->latest_attempt->payment_method_details;
380
381 foreach ( array( 'ideal', 'sofort', 'bancontact' ) as $payment_method_type ) {
382 if ( ! empty( $payment_method_details->$payment_method_type ) ) {
383 return $payment_method_details->$payment_method_type->generated_sepa_debit;
384 }
385 }
386 }
387
388 if ( ! empty( $setup_intent->payment_method ) ) {
389 return $setup_intent->payment_method;
390 }
391
392 return false;
393 }
394
395 /**
396 * Create a pending Stripe link payment on entry creation.
397 * Stripe link uses confirmPayment with a return URL which gets called after this.
398 * The payment is then updated from pending status later in another request, either when the return URL is loaded or with a webhook.
399 *
400 * @since 6.5, introduced in v3.0 of the Stripe add on.
401 *
402 * @param array $atts {
403 * The details needs to create a payment.
404 *
405 * @type stdClass $form
406 * @type stdClass $entry
407 * @type WP_Post $action
408 * @type string $amount
409 * @type object $customer
410 * }
411 *
412 * @return bool True on success, false on failure.
413 */
414 public static function create_pending_stripe_link_payment( $atts ) {
415 if ( empty( $atts['form'] ) || empty( $atts['entry'] ) || empty( $atts['action'] ) || ! isset( $atts['amount'] ) || empty( $atts['customer'] ) ) {
416 return false;
417 }
418
419 $form = $atts['form'];
420 $action = $atts['action'];
421 $intent_id = self::verify_intent( $form->id, $action );
422
423 if ( ! $intent_id ) {
424 return false;
425 }
426
427 $is_setup_intent = str_starts_with( $intent_id, 'seti_' );
428 $entry = $atts['entry'];
429 $amount = $atts['amount'];
430 $customer = $atts['customer'];
431
432 if ( ! $is_setup_intent ) {
433 // Update the amount and set the customer before confirming the payment.
434 $updated = FrmStrpLiteAppHelper::call_stripe_helper_class(
435 'update_intent',
436 $intent_id,
437 array(
438 'amount' => $amount,
439 'customer' => $customer->id,
440 )
441 );
442
443 if ( ! $updated ) {
444 return false;
445 }
446 }
447
448 self::add_temporary_referer_meta( (int) $entry->id );
449
450 $frm_payment = new FrmTransLitePayment();
451 $payment_id = $frm_payment->create(
452 array(
453 'paysys' => 'stripe',
454 'amount' => FrmTransLiteAppHelper::get_formatted_amount_for_currency( $amount, $action ),
455 'status' => 'pending',
456 'item_id' => $entry->id,
457 'action_id' => $action->ID,
458 'receipt_id' => $intent_id,
459 'sub_id' => '',
460 'test' => 'test' === FrmStrpLiteAppHelper::active_mode() ? 1 : 0,
461 )
462 );
463
464 return (bool) $payment_id;
465 }
466
467 /**
468 * Verify a payment intent or setup intent client secret is in the POST data and is valid.
469 *
470 * @since 6.5, introduced in v3.0 of the Stripe add on.
471 *
472 * @param int|string $form_id
473 * @param WP_Post $action
474 *
475 * @return false|string String intent id on success, False if intent is missing or cannot be verified.
476 */
477 private static function verify_intent( $form_id, $action ) {
478 $client_secrets = FrmAppHelper::get_post_param( 'frmintent' . $form_id, array(), 'sanitize_text_field' );
479
480 if ( ! $client_secrets ) {
481 return false;
482 }
483
484 $client_secret = reset( $client_secrets );
485 list( $prefix, $intent_id ) = explode( '_', $client_secret );
486 $intent_id = $prefix . '_' . $intent_id;
487 $is_setup_intent = str_starts_with( $intent_id, 'seti_' );
488 $function_name = $is_setup_intent ? 'get_setup_intent' : 'get_intent';
489 $intent = FrmStrpLiteAppHelper::call_stripe_helper_class( $function_name, $intent_id );
490
491 if ( ! $intent || $intent->client_secret !== $client_secret || ! self::intent_matches_form_action( $intent, $action ) ) {
492 return false;
493 }
494
495 if ( isset( $intent->charges ) && is_object( $intent->charges ) && ! empty( $intent->charges->data ) ) {
496 // The intent should not have any charges yet.
497 // If it does, the intent is invalid.
498 return false;
499 }
500
501 $frm_payment = new FrmTransLitePayment();
502 $payment = $frm_payment->get_one_by( $intent_id, 'receipt_id' );
503
504 if ( $payment ) {
505 // A duplicate payment should not exist.
506 return false;
507 }
508
509 return $intent_id;
510 }
511
512 /**
513 * Check if an intent matches a form action.
514 *
515 * @since 6.29
516 *
517 * @param object $intent
518 * @param WP_Post $action
519 *
520 * @return bool
521 */
522 private static function intent_matches_form_action( $intent, $action ) {
523 if ( ! isset( $intent->metadata ) || ! is_object( $intent->metadata ) || empty( $intent->metadata->action ) ) {
524 // Avoid false positive if the intent is missing metadata.
525 return true;
526 }
527
528 return (int) $intent->metadata->action === $action->ID;
529 }
530
531 /**
532 * Set the referer URL as field ID 0 in entry meta.
533 * This is required for iDEAL, sofort, and other payment methods that include an additional redirect step.
534 * It is used for the redirect in FrmStrpLinkRedirectHelper.
535 * It is deleted after the redirect happens.
536 *
537 * @param int $entry_id
538 *
539 * @return void
540 */
541 private static function add_temporary_referer_meta( $entry_id ) {
542 $referer = FrmAppHelper::get_server_value( 'HTTP_REFERER' );
543 $query_args_to_strip_from_referer = array(
544 'frm_link_error',
545 'payment_intent',
546 'payment_intent_client_secret',
547 'setup_intent',
548 'setup_intent_client_secret',
549 );
550
551 foreach ( $query_args_to_strip_from_referer as $arg ) {
552 $referer = remove_query_arg( $arg, $referer );
553 }
554
555 $meta_value = json_encode( compact( 'referer' ) );
556 FrmEntryMeta::add_entry_meta( $entry_id, 0, '', $meta_value );
557 }
558
559 /**
560 * Flag a form with the frm_stripe_link_form class so it is identifiable when initializing in JavaScript.
561 *
562 * @since 6.5, introduced in v3.0 of the Stripe add on.
563 *
564 * @param stdClass $form
565 *
566 * @return void
567 */
568 public static function add_form_classes( $form ) {
569 if ( false === FrmStrpLiteActionsController::get_stripe_link_action( $form->id ) ) {
570 return;
571 }
572
573 echo ' frm_stripe_link_form ';
574 }
575
576 /**
577 * We need to force AJAX submit with Stripe link to avoid the page reloading before confirmPayment is called after entry creation.
578 *
579 * @since 6.5, introduced in v3.0 of the Stripe add on.
580 *
581 * @param mixed $form
582 *
583 * @return mixed
584 */
585 public static function force_ajax_submit_for_stripe_link( $form ) {
586 if ( ! is_object( $form ) ) {
587 return $form;
588 }
589
590 if ( ! empty( $form->options['ajax_submit'] ) ) {
591 // AJAX is already on so we can exit early.
592 return $form;
593 }
594
595 if ( false !== FrmStrpLiteActionsController::get_stripe_link_action( $form->id ) ) {
596 $form->options['ajax_submit'] = '1';
597 }
598
599 return $form;
600 }
601 }
602