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
formidable / paypal / controllers / FrmPayPalLiteAppController.php

FrmPayPalLiteAppController.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More trunk, at paypal/controllers/FrmPayPalLiteAppController.php

859 lines 25.0 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 class FrmPayPalLiteAppController {
7
8 /**
9 * Add the gateway for compatibility with the Payments submodule.
10 * This adds the PayPal checkbox option to the list of gateways.
11 *
12 * @param array $gateways
13 *
14 * @return array
15 */
16 public static function add_gateway( $gateways ) {
17 $gateways['paypal'] = array(
18 'label' => 'PayPal',
19 'user_label' => __( 'Payment', 'formidable' ),
20 'class' => 'PayPalLite',
21 'recurring' => true,
22 'include' => array(
23 'billing_first_name',
24 'billing_last_name',
25 'credit_card',
26 'billing_address',
27 ),
28 );
29 return $gateways;
30 }
31
32 /**
33 * Handle the request to initialize with PayPal Api
34 */
35 public static function handle_oauth() {
36 FrmAppHelper::permission_check( 'frm_change_settings' );
37
38 if ( ! check_admin_referer( 'frm_ajax', 'nonce' ) ) {
39 wp_send_json_error();
40 }
41
42 $redirect_url = FrmPayPalLiteConnectHelper::get_oauth_redirect_url();
43
44 if ( false === $redirect_url ) {
45 wp_send_json_error( 'Unable to connect to PayPal successfully' );
46 }
47
48 $response_data = array(
49 'redirect_url' => $redirect_url,
50 );
51 wp_send_json_success( $response_data );
52 }
53
54 public static function handle_disconnect() {
55 FrmAppHelper::permission_check( 'frm_change_settings' );
56
57 if ( ! check_admin_referer( 'frm_ajax', 'nonce' ) ) {
58 wp_send_json_error();
59 }
60
61 FrmPayPalLiteConnectHelper::handle_disconnect();
62 wp_send_json_success();
63 }
64
65 /**
66 * Get the current amount for a PayPal action via AJAX.
67 * Used to update the Pay Later messaging when price fields change.
68 *
69 * @since 6.31
70 *
71 * @return void
72 */
73 public static function get_amount() {
74 check_ajax_referer( 'frm_paypal_ajax', 'nonce' );
75
76 $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
77
78 if ( ! $form_id ) {
79 wp_send_json_error( __( 'Invalid form ID', 'formidable' ) );
80 }
81
82 $actions = FrmPayPalLiteActionsController::get_actions_before_submit( $form_id );
83
84 if ( ! $actions ) {
85 wp_send_json_error( __( 'No PayPal actions found for this form', 'formidable' ) );
86 }
87
88 $action = reset( $actions );
89 $amount = self::get_amount_value_for_verification( $action );
90
91 wp_send_json_success( array( 'amount' => $amount ) );
92 }
93
94 /**
95 * Extract pricing data from posted form values.
96 *
97 * @since 6.31
98 *
99 * @param int $form_id The form ID.
100 *
101 * @return array Array of products with prices and quantities.
102 */
103 // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
104 private static function get_pricing_data_from_posted_values( $form_id ) {
105 $products = array();
106 $fields = FrmField::get_all_for_form( $form_id );
107
108 if ( ! $fields ) {
109 return $products;
110 }
111
112 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
113 $posted_data = $_POST['item_meta'] ?? array();
114
115 foreach ( $fields as $field ) {
116 if ( ! in_array( $field->type, array( 'product', 'quantity', 'total' ), true ) ) {
117 continue;
118 }
119
120 $field_id = $field->id;
121 $value = $posted_data[ $field_id ] ?? '';
122
123 if ( ! $value ) {
124 continue;
125 }
126
127 if ( 'product' === $field->type ) {
128 $product_field = FrmFieldFactory::get_field_object( $field );
129
130 if ( method_exists( $product_field, 'get_posted_price' ) ) {
131 $price = $product_field->get_posted_price( $value );
132
133 if ( $price ) {
134 $products[] = array(
135 'name' => $field->name,
136 'price' => is_array( $price ) ? array_sum( $price ) : $price,
137 'quantity' => 1,
138 'type' => 'product',
139 'field_id' => $field_id,
140 );
141 }
142 }
143 } elseif ( 'quantity' === $field->type ) {
144 $quantity = is_numeric( $value ) ? (int) $value : 1;
145 // Quantity fields are linked to product fields via product_field setting
146 $product_field_ids = FrmField::get_option( $field, 'product_field' );
147
148 if ( $product_field_ids ) {
149 // This quantity will be associated with its product field
150 // We'll handle the association in the product processing
151 $products[] = array(
152 'name' => $field->name,
153 'price' => 0,
154 // Quantity fields don't have price
155 'quantity' => $quantity,
156 'type' => 'quantity',
157 'product_field_ids' => (array) $product_field_ids,
158 );
159 }
160 }//end if
161 }//end foreach
162
163 // Associate quantity fields with their products
164 $final_products = array();
165 $product_quantities = array();
166
167 foreach ( $products as $item ) {
168 if ( 'quantity' !== $item['type'] ) {
169 continue;
170 }
171
172 foreach ( $item['product_field_ids'] as $product_field_id ) {
173 $product_quantities[ $product_field_id ] = $item['quantity'];
174 }
175 }
176
177 foreach ( $products as $item ) {
178 if ( 'product' !== $item['type'] ) {
179 continue;
180 }
181
182 $quantity = $product_quantities[ $item['field_id'] ] ?? 1;
183 $final_products[] = array(
184 'name' => $item['name'],
185 'price' => $item['price'],
186 'quantity' => $quantity,
187 );
188 }
189
190 return $final_products;
191 }
192
193 /**
194 * Create a PayPal order via AJAX.
195 */
196 public static function create_order() {
197 check_ajax_referer( 'frm_paypal_ajax', 'nonce' );
198
199 // Check if PayPal is connected before attempting to create an order.
200 $connection_check = self::check_paypal_connection();
201
202 if ( is_wp_error( $connection_check ) ) {
203 wp_send_json_error( $connection_check->get_error_message() );
204 }
205
206 $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
207
208 if ( ! $form_id ) {
209 wp_send_json_error( __( 'Invalid form ID', 'formidable' ) );
210 }
211
212 $payment_source = FrmAppHelper::get_post_param( 'payment_source', '', 'sanitize_text_field' );
213
214 if ( ! $payment_source ) {
215 wp_send_json_error( __( 'No payment source provided', 'formidable' ) );
216 }
217
218 if ( ! in_array( $payment_source, self::get_valid_payment_sources(), true ) ) {
219 wp_send_json_error( __( 'Invalid payment source', 'formidable' ) );
220 }
221
222 $actions = FrmPayPalLiteActionsController::get_actions_before_submit( $form_id );
223
224 if ( ! $actions ) {
225 wp_send_json_error( __( 'No PayPal actions found for this form', 'formidable' ) );
226 }
227
228 $action = reset( $actions );
229 $amount = self::get_amount_value_for_verification( $action );
230 $payer = self::get_payer_data_from_posted_values( $action );
231 $shipping = self::get_shipping_data_from_posted_values( $action );
232 $shipping_preference = self::get_shipping_preference( $action );
233 $pricing_data = self::get_pricing_data_from_posted_values( $form_id );
234 $description = self::process_shortcodes_for_action( $action->post_content['description'] ?? '', $action );
235
236 if ( 0.0 === floatval( $amount ) ) {
237 wp_send_json_error( __( 'Order amount cannot be zero.', 'formidable' ) );
238 }
239
240 // PayPal expects the amount in a format like 10.00, so format it.
241 $amount = number_format( floatval( $amount ), 2, '.', '' );
242 $currency = strtoupper( $action->post_content['currency'] );
243
244 $order_response = FrmPayPalLiteConnectHelper::create_order( $amount, $currency, $payment_source, $payer, $shipping_preference, $pricing_data, $shipping, $description );
245
246 if ( class_exists( 'FrmLog' ) ) {
247 $log = new FrmLog();
248 $log->add(
249 array(
250 'title' => 'PayPal Order Response',
251 'content' => print_r( $order_response, true ),
252 )
253 );
254 }
255
256 if ( false === $order_response ) {
257 $error = FrmPayPalLiteConnectHelper::get_latest_error_from_paypal_api();
258 wp_send_json_error( self::format_paypal_error( $error, 'Failed to create PayPal order' ) );
259 }
260
261 if ( ! isset( $order_response->order_id ) ) {
262 // Check if the response is a structured error with debug_id
263 if ( isset( $order_response->message ) && isset( $order_response->debug_id ) ) {
264 wp_send_json_error( self::format_paypal_error( $order_response->message . '{{debug_id:' . $order_response->debug_id . '}}', 'Failed to create PayPal order' ) );
265 }
266
267 wp_send_json_error( 'Failed to create PayPal order' );
268 }
269
270 wp_send_json_success( array( 'orderID' => $order_response->order_id ) );
271 }
272
273 /**
274 * @since 6.31
275 *
276 * @param WP_Post $action
277 *
278 * @return array
279 */
280 private static function get_payer_data_from_posted_values( $action ) {
281 $email_setting = $action->post_content['email'];
282 $first_name_setting = $action->post_content['billing_first_name'];
283 $last_name_setting = $action->post_content['billing_last_name'];
284
285 // @phpstan-ignore-next-line
286 $address_setting = $action->post_content['billing_address'] ?? '';
287
288 $entry = self::generate_false_entry();
289 $first_name = $first_name_setting && isset( $entry->metas[ $first_name_setting ] ) ? $entry->metas[ $first_name_setting ] : '';
290 $last_name = $last_name_setting && isset( $entry->metas[ $last_name_setting ] ) ? $entry->metas[ $last_name_setting ] : '';
291 $address = $address_setting && isset( $entry->metas[ $address_setting ] ) ? $entry->metas[ $address_setting ] : '';
292
293 if ( is_array( $first_name ) && isset( $first_name['first'] ) ) {
294 $first_name = $first_name['first'];
295 }
296
297 if ( is_array( $last_name ) && isset( $last_name['last'] ) ) {
298 $last_name = $last_name['last'];
299 }
300
301 $payer = array();
302
303 if ( $first_name && $last_name ) {
304 $payer['name'] = array(
305 'given_name' => $first_name,
306 'surname' => $last_name,
307 );
308 }
309
310 if ( $email_setting ) {
311 $shortcode_atts = array(
312 'entry' => $entry,
313 'form' => $action->menu_order,
314 'value' => $email_setting,
315 );
316 $payer['email_address'] = FrmTransLiteAppHelper::process_shortcodes( $shortcode_atts );
317 }
318
319 self::maybe_add_address_data( $payer, $address, (int) $address_setting );
320
321 return $payer;
322 }
323
324 /**
325 * Build shipping data from the action's shipping field settings.
326 *
327 * @since 6.31
328 *
329 * @param WP_Post $action
330 *
331 * @return array
332 */
333 private static function get_shipping_data_from_posted_values( $action ) {
334 $settings = $action->post_content;
335 $email_setting = ! empty( $settings['shipping_email'] ) ? $settings['shipping_email'] : '';
336 $first_name_setting = ! empty( $settings['shipping_first_name'] ) ? $settings['shipping_first_name'] : '';
337 $last_name_setting = ! empty( $settings['shipping_last_name'] ) ? $settings['shipping_last_name'] : '';
338 $address_setting = ! empty( $settings['shipping_address'] ) ? $settings['shipping_address'] : '';
339
340 if ( ! $email_setting && ! $first_name_setting && ! $last_name_setting && ! $address_setting ) {
341 return array();
342 }
343
344 $entry = self::generate_false_entry();
345 $first_name = $first_name_setting && isset( $entry->metas[ $first_name_setting ] ) ? $entry->metas[ $first_name_setting ] : '';
346 $last_name = $last_name_setting && isset( $entry->metas[ $last_name_setting ] ) ? $entry->metas[ $last_name_setting ] : '';
347
348 if ( is_array( $first_name ) && isset( $first_name['first'] ) ) {
349 $first_name = $first_name['first'];
350 }
351
352 if ( is_array( $last_name ) && isset( $last_name['last'] ) ) {
353 $last_name = $last_name['last'];
354 }
355
356 $shipping = array();
357
358 if ( $email_setting ) {
359 $shortcode_atts = array(
360 'entry' => $entry,
361 'form' => $action->menu_order,
362 'value' => $email_setting,
363 );
364 $email = FrmTransLiteAppHelper::process_shortcodes( $shortcode_atts );
365
366 if ( $email ) {
367 $shipping['email_address'] = $email;
368 }
369 }
370
371 if ( $first_name || $last_name ) {
372 $shipping['name'] = array(
373 'full_name' => trim( $first_name . ' ' . $last_name ),
374 );
375 }
376
377 if ( ! $address_setting ) {
378 return $shipping;
379 }
380
381 $address = $entry->metas[ $address_setting ] ?? '';
382 $formatted_address = self::format_address_for_paypal( $address, (int) $address_setting );
383
384 if ( $formatted_address ) {
385 $shipping['address'] = $formatted_address;
386 }
387
388 return $shipping;
389 }
390
391 /**
392 * Format a Formidable address field value into a PayPal address array.
393 *
394 * @since 6.31
395 *
396 * @param mixed $address The address field value.
397 * @param int $address_field_id The field ID.
398 *
399 * @return array|false The formatted address array, or false if invalid.
400 */
401 private static function format_address_for_paypal( $address, $address_field_id ) {
402 if ( ! is_array( $address ) || ! isset( $address['line1'] ) ) {
403 return false;
404 }
405
406 $address_field = FrmField::getOne( $address_field_id );
407
408 if ( ! $address_field ) {
409 return false;
410 }
411
412 if ( 'us' === $address_field->field_options['address_type'] ) {
413 $country_code = 'US';
414 } else {
415 $country_code = FrmAddressesController::get_country_code( $address['country'] );
416 }
417
418 if ( ! $address['line1'] || ! $address['city'] || ! $address['state'] || ! $address['zip'] || ! $country_code ) {
419 return false;
420 }
421
422 return array(
423 'address_line_1' => $address['line1'],
424 'address_line_2' => $address['line2'] ?? '',
425 'admin_area_2' => $address['city'],
426 'admin_area_1' => $address['state'],
427 'postal_code' => $address['zip'],
428 'country_code' => $country_code,
429 );
430 }
431
432 /**
433 * @since 6.31
434 *
435 * @param WP_Post $action
436 *
437 * @return string
438 */
439 private static function get_shipping_preference( $action ) {
440 $setting = ! empty( $action->post_content['shipping_preference'] ) ? $action->post_content['shipping_preference'] : 'use_paypal_account_data';
441
442 switch ( $setting ) {
443 case 'use_address_field_data':
444 return 'SET_PROVIDED_ADDRESS';
445
446 case 'no_shipping':
447 return 'NO_SHIPPING';
448
449 case 'use_paypal_account_data':
450 default:
451 return 'GET_FROM_FILE';
452 }
453 }
454
455 /**
456 * @since 6.31
457 *
458 * @param array $payer
459 * @param array $address
460 * @param int $address_field_id
461 *
462 * @return void
463 */
464 private static function maybe_add_address_data( &$payer, $address, $address_field_id ) {
465 if ( ! is_array( $address ) || ! isset( $address['line1'] ) ) {
466 return;
467 }
468
469 $address_field = FrmField::getOne( $address_field_id );
470
471 if ( ! $address_field ) {
472 return;
473 }
474
475 if ( 'us' === $address_field->field_options['address_type'] ) {
476 $country_code = 'US';
477 } else {
478 $country_code = FrmAddressesController::get_country_code( $address['country'] );
479 }
480
481 if ( ! $address['line1'] || ! $address['city'] || ! $address['state'] || ! $address['zip'] || ! $country_code ) {
482 return;
483 }
484
485 $payer['address'] = array(
486 'address_line_1' => $address['line1'],
487 'address_line_2' => $address['line2'] ?? '',
488 'admin_area_2' => $address['city'],
489 'admin_area_1' => $address['state'],
490 'postal_code' => $address['zip'],
491 'country_code' => $country_code,
492 );
493 }
494
495 /**
496 * @since 6.31
497 *
498 * @return array<string>
499 */
500 private static function get_valid_payment_sources() {
501 $sources = array(
502 'card',
503 'paypal',
504 'apple_pay',
505 'bancontact',
506 'blik',
507 'eps',
508 'giropay',
509 'ideal',
510 'mybank',
511 'p24',
512 'sepa',
513 'sofort',
514 'trustly',
515 'venmo',
516 'paylater',
517 'google_pay',
518 );
519
520 /**
521 * @since 6.31
522 *
523 * @param array<string> $sources
524 */
525 return apply_filters( 'frm_paypal_valid_payment_sources', $sources );
526 }
527
528 /**
529 * Get the amount value for verification.
530 *
531 * @param WP_Post $action
532 *
533 * @return string
534 */
535 private static function get_amount_value_for_verification( $action ) {
536 $amount = $action->post_content['amount'];
537
538 if ( ! str_contains( $amount, '[' ) ) {
539 return $amount;
540 }
541
542 $form = FrmForm::getOne( $action->menu_order );
543
544 if ( ! $form ) {
545 return $amount;
546 }
547
548 // Update amount based on field shortcodes.
549 $entry = self::generate_false_entry();
550
551 return number_format( floatval( FrmPayPalLiteActionsController::prepare_amount( $amount, compact( 'form', 'entry', 'action' ) ) ) / 100, 2, '.', '' );
552 }
553
554 /**
555 * Create an entry object with posted values.
556 *
557 * @since 6.31
558 *
559 * @return stdClass
560 */
561 private static function generate_false_entry() {
562 $entry = new stdClass();
563 $entry->post_id = 0;
564 $entry->id = 0;
565 $entry->item_key = '';
566 $entry->metas = array();
567
568 // phpcs:ignore WordPress.Security.NonceVerification.Missing
569 foreach ( $_POST as $k => $v ) {
570 $k = sanitize_text_field( stripslashes( $k ) );
571 $v = wp_unslash( $v );
572
573 if ( $k !== 'item_meta' ) {
574 FrmAppHelper::sanitize_value( 'wp_kses_post', $v );
575 $entry->{$k} = $v;
576 continue;
577 }
578
579 if ( ! is_array( $v ) ) {
580 continue;
581 }
582
583 foreach ( $v as $f => $value ) {
584 FrmAppHelper::sanitize_value( 'wp_kses_post', $value );
585 $entry->metas[ absint( $f ) ] = $value;
586 }
587 }
588
589 return $entry;
590 }
591
592 /**
593 * Create a PayPal subscription object via AJAX.
594 *
595 * @return void
596 */
597 public static function create_subscription() {
598 check_ajax_referer( 'frm_paypal_ajax', 'nonce' );
599
600 // Check if PayPal is connected before attempting to create a subscription.
601 $connection_check = self::check_paypal_connection();
602
603 if ( is_wp_error( $connection_check ) ) {
604 wp_send_json_error( $connection_check->get_error_message() );
605 }
606
607 $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
608
609 if ( ! $form_id ) {
610 wp_send_json_error( __( 'Invalid form ID', 'formidable' ) );
611 }
612
613 $actions = FrmPayPalLiteActionsController::get_actions_before_submit( $form_id );
614
615 if ( ! $actions ) {
616 wp_send_json_error( __( 'No PayPal actions found for this form', 'formidable' ) );
617 }
618
619 $action = reset( $actions );
620 $amount = self::get_amount_value_for_verification( $action );
621
622 // PayPal expects the amount in a format like 10.00, so format it.
623 $amount = number_format( floatval( $amount ), 2, '.', '' );
624 $currency = strtoupper( $action->post_content['currency'] );
625
626 // Pass $product_name, $interval and $interval_count to the helper
627 // As well as trial period and the maximum number of payments.
628 // Also send subscriber email and description.
629 $product_name = self::process_shortcodes_for_action( $action->post_content['product_name'] ?? '', $action );
630 $interval = $action->post_content['interval'] ?? '';
631 $interval_count = $action->post_content['interval_count'] ?? 1;
632 $trial_period = $action->post_content['trial_interval_count'] ?? '';
633 $payment_limit = $action->post_content['payment_limit'] ?? '';
634 $product_type = $action->post_content['product_type'] ?? 'SERVICE';
635 $description = self::process_shortcodes_for_action( $action->post_content['description'] ?? '', $action );
636
637 if ( ! $product_name ) {
638 wp_send_json_error( __( 'A product name is required for subscriptions. Please update your PayPal action settings.', 'formidable' ) );
639 }
640
641 if ( ! $interval ) {
642 wp_send_json_error( __( 'A billing interval is required for subscriptions. Please update your PayPal action settings.', 'formidable' ) );
643 }
644
645 if ( ! $amount || '0.00' === $amount ) {
646 wp_send_json_error( __( 'A valid amount is required for subscriptions. Zero amounts are not allowed.', 'formidable' ) );
647 }
648
649 $data = array(
650 'amount' => $amount,
651 'currency' => $currency,
652 'product_name' => $product_name,
653 'product_type' => $product_type,
654 'interval' => $interval,
655 'interval_count' => $interval_count,
656 'trial_period' => $trial_period,
657 'payment_limit' => $payment_limit,
658 'payer' => self::get_payer_data_from_posted_values( $action ),
659 'shipping_preference' => self::get_shipping_preference( $action ),
660 'description' => $description,
661 );
662
663 $response = FrmPayPalLiteConnectHelper::create_subscription( $data );
664
665 if ( false === $response ) {
666 $error = FrmPayPalLiteConnectHelper::get_latest_error_from_paypal_api();
667 wp_send_json_error( self::format_paypal_error( $error, 'Failed to create PayPal subscription' ) );
668 }
669
670 // Check if response is a structured error with message and debug_id (array or object)
671 if ( is_object( $response ) && isset( $response->message ) && isset( $response->debug_id ) ) {
672 wp_send_json_error( $response );
673 }
674
675 if ( ! isset( $response->subscription_id ) ) {
676 wp_send_json_error( 'Failed to create PayPal subscription' );
677 }
678
679 wp_send_json_success( array( 'subscriptionID' => $response->subscription_id ) );
680 }
681
682 /**
683 * Handle a PayPal error reported by the frontend JavaScript.
684 *
685 * Logs the debug ID and error details, then returns a display message
686 * that includes the debug ID only when the current user has permission.
687 *
688 * @since 6.31
689 *
690 * @return void
691 */
692 public static function report_error() {
693 check_ajax_referer( 'frm_paypal_ajax', 'nonce' );
694
695 $error_message = substr( FrmAppHelper::get_post_param( 'error_message', '', 'sanitize_text_field' ), 0, 500 );
696 $debug_id = substr( FrmAppHelper::get_post_param( 'debug_id', '', 'sanitize_text_field' ), 0, 50 );
697 $context = substr( FrmAppHelper::get_post_param( 'context', '', 'sanitize_text_field' ), 0, 100 );
698
699 if ( $debug_id ) {
700 self::log_paypal_debug_id( $debug_id, $error_message, $context );
701 }
702
703 $display_message = $error_message ? $error_message : __( 'Payment failed. Please try again.', 'formidable' );
704
705 if ( $debug_id && current_user_can( 'frm_change_settings' ) ) {
706 $display_message .= "\n\nDebug ID: " . $debug_id;
707 }
708
709 wp_send_json_success( array( 'message' => $display_message ) );
710 }
711
712 /**
713 * Log a PayPal debug ID to the recent debug IDs option.
714 *
715 * Stores up to 20 entries, newest first.
716 *
717 * @since 6.31
718 *
719 * @param string $debug_id The PayPal debug ID.
720 * @param string $error_message The associated error message.
721 * @param string $context The context where the error occurred (e.g. 'create_order', 'card_submit').
722 *
723 * @return void
724 */
725 public static function log_paypal_debug_id( $debug_id, $error_message = '', $context = '' ) {
726 $option_name = 'frm_paypal_debug_ids';
727 $max_entries = 20;
728 $entries = get_option( $option_name, array() );
729
730 if ( ! is_array( $entries ) ) {
731 $entries = array();
732 }
733
734 if ( $error_message && preg_match( '/^[A-Z_]+$/', $error_message ) ) {
735 $prefixes = array( 'REFUND_FAILED_', 'REFUND_' );
736 $reason = str_replace( $prefixes, '', $error_message );
737
738 if ( $reason === $error_message ) {
739 $error_message = ucwords( strtolower( str_replace( '_', ' ', $error_message ) ) );
740 } else {
741 $error_message = 'Refund Failed (' . ucwords( strtolower( str_replace( '_', ' ', $reason ) ) ) . ')';
742 }
743 }
744
745 $entry = array(
746 'debug_id' => $debug_id,
747 'error_message' => $error_message,
748 'context' => $context,
749 'timestamp' => gmdate( 'Y-m-d H:i:s' ),
750 );
751
752 array_unshift( $entries, $entry );
753 $entries = array_slice( $entries, 0, $max_entries );
754
755 update_option( $option_name, $entries, false );
756 }
757
758 /**
759 * Process shortcodes in an action setting value using posted form data.
760 *
761 * @since 6.31
762 *
763 * @param string $value The value that may contain shortcodes.
764 * @param WP_Post $action The payment action.
765 *
766 * @return string
767 */
768 private static function process_shortcodes_for_action( $value, $action ) {
769 if ( ! str_contains( $value, '[' ) ) {
770 return $value;
771 }
772
773 $form = FrmForm::getOne( $action->menu_order );
774
775 if ( ! $form ) {
776 return $value;
777 }
778
779 $entry = self::generate_false_entry();
780
781 return FrmTransLiteAppHelper::process_shortcodes(
782 array(
783 'value' => $value,
784 'form' => $form,
785 'entry' => $entry,
786 )
787 );
788 }
789
790 /**
791 * Check if PayPal is connected before attempting to create an order or subscription.
792 *
793 * @since 6.31
794 *
795 * @return true|WP_Error True if connected, WP_Error with message if not connected.
796 */
797 private static function check_paypal_connection() {
798 $merchant_id = FrmPayPalLiteConnectHelper::get_merchant_id();
799
800 if ( ! $merchant_id ) {
801 $message = __( 'PayPal is not connected. Please connect your PayPal account to process payments.', 'formidable' );
802
803 if ( current_user_can( 'frm_change_settings' ) ) {
804 $message .= ' ' . __( 'You can connect PayPal in Global Settings, under the Payments section.', 'formidable' );
805 }
806
807 return new WP_Error( 'paypal_not_connected', $message );
808 }
809
810 return true;
811 }
812
813 /**
814 * Parse a PayPal API error string and conditionally include the debug ID.
815 *
816 * The PayPal API addon embeds debug IDs using a {{debug_id:...}} delimiter.
817 * This method strips that token and appends a human-readable debug ID line
818 * only for users who can edit forms.
819 *
820 * @since 6.31
821 *
822 * @param array|string|null $error The error string from the PayPal API, possibly containing a debug ID token.
823 * @param string $fallback The fallback message when no error is available.
824 *
825 * @return string
826 */
827 private static function format_paypal_error( $error, $fallback ) {
828 if ( ! $error ) {
829 return $fallback;
830 }
831
832 // If error is already a structured array with message and debug_id, return it directly
833 if ( is_array( $error ) && isset( $error['message'] ) && isset( $error['debug_id'] ) ) {
834 return $error;
835 }
836
837 if ( ! preg_match( '/\{\{debug_id:([^}]+)\}\}/', $error, $matches ) ) {
838 return $error;
839 }
840
841 $clean_message = str_replace( $matches[0], '', $error );
842 $clean_message = trim( $clean_message );
843
844 // Always return structured error with debug_id so JavaScript can extract and send it
845 return array(
846 'message' => $clean_message ? $clean_message : $fallback,
847 'debug_id' => $matches[1],
848 );
849 }
850
851 /**
852 * @deprecated 6.32.1
853 */
854 public static function create_vault_setup_token() {
855 _deprecated_function( __METHOD__, '6.32.1' );
856 wp_send_json_error( 'This API endpoint is no longer in use.' );
857 }
858 }
859