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/models/FrmStrpLiteAuth.php +315 -168 6.5 → trunk View file →
@@ -5,15 +5,13 @@
5 5
6 6 class FrmStrpLiteAuth {
7 7
8 8 /**
9 - * Payment details are stored after checking the request params.
10 - * The details are then accessed later in self::maybe_show_message.
11 - * These details include entry, intent, and payment.
9 + * All of the form IDs with payment details in the URL params will be included in this array.
12 10 *
13 11 * @var array
14 12 */
15 - private static $details_by_form_id = array();
13 + private static $form_ids = array();
16 14
17 15 /**
18 16 * If returning from Stripe to authorize a payment, show the message.
19 17 * This is used for 3D secure and for Stripe link.
@@ -20,12 +18,14 @@
20 18 *
21 19 * @since 6.5, introduced in v2.0 of the Stripe add on.
22 20 *
23 21 * @param string $html Form HTML that gets filtered through frm_filter_final_form.
22 + *
24 23 * @return string
25 24 */
26 25 public static function maybe_show_message( $html ) {
27 26 $link_error = FrmAppHelper::simple_get( 'frm_link_error' );
27 +
28 28 if ( $link_error ) {
29 29 $message = '<div class="frm_error_style">' . self::get_message_for_stripe_link_code( $link_error ) . '</div>';
30 30 self::insert_error_message( $message, $html );
31 31 return $html;
@@ -31,23 +31,28 @@
31 31 return $html;
32 32 }
33 33
34 34 $form_id = self::check_html_for_form_id_match( $html );
35 +
35 36 if ( false === $form_id ) {
36 37 return $html;
37 38 }
38 39
39 - $details = self::$details_by_form_id[ $form_id ];
40 - $atts = array(
40 + $details = FrmStrpLiteUrlParamHelper::get_details_for_form( $form_id );
41 +
42 + if ( ! is_array( $details ) ) {
43 + return $html;
44 + }
45 +
46 + $atts = array(
41 47 'fields' => FrmFieldsHelper::get_form_fields( $form_id ),
42 48 'entry' => $details['entry'],
43 49 );
44 50 self::prepare_success_atts( $atts );
45 51
46 - $intent = $details['intent'];
47 - $payment = $details['payment'];
52 + $intent = $details['intent'];
48 53
49 - if ( in_array( $intent->status, array( 'requires_source', 'requires_payment_method', 'canceled' ), true ) ) {
54 + if ( self::intent_has_failed_status( $intent ) ) {
50 55 $message = '<div class="frm_error_style">' . $intent->last_payment_error->message . '</div>';
51 56 self::insert_error_message( $message, $html );
52 57 return $html;
53 58 }
@@ -52,25 +57,24 @@
52 57 return $html;
53 58 }
54 59
55 60 $intent_is_processing = 'processing' === $intent->status;
61 +
56 62 if ( $intent_is_processing ) {
57 63 // Append an additional processing message to the end of the success message.
58 - $filter = function( $message ) {
64 + $filter = function ( $message ) {
59 65 $stripe_settings = FrmStrpLiteAppHelper::get_settings();
60 - $message .= '<p>' . esc_html( $stripe_settings->settings->processing_message ) . '</p>';
61 - return $message;
66 + return $message . ( '<p>' . esc_html( $stripe_settings->settings->processing_message ) . '</p>' );
62 67 };
63 68 add_filter( 'frm_content', $filter );
64 69 }
65 70
66 71 ob_start();
67 - FrmFormsController::run_success_action( $atts );
68 - $message = ob_get_contents();
69 - ob_end_clean();
72 + FrmFormsController::run_on_submit_actions( $atts );
73 + $message = ob_get_clean();
70 74
71 75 // Clean up the filter we added above so no other success messages get altered if there are multiple forms.
72 - if ( $intent_is_processing && isset( $filter ) ) {
76 + if ( $intent_is_processing ) {
73 77 remove_filter( 'frm_content', $filter );
74 78 }
75 79
76 80 return $message;
@@ -76,81 +80,26 @@
76 80 return $message;
77 81 }
78 82
79 83 /**
80 - * Check the URL params for Stripe intent details.
81 - * When these params are detected, the form is replaced with a success message.
82 - * These params are used in 3D secure as well as Stripe Link.
84 + * @param int|string $form_id
83 85 *
84 - * The params include:
85 - * - The ID of the payment intent or setup intent.
86 - * - The ID of the entry.
87 - * - The client secret which is used to verify the intent.
88 - * - The charge ID (if applicable)
89 - *
90 - * @since 6.5
91 - *
92 - * @param string|int $form_id
93 86 * @return array|false
94 87 */
95 88 private static function check_request_params( $form_id ) {
96 - $form_id = (int) $form_id;
97 - $intent_id = FrmAppHelper::simple_get( 'payment_intent' );
98 - $is_setup_intent = false;
99 -
100 - if ( ! $intent_id ) {
101 - $intent_id = FrmAppHelper::simple_get( 'setup_intent' );
102 - if ( ! $intent_id ) {
103 - return false;
104 - }
105 -
106 - $is_setup_intent = true;
107 - }
108 -
109 - $entry_id = FrmAppHelper::simple_get( 'frmstrp', 'absint', 0 );
110 - if ( ! $entry_id ) {
111 - return false;
112 - }
113 -
114 - $entry = FrmEntry::getOne( $entry_id );
115 - if ( ! $entry || (int) $entry->form_id !== $form_id ) {
116 - return false;
117 - }
118 -
119 - $charge_id = FrmAppHelper::simple_get( 'charge' );
120 - $has_charge = (bool) $charge_id;
121 - $frm_payment = new FrmTransLitePayment();
122 -
123 - if ( $has_charge ) {
124 - // Stripe link payments use charge id.
125 - $payment = $frm_payment->get_one_by( $charge_id, 'receipt_id' );
126 - } else {
127 - // 3D secure payments use intent id.
128 - $payment = $frm_payment->get_one_by( $intent_id, 'receipt_id' );
129 - }
130 -
131 - if ( ! $payment || (int) $payment->item_id !== (int) $entry->id ) {
132 - return false;
133 - }
134 -
135 89 if ( ! FrmStrpLiteAppHelper::stripe_is_configured() ) {
136 90 return false;
137 91 }
138 92
139 - $intent_function_name = $is_setup_intent ? 'get_setup_intent' : 'get_intent';
140 - $intent = FrmStrpLiteAppHelper::call_stripe_helper_class( $intent_function_name, $intent_id );
93 + $details = FrmStrpLiteUrlParamHelper::get_details_for_form( $form_id );
141 94
142 - if ( ! $intent || ! self::verify_client_secret( $intent, $is_setup_intent ) ) {
95 + if ( ! is_array( $details ) ) {
143 96 return false;
144 97 }
145 98
146 - self::$details_by_form_id[ $form_id ] = array(
147 - 'entry' => $entry,
148 - 'intent' => $intent,
149 - 'payment' => $payment,
150 - );
99 + self::$form_ids[] = $form_id;
151 100
152 - return self::$details_by_form_id[ $form_id ];
101 + return $details;
153 102 }
154 103
155 104 /**
156 105 * The frm_filter_final_form filter only passes form HTML as a string.
@@ -159,19 +108,16 @@
159 108 *
160 109 * @since 6.5
161 110 *
162 111 * @param string $html
163 - * @return int|false Matching form id or false if there is no match.
112 + *
113 + * @return false|int Matching form id or false if there is no match.
164 114 */
165 115 private static function check_html_for_form_id_match( $html ) {
166 - if ( empty( self::$details_by_form_id ) ) {
167 - return false;
168 - }
116 + foreach ( self::$form_ids as $form_id ) {
117 + $substring = '<input type="hidden" name="form_id" value="' . $form_id . '"';
169 118
170 - $form_ids = array_keys( self::$details_by_form_id );
171 - foreach ( $form_ids as $form_id ) {
172 - $substring = '<input type="hidden" name="form_id" value="' . $form_id . '"';
173 - if ( strpos( $html, $substring ) ) {
119 + if ( str_contains( $html, $substring ) ) {
174 120 return $form_id;
175 121 }
176 122 }
177 123
@@ -178,23 +124,8 @@
178 124 return false;
179 125 }
180 126
181 127 /**
182 - * Check the client secret in the URL, verify it matches the Stripe object and isn't being manipulated.
183 - *
184 - * @since 6.5, introduced in v3.0 of the Stripe add on.
185 - *
186 - * @param object $intent
187 - * @param bool $is_setup_intent
188 - * @return bool True if the client secret is set and valid.
189 - */
190 - private static function verify_client_secret( $intent, $is_setup_intent ) {
191 - $client_secret_param = $is_setup_intent ? 'setup_intent_client_secret' : 'payment_intent_client_secret';
192 - $client_secret = FrmAppHelper::simple_get( $client_secret_param );
193 - return $client_secret && $client_secret === $intent->client_secret;
194 - }
195 -
196 - /**
197 128 * Translate an error code into a readable message for the front end.
198 129 * FrmStrpLiteLinkRedirectHelper uses these codes to redirect errors that are then handled in self::maybe_show_message.
199 130 *
200 131 * @since 6.5, introduced in v3.0 of the Stripe add on.
@@ -199,8 +130,9 @@
199 130 *
200 131 * @since 6.5, introduced in v3.0 of the Stripe add on.
201 132 *
202 133 * @param string $code
134 + *
203 135 * @return string
204 136 */
205 137 private static function get_message_for_stripe_link_code( $code ) {
206 138 switch ( $code ) {
@@ -219,8 +151,10 @@
219 151 case 'create_subscription_failed':
220 152 return __( 'Something went wrong when trying to create a subscription.', 'formidable' );
221 153 case 'payment_failed':
222 154 return __( 'Payment was not successfully processed.', 'formidable' );
155 + case 'amount_mismatch':
156 + return __( 'The payment amount does not match the expected amount.', 'formidable' );
223 157 }
224 158 return '';
225 159 }
226 160
@@ -229,8 +163,9 @@
229 163 *
230 164 * @since 6.5, introduced in v2.0 of the Stripe add on.
231 165 *
232 166 * @param array $atts
167 + *
233 168 * @return void
234 169 */
235 170 private static function prepare_success_atts( &$atts ) {
236 171 $atts['form'] = FrmForm::getOne( $atts['entry']->form_id );
@@ -236,8 +171,20 @@
236 171 $atts['form'] = FrmForm::getOne( $atts['entry']->form_id );
237 172 $atts['entry_id'] = $atts['entry']->id;
238 173 $opt = 'success_action';
239 174 $atts['conf_method'] = ! empty( $atts['form']->options[ $opt ] ) ? $atts['form']->options[ $opt ] : 'message';
175 +
176 + $actions = FrmFormsController::get_met_on_submit_actions( $atts, 'create' );
177 +
178 + if ( ! $actions ) {
179 + return;
180 + }
181 +
182 + $action = reset( $actions );
183 +
184 + if ( ! empty( $action->post_content['success_action'] ) && 'message' === $action->post_content['success_action'] ) {
185 + $atts['conf_method'] = $action->post_content['success_action'];
186 + }
240 187 }
241 188
242 189 /**
243 190 * Insert a message/error where the form styling will be applied.
@@ -242,12 +189,18 @@
242 189 /**
243 190 * Insert a message/error where the form styling will be applied.
244 191 *
245 192 * @since 6.5, introduced in v2.0 of the Stripe add on.
193 + *
194 + * @param string $message Message.
195 + * @param string $form Form.
196 + *
197 + * @return void
246 198 */
247 199 private static function insert_error_message( $message, &$form ) {
248 200 $add_after = '<fieldset>';
249 - $pos = strpos( $form, $add_after );
201 + $pos = strpos( $form, $add_after );
202 +
250 203 if ( $pos !== false ) {
251 204 $form = substr_replace( $form, $add_after . $message, $pos, strlen( $add_after ) );
252 205 }
253 206 }
@@ -255,13 +208,15 @@
255 208 /**
256 209 * Include the token if going between pages.
257 210 *
258 211 * @param object $form The form being submitted.
212 + *
259 213 * @return void
260 214 */
261 215 public static function add_hidden_token_field( $form ) {
262 216 $posted_form = FrmAppHelper::get_param( 'form_id', 0, 'post', 'absint' );
263 - if ( $posted_form != $form->id || FrmFormsController::just_created_entry( $form->id ) ) {
217 +
218 + if ( $posted_form !== (int) $form->id || FrmFormsController::just_created_entry( $form->id ) ) {
264 219 // Check to make sure the correct form was submitted.
265 220 // Was an entry already created and the form should be loaded fresh?
266 221
267 222 $intents = self::maybe_create_intents( $form->id );
@@ -270,10 +225,11 @@
270 225 return;
271 226 }
272 227
273 228 $intents = self::get_payment_intents( 'frmintent' . $form->id );
274 - if ( ! empty( $intents ) ) {
275 - self::update_intent_pricing( $form->id, $intents );
229 +
230 + if ( $intents ) {
231 + self::update_intent_pricing( $form->id, $intents, $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
276 232 } else {
277 233 $intents = self::maybe_create_intents( $form->id );
278 234 }
279 235
@@ -286,8 +242,9 @@
286 242 * @since 6.5, introduced in v2.02 of the Stripe add on.
287 243 *
288 244 * @param array $intents
289 245 * @param stdClass $form
246 + *
290 247 * @return void
291 248 */
292 249 private static function include_intents_in_form( $intents, $form ) {
293 250 foreach ( $intents as $intent ) {
@@ -308,8 +265,9 @@
308 265 *
309 266 * @since 6.5, introduced in v2.0 of the Stripe add on.
310 267 *
311 268 * @param string $name
269 + *
312 270 * @return mixed
313 271 */
314 272 public static function get_payment_intents( $name ) {
315 273 // phpcs:ignore WordPress.Security.NonceVerification.Missing
@@ -315,9 +273,11 @@
315 273 // phpcs:ignore WordPress.Security.NonceVerification.Missing
316 274 if ( ! isset( $_POST[ $name ] ) ) {
317 275 return array();
318 276 }
319 - $intents = $_POST[ $name ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
277 +
278 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
279 + $intents = $_POST[ $name ];
320 280 FrmAppHelper::sanitize_value( 'sanitize_text_field', $intents );
321 281 return $intents;
322 282 }
323 283
@@ -334,9 +294,11 @@
334 294 if ( empty( $_POST['form'] ) ) {
335 295 wp_die();
336 296 }
337 297
338 - $form = json_decode( stripslashes( $_POST['form'] ), true ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
298 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
299 + $form = json_decode( stripslashes( $_POST['form'] ), true );
300 +
339 301 if ( ! is_array( $form ) ) {
340 302 wp_die();
341 303 }
342 304
@@ -342,26 +304,25 @@
342 304
343 305 self::format_form_data( $form );
344 306
345 307 $form_id = absint( $form['form_id'] );
346 - $intents = isset( $form[ 'frmintent' . $form_id ] ) ? $form[ 'frmintent' . $form_id ] : array();
308 + $intents = $form[ 'frmintent' . $form_id ] ?? array();
347 309
348 - if ( empty( $intents ) ) {
310 + if ( ! $intents ) {
349 311 wp_die();
350 312 }
351 313
352 - if ( ! is_array( $intents ) ) {
353 - $intents = array( $intents );
354 - } else {
314 + if ( is_array( $intents ) ) {
355 315 foreach ( $intents as $k => $intent ) {
356 316 if ( is_array( $intent ) && isset( $intent[ $k ] ) ) {
357 317 $intents[ $k ] = $intent[ $k ];
358 318 }
359 319 }
320 + } else {
321 + $intents = array( $intents );
360 322 }
361 323
362 - $_POST = $form;
363 - self::update_intent_pricing( $form_id, $intents );
324 + self::update_intent_pricing( $form_id, $intents, $form );
364 325
365 326 wp_die();
366 327 }
367 328
@@ -368,20 +329,23 @@
368 329 /**
369 330 * Update pricing on page turn and non-ajax validation.
370 331 *
371 332 * @since 6.5, introduced in v2.0 of the Stripe add on.
372 - * @param int $form_id
373 - * @param array $intents
333 + *
334 + * @param int|string $form_id
335 + * @param array $intents
336 + * @param array $form_data
337 + *
374 338 * @return void
375 339 */
376 - private static function update_intent_pricing( $form_id, &$intents ) {
377 - // phpcs:ignore WordPress.Security.NonceVerification.Missing
378 - if ( ! isset( $_POST['form_id'] ) || absint( $_POST['form_id'] ) != $form_id ) {
340 + private static function update_intent_pricing( $form_id, &$intents, $form_data ) {
341 + if ( ! isset( $form_data['form_id'] ) || absint( $form_data['form_id'] ) !== (int) $form_id ) {
379 342 return;
380 343 }
381 344
382 345 $actions = FrmStrpLiteActionsController::get_actions_before_submit( $form_id );
383 - if ( empty( $actions ) || empty( $intents ) ) {
346 +
347 + if ( ! $actions || ! $intents ) {
384 348 return;
385 349 }
386 350
387 351 $form = FrmForm::getOne( $form_id );
@@ -396,18 +360,26 @@
396 360 }
397 361
398 362 foreach ( $intents as $k => $intent ) {
399 363 $intent_id = explode( '_secret_', $intent )[0];
400 - $is_setup_intent = 0 === strpos( $intent_id, 'seti_' );
364 + $is_setup_intent = str_starts_with( $intent_id, 'seti_' );
365 +
401 366 if ( $is_setup_intent ) {
402 367 continue;
403 368 }
404 369
405 - $saved = FrmStrpLiteAppHelper::call_stripe_helper_class( 'get_intent', $intent_id );
370 + $saved = FrmStrpLiteAppHelper::call_stripe_helper_class( 'get_intent', $intent_id );
371 +
372 + if ( empty( $saved->metadata->action ) ) {
373 + continue;
374 + }
375 +
406 376 foreach ( $actions as $action ) {
377 + // phpcs:ignore Universal.Operators.StrictComparisons
407 378 if ( $saved->metadata->action != $action->ID ) {
408 379 continue;
409 380 }
381 +
410 382 $intents[ $k ] = array(
411 383 'id' => $intent,
412 384 'action' => $action->ID,
413 385 );
@@ -412,23 +384,26 @@
412 384 'action' => $action->ID,
413 385 );
414 386
415 387 $amount = $action->post_content['amount'];
416 - if ( strpos( $amount, '[' ) === false ) {
388 +
389 + if ( ! str_contains( $amount, '[' ) ) {
417 390 // The amount is static, so it doesn't need an update.
418 391 continue;
419 392 }
420 393
421 394 // Update amount based on field shortcodes.
422 - $entry = self::generate_false_entry();
395 + $entry = self::generate_false_entry( $form_data );
423 396 $amount = FrmStrpLiteActionsController::prepare_amount( $amount, compact( 'form', 'entry', 'action' ) );
397 +
398 + // phpcs:ignore Universal.Operators.StrictComparisons
424 399 if ( $saved->amount == $amount || $amount == '000' ) {
425 400 continue;
426 401 }
427 402
428 403 FrmStrpLiteAppHelper::call_stripe_helper_class( 'update_intent', $intent_id, array( 'amount' => $amount ) );
429 - }
430 - }
404 + }//end foreach
405 + }//end foreach
431 406 }
432 407
433 408 /**
434 409 * Create an entry object with posted values.
@@ -433,30 +408,34 @@
433 408 /**
434 409 * Create an entry object with posted values.
435 410 *
436 411 * @since 6.5, introduced in v2.0 of the Stripe add on.
412 + *
413 + * @param array $form_data
414 + *
437 415 * @return stdClass
438 416 */
439 - private static function generate_false_entry() {
440 - $entry = new stdClass();
441 - $entry->post_id = 0;
442 - $entry->id = 0;
443 - $entry->metas = array();
417 + private static function generate_false_entry( $form_data ) {
418 + $entry = new stdClass();
419 + $entry->post_id = 0;
420 + $entry->id = 0;
421 + $entry->item_key = '';
422 + $entry->metas = array();
444 423
445 - // phpcs:ignore WordPress.Security.NonceVerification.Missing
446 - foreach ( $_POST as $k => $v ) {
424 + foreach ( $form_data as $k => $v ) {
447 425 $k = sanitize_text_field( stripslashes( $k ) );
448 426 $v = wp_unslash( $v );
449 427
450 - if ( $k === 'item_meta' ) {
451 - foreach ( $v as $f => $value ) {
452 - FrmAppHelper::sanitize_value( 'wp_kses_post', $value );
453 - $entry->metas[ absint( $f ) ] = $value;
454 - }
455 - } else {
428 + if ( $k !== 'item_meta' ) {
456 429 FrmAppHelper::sanitize_value( 'wp_kses_post', $v );
457 430 $entry->{$k} = $v;
431 + continue;
458 432 }
433 +
434 + foreach ( $v as $f => $value ) {
435 + FrmAppHelper::sanitize_value( 'wp_kses_post', $value );
436 + $entry->metas[ absint( $f ) ] = $value;
437 + }
459 438 }
460 439
461 440 return $entry;
462 441 }
@@ -466,8 +445,9 @@
466 445 *
467 446 * @since 6.5, introduced in v2.0 of the Stripe add on.
468 447 *
469 448 * @param array $form
449 + *
470 450 * @return void
471 451 */
472 452 private static function format_form_data( &$form ) {
473 453 $formatted = array();
@@ -473,16 +453,18 @@
473 453 $formatted = array();
474 454
475 455 foreach ( $form as $input ) {
476 456 $key = $input['name'];
477 - if ( isset( $formatted[ $key ] ) ) {
478 - if ( is_array( $formatted[ $key ] ) ) {
479 - $formatted[ $key ][] = $input['value'];
480 - } else {
481 - $formatted[ $key ] = array( $formatted[ $key ], $input['value'] );
482 - }
457 +
458 + if ( ! isset( $formatted[ $key ] ) ) {
459 + $formatted[ $key ] = $input['value'];
460 + continue;
461 + }
462 +
463 + if ( is_array( $formatted[ $key ] ) ) {
464 + $formatted[ $key ][] = $input['value'];
483 465 } else {
484 - $formatted[ $key ] = $input['value'];
466 + $formatted[ $key ] = array( $formatted[ $key ], $input['value'] );
485 467 }
486 468 }
487 469
488 470 parse_str( http_build_query( $formatted ), $form );
@@ -493,19 +475,26 @@
493 475 * This only happens in two cases: For stripe link, and when processing a one-time payment before the entry is created.
494 476 *
495 477 * @since 6.5, introduced in v2.0 of the Stripe add on.
496 478 *
497 - * @param string|int $form_id
479 + * @param int|string $form_id
480 + *
498 481 * @return array
499 482 */
500 483 private static function maybe_create_intents( $form_id ) {
501 484 $intents = array();
485 + $details = self::check_request_params( $form_id );
502 486
503 - $details = self::check_request_params( $form_id );
504 487 if ( is_array( $details ) ) {
488 + $payment = $details['payment'];
489 + $intent = $details['intent'];
490 + $payment_failed = self::payment_failed( $payment, $intent );
491 +
505 492 // Exit early if the request params are set.
506 493 // This way an extra payment intent isn't created for Stripe Link.
507 - return $intents;
494 + if ( ! $payment_failed ) {
495 + return $intents;
496 + }
508 497 }
509 498
510 499 if ( ! FrmStrpLiteAppHelper::call_stripe_helper_class( 'initialize_api' ) ) {
511 500 // Stripe is not configured, so don't create intents.
@@ -515,9 +504,18 @@
515 504 $actions = FrmStrpLiteActionsController::get_actions_before_submit( $form_id );
516 505 self::add_amount_to_actions( $form_id, $actions );
517 506
518 507 foreach ( $actions as $action ) {
508 + if ( is_array( $details ) && self::intent_has_failed_status( $details['intent'] ) ) {
509 + $intents[] = array(
510 + 'id' => $details['intent']->client_secret,
511 + 'action' => $action->ID,
512 + );
513 + continue;
514 + }
515 +
519 516 $intent = self::create_intent( $action );
517 +
520 518 if ( ! is_object( $intent ) ) {
521 519 // A non-object is a string error message.
522 520 // The error gets logged to results.log so we can just skip it.
523 521 // Reasons it could fail is because a payment method type was specified that will not work.
@@ -531,9 +529,9 @@
531 529 $intents[] = array(
532 530 'id' => $intent->client_secret,
533 531 'action' => $action->ID,
534 532 );
535 - }
533 + }//end foreach
536 534
537 535 return $intents;
538 536 }
539 537
@@ -542,14 +540,19 @@
542 540 *
543 541 * @since 3.0 This code was moved out of self::maybe_create_intents into a new function.
544 542 *
545 543 * @param WP_Post $action
544 + *
546 545 * @return mixed
547 546 */
548 547 private static function create_intent( $action ) {
549 - $amount = $action->post_content['amount'];
548 + $amount = $action->post_content['amount'];
549 + $currency = $action->post_content['currency'];
550 +
551 + // phpcs:ignore Universal.Operators.StrictComparisons
550 552 if ( $amount == '000' ) {
551 - $amount = 100; // Create the intent when the form loads.
553 + // Create the intent when the form loads.
554 + $amount = in_array( strtolower( $currency ), array( 'aud', 'cad', 'eur', 'gbp', 'usd' ), true ) ? 100 : 1000;
552 555 }
553 556
554 557 if ( 'recurring' === $action->post_content['type'] ) {
555 558 $payment_method_types = FrmStrpLitePaymentTypeHandler::get_payment_method_types( $action );
@@ -557,12 +560,14 @@
557 560 }
558 561
559 562 $new_charge = array(
560 563 'amount' => $amount,
561 - 'currency' => $action->post_content['currency'],
564 + 'currency' => $currency,
562 565 'metadata' => array( 'action' => $action->ID ),
563 566 );
564 567
568 + $new_charge = self::maybe_add_statement_descriptor( $new_charge );
569 +
565 570 if ( FrmStrpLitePaymentTypeHandler::should_use_automatic_payment_methods( $action ) ) {
566 571 $new_charge['automatic_payment_methods'] = array( 'enabled' => true );
567 572 } else {
568 573 $payment_method_types = FrmStrpLitePaymentTypeHandler::get_payment_method_types( $action );
@@ -572,14 +577,104 @@
572 577 return FrmStrpLiteAppHelper::call_stripe_helper_class( 'create_intent', $new_charge );
573 578 }
574 579
575 580 /**
581 + * Add the statement descriptor to the intent data, if it is valid.
582 + *
583 + * @param array $intent_data
584 + *
585 + * @return array
586 + */
587 + private static function maybe_add_statement_descriptor( $intent_data ) {
588 + $statement_descriptor = self::get_statement_descriptor();
589 +
590 + if ( false !== $statement_descriptor ) {
591 + $intent_data['statement_descriptor'] = $statement_descriptor;
592 + }
593 +
594 + return $intent_data;
595 + }
596 +
597 + /**
598 + * Get the statement descriptor for a payment intent.
599 + *
600 + * @since 6.23
601 + *
602 + * @return false|string False if the statement descriptor is not valid.
603 + */
604 + private static function get_statement_descriptor() {
605 + $name = get_bloginfo( 'name' );
606 +
607 + /**
608 + * Filters the statement descriptor for a payment intent.
609 + * This way a site can use the name they want on their statements.
610 + *
611 + * @since 6.23
612 + *
613 + * @param string $name The name of the site.
614 + */
615 + $name = apply_filters( 'frm_stripe_statement_descriptor', $name );
616 +
617 + if ( ! is_string( $name ) ) {
618 + return false;
619 + }
620 +
621 + $name = self::strip_special_characters_from_statement_descriptor( $name );
622 +
623 + return self::statement_descriptor_is_valid( $name ) ? $name : false;
624 + }
625 +
626 + /**
627 + * Remove the special characters that Stripe doesn't allow in statement descriptors, in case any exist.
628 + *
629 + * @since 6.23
630 + *
631 + * @param string $name The name of the site.
632 + *
633 + * @return string The name with special characters removed.
634 + */
635 + private static function strip_special_characters_from_statement_descriptor( $name ) {
636 + $special_characters = array(
637 + '<',
638 + '>',
639 + '\\',
640 + "'",
641 + '"',
642 + '*',
643 + );
644 + return str_replace( $special_characters, '', $name );
645 + }
646 +
647 + /**
648 + * Stripe includes requirements at https://docs.stripe.com/get-started/account/statement-descriptors
649 + * We need to make sure that the descriptor contains only Latin characters, and that it is between 5 and 22 characters long.
650 + *
651 + * @since 6.23
652 + *
653 + * @param string $name Passed by reference, as this is updated if it is too long.
654 + *
655 + * @return bool
656 + */
657 + private static function statement_descriptor_is_valid( &$name ) {
658 + if ( strlen( $name ) < 5 ) {
659 + return false;
660 + }
661 +
662 + if ( strlen( $name ) > 22 ) {
663 + $name = substr( $name, 0, 22 );
664 + }
665 +
666 + return (bool) preg_match( '/^[a-zA-Z0-9\s\p{P}]+$/', $name );
667 + }
668 +
669 + /**
576 670 * Create a customer and an associated setup intent for a recurring Stripe link payment.
577 671 *
578 672 * @since 6.5, introduced in v3.0 of the Stripe add on.
579 673 *
580 674 * @param array $payment_method_types
581 - * @return object|false
675 + *
676 + * @return false|object
582 677 */
583 678 private static function create_setup_intent( $payment_method_types ) {
584 679 $payment_info = array(
585 680 'user_id' => FrmTransLiteAppHelper::get_user_id_for_current_payment(),
@@ -586,8 +681,9 @@
586 681 );
587 682
588 683 // We need to add a customer to support subscriptions with link.
589 684 $customer = FrmStrpLiteAppHelper::call_stripe_helper_class( 'get_customer', $payment_info );
685 +
590 686 if ( ! is_object( $customer ) ) {
591 687 return false;
592 688 }
593 689
@@ -596,20 +692,22 @@
596 692
597 693 /**
598 694 * @since 6.5, introduced in v2.0 of the Stripe add on.
599 695 *
600 - * @param string|int $form_id
696 + * @param int|string $form_id
601 697 * @param array $actions
698 + *
602 699 * @return void
603 700 */
604 701 private static function add_amount_to_actions( $form_id, &$actions ) {
605 - if ( empty( $actions ) ) {
702 + if ( ! $actions ) {
606 703 return;
607 704 }
705 +
608 706 $form = FrmForm::getOne( $form_id );
609 707
610 708 foreach ( $actions as $k => $action ) {
611 - $amount = self::get_amount_before_submit( compact( 'action', 'form' ) );
709 + $amount = self::get_amount_before_submit( compact( 'action', 'form' ) );
612 710 $actions[ $k ]->post_content['amount'] = $amount;
613 711 }
614 712 }
615 713
@@ -616,12 +714,12 @@
616 714 /**
617 715 * @since 6.5, introduced in v2.0 of the Stripe add on.
618 716 *
619 717 * @param array $atts
718 + *
620 719 * @return string
621 720 */
622 721 private static function get_amount_before_submit( $atts ) {
623 - $amount = $atts['action']->post_content['amount'];
624 722 return FrmStrpLiteActionsController::prepare_amount( $atts['action']->post_content['amount'], $atts );
625 723 }
626 724
627 725 /**
@@ -631,8 +729,9 @@
631 729 *
632 730 * @since 6.5, introduced in v2.0 of the Stripe add on.
633 731 *
634 732 * @param array $atts
733 + *
635 734 * @return string
636 735 */
637 736 public static function return_url( $atts ) {
638 737 $atts = array(
@@ -639,15 +738,9 @@
639 738 'entry' => $atts['entry'],
640 739 );
641 740 self::prepare_success_atts( $atts );
642 741
643 - if ( $atts['conf_method'] === 'redirect' ) {
644 - $redirect = self::get_redirect_url( $atts );
645 - } else {
646 - $redirect = self::get_message_url( $atts );
647 - }
648 -
649 - return $redirect;
742 + return $atts['conf_method'] === 'redirect' ? self::get_redirect_url( $atts ) : self::get_message_url( $atts );
650 743 }
651 744
652 745 /**
653 746 * If the form should redirect, get the url to redirect to.
@@ -654,15 +747,28 @@
654 747 *
655 748 * @since 6.5, introduced in v2.0 of the Stripe add on.
656 749 *
657 750 * @param array $atts {
751 + * The form and entry details.
752 + *
658 753 * @type stdClass $form
659 754 * @type stdClass $entry
660 755 * }
756 + *
661 757 * @return string
662 758 */
663 759 private static function get_redirect_url( $atts ) {
664 - $success_url = trim( $atts['form']->options['success_url'] );
760 + $actions = FrmFormsController::get_met_on_submit_actions( $atts );
761 +
762 + if ( $actions ) {
763 + $success_url = reset( $actions )->post_content['success_url'];
764 + }
765 +
766 + if ( empty( $success_url ) ) {
767 + $success_url = $atts['form']->options['success_url'];
768 + }
769 +
770 + $success_url = trim( $success_url );
665 771 $success_url = apply_filters( 'frm_content', $success_url, $atts['form'], $atts['entry'] );
666 772 $success_url = do_shortcode( $success_url );
667 773 $atts['id'] = $atts['entry']->id;
668 774
@@ -670,19 +776,23 @@
670 776 return apply_filters( 'frm_redirect_url', $success_url, $atts['form'], $atts );
671 777 }
672 778
673 779 /**
674 - * If the form should should a message, apend it to the success url.
780 + * If the form should should a message, append it to the success url.
675 781 *
676 782 * @since 6.5, introduced in v2.0 of the Stripe add on.
677 783 *
678 784 * @param array $atts
785 + *
786 + * @return string
679 787 */
680 788 private static function get_message_url( $atts ) {
681 789 $url = self::get_referer_url( $atts['entry_id'], false );
790 +
682 791 if ( false === $url ) {
683 792 $url = FrmAppHelper::get_server_value( 'HTTP_REFERER' );
684 793 }
794 +
685 795 return add_query_arg( array( 'frmstrp' => $atts['entry_id'] ), $url );
686 796 }
687 797
688 798 /**
@@ -687,11 +797,12 @@
687 797
688 798 /**
689 799 * @since 6.5
690 800 *
691 - * @param string|int $entry_id
801 + * @param int|string $entry_id
692 802 * @param bool $delete_meta
693 - * @return string|false
803 + *
804 + * @return false|string
694 805 */
695 806 public static function get_referer_url( $entry_id, $delete_meta = true ) {
696 807 $row = FrmDb::get_row(
697 808 'frm_item_metas',
@@ -701,8 +812,9 @@
701 812 'meta_value LIKE' => '{"referer":',
702 813 ),
703 814 'id, meta_value'
704 815 );
816 +
705 817 if ( ! $row ) {
706 818 return false;
707 819 }
708 820
@@ -712,9 +824,12 @@
712 824 if ( ! is_array( $meta ) || empty( $meta['referer'] ) ) {
713 825 return false;
714 826 }
715 827
716 - self::delete_temporary_referer_meta( (int) $row->id );
828 + if ( $delete_meta ) {
829 + self::delete_temporary_referer_meta( (int) $row->id );
830 + }
831 +
717 832 return $meta['referer'];
718 833 }
719 834
720 835 /**
@@ -720,11 +835,43 @@
720 835 /**
721 836 * Delete the referer meta as we'll no longer need it.
722 837 *
723 838 * @param int $row_id
839 + *
724 840 * @return void
725 841 */
726 842 private static function delete_temporary_referer_meta( $row_id ) {
727 843 global $wpdb;
728 844 $wpdb->delete( $wpdb->prefix . 'frm_item_metas', array( 'id' => $row_id ) );
845 + }
846 +
847 + /**
848 + * Check if a payment or setup intent has failed.
849 + *
850 + * @since 6.5.1
851 + *
852 + * @param object $intent
853 + *
854 + * @return bool
855 + */
856 + private static function intent_has_failed_status( $intent ) {
857 + return in_array( $intent->status, array( 'requires_source', 'requires_payment_method', 'canceled' ), true );
858 + }
859 +
860 + /**
861 + * Check if a payment failed.
862 + *
863 + * @since 6.8
864 + *
865 + * @param object $payment
866 + * @param object $intent
867 + *
868 + * @return bool
869 + */
870 + public static function payment_failed( $payment, $intent ) {
871 + if ( self::intent_has_failed_status( $intent ) ) {
872 + return true;
873 + }
874 + // The $intent will be "succeeded" with a failed payment when testing with the 4000000000000341 credit card.
875 + return 'payment_failed' === FrmAppHelper::simple_get( 'frm_link_error' ) && 'failed' === $payment->status;
729 876 }
730 877 }