PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
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
← All changes | stripe/controllers/FrmStrpLiteLinkController.php +95 -17 6.26 → trunk View file →
@@ -55,11 +55,10 @@
55 55 */
56 56 private static function handle_one_time_stripe_link_return_url( $intent_id, $client_secret ) {
57 57 $redirect_helper = new FrmStrpLiteLinkRedirectHelper( $intent_id, $client_secret );
58 58 $frm_payment = new FrmTransLitePayment();
59 + $payment = $frm_payment->get_one_by( $intent_id, 'receipt_id' );
59 60
60 - $payment = $frm_payment->get_one_by( $intent_id, 'receipt_id' );
61 -
62 61 if ( ! $payment ) {
63 62 $redirect_helper->handle_error( 'no_payment_record' );
64 63 die();
65 64 }
@@ -102,8 +101,26 @@
102 101 $redirect_helper->handle_error( 'no_stripe_link_action' );
103 102 die();
104 103 }
105 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 +
106 123 if ( 'succeeded' !== $intent->status ) {
107 124 if ( 'processing' === $intent->status ) {
108 125 FrmTransLitePaymentsController::change_payment_status( $payment, 'processing' );
109 126 $redirect_helper->handle_success( $entry, '' );
@@ -128,16 +145,39 @@
128 145 }
129 146
130 147 self::maybe_update_intent( $intent, $action, $entry );
131 148
132 - $frm_payment->update( $payment->id, $new_payment_values );
133 - FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) );
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 );
134 152
153 + if ( $needs_triggers && $updated ) {
154 + FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) );
155 + }
156 +
135 157 $redirect_helper->handle_success( $entry, isset( $charge ) ? $charge->id : '' );
136 158 die();
137 159 }
138 160
139 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 + /**
140 180 * Try to add the description to a Stripe link payment after it was confirmed.
141 181 *
142 182 * @param object $intent
143 183 * @param stdClass|WP_Post $action
@@ -368,31 +408,31 @@
368 408 * @type string $amount
369 409 * @type object $customer
370 410 * }
371 411 *
372 - * @return void
412 + * @return bool True on success, false on failure.
373 413 */
374 414 public static function create_pending_stripe_link_payment( $atts ) {
375 415 if ( empty( $atts['form'] ) || empty( $atts['entry'] ) || empty( $atts['action'] ) || ! isset( $atts['amount'] ) || empty( $atts['customer'] ) ) {
376 - return;
416 + return false;
377 417 }
378 418
379 419 $form = $atts['form'];
380 - $intent_id = self::verify_intent( $form->id );
420 + $action = $atts['action'];
421 + $intent_id = self::verify_intent( $form->id, $action );
381 422
382 423 if ( ! $intent_id ) {
383 - return;
424 + return false;
384 425 }
385 426
386 - $is_setup_intent = 0 === strpos( $intent_id, 'seti_' );
427 + $is_setup_intent = str_starts_with( $intent_id, 'seti_' );
387 428 $entry = $atts['entry'];
388 - $action = $atts['action'];
389 429 $amount = $atts['amount'];
390 430 $customer = $atts['customer'];
391 431
392 432 if ( ! $is_setup_intent ) {
393 433 // Update the amount and set the customer before confirming the payment.
394 - FrmStrpLiteAppHelper::call_stripe_helper_class(
434 + $updated = FrmStrpLiteAppHelper::call_stripe_helper_class(
395 435 'update_intent',
396 436 $intent_id,
397 437 array(
398 438 'amount' => $amount,
@@ -398,14 +438,18 @@
398 438 'amount' => $amount,
399 439 'customer' => $customer->id,
400 440 )
401 441 );
442 +
443 + if ( ! $updated ) {
444 + return false;
445 + }
402 446 }
403 447
404 448 self::add_temporary_referer_meta( (int) $entry->id );
405 449
406 450 $frm_payment = new FrmTransLitePayment();
407 - $frm_payment->create(
451 + $payment_id = $frm_payment->create(
408 452 array(
409 453 'paysys' => 'stripe',
410 454 'amount' => FrmTransLiteAppHelper::get_formatted_amount_for_currency( $amount, $action ),
411 455 'status' => 'pending',
@@ -415,8 +459,10 @@
415 459 'sub_id' => '',
416 460 'test' => 'test' === FrmStrpLiteAppHelper::active_mode() ? 1 : 0,
417 461 )
418 462 );
463 +
464 + return (bool) $payment_id;
419 465 }
420 466
421 467 /**
422 468 * Verify a payment intent or setup intent client secret is in the POST data and is valid.
@@ -423,12 +469,13 @@
423 469 *
424 470 * @since 6.5, introduced in v3.0 of the Stripe add on.
425 471 *
426 472 * @param int|string $form_id
473 + * @param WP_Post $action
427 474 *
428 475 * @return false|string String intent id on success, False if intent is missing or cannot be verified.
429 476 */
430 - private static function verify_intent( $form_id ) {
477 + private static function verify_intent( $form_id, $action ) {
431 478 $client_secrets = FrmAppHelper::get_post_param( 'frmintent' . $form_id, array(), 'sanitize_text_field' );
432 479
433 480 if ( ! $client_secrets ) {
434 481 return false;
@@ -436,19 +483,50 @@
436 483
437 484 $client_secret = reset( $client_secrets );
438 485 list( $prefix, $intent_id ) = explode( '_', $client_secret );
439 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 );
440 490
441 - $is_setup_intent = 0 === strpos( $intent_id, 'seti_' );
491 + if ( ! $intent || $intent->client_secret !== $client_secret || ! self::intent_matches_form_action( $intent, $action ) ) {
492 + return false;
493 + }
442 494
443 - $function_name = $is_setup_intent ? 'get_setup_intent' : 'get_intent';
444 - $intent = FrmStrpLiteAppHelper::call_stripe_helper_class( $function_name, $intent_id );
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 + }
445 500
446 - if ( ! $intent || $intent->client_secret !== $client_secret ) {
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.
447 506 return false;
448 507 }
449 508
450 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;
451 529 }
452 530
453 531 /**
454 532 * Set the referer URL as field ID 0 in entry meta.