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 +186 -29 6.5 → trunk View file →
@@ -49,15 +49,16 @@
49 49 * @since 6.5, introduced in v3.0 of the Stripe add on.
50 50 *
51 51 * @param string $intent_id
52 52 * @param string $client_secret
53 + *
53 54 * @return void
54 55 */
55 56 private static function handle_one_time_stripe_link_return_url( $intent_id, $client_secret ) {
56 57 $redirect_helper = new FrmStrpLiteLinkRedirectHelper( $intent_id, $client_secret );
57 58 $frm_payment = new FrmTransLitePayment();
59 + $payment = $frm_payment->get_one_by( $intent_id, 'receipt_id' );
58 60
59 - $payment = $frm_payment->get_one_by( $intent_id, 'receipt_id' );
60 61 if ( ! $payment ) {
61 62 $redirect_helper->handle_error( 'no_payment_record' );
62 63 die();
63 64 }
@@ -62,8 +63,9 @@
62 63 die();
63 64 }
64 65
65 66 $intent = FrmStrpLiteAppHelper::call_stripe_helper_class( 'get_intent', $intent_id );
67 +
66 68 if ( ! is_object( $intent ) ) {
67 69 $redirect_helper->handle_error( 'intent_does_not_exist' );
68 70 die();
69 71 }
@@ -73,10 +75,11 @@
73 75 $redirect_helper->handle_error( 'unable_to_verify' );
74 76 die();
75 77 }
76 78
77 - $status = 'succeeded' === $intent->status ? 'complete' : 'authorized';
78 - $new_payment_values = compact( 'status' );
79 + $status = 'succeeded' === $intent->status ? 'complete' : 'authorized';
80 + $new_payment_values = (array) $payment;
81 + $new_payment_values['status'] = $status;
79 82
80 83 if ( 'complete' === $status ) {
81 84 $charge = reset( $intent->charges->data );
82 85 $new_payment_values['receipt_id'] = $charge->id;
@@ -92,13 +95,32 @@
92 95
93 96 $redirect_helper->set_entry_id( $entry->id );
94 97
95 98 $action = FrmStrpLiteActionsController::get_stripe_link_action( $entry->form_id );
99 +
96 100 if ( ! $action ) {
97 101 $redirect_helper->handle_error( 'no_stripe_link_action' );
98 102 die();
99 103 }
100 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 +
101 123 if ( 'succeeded' !== $intent->status ) {
102 124 if ( 'processing' === $intent->status ) {
103 125 FrmTransLitePaymentsController::change_payment_status( $payment, 'processing' );
104 126 $redirect_helper->handle_success( $entry, '' );
@@ -123,21 +145,45 @@
123 145 }
124 146
125 147 self::maybe_update_intent( $intent, $action, $entry );
126 148
127 - $frm_payment->update( $payment->id, $new_payment_values );
128 - 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 );
129 152
153 + if ( $needs_triggers && $updated ) {
154 + FrmTransLiteActionsController::trigger_payment_status_change( compact( 'status', 'payment' ) );
155 + }
156 +
130 157 $redirect_helper->handle_success( $entry, isset( $charge ) ? $charge->id : '' );
131 158 die();
132 159 }
133 160
134 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 + /**
135 180 * Try to add the description to a Stripe link payment after it was confirmed.
136 181 *
137 182 * @param object $intent
138 - * @param WP_Post|stdClass $action
183 + * @param stdClass|WP_Post $action
139 184 * @param stdClass $entry
185 + *
140 186 * @return void
141 187 */
142 188 private static function maybe_update_intent( $intent, $action, $entry ) {
143 189 if ( empty( $action->post_content['description'] ) ) {
@@ -148,9 +194,9 @@
148 194 'entry' => $entry,
149 195 'form' => $entry->form_id,
150 196 'value' => $action->post_content['description'],
151 197 );
152 - $new_values = array( 'description' => FrmTransLiteAppHelper::process_shortcodes( $shortcode_atts ) );
198 + $new_values = array( 'description' => FrmTransLiteAppHelper::process_shortcodes( $shortcode_atts ) );
153 199 FrmStrpLiteAppHelper::call_stripe_helper_class( 'update_intent', $intent->id, $new_values );
154 200 }
155 201
156 202 /**
@@ -161,8 +207,9 @@
161 207 * @since 6.5, introduced in v3.0 of the Stripe add on.
162 208 *
163 209 * @param string $setup_id
164 210 * @param string $client_secret
211 + *
165 212 * @return void
166 213 */
167 214 private static function handle_recurring_stripe_link_return_url( $setup_id, $client_secret ) {
168 215 $redirect_helper = new FrmStrpLiteLinkRedirectHelper( $setup_id, $client_secret );
@@ -175,8 +222,9 @@
175 222 }
176 223
177 224 // Verify the setup intent.
178 225 $setup_intent = FrmStrpLiteAppHelper::call_stripe_helper_class( 'get_setup_intent', $setup_id );
226 +
179 227 if ( ! is_object( $setup_intent ) ) {
180 228 $redirect_helper->handle_error( 'intent_does_not_exist' );
181 229 die();
182 230 }
@@ -188,8 +236,9 @@
188 236 }
189 237
190 238 // Verify the entry.
191 239 $entry = FrmEntry::getOne( $payment->item_id );
240 +
192 241 if ( ! is_object( $entry ) ) {
193 242 $redirect_helper->handle_error( 'no_entry_found' );
194 243 die();
195 244 }
@@ -197,8 +246,9 @@
197 246 $redirect_helper->set_entry_id( $entry->id );
198 247
199 248 // Verify it's an action with Stripe link enabled.
200 249 $action = FrmStrpLiteActionsController::get_stripe_link_action( $entry->form_id );
250 +
201 251 if ( ! is_object( $action ) ) {
202 252 $redirect_helper->handle_error( 'no_stripe_link_action' );
203 253 die();
204 254 }
@@ -204,9 +254,11 @@
204 254 }
205 255
206 256 $customer_id = $setup_intent->customer;
207 257 $payment_method_id = self::get_link_payment_method( $setup_intent );
258 +
208 259 if ( ! $payment_method_id ) {
260 + FrmTransLitePaymentsController::change_payment_status( $payment, 'failed' );
209 261 $redirect_helper->handle_error( 'did_not_complete' );
210 262 die();
211 263 }
212 264
@@ -213,15 +265,15 @@
213 265 $amount = $payment->amount * 100;
214 266 $new_charge = array(
215 267 'customer' => $customer_id,
216 268 'default_payment_method' => $payment_method_id,
217 - 'plan' => FrmStrpLiteSubscriptionHelper::get_plan_from_atts(
269 + 'plan' => FrmStrpLiteSubscriptionHelper::get_plan_from_atts(
218 270 array(
219 271 'action' => $action,
220 272 'amount' => $amount,
221 273 )
222 274 ),
223 - 'expand' => array( 'latest_invoice.charge' ),
275 + 'expand' => array( 'latest_invoice.charge' ),
224 276 );
225 277
226 278 if ( ! FrmStrpLitePaymentTypeHandler::should_use_automatic_payment_methods( $action ) ) {
227 279 $new_charge['payment_settings'] = array(
@@ -234,13 +286,16 @@
234 286 'entry' => $entry,
235 287 );
236 288
237 289 $trial_end = FrmStrpLiteActionsController::get_trial_end_time( $atts );
290 +
238 291 if ( $trial_end ) {
239 292 $new_charge['trial_end'] = $trial_end;
240 293 }
241 294
242 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 +
243 298 if ( ! is_object( $subscription ) ) {
244 299 $redirect_helper->handle_error( 'create_subscription_failed' );
245 300 die();
246 301 }
@@ -245,8 +300,9 @@
245 300 die();
246 301 }
247 302
248 303 if ( 'succeeded' !== $setup_intent->status ) {
304 + FrmTransLitePaymentsController::change_payment_status( $payment, 'failed' );
249 305 $redirect_helper->handle_error( 'payment_failed' );
250 306 die();
251 307 }
252 308
@@ -251,19 +307,35 @@
251 307 }
252 308
253 309 $customer_has_been_charged = ! empty( $subscription->latest_invoice->charge );
254 310 $atts['charge'] = FrmStrpLiteSubscriptionHelper::prepare_charge_object_for_subscription( $subscription, $amount );
255 - $new_payment_values = array();
311 + $new_payment_values = (array) $payment;
256 312
257 313 if ( $customer_has_been_charged ) {
258 314 $charge = $subscription->latest_invoice->charge;
259 315 $new_payment_values['receipt_id'] = $charge->id;
260 - $new_payment_values['status'] = 'pending' === $charge->status ? 'processing' : 'complete';
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 + }
261 333 } elseif ( $trial_end ) {
262 334 $new_payment_values['amount'] = 0;
263 335 $new_payment_values['begin_date'] = gmdate( 'Y-m-d', time() );
264 336 $new_payment_values['expire_date'] = gmdate( 'Y-m-d', $trial_end );
265 - }
337 + }//end if
266 338
267 339 $new_payment_values['sub_id'] = FrmStrpLiteSubscriptionHelper::create_new_subscription( $atts );
268 340
269 341 $frm_payment->update( $payment->id, $new_payment_values );
@@ -268,10 +340,24 @@
268 340
269 341 $frm_payment->update( $payment->id, $new_payment_values );
270 342
271 343 if ( $customer_has_been_charged ) {
344 + // Set the payment to complete.
272 345 $status = 'complete';
273 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 + );
274 360 }
275 361
276 362 $redirect_helper->handle_success( $entry, isset( $charge ) ? $charge->id : '' );
277 363 die();
@@ -284,14 +370,26 @@
284 370 *
285 371 * @since 6.5, introduced in v3.0 of the Stripe add on.
286 372 *
287 373 * @param object $setup_intent
288 - * @return string|false
374 + *
375 + * @return false|string
289 376 */
290 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 +
291 388 if ( ! empty( $setup_intent->payment_method ) ) {
292 389 return $setup_intent->payment_method;
293 390 }
391 +
294 392 return false;
295 393 }
296 394
297 395 /**
@@ -301,8 +399,10 @@
301 399 *
302 400 * @since 6.5, introduced in v3.0 of the Stripe add on.
303 401 *
304 402 * @param array $atts {
403 + * The details needs to create a payment.
404 + *
305 405 * @type stdClass $form
306 406 * @type stdClass $entry
307 407 * @type WP_Post $action
308 408 * @type string $amount
@@ -307,31 +407,32 @@
307 407 * @type WP_Post $action
308 408 * @type string $amount
309 409 * @type object $customer
310 410 * }
311 - * @return void
411 + *
412 + * @return bool True on success, false on failure.
312 413 */
313 414 public static function create_pending_stripe_link_payment( $atts ) {
314 415 if ( empty( $atts['form'] ) || empty( $atts['entry'] ) || empty( $atts['action'] ) || ! isset( $atts['amount'] ) || empty( $atts['customer'] ) ) {
315 - return;
416 + return false;
316 417 }
317 418
318 419 $form = $atts['form'];
319 - $intent_id = self::verify_intent( $form->id );
420 + $action = $atts['action'];
421 + $intent_id = self::verify_intent( $form->id, $action );
320 422
321 423 if ( ! $intent_id ) {
322 - return;
424 + return false;
323 425 }
324 426
325 - $is_setup_intent = 0 === strpos( $intent_id, 'seti_' );
427 + $is_setup_intent = str_starts_with( $intent_id, 'seti_' );
326 428 $entry = $atts['entry'];
327 - $action = $atts['action'];
328 429 $amount = $atts['amount'];
329 430 $customer = $atts['customer'];
330 431
331 432 if ( ! $is_setup_intent ) {
332 433 // Update the amount and set the customer before confirming the payment.
333 - FrmStrpLiteAppHelper::call_stripe_helper_class(
434 + $updated = FrmStrpLiteAppHelper::call_stripe_helper_class(
334 435 'update_intent',
335 436 $intent_id,
336 437 array(
337 438 'amount' => $amount,
@@ -337,14 +438,18 @@
337 438 'amount' => $amount,
338 439 'customer' => $customer->id,
339 440 )
340 441 );
442 +
443 + if ( ! $updated ) {
444 + return false;
445 + }
341 446 }
342 447
343 448 self::add_temporary_referer_meta( (int) $entry->id );
344 449
345 450 $frm_payment = new FrmTransLitePayment();
346 - $frm_payment->create(
451 + $payment_id = $frm_payment->create(
347 452 array(
348 453 'paysys' => 'stripe',
349 454 'amount' => FrmTransLiteAppHelper::get_formatted_amount_for_currency( $amount, $action ),
350 455 'status' => 'pending',
@@ -351,10 +456,13 @@
351 456 'item_id' => $entry->id,
352 457 'action_id' => $action->ID,
353 458 'receipt_id' => $intent_id,
354 459 'sub_id' => '',
460 + 'test' => 'test' === FrmStrpLiteAppHelper::active_mode() ? 1 : 0,
355 461 )
356 462 );
463 +
464 + return (bool) $payment_id;
357 465 }
358 466
359 467 /**
360 468 * Verify a payment intent or setup intent client secret is in the POST data and is valid.
@@ -360,13 +468,16 @@
360 468 * Verify a payment intent or setup intent client secret is in the POST data and is valid.
361 469 *
362 470 * @since 6.5, introduced in v3.0 of the Stripe add on.
363 471 *
364 - * @param string|int $form_id
365 - * @return string|false String intent id on success, False if intent is missing or cannot be verified.
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.
366 476 */
367 - private static function verify_intent( $form_id ) {
477 + private static function verify_intent( $form_id, $action ) {
368 478 $client_secrets = FrmAppHelper::get_post_param( 'frmintent' . $form_id, array(), 'sanitize_text_field' );
479 +
369 480 if ( ! $client_secrets ) {
370 481 return false;
371 482 }
372 483
@@ -372,15 +483,27 @@
372 483
373 484 $client_secret = reset( $client_secrets );
374 485 list( $prefix, $intent_id ) = explode( '_', $client_secret );
375 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 );
376 490
377 - $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 + }
378 494
379 - $function_name = $is_setup_intent ? 'get_setup_intent' : 'get_intent';
380 - $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 + }
381 500
382 - 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.
383 506 return false;
384 507 }
385 508
386 509 return $intent_id;
@@ -386,8 +509,27 @@
386 509 return $intent_id;
387 510 }
388 511
389 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 + /**
390 532 * Set the referer URL as field ID 0 in entry meta.
391 533 * This is required for iDEAL, sofort, and other payment methods that include an additional redirect step.
392 534 * It is used for the redirect in FrmStrpLinkRedirectHelper.
393 535 * It is deleted after the redirect happens.
@@ -392,12 +534,25 @@
392 534 * It is used for the redirect in FrmStrpLinkRedirectHelper.
393 535 * It is deleted after the redirect happens.
394 536 *
395 537 * @param int $entry_id
538 + *
396 539 * @return void
397 540 */
398 541 private static function add_temporary_referer_meta( $entry_id ) {
399 - $referer = FrmAppHelper::get_server_value( 'HTTP_REFERER' );
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 +
400 555 $meta_value = json_encode( compact( 'referer' ) );
401 556 FrmEntryMeta::add_entry_meta( $entry_id, 0, '', $meta_value );
402 557 }
403 558
@@ -406,8 +561,9 @@
406 561 *
407 562 * @since 6.5, introduced in v3.0 of the Stripe add on.
408 563 *
409 564 * @param stdClass $form
565 + *
410 566 * @return void
411 567 */
412 568 public static function add_form_classes( $form ) {
413 569 if ( false === FrmStrpLiteActionsController::get_stripe_link_action( $form->id ) ) {
@@ -422,8 +578,9 @@
422 578 *
423 579 * @since 6.5, introduced in v3.0 of the Stripe add on.
424 580 *
425 581 * @param mixed $form
582 + *
426 583 * @return mixed
427 584 */
428 585 public static function force_ajax_submit_for_stripe_link( $form ) {
429 586 if ( ! is_object( $form ) ) {