PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.32.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.32.1
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.32.1, at stripe/controllers/FrmStrpLiteLinkController.php

579 lines 18.5 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 $frm_payment->update( $payment->id, $new_payment_values );
150 FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) );
151
152 $redirect_helper->handle_success( $entry, isset( $charge ) ? $charge->id : '' );
153 die();
154 }
155
156 /**
157 * Try to add the description to a Stripe link payment after it was confirmed.
158 *
159 * @param object $intent
160 * @param stdClass|WP_Post $action
161 * @param stdClass $entry
162 *
163 * @return void
164 */
165 private static function maybe_update_intent( $intent, $action, $entry ) {
166 if ( empty( $action->post_content['description'] ) ) {
167 return;
168 }
169
170 $shortcode_atts = array(
171 'entry' => $entry,
172 'form' => $entry->form_id,
173 'value' => $action->post_content['description'],
174 );
175 $new_values = array( 'description' => FrmTransLiteAppHelper::process_shortcodes( $shortcode_atts ) );
176 FrmStrpLiteAppHelper::call_stripe_helper_class( 'update_intent', $intent->id, $new_values );
177 }
178
179 /**
180 * Handle return URL for a stripe link recurring payment which uses setup intents.
181 * This will redirect and get handled by FrmStrpLiteAuth::maybe_show_message or possibly by redirected if there is a success URL set.
182 * If the setup intent is completed, the subscription will be created as well.
183 *
184 * @since 6.5, introduced in v3.0 of the Stripe add on.
185 *
186 * @param string $setup_id
187 * @param string $client_secret
188 *
189 * @return void
190 */
191 private static function handle_recurring_stripe_link_return_url( $setup_id, $client_secret ) {
192 $redirect_helper = new FrmStrpLiteLinkRedirectHelper( $setup_id, $client_secret );
193 $frm_payment = new FrmTransLitePayment();
194 $payment = $frm_payment->get_one_by( $setup_id, 'receipt_id' );
195
196 if ( ! is_object( $payment ) ) {
197 $redirect_helper->handle_error( 'no_payment_record' );
198 die();
199 }
200
201 // Verify the setup intent.
202 $setup_intent = FrmStrpLiteAppHelper::call_stripe_helper_class( 'get_setup_intent', $setup_id );
203
204 if ( ! is_object( $setup_intent ) ) {
205 $redirect_helper->handle_error( 'intent_does_not_exist' );
206 die();
207 }
208
209 // Verify the client secret.
210 if ( $setup_intent->client_secret !== $client_secret ) {
211 $redirect_helper->handle_error( 'unable_to_verify' );
212 die();
213 }
214
215 // Verify the entry.
216 $entry = FrmEntry::getOne( $payment->item_id );
217
218 if ( ! is_object( $entry ) ) {
219 $redirect_helper->handle_error( 'no_entry_found' );
220 die();
221 }
222
223 $redirect_helper->set_entry_id( $entry->id );
224
225 // Verify it's an action with Stripe link enabled.
226 $action = FrmStrpLiteActionsController::get_stripe_link_action( $entry->form_id );
227
228 if ( ! is_object( $action ) ) {
229 $redirect_helper->handle_error( 'no_stripe_link_action' );
230 die();
231 }
232
233 $customer_id = $setup_intent->customer;
234 $payment_method_id = self::get_link_payment_method( $setup_intent );
235
236 if ( ! $payment_method_id ) {
237 FrmTransLitePaymentsController::change_payment_status( $payment, 'failed' );
238 $redirect_helper->handle_error( 'did_not_complete' );
239 die();
240 }
241
242 $amount = $payment->amount * 100;
243 $new_charge = array(
244 'customer' => $customer_id,
245 'default_payment_method' => $payment_method_id,
246 'plan' => FrmStrpLiteSubscriptionHelper::get_plan_from_atts(
247 array(
248 'action' => $action,
249 'amount' => $amount,
250 )
251 ),
252 'expand' => array( 'latest_invoice.charge' ),
253 );
254
255 if ( ! FrmStrpLitePaymentTypeHandler::should_use_automatic_payment_methods( $action ) ) {
256 $new_charge['payment_settings'] = array(
257 'payment_method_types' => FrmStrpLitePaymentTypeHandler::get_payment_method_types( $action ),
258 );
259 }
260
261 $atts = array(
262 'action' => $action,
263 'entry' => $entry,
264 );
265
266 $trial_end = FrmStrpLiteActionsController::get_trial_end_time( $atts );
267
268 if ( $trial_end ) {
269 $new_charge['trial_end'] = $trial_end;
270 }
271
272 $subscription = FrmStrpLiteAppHelper::call_stripe_helper_class( 'create_subscription', $new_charge );
273 $subscription = FrmStrpLiteSubscriptionHelper::maybe_create_missing_plan_and_create_subscription( $subscription, $new_charge, $action, $amount );
274
275 if ( ! is_object( $subscription ) ) {
276 $redirect_helper->handle_error( 'create_subscription_failed' );
277 die();
278 }
279
280 if ( 'succeeded' !== $setup_intent->status ) {
281 FrmTransLitePaymentsController::change_payment_status( $payment, 'failed' );
282 $redirect_helper->handle_error( 'payment_failed' );
283 die();
284 }
285
286 $customer_has_been_charged = ! empty( $subscription->latest_invoice->charge );
287 $atts['charge'] = FrmStrpLiteSubscriptionHelper::prepare_charge_object_for_subscription( $subscription, $amount );
288 $new_payment_values = (array) $payment;
289
290 if ( $customer_has_been_charged ) {
291 $charge = $subscription->latest_invoice->charge;
292 $new_payment_values['receipt_id'] = $charge->id;
293
294 if ( 'failed' === $charge->status ) {
295 FrmTransLitePaymentsController::change_payment_status( $payment, 'failed' );
296
297 $new_payment_values['receipt_id'] = $charge->id;
298 $frm_payment->update( $payment->id, $new_payment_values );
299
300 $redirect_helper->handle_error( 'payment_failed', $charge->id );
301 }
302
303 $new_payment_values['status'] = 'pending' === $charge->status ? 'processing' : 'complete';
304
305 $new_payment_values['expire_date'] = '0000-00-00';
306
307 foreach ( $subscription->latest_invoice->lines->data as $line ) {
308 $new_payment_values['expire_date'] = gmdate( 'Y-m-d', $line->period->end );
309 }
310 } elseif ( $trial_end ) {
311 $new_payment_values['amount'] = 0;
312 $new_payment_values['begin_date'] = gmdate( 'Y-m-d', time() );
313 $new_payment_values['expire_date'] = gmdate( 'Y-m-d', $trial_end );
314 }//end if
315
316 $new_payment_values['sub_id'] = FrmStrpLiteSubscriptionHelper::create_new_subscription( $atts );
317
318 $frm_payment->update( $payment->id, $new_payment_values );
319
320 if ( $customer_has_been_charged ) {
321 // Set the payment to complete.
322 $status = 'complete';
323 FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) );
324
325 // Update the next billing date.
326 $next_bill_date = gmdate( 'Y-m-d' );
327
328 foreach ( $subscription->latest_invoice->lines->data as $line ) {
329 $next_bill_date = gmdate( 'Y-m-d', $line->period->end );
330 }
331
332 $frm_sub = new FrmTransLiteSubscription();
333 $frm_sub->update(
334 $new_payment_values['sub_id'],
335 array( 'next_bill_date' => $next_bill_date )
336 );
337 }
338
339 $redirect_helper->handle_success( $entry, isset( $charge ) ? $charge->id : '' );
340 die();
341 }
342
343 /**
344 * Check for a link payment method associated with a customer for a Stripe link recurring payment/subscription.
345 * This gets created on Stripe's end after confirmSetup is called client-side in the Stripe add on.
346 * This is required in order to associate a payment method with the subscription that gets created.
347 *
348 * @since 6.5, introduced in v3.0 of the Stripe add on.
349 *
350 * @param object $setup_intent
351 *
352 * @return false|string
353 */
354 private static function get_link_payment_method( $setup_intent ) {
355 if ( is_object( $setup_intent->latest_attempt ) && ! empty( $setup_intent->latest_attempt->payment_method_details ) ) {
356 $payment_method_details = $setup_intent->latest_attempt->payment_method_details;
357
358 foreach ( array( 'ideal', 'sofort', 'bancontact' ) as $payment_method_type ) {
359 if ( ! empty( $payment_method_details->$payment_method_type ) ) {
360 return $payment_method_details->$payment_method_type->generated_sepa_debit;
361 }
362 }
363 }
364
365 if ( ! empty( $setup_intent->payment_method ) ) {
366 return $setup_intent->payment_method;
367 }
368
369 return false;
370 }
371
372 /**
373 * Create a pending Stripe link payment on entry creation.
374 * Stripe link uses confirmPayment with a return URL which gets called after this.
375 * The payment is then updated from pending status later in another request, either when the return URL is loaded or with a webhook.
376 *
377 * @since 6.5, introduced in v3.0 of the Stripe add on.
378 *
379 * @param array $atts {
380 * The details needs to create a payment.
381 *
382 * @type stdClass $form
383 * @type stdClass $entry
384 * @type WP_Post $action
385 * @type string $amount
386 * @type object $customer
387 * }
388 *
389 * @return bool True on success, false on failure.
390 */
391 public static function create_pending_stripe_link_payment( $atts ) {
392 if ( empty( $atts['form'] ) || empty( $atts['entry'] ) || empty( $atts['action'] ) || ! isset( $atts['amount'] ) || empty( $atts['customer'] ) ) {
393 return false;
394 }
395
396 $form = $atts['form'];
397 $action = $atts['action'];
398 $intent_id = self::verify_intent( $form->id, $action );
399
400 if ( ! $intent_id ) {
401 return false;
402 }
403
404 $is_setup_intent = str_starts_with( $intent_id, 'seti_' );
405 $entry = $atts['entry'];
406 $amount = $atts['amount'];
407 $customer = $atts['customer'];
408
409 if ( ! $is_setup_intent ) {
410 // Update the amount and set the customer before confirming the payment.
411 $updated = FrmStrpLiteAppHelper::call_stripe_helper_class(
412 'update_intent',
413 $intent_id,
414 array(
415 'amount' => $amount,
416 'customer' => $customer->id,
417 )
418 );
419
420 if ( ! $updated ) {
421 return false;
422 }
423 }
424
425 self::add_temporary_referer_meta( (int) $entry->id );
426
427 $frm_payment = new FrmTransLitePayment();
428 $payment_id = $frm_payment->create(
429 array(
430 'paysys' => 'stripe',
431 'amount' => FrmTransLiteAppHelper::get_formatted_amount_for_currency( $amount, $action ),
432 'status' => 'pending',
433 'item_id' => $entry->id,
434 'action_id' => $action->ID,
435 'receipt_id' => $intent_id,
436 'sub_id' => '',
437 'test' => 'test' === FrmStrpLiteAppHelper::active_mode() ? 1 : 0,
438 )
439 );
440
441 return (bool) $payment_id;
442 }
443
444 /**
445 * Verify a payment intent or setup intent client secret is in the POST data and is valid.
446 *
447 * @since 6.5, introduced in v3.0 of the Stripe add on.
448 *
449 * @param int|string $form_id
450 * @param WP_Post $action
451 *
452 * @return false|string String intent id on success, False if intent is missing or cannot be verified.
453 */
454 private static function verify_intent( $form_id, $action ) {
455 $client_secrets = FrmAppHelper::get_post_param( 'frmintent' . $form_id, array(), 'sanitize_text_field' );
456
457 if ( ! $client_secrets ) {
458 return false;
459 }
460
461 $client_secret = reset( $client_secrets );
462 list( $prefix, $intent_id ) = explode( '_', $client_secret );
463 $intent_id = $prefix . '_' . $intent_id;
464 $is_setup_intent = str_starts_with( $intent_id, 'seti_' );
465 $function_name = $is_setup_intent ? 'get_setup_intent' : 'get_intent';
466 $intent = FrmStrpLiteAppHelper::call_stripe_helper_class( $function_name, $intent_id );
467
468 if ( ! $intent || $intent->client_secret !== $client_secret || ! self::intent_matches_form_action( $intent, $action ) ) {
469 return false;
470 }
471
472 if ( isset( $intent->charges ) && is_object( $intent->charges ) && ! empty( $intent->charges->data ) ) {
473 // The intent should not have any charges yet.
474 // If it does, the intent is invalid.
475 return false;
476 }
477
478 $frm_payment = new FrmTransLitePayment();
479 $payment = $frm_payment->get_one_by( $intent_id, 'receipt_id' );
480
481 if ( $payment ) {
482 // A duplicate payment should not exist.
483 return false;
484 }
485
486 return $intent_id;
487 }
488
489 /**
490 * Check if an intent matches a form action.
491 *
492 * @since 6.29
493 *
494 * @param object $intent
495 * @param WP_Post $action
496 *
497 * @return bool
498 */
499 private static function intent_matches_form_action( $intent, $action ) {
500 if ( ! isset( $intent->metadata ) || ! is_object( $intent->metadata ) || empty( $intent->metadata->action ) ) {
501 // Avoid false positive if the intent is missing metadata.
502 return true;
503 }
504
505 return (int) $intent->metadata->action === $action->ID;
506 }
507
508 /**
509 * Set the referer URL as field ID 0 in entry meta.
510 * This is required for iDEAL, sofort, and other payment methods that include an additional redirect step.
511 * It is used for the redirect in FrmStrpLinkRedirectHelper.
512 * It is deleted after the redirect happens.
513 *
514 * @param int $entry_id
515 *
516 * @return void
517 */
518 private static function add_temporary_referer_meta( $entry_id ) {
519 $referer = FrmAppHelper::get_server_value( 'HTTP_REFERER' );
520 $query_args_to_strip_from_referer = array(
521 'frm_link_error',
522 'payment_intent',
523 'payment_intent_client_secret',
524 'setup_intent',
525 'setup_intent_client_secret',
526 );
527
528 foreach ( $query_args_to_strip_from_referer as $arg ) {
529 $referer = remove_query_arg( $arg, $referer );
530 }
531
532 $meta_value = json_encode( compact( 'referer' ) );
533 FrmEntryMeta::add_entry_meta( $entry_id, 0, '', $meta_value );
534 }
535
536 /**
537 * Flag a form with the frm_stripe_link_form class so it is identifiable when initializing in JavaScript.
538 *
539 * @since 6.5, introduced in v3.0 of the Stripe add on.
540 *
541 * @param stdClass $form
542 *
543 * @return void
544 */
545 public static function add_form_classes( $form ) {
546 if ( false === FrmStrpLiteActionsController::get_stripe_link_action( $form->id ) ) {
547 return;
548 }
549
550 echo ' frm_stripe_link_form ';
551 }
552
553 /**
554 * We need to force AJAX submit with Stripe link to avoid the page reloading before confirmPayment is called after entry creation.
555 *
556 * @since 6.5, introduced in v3.0 of the Stripe add on.
557 *
558 * @param mixed $form
559 *
560 * @return mixed
561 */
562 public static function force_ajax_submit_for_stripe_link( $form ) {
563 if ( ! is_object( $form ) ) {
564 return $form;
565 }
566
567 if ( ! empty( $form->options['ajax_submit'] ) ) {
568 // AJAX is already on so we can exit early.
569 return $form;
570 }
571
572 if ( false !== FrmStrpLiteActionsController::get_stripe_link_action( $form->id ) ) {
573 $form->options['ajax_submit'] = '1';
574 }
575
576 return $form;
577 }
578 }
579