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 / helpers / FrmPayPalLiteConnectHelper.php

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

1,169 lines 31.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:ignore SlevomatCodingStandard.Files.FileLength.FileTooLong
3 if ( ! defined( 'ABSPATH' ) ) {
4 die( 'You are not allowed to call this page directly.' );
5 }
6
7 class FrmPayPalLiteConnectHelper {
8
9 /**
10 * Track the latest error when calling the PayPal API.
11 *
12 * @since 6.31
13 *
14 * @var string|null
15 */
16 public static $latest_error_from_paypal_api = '';
17
18 /**
19 * Track the latest debug ID from PayPal API responses.
20 *
21 * @since 6.31
22 *
23 * @var string
24 */
25 private static $latest_debug_id_from_paypal_api = '';
26
27 /**
28 * @return void
29 */
30 public static function render_settings_container() {
31 $settings = FrmPayPalLiteAppHelper::get_settings();
32
33 self::register_settings_scripts();
34
35 FrmPayPalLiteAppHelper::fee_education( 'paypal-global-settings-tip' );
36
37 include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/connect-settings-container.php';
38 }
39
40 public static function handle_render_seller_status() {
41 FrmAppHelper::permission_check( 'frm_change_settings' );
42
43 if ( ! check_admin_referer( 'frm_ajax', 'nonce' ) ) {
44 wp_send_json_error();
45 }
46
47 ob_start();
48 $success = self::render_seller_status();
49 $response = ob_get_clean();
50
51 if ( ! $success ) {
52 wp_send_json_error( $response );
53 }
54
55 wp_send_json_success( $response );
56 }
57
58 /**
59 * @since 6.31
60 *
61 * @return bool
62 */
63 public static function render_seller_status() {
64 FrmAppHelper::permission_check( 'frm_change_settings' );
65
66 if ( ! check_admin_referer( 'frm_ajax', 'nonce' ) ) {
67 self::render_error( __( 'Invalid nonce.', 'formidable' ) );
68 return false;
69 }
70
71 $mode = self::get_mode_value_from_post();
72 $merchant_id = self::get_merchant_id( $mode );
73
74 if ( ! $merchant_id ) {
75 // Do not render any message when not connected.
76 // And return true so it does not try to handle it as an error.
77 return true;
78 }
79
80 $status = self::get_seller_status();
81
82 /*
83 $status = new stdClass();
84 $status->payments_receivable = true;
85 $status->primary_email_confirmed = true;
86 $status->oauth_integrations = true;
87 $status->primary_email = '[email protected]';
88 */
89
90 if ( ! is_object( $status ) ) {
91 self::render_error( __( 'Unable to retrieve seller status.', 'formidable' ), '', $merchant_id );
92 return false;
93 }
94
95 $email = $status->primary_email ?? '';
96 $paypal_settings_url = self::get_paypal_account_settings_url( $mode );
97
98 if ( empty( $status->primary_email_confirmed ) ) {
99 self::render_error( __( 'Primary email not confirmed.', 'formidable' ), $email, $merchant_id, $paypal_settings_url );
100 return false;
101 }
102
103 if ( ! $status->payments_receivable ) {
104 self::render_error( __( 'Payments are not receivable.', 'formidable' ), $email, $merchant_id, $paypal_settings_url );
105 return false;
106 }
107
108 if ( ! $status->oauth_integrations ) {
109 self::render_error(
110 __( 'OAuth integrations are not enabled. Please finish connecting your PayPal account.', 'formidable' ),
111 $email,
112 $merchant_id,
113 '',
114 $mode
115 );
116 return false;
117 }
118
119 // OAuth integrations are valid. Clear any stored tracking_id from a prior incomplete onboarding.
120 delete_option( self::get_tracking_id_option_name( $mode ) );
121
122 $product = self::check_for_product( $status->products, 'PPCP_CUSTOM' );
123 $only_supports_checkout_button = false;
124
125 if ( ! $product || empty( $product->capabilities ) ) {
126 $product = self::check_for_product( $status->products, 'EXPRESS_CHECKOUT' );
127
128 if ( ! $product ) {
129 self::render_error( __( 'No data was found for expected PayPal product.', 'formidable' ), $email, $merchant_id );
130 return false;
131 }
132
133 if ( 'ACTIVE' !== $product->status ) {
134 self::render_error( __( 'PayPal Checkout is not available.', 'formidable' ), $email, $merchant_id );
135 return false;
136 }
137
138 $only_supports_checkout_button = true;
139 }
140
141 if ( $email ) {
142 update_option( self::get_paypal_seller_status_option_name( $mode ), $status, false );
143 }
144
145 echo '<div class="frm_message">';
146 esc_html_e( 'Your seller status is valid.', 'formidable' );
147 echo '<br>';
148
149 self::echo_email( $email );
150 self::echo_merchant_id( $merchant_id );
151
152 echo '<br>';
153 echo '<br>';
154 echo '<b>' . esc_html__( 'Enabled scopes:', 'formidable' ) . '</b>';
155 echo '<ul style="list-style: unset; padding-left: 15px; margin-top: 0; margin-bottom: 0;">';
156 echo '<li>';
157 /**
158 * @var string[] $scopes
159 */
160 $scopes = $status->oauth_integrations[0]->oauth_third_party[0]->scopes;
161 echo implode( '</li><li>', array_map( 'esc_html', $scopes ) );
162 echo '</li>';
163 echo '</ul>';
164
165 echo '<br>';
166 echo '<b>' . esc_html__( 'Enabled capabilities:', 'formidable' ) . '</b>';
167 echo '<ul style="list-style: unset; padding-left: 15px; margin-top: 0; margin-bottom: 0;">';
168
169 echo '<li>' . esc_html__( 'PayPal Checkout', 'formidable' ) . '</li>';
170
171 $can_process_card_fields = ! $only_supports_checkout_button && in_array( 'CUSTOM_CARD_PROCESSING', $product->capabilities, true );
172
173 if ( $can_process_card_fields ) {
174 echo '<li>' . esc_html__( 'Card Processing', 'formidable' ) . '</li>';
175 }
176 echo '</ul>';
177
178 if ( $can_process_card_fields ) {
179 self::render_acdc_vetting_status( $product );
180 }
181
182 echo '</div>';
183
184 return true;
185 }
186
187 /**
188 * @since 6.31
189 *
190 * @param bool|object $product
191 *
192 * @return void
193 */
194 private static function render_acdc_vetting_status( $product ) {
195 $vetting_status = $product && ! empty( $product->vetting_status ) ? $product->vetting_status : 'NOT_SET';
196
197 echo '<br>';
198 echo '<b>' . esc_html__( 'ACDC Application Vetting Status:', 'formidable' ) . '</b>';
199 echo '&nbsp;';
200 echo esc_html( self::get_acdc_vetting_status_message( $vetting_status ) );
201
202 if ( ! in_array( $vetting_status, array( 'DECLINED', 'DENIED', 'NEED_MORE_DATA' ), true ) ) {
203 return;
204 }
205
206 echo '&nbsp;';
207 echo '<a href="https://www.paypal.com/bizsignup/entry/product/ppcp" target="_blank" rel="noopener noreferrer">';
208 esc_html_e( 'Reapply for Advanced Card Processing', 'formidable' );
209 echo '</a>';
210 }
211
212 /**
213 * @since 6.31
214 *
215 * @param string $vetting_status
216 *
217 * @return string
218 */
219 private static function get_acdc_vetting_status_message( $vetting_status ) {
220 switch ( $vetting_status ) {
221 case 'NOT_SET':
222 return 'Unavailable';
223 case 'APPROVED':
224 case 'SUBSCRIBED':
225 return 'Approved';
226 case 'PENDING':
227 return 'Pending';
228 case 'IN_REVIEW':
229 return 'In Review';
230 case 'DECLINED':
231 return 'Declined';
232 case 'NEED_MORE_DATA':
233 return 'Needs More Data';
234 case 'DENIED':
235 return 'Denied';
236 default:
237 return '';
238 }
239 }
240
241 /**
242 * @since 6.31
243 *
244 * @param string $mode
245 *
246 * @return void
247 */
248 public static function render_seller_status_placeholder( $mode ) {
249 include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/seller-status-placeholder.php';
250 }
251
252 /**
253 * Get the PayPal account settings URL for the given mode.
254 *
255 * @since 6.31
256 *
257 * @param string $mode 'test' or 'live'.
258 *
259 * @return string
260 */
261 private static function get_paypal_account_settings_url( $mode ) {
262 if ( 'test' === $mode ) {
263 return 'https://www.sandbox.paypal.com/businessprofile/settings';
264 }
265 return 'https://www.paypal.com/businessprofile/settings';
266 }
267
268 /**
269 * @param array $products
270 * @param string $name
271 *
272 * @return bool|object
273 */
274 private static function check_for_product( $products, $name = 'PPCP_CUSTOM' ) {
275 foreach ( $products as $current_product ) {
276 if ( $name === $current_product->name ) {
277 return $current_product;
278 }
279 }
280 return false;
281 }
282
283 /**
284 * @param string $email
285 *
286 * @return void
287 */
288 private static function echo_email( $email ) {
289 if ( ! $email ) {
290 return;
291 }
292
293 echo '<br>';
294 echo '<b>' . esc_html__( 'Connected account:', 'formidable' ) . '</b>';
295 echo '<br>';
296 echo esc_html( $email );
297 }
298
299 /**
300 * @param string $merchant_id
301 *
302 * @return void
303 */
304 private static function echo_merchant_id( $merchant_id ) {
305 echo '<br>';
306 echo '<b>' . esc_html__( 'Merchant ID:', 'formidable' ) . '</b>';
307 echo '&nbsp;';
308
309 if ( $merchant_id ) {
310 echo esc_html( $merchant_id );
311 } else {
312 esc_html_e( 'N/A', 'formidable' );
313 }
314 }
315
316 /**
317 * @param string $message
318 * @param string $email
319 * @param string $merchant_id
320 * @param string $link URL to help the user resolve the issue.
321 * @param string $reconnect_mode When set to 'test' or 'live', renders a Reconnect button
322 * that triggers the OAuth flow again for that mode.
323 *
324 * @return void
325 */
326 private static function render_error( $message, $email = '', $merchant_id = '', $link = '', $reconnect_mode = '' ) {
327 echo '<div class="frm_error_style">';
328 echo wp_kses_post( $message );
329 self::echo_email( $email );
330 self::echo_merchant_id( $merchant_id );
331
332 if ( $link ) {
333 echo '<br><br>';
334 echo '<a href="' . esc_url( $link ) . '" target="_blank" rel="noopener noreferrer">';
335 esc_html_e( 'Resolve this issue', 'formidable' );
336 echo '</a>';
337 }
338
339 if ( in_array( $reconnect_mode, array( 'test', 'live' ), true ) ) {
340 echo '<br><br>';
341 echo '<a class="frm-connect-paypal-with-oauth button-secondary frm-button-secondary" data-mode="' . esc_attr( $reconnect_mode ) . '" data-reconnect="1" href="#">';
342 esc_html_e( 'Reconnect', 'formidable' );
343 echo '</a>';
344 }
345
346 echo '</div>';
347 }
348
349 /**
350 * @param string $mode
351 *
352 * @return void
353 */
354 public static function render_settings_for_mode( $mode ) {
355 $connected = (bool) self::get_merchant_id( $mode );
356 include FrmPayPalLiteAppHelper::plugin_path() . '/views/settings/connect-settings-box.php';
357 }
358
359 /**
360 * @return void
361 */
362 private static function register_settings_scripts() {
363 $script_url = FrmPayPalLiteAppHelper::plugin_url() . '/js/settings.js';
364 $dependencies = array( 'formidable_dom' );
365 wp_register_script( 'formidable_paypal_settings', $script_url, $dependencies, FrmAppHelper::plugin_version(), true );
366 wp_enqueue_script( 'formidable_paypal_settings' );
367 }
368
369 /**
370 * @return false|string
371 */
372 public static function get_oauth_redirect_url() {
373 $mode = FrmAppHelper::get_post_param( 'mode', 'test', 'sanitize_text_field' );
374 $tracking_id = get_option( self::get_tracking_id_option_name( $mode ) );
375
376 if ( self::get_merchant_id( $mode ) && ! $tracking_id ) {
377 // Do not allow for initialize if there is already a configured account id,
378 // unless a tracking_id is stored, which indicates the user is re-onboarding
379 // after an incomplete OAuth integration.
380 return false;
381 }
382
383 $additional_body = array(
384 'password' => self::generate_client_password( $mode ),
385 'user_id' => get_current_user_id(),
386 'frm_paypal_api_mode' => $mode,
387 );
388
389 if ( $tracking_id ) {
390 // Reuse the existing tracking_id so the Connect server can resume onboarding.
391 $additional_body['tracking_id'] = $tracking_id;
392 }
393
394 // Clear the transient so it doesn't fail.
395 delete_option( 'frm_paypal_lite_last_verify_attempt' );
396 $data = self::post_to_connect_server( 'oauth_request', $additional_body );
397
398 if ( is_string( $data ) ) {
399 self::$latest_error_from_paypal_api = $data;
400 FrmTransLiteLog::log_message( 'PayPal OAuth Error', $data );
401 return false;
402 }
403
404 if ( ! empty( $data->password ) ) {
405 update_option( self::get_server_side_token_option_name( $mode ), $data->password, false );
406 }
407
408 if ( ! empty( $data->tracking_id ) ) {
409 update_option( self::get_tracking_id_option_name( $mode ), $data->tracking_id, false );
410 }
411
412 if ( ! is_object( $data ) || empty( $data->redirect_url ) ) {
413 return false;
414 }
415
416 return $data->redirect_url;
417 }
418
419 /**
420 * @param string $action
421 * @param array $additional_body
422 *
423 * @return object|string
424 */
425 private static function post_to_connect_server( $action, $additional_body = array() ) {
426 $body = array(
427 'frm_paypal_api_action' => $action,
428 'frm_paypal_api_mode' => FrmPayPalLiteAppHelper::active_mode(),
429 );
430 $body = array_merge( $body, $additional_body );
431 $url = self::get_url_to_connect_server();
432 $headers = self::build_headers_for_post();
433
434 // (Seconds) default timeout is 5. we want a bit more time to work with.
435 $timeout = 45;
436
437 self::try_to_extend_server_timeout( $timeout );
438
439 $args = compact( 'body', 'headers', 'timeout' );
440 $response = wp_remote_post( $url, $args );
441
442 if ( ! self::validate_response( $response ) ) {
443 return 'Response from server is invalid';
444 }
445
446 $body = self::pull_response_body( $response );
447
448 if ( empty( $body->success ) ) {
449 $error_message = 'Response from server was not successful';
450 $debug_id = '';
451
452 // Handle structured error response with message and debug_id
453 $data = $body->data ?? null;
454
455 if ( is_object( $data ) ) {
456 if ( ! empty( $data->message ) ) {
457 $error_message = $data->message;
458 }
459
460 if ( ! empty( $data->debug_id ) ) {
461 $debug_id = $data->debug_id;
462 }
463 } elseif ( is_string( $data ) ) {
464 $error_message = $data;
465 }
466
467 // Check for debug_id at top level as well
468 if ( ! $debug_id && ! empty( $body->debug_id ) ) {
469 $debug_id = $body->debug_id;
470 }
471
472 // Parse debug_id from error message if not found
473 if ( ! $debug_id && preg_match( '/\{\{debug_id:([^}]+)\}\}/', $error_message, $matches ) ) {
474 $debug_id = $matches[1];
475 }
476
477 if ( $debug_id ) {
478 $clean_message = trim( preg_replace( '/\{\{debug_id:[^}]+\}\}/', '', $error_message ) );
479 FrmPayPalLiteAppController::log_paypal_debug_id( $debug_id, $clean_message, $action );
480 // Return structured error with debug_id so it can be passed to JavaScript
481 return array(
482 'message' => $clean_message ? $clean_message : $error_message,
483 'debug_id' => $debug_id,
484 );
485 }
486
487 return $error_message;
488 }//end if
489
490 return $body->data ?? array();
491 }
492
493 /**
494 * @param array $response
495 *
496 * @return mixed
497 */
498 private static function pull_response_body( $response ) {
499 $http_response = $response['http_response'];
500 $response_object = $http_response->get_response_object();
501 return json_decode( $response_object->body );
502 }
503
504 /**
505 * @param mixed $response
506 *
507 * @return bool
508 */
509 private static function validate_response( $response ) {
510 return ! is_wp_error( $response ) && is_array( $response ) && isset( $response['http_response'] );
511 }
512
513 /**
514 * @return string
515 */
516 private static function get_url_to_connect_server() {
517 return 'https://api.strategy11.com/';
518 }
519
520 /**
521 * @return array
522 */
523 private static function build_headers_for_post() {
524 $password = self::maybe_get_pro_license();
525
526 if ( false === $password ) {
527 $password = 'lite_' . self::get_uuid();
528 }
529
530 $site_url = home_url();
531 $site_url = self::maybe_fix_wpml_url( $site_url );
532 // Remove protocol from url (our url cannot include the colon).
533 $site_url = preg_replace( '#^https?://#', '', $site_url );
534 // Remove port from url (mostly helpful in development).
535 $site_url = preg_replace( '/:[0-9]+/', '', $site_url );
536 $site_url = self::strip_lang_from_url( $site_url );
537
538 // $password is either a Pro license or a uuid (See FrmUsage::uuid).
539 return array(
540 'Authorization' => 'Basic ' . base64_encode( $site_url . ':' . $password ),
541 );
542 }
543
544 /**
545 * Get a unique ID to use for connecting Lite users.
546 *
547 * @return string
548 */
549 private static function get_uuid() {
550 $usage = new FrmUsage();
551 return $usage->uuid();
552 }
553
554 /**
555 * WPML might add a language to the url. Don't send that to the server.
556 *
557 * @param string $url URL to strip language from.
558 *
559 * @return string
560 */
561 private static function strip_lang_from_url( $url ) {
562 $split_on_language = explode( '/?lang=', $url );
563 return 2 === count( $split_on_language ) ? $split_on_language[0] : $url;
564 }
565
566 /**
567 * WPML alters the output of home_url.
568 * If it is active, use the WPML "absolute home" URL which is not modified.
569 *
570 * @param string $url URL to maybe fix.
571 *
572 * @return string
573 */
574 private static function maybe_fix_wpml_url( $url ) {
575 if ( defined( 'ICL_SITEPRESS_VERSION' ) && ! ICL_PLUGIN_INACTIVE && class_exists( 'SitePress' ) ) {
576 global $wpml_url_converter;
577 $url = $wpml_url_converter->get_abs_home();
578 }
579 return $url;
580 }
581
582 /**
583 * Get a Pro license when Pro is active.
584 * Otherwise we'll use a uuid to support Lite.
585 *
586 * @return false|string
587 */
588 private static function maybe_get_pro_license() {
589 if ( FrmAppHelper::pro_is_installed() ) {
590 $pro_license = FrmAddonsController::get_pro_license();
591
592 if ( $pro_license ) {
593 $password = $pro_license;
594 }
595 }
596
597 return ! empty( $password ) ? $password : false;
598 }
599
600 /**
601 * Try to make sure the server time limit exceeds the request time limit.
602 *
603 * @param int $timeout seconds.
604 *
605 * @return void
606 */
607 private static function try_to_extend_server_timeout( $timeout ) {
608 if ( function_exists( 'set_time_limit' ) ) {
609 set_time_limit( $timeout + 10 );
610 }
611 }
612
613 /**
614 * @param string $mode either 'auto', 'live', or 'test'.
615 *
616 * @return string
617 */
618 private static function get_server_side_token_option_name( $mode = 'auto' ) {
619 return self::get_paypal_connect_option_name( 'server_password', $mode );
620 }
621
622 /**
623 * @param string $mode either 'auto', 'live', or 'test'.
624 *
625 * @return string
626 */
627 private static function get_tracking_id_option_name( $mode = 'auto' ) {
628 return self::get_paypal_connect_option_name( 'tracking_id', $mode );
629 }
630
631 /**
632 * Generate a new client password for authenticating with Connect Service and save it locally as an option.
633 *
634 * @param string $mode 'live' or 'test'.
635 *
636 * @return string the client password.
637 */
638 private static function generate_client_password( $mode ) {
639 $client_password = wp_generate_password();
640 update_option( self::get_client_side_token_option_name( $mode ), $client_password, false );
641 return $client_password;
642 }
643
644 /**
645 * @param string $mode either 'auto', 'live', or 'test'.
646 *
647 * @return string
648 */
649 private static function get_client_side_token_option_name( $mode = 'auto' ) {
650 return self::get_paypal_connect_option_name( 'client_password', $mode );
651 }
652
653 /**
654 * @param string $mode
655 *
656 * @return string
657 */
658 private static function get_paypal_seller_status_option_name( $mode = 'auto' ) {
659 return self::get_paypal_connect_option_name( 'seller_status', $mode );
660 }
661
662 /**
663 * @return string
664 */
665 private static function get_mode_value() {
666 $settings = FrmPayPalLiteAppHelper::get_settings();
667 return $settings->settings->test_mode ? 'test' : 'live';
668 }
669
670 /**
671 * @param string $mode either 'auto', 'live', or 'test'.
672 *
673 * @return bool|string
674 */
675 public static function get_merchant_id( $mode = 'auto' ) {
676 if ( 'auto' === $mode ) {
677 $mode = self::get_mode_value();
678 }
679 return get_option( self::get_merchant_id_option_name( $mode ) );
680 }
681
682 /**
683 * @param string $mode either 'auto', 'live', or 'test'.
684 *
685 * @return string
686 */
687 private static function get_merchant_id_option_name( $mode = 'auto' ) {
688 return self::get_paypal_connect_option_name( 'merchant_id', $mode );
689 }
690
691 /**
692 * @param string $mode either 'auto', 'live', or 'test'.
693 *
694 * @return string
695 */
696 private static function get_merchant_currency_option_name( $mode = 'auto' ) {
697 return self::get_paypal_connect_option_name( 'merchant_currency', $mode );
698 }
699
700 /**
701 * @param string $key 'merchant_id', 'client_password', 'server_password'.
702 * @param string $mode either 'auto', 'live', or 'test'.
703 *
704 * @return string
705 */
706 private static function get_paypal_connect_option_name( $key, $mode = 'auto' ) {
707 return 'frm_paypal_connect_' . $key . self::get_active_mode_option_name_suffix( $mode );
708 }
709
710 /**
711 * @param string $mode either 'auto', 'live', or 'test'.
712 *
713 * @return string either _test or _live.
714 */
715 private static function get_active_mode_option_name_suffix( $mode = 'auto' ) {
716 if ( 'auto' !== $mode ) {
717 return '_' . $mode;
718 }
719 return '_' . FrmPayPalLiteAppHelper::active_mode();
720 }
721
722 public static function check_for_redirects() {
723 if ( self::user_landed_on_the_oauth_return_url() ) {
724 self::redirect_oauth();
725 }
726 }
727
728 /**
729 * @return bool
730 */
731 private static function user_landed_on_the_oauth_return_url() {
732 return isset( $_GET['frm_paypal_api_return_oauth'] );
733 }
734
735 private static function redirect_oauth() {
736 $connected = self::check_server_for_oauth_merchant_id();
737 wp_safe_redirect( self::get_url_for_paypal_settings( $connected ) );
738 exit;
739 }
740
741 /**
742 * @param bool $connected
743 *
744 * @return string
745 */
746 private static function get_url_for_paypal_settings( $connected ) {
747 return admin_url( 'admin.php?page=formidable-settings&t=paypal_settings&connected=' . intval( $connected ) );
748 }
749
750 /**
751 * @return bool
752 */
753 private static function check_server_for_oauth_merchant_id() {
754 $mode = 'test' === FrmAppHelper::simple_get( 'mode' ) ? 'test' : 'live';
755 $tracking_id = get_option( self::get_tracking_id_option_name( $mode ) );
756 $is_reconnect = (bool) $tracking_id;
757
758 if ( self::get_merchant_id( $mode ) && ! $is_reconnect ) {
759 // Do not allow for initialize if there is already a configured merchant id,
760 // unless a tracking_id is stored, which indicates the user is re-onboarding
761 // after an incomplete OAuth integration and the new credentials must be synced.
762 return false;
763 }
764
765 $body = array(
766 'server_password' => get_option( self::get_server_side_token_option_name( $mode ) ),
767 'client_password' => get_option( self::get_client_side_token_option_name( $mode ) ),
768 'frm_paypal_api_mode' => $mode,
769 );
770
771 if ( $tracking_id ) {
772 $body['tracking_id'] = $tracking_id;
773 }
774
775 $data = self::post_to_connect_server( 'oauth_merchant_status', $body );
776
777 if ( is_object( $data ) && ! empty( $data->merchant_id ) ) {
778 update_option( self::get_merchant_id_option_name( $mode ), $data->merchant_id, false );
779
780 // Invalidate the cached seller status so the next render fetches fresh data
781 // with the newly synced credentials.
782 delete_option( self::get_paypal_seller_status_option_name( $mode ) );
783
784 FrmTransLiteAppController::install();
785
786 return true;
787 }
788
789 return false;
790 }
791
792 /**
793 * @param string $action
794 * @param array $additional_body
795 *
796 * @return false|object
797 */
798 private static function post_with_authenticated_body( $action, $additional_body = array() ) {
799 $body = array_merge( self::get_standard_authenticated_body(), $additional_body );
800 $response = self::post_to_connect_server( $action, $body );
801
802 if ( is_object( $response ) ) {
803 return $response;
804 }
805
806 if ( is_array( $response ) ) {
807 // Arrays with error data (e.g., from mock responses) should be preserved
808 // Only convert empty arrays to empty objects
809 if ( $response ) {
810 // Check if this is an error response with message and debug_id
811 if ( isset( $response['message'] ) && ( isset( $response['debug_id'] ) || isset( $response['debugId'] ) ) ) {
812 self::$latest_error_from_paypal_api = $response['message'];
813 // PayPal API returns debug_id (snake_case) in some cases and debugId (camelCase) in others
814 self::$latest_debug_id_from_paypal_api = $response['debug_id'] ?? $response['debugId'] ?? '';
815
816 if ( class_exists( 'FrmTransLiteLog' ) ) {
817 FrmTransLiteLog::log_message( 'PayPal API Error', $response['message'] );
818 }
819 // Return the array with error details so the caller can extract them
820 return (object) $response;
821 }
822 // Convert array to object for consistency
823 return (object) $response;
824 }
825
826 return new stdClass();
827 }//end if
828
829 if ( ! is_string( $response ) ) {
830 self::$latest_error_from_paypal_api = '';
831 return false;
832 }
833
834 self::$latest_error_from_paypal_api = $response;
835
836 // Extract debug_id from formatted error string if present
837 if ( preg_match( '/{{debug_id:([^}]+)}}/', $response, $matches ) ) {
838 self::$latest_debug_id_from_paypal_api = $matches[1];
839 // Remove the debug_id token from the error message for display
840 self::$latest_error_from_paypal_api = preg_replace( '/\s*{{debug_id:[^}]+}}/', '', $response );
841 }
842
843 FrmTransLiteLog::log_message( 'PayPal API Error', $response );
844
845 return false;
846 }
847
848 /**
849 * @return array
850 */
851 private static function get_standard_authenticated_body() {
852 return self::get_body_for_mode( FrmPayPalLiteAppHelper::active_mode() );
853 }
854
855 /**
856 * Check $_POST for live or test mode value as it can be updated in real time from PayPal Settings and can be configured before the update is saved.
857 *
858 * @return string 'test' or 'live'
859 */
860 private static function get_mode_value_from_post() {
861 // phpcs:ignore WordPress.Security.NonceVerification.Missing
862 if ( empty( $_POST ) || ! array_key_exists( 'testMode', $_POST ) ) {
863 return FrmPayPalLiteAppHelper::active_mode();
864 }
865
866 $test_mode = FrmAppHelper::get_param( 'testMode', '', 'post', 'absint' );
867 return $test_mode ? 'test' : 'live';
868 }
869
870 /**
871 * Get the standard body with account id, mode, and passwords to send to the connect server.
872 *
873 * @since 6.32.1
874 *
875 * @param string $mode 'live' or 'test'.
876 *
877 * @return array
878 */
879 private static function get_body_for_mode( $mode ) {
880 return array(
881 'merchant_id' => get_option( self::get_merchant_id_option_name( $mode ) ),
882 'server_password' => get_option( self::get_server_side_token_option_name( $mode ) ),
883 'client_password' => get_option( self::get_client_side_token_option_name( $mode ) ),
884 'frm_paypal_api_mode' => $mode,
885 );
886 }
887
888 /**
889 * @return string|null
890 */
891 public static function get_latest_error_from_paypal_api() {
892 return self::$latest_error_from_paypal_api;
893 }
894
895 /**
896 * @return string
897 */
898 public static function get_latest_debug_id_from_paypal_api() {
899 return self::$latest_debug_id_from_paypal_api;
900 }
901
902 /**
903 * @return array
904 */
905 public static function get_unprocessed_event_ids() {
906 $data = self::post_with_authenticated_body( 'get_unprocessed_event_ids' );
907
908 if ( false === $data || empty( $data->event_ids ) ) {
909 return array();
910 }
911
912 /**
913 * @var array $data->event_ids
914 */
915 return $data->event_ids;
916 }
917
918 /**
919 * @param string $event_id
920 *
921 * @return false|object
922 */
923 public static function get_event( $event_id ) {
924 $event = wp_cache_get( $event_id, 'frm_paypal' );
925
926 if ( is_object( $event ) ) {
927 return $event;
928 }
929
930 $event = self::post_with_authenticated_body( 'get_event', compact( 'event_id' ) );
931
932 if ( false === $event || empty( $event->event ) ) {
933 return false;
934 }
935
936 wp_cache_set( $event_id, $event->event, 'frm_paypal' );
937
938 return $event->event;
939 }
940
941 /**
942 * @param string $event_id
943 *
944 * @return false|object
945 */
946 public static function process_event( $event_id ) {
947 return self::post_with_authenticated_body( 'process_event', compact( 'event_id' ) );
948 }
949
950 public static function handle_disconnect() {
951 self::disconnect();
952 self::reset_paypal_api_integration();
953 FrmTransLiteAppHelper::trigger_gateway_disconnected_hook( 'paypal', self::get_mode_value_from_post() );
954 wp_send_json_success();
955 }
956
957 /**
958 * @return false|object
959 */
960 private static function disconnect() {
961 $additional_body = self::get_body_for_mode( self::get_mode_value_from_post() );
962 return self::post_with_authenticated_body( 'disconnect', $additional_body );
963 }
964
965 /**
966 * Delete every PayPal API option, calling when disconnecting.
967 *
968 * @return void
969 */
970 public static function reset_paypal_api_integration() {
971 $mode = self::get_mode_value_from_post();
972 delete_option( self::get_merchant_id_option_name( $mode ) );
973 delete_option( self::get_server_side_token_option_name( $mode ) );
974 delete_option( self::get_client_side_token_option_name( $mode ) );
975 delete_option( self::get_merchant_currency_option_name( $mode ) );
976 delete_option( self::get_paypal_seller_status_option_name( $mode ) );
977 delete_option( self::get_tracking_id_option_name( $mode ) );
978 }
979
980 /**
981 * @since 6.31
982 *
983 * @return bool
984 */
985 public static function at_least_one_mode_is_setup() {
986 return self::get_merchant_id( 'test' ) || self::get_merchant_id( 'live' );
987 }
988
989 /**
990 * Verify a site identifier is a match.
991 */
992 public static function verify() {
993 $option_name = 'frm_paypal_lite_last_verify_attempt';
994 $last_request = get_option( $option_name );
995
996 if ( $last_request && $last_request > strtotime( '-1 day' ) ) {
997 wp_send_json_error( 'Too many requests' );
998 }
999
1000 $site_identifier = FrmAppHelper::get_post_param( 'site_identifier' );
1001 $usage = new FrmUsage();
1002
1003 update_option( $option_name, time() );
1004
1005 if ( $site_identifier === $usage->uuid() ) {
1006 wp_send_json_success();
1007 }
1008
1009 wp_send_json_error();
1010 }
1011
1012 /**
1013 * Create a PayPal order.
1014 *
1015 * @param string $amount
1016 * @param string $currency
1017 * @param string $payment_source Valid values are 'card', 'paypal'.
1018 * @param array $payer
1019 * @param string $shipping_preference
1020 * @param array $pricing_data Optional. Array of products with prices and quantities.
1021 * @param array $shipping Optional. Shipping name and address data.
1022 * @param string $description Optional. Description for the order.
1023 *
1024 * @return false|object
1025 */
1026 public static function create_order( $amount, $currency, $payment_source, $payer, $shipping_preference, $pricing_data = array(), $shipping = array(), $description = '' ) {
1027 $brand_name = self::get_brand_name();
1028
1029 // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
1030 return self::post_with_authenticated_body( 'create_order', compact( 'amount', 'currency', 'payment_source', 'brand_name', 'payer', 'shipping_preference', 'pricing_data', 'shipping', 'description' ) );
1031 }
1032
1033 /**
1034 * @since 6.31
1035 *
1036 * @return string
1037 */
1038 private static function get_brand_name() {
1039 $brand_name = get_bloginfo( 'name' );
1040
1041 /**
1042 * Allow people to modify the brand name used in the PayPal order.
1043 *
1044 * @since 6.31
1045 *
1046 * @param string $brand_name
1047 *
1048 * @return string
1049 */
1050 $filtered_brand_name = apply_filters( 'frm_paypal_brand_name', $brand_name );
1051
1052 if ( is_string( $filtered_brand_name ) ) {
1053 return $filtered_brand_name;
1054 }
1055
1056 _doing_it_wrong( 'FrmPayPalLiteConnectHelper::get_brand_name', 'The frm_paypal_brand_name filter must return a string.', '6.31' );
1057
1058 return $brand_name;
1059 }
1060
1061 /**
1062 * @param string $order_id
1063 *
1064 * @return false|object
1065 */
1066 public static function capture_order( $order_id ) {
1067 return self::post_with_authenticated_body( 'capture_order', compact( 'order_id' ) );
1068 }
1069
1070 /**
1071 * @param string $capture_id
1072 *
1073 * @return false|object
1074 */
1075 public static function refund_payment( $capture_id ) {
1076 return self::post_with_authenticated_body( 'refund_capture', array( 'capture_id' => $capture_id ) );
1077 }
1078
1079 /**
1080 * @param string $subscription_id
1081 *
1082 * @return false|object
1083 */
1084 public static function cancel_subscription( $subscription_id ) {
1085 return self::post_with_authenticated_body( 'cancel_subscription', compact( 'subscription_id' ) );
1086 }
1087
1088 /**
1089 * @param array $data Subscription data.
1090 *
1091 * @return false|object
1092 */
1093 public static function create_subscription( $data ) {
1094 $data['brand_name'] = self::get_brand_name();
1095 return self::post_with_authenticated_body( 'create_subscription', compact( 'data' ) );
1096 }
1097
1098 /**
1099 * @return false|object
1100 */
1101 public static function get_seller_status() {
1102 $mode = self::get_mode_value_from_post();
1103 $status = get_option( self::get_paypal_seller_status_option_name( $mode ) );
1104
1105 if ( is_object( $status ) ) {
1106 return $status;
1107 }
1108
1109 $additional_body = self::get_body_for_mode( $mode );
1110
1111 return self::post_with_authenticated_body( 'get_seller_status', $additional_body );
1112 }
1113
1114 /**
1115 * @since 6.31
1116 *
1117 * @param string $capture_id
1118 *
1119 * @return false|object
1120 */
1121 public static function get_capture( $capture_id ) {
1122 return self::post_with_authenticated_body( 'get_capture', compact( 'capture_id' ) );
1123 }
1124
1125 /**
1126 * @since 6.31
1127 *
1128 * @param string $order_id
1129 *
1130 * @return false|object
1131 */
1132 public static function get_order( $order_id ) {
1133 return self::post_with_authenticated_body( 'get_order', compact( 'order_id' ) );
1134 }
1135
1136 /**
1137 * @since 6.31
1138 *
1139 * @param string $subscription_id The PayPal subscription ID.
1140 *
1141 * @return false|object
1142 */
1143 public static function get_subscription( $subscription_id ) {
1144 return self::post_with_authenticated_body( 'get_subscription', compact( 'subscription_id' ) );
1145 }
1146
1147 /**
1148 * @since 6.31
1149 *
1150 * @return string
1151 */
1152 public static function get_bn_code() {
1153 return 'Strategy11LLCPPCP_SP';
1154 }
1155
1156 /**
1157 * @since 6.31
1158 * @deprecated 6.32.1
1159 *
1160 * @param array $data Setup token data including payment_source.
1161 *
1162 * @return false|object
1163 */
1164 public static function create_vault_setup_token( $data = array() ) {
1165 _deprecated_function( __METHOD__, '6.32.1' );
1166 return false;
1167 }
1168 }
1169