Files
wp-multisite-waas/inc/managers/class-gateway-manager.php
2025-02-08 13:57:32 -07:00

568 lines
14 KiB
PHP

<?php
/**
* Gateway Manager
*
* Manages the registering and activation of gateways.
*
* @package WP_Ultimo
* @subpackage Managers/Gateway
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use Psr\Log\LogLevel;
use WP_Ultimo\Managers\Base_Manager;
use WP_Ultimo\Gateways\Ignorable_Exception;
use WP_Ultimo\Gateways\Free_Gateway;
use WP_Ultimo\Gateways\Stripe_Gateway;
use WP_Ultimo\Gateways\Stripe_Checkout_Gateway;
use WP_Ultimo\Gateways\PayPal_Gateway;
use WP_Ultimo\Gateways\Manual_Gateway;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Manages the registering and activation of gateways.
*
* @since 2.0.0
*/
class Gateway_Manager extends Base_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* Lists the registered gateways.
*
* @since 2.0.0
* @var array
*/
protected $registered_gateways = [];
/**
* Lists the gateways that are enabled.
*
* @since 2.0.0
* @var array
*/
protected $enabled_gateways = [];
/**
* Keeps a list of the gateways with auto-renew.
*
* @since 2.0.0
* @var array
*/
protected $auto_renewable_gateways = [];
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init(): void {
add_action('plugins_loaded', [$this, 'on_load']);
}
/**
* Runs after all plugins have been loaded to allow for add-ons to hook into it correctly.
*
* @since 2.0.0
* @return void
*/
public function on_load(): void {
/*
* Adds our own default gateways.
*/
add_action('wu_register_gateways', [$this, 'add_default_gateways'], 5);
/*
* Allow developers to add new gateways.
*/
add_action(
'init',
function () {
do_action('wu_register_gateways');
},
19
);
/*
* Adds the Gateway selection fields
*/
add_action('init', [$this, 'add_gateway_selector_field']);
/*
* Handle gateway confirmations.
* We need it both on the front-end and the back-end.
*/
add_action('template_redirect', [$this, 'process_gateway_confirmations'], -99999);
add_action('load-admin_page_wu-checkout', [$this, 'process_gateway_confirmations'], -99999);
/*
* Waits for webhook signals and deal with them.
*/
add_action('init', [$this, 'maybe_process_webhooks'], 1);
/*
* Waits for webhook signals and deal with them.
*/
add_action('admin_init', [$this, 'maybe_process_v1_webhooks'], 1);
}
/**
* Checks if we need to process webhooks received by gateways.
*
* @since 2.0.0
* @return void
*/
public function maybe_process_webhooks(): void {
$gateway = wu_request('wu-gateway');
if ($gateway && ! is_admin() && is_main_site()) {
/*
* Do not cache this!
*/
!defined('DONOTCACHEPAGE') && define('DONOTCACHEPAGE', true); // phpcs:ignore
try {
/*
* Passes it down to Gateways.
*
* Gateways will hook into here
* to handle their respective webhook
* calls.
*
* We also wrap it inside a try/catch
* to make sure we log errors,
* tell network admins, and make sure
* the gateway tries again by sending back
* a non-200 HTTP code.
*/
do_action("wu_{$gateway}_process_webhooks");
http_response_code(200);
die('Thanks!');
} catch (Ignorable_Exception $e) {
$message = sprintf('We failed to handle a webhook call, but in this case, no further action is necessary. Message: %s', $e->getMessage());
wu_log_add("wu-{$gateway}-webhook-errors", $message);
/*
* Send the error back, but with a 200.
*/
wp_send_json_error(new \WP_Error('webhook-error', $message), 200);
} catch (\Throwable $e) {
$file = $e->getFile();
$line = $e->getLine();
$message = sprintf('We failed to handle a webhook call. Error: %s', $e->getMessage());
$message .= PHP_EOL . "Location: {$file}:{$line}";
$message .= PHP_EOL . $e->getTraceAsString();
wu_log_add("wu-{$gateway}-webhook-errors", $message, LogLevel::ERROR);
/*
* Force a 500.
*
* Most gateways will try again later when
* a non-200 code is returned.
*/
wp_send_json_error(new \WP_Error('webhook-error', $message), 500);
}
}
}
/**
* Checks if we need to process webhooks received by legacy gateways.
*
* @since 2.0.4
* @return void
*/
public function maybe_process_v1_webhooks(): void {
$action = wu_request('action', '');
if ($action && str_contains((string) $action, 'notify_gateway_')) {
/*
* Get the gateway id from the action.
*/
$gateway_id = str_replace(['nopriv_', 'notify_gateway_'], '', (string) $action);
$gateway = wu_get_gateway($gateway_id);
if ($gateway) {
$gateway->before_backwards_compatible_webhook();
/*
* Do not cache this!
*/
!defined('DONOTCACHEPAGE') && define('DONOTCACHEPAGE', true); // phpcs:ignore
try {
/*
* Passes it down to Gateways.
*
* Gateways will hook into here
* to handle their respective webhook
* calls.
*
* We also wrap it inside a try/catch
* to make sure we log errors,
* tell network admins, and make sure
* the gateway tries again by sending back
* a non-200 HTTP code.
*/
do_action("wu_{$gateway_id}_process_webhooks");
http_response_code(200);
die('Thanks!');
} catch (Ignorable_Exception $e) {
$message = sprintf('We failed to handle a webhook call, but in this case, no further action is necessary. Message: %s', $e->getMessage());
wu_log_add("wu-{$gateway_id}-webhook-errors", $message);
/*
* Send the error back, but with a 200.
*/
wp_send_json_error(new \WP_Error('webhook-error', $message), 200);
} catch (\Throwable $e) {
$message = sprintf('We failed to handle a webhook call. Error: %s', $e->getMessage());
wu_log_add("wu-{$gateway_id}-webhook-errors", $message, LogLevel::ERROR);
/*
* Force a 500.
*
* Most gateways will try again later when
* a non-200 code is returned.
*/
http_response_code(500);
wp_send_json_error(new \WP_Error('webhook-error', $message));
}
}
}
}
/**
* Let gateways deal with their confirmation steps.
*
* This is the case for PayPal Express.
*
* @since 2.0.0
* @return void
*/
public function process_gateway_confirmations(): void {
/*
* First we check for the confirmation parameter.
*/
if ( ! wu_request('wu-confirm') || (wu_request('status') && wu_request('status') === 'done')) {
return;
}
ob_start();
add_filter('body_class', fn($classes) => array_merge($classes, ['wu-process-confirmation']));
$gateway_id = sanitize_text_field(wu_request('wu-confirm'));
$gateway = wu_get_gateway($gateway_id);
if ( ! $gateway) {
$error = new \WP_Error('missing_gateway', __('Missing gateway parameter.', 'wp-ultimo'));
wp_die(
$error,
__('Error', 'wp-ultimo'),
[
'back_link' => true,
'response' => '200',
]
);
}
try {
$payment_hash = wu_request('payment');
$payment = wu_get_payment_by_hash($payment_hash);
if ($payment) {
$gateway->set_payment($payment);
}
/*
* Pass it down to the gateway.
*
* Here you can throw exceptions, that
* we will catch it and throw it as a wp_die
* message.
*/
$results = $gateway->process_confirmation();
if (is_wp_error($results)) {
wp_die(
$results,
__('Error', 'wp-ultimo'),
[
'back_link' => true,
'response' => '200',
]
);
}
} catch (\Throwable $e) {
$error = new \WP_Error('confirm-error-' . $e->getCode(), $e->getMessage());
wp_die(
$error,
__('Error', 'wp-ultimo'),
[
'back_link' => true,
'response' => '200',
]
);
}
$output = ob_get_clean();
if ( ! empty($output)) {
/*
* Add a filter to bypass the checkout form.
* This is used for PayPal confirmation page.
*/
add_action('wu_bypass_checkout_form', fn($bypass, $atts) => $output, 10, 2);
}
}
/**
* Adds the field that enabled and disables Payment Gateways on the settings.
*
* @since 2.0.0
* @return void
*/
public function add_gateway_selector_field(): void {
wu_register_settings_field(
'payment-gateways',
'active_gateways',
[
'title' => __('Active Payment Gateways', 'wp-ultimo'),
'desc' => __('Payment gateways are what your customers will use to pay.', 'wp-ultimo'),
'type' => 'multiselect',
'columns' => 2,
'options' => [$this, 'get_gateways_as_options'],
'default' => [],
]
);
}
/**
* Returns the list of registered gateways as options for the gateway selector setting.
*
* @since 2.0.0
* @return array
*/
public function get_gateways_as_options() {
/*
* We use this to order the options.
*/
$active_gateways = wu_get_setting('active_gateways', []);
$gateways = $this->get_registered_gateways();
$gateways = array_filter($gateways, fn($item) => $item['hidden'] === false);
return $gateways;
}
/**
* Loads the default gateways.
*
* @since 2.0.0
* @return void
*/
public function add_default_gateways(): void {
/*
* Free Payments
*/
wu_register_gateway('free', __('Free', 'wp-ultimo'), '', Free_Gateway::class, true);
/*
* Stripe Payments
*/
$stripe_desc = __('Stripe is a suite of payment APIs that powers commerce for businesses of all sizes, including subscription management.', 'wp-ultimo');
wu_register_gateway('stripe', __('Stripe', 'wp-ultimo'), $stripe_desc, Stripe_Gateway::class);
/*
* Stripe Checkout Payments
*/
$stripe_checkout_desc = __('Stripe Checkout is the hosted solution for checkouts using Stripe.', 'wp-ultimo');
wu_register_gateway('stripe-checkout', __('Stripe Checkout', 'wp-ultimo'), $stripe_checkout_desc, Stripe_Checkout_Gateway::class);
/*
* PayPal Payments
*/
$paypal_desc = __('PayPal is the leading provider in checkout solutions and it is the easier way to get your network subscriptions going.', 'wp-ultimo');
wu_register_gateway('paypal', __('PayPal', 'wp-ultimo'), $paypal_desc, PayPal_Gateway::class);
/*
* Manual Payments
*/
$manual_desc = __('Use the Manual Gateway to allow users to pay you directly via bank transfers, checks, or other channels.', 'wp-ultimo');
wu_register_gateway('manual', __('Manual', 'wp-ultimo'), $manual_desc, Manual_Gateway::class);
}
/**
* Checks if a gateway was already registered.
*
* @since 2.0.0
* @param string $id The id of the gateway.
* @return boolean
*/
public function is_gateway_registered($id) {
return is_array($this->registered_gateways) && isset($this->registered_gateways[ $id ]);
}
/**
* Returns a list of all the registered gateways
*
* @since 2.0.0
* @return array
*/
public function get_registered_gateways() {
return $this->registered_gateways;
}
/**
* Returns a particular Gateway registered
*
* @since 2.0.0
* @param string $id The id of the gateway.
* @return array
*/
public function get_gateway($id) {
return $this->is_gateway_registered($id) ? $this->registered_gateways[ $id ] : false;
}
/**
* Adds a new Gateway to the System. Used by gateways to make themselves visible.
*
* @since 2.0.0
*
* @param string $id ID of the gateway. This is how we will identify the gateway in the system.
* @param string $title Name of the gateway.
* @param string $desc A description of the gateway to help super admins understand what services they integrate with.
* @param string $class_name Gateway class name.
* @param bool $hidden If we need to hide this gateway publicly.
* @return bool
*/
public function register_gateway($id, $title, $desc, $class_name, $hidden = false) {
// Checks if gateway was already added
if ($this->is_gateway_registered($id)) {
return;
}
$active_gateways = (array) wu_get_setting('active_gateways', []);
// Adds to the global
$this->registered_gateways[ $id ] = [
'id' => $id,
'title' => $title,
'desc' => $desc,
'class_name' => $class_name,
'active' => in_array($id, $active_gateways, true),
'active' => in_array($id, $active_gateways, true),
'hidden' => (bool) $hidden,
'gateway' => $class_name, // Deprecated.
];
$this->install_hooks($class_name);
// Return the value
return true;
}
/**
* Adds additional hooks for each of the gateway registered.
*
* @since 2.0.0
*
* @param string $class_name Gateway class name.
* @return void
*/
public function install_hooks($class_name): void {
$gateway = new $class_name();
$gateway_id = $gateway->get_id();
/*
* If the gateway supports recurring
* payments, add it to the list.
*/
if ($gateway->supports_recurring()) {
$this->auto_renewable_gateways[] = $gateway_id;
}
add_action('wu_checkout_scripts', [$gateway, 'register_scripts']);
$gateway->hooks();
add_action('wu_settings_payment_gateways', [$gateway, 'settings']);
add_action("wu_{$gateway_id}_process_webhooks", [$gateway, 'process_webhooks']);
add_action("wu_{$gateway_id}_remote_payment_url", [$gateway, 'get_payment_url_on_gateway']);
add_action("wu_{$gateway_id}_remote_subscription_url", [$gateway, 'get_subscription_url_on_gateway']);
add_action("wu_{$gateway_id}_remote_customer_url", [$gateway, 'get_customer_url_on_gateway']);
/*
* Renders the gateway fields.
*/
add_action(
'wu_checkout_gateway_fields',
function ($checkout) use ($gateway) {
$field_content = call_user_func([$gateway, 'fields']);
ob_start();
?>
<div v-cloak v-show="gateway == '<?php echo esc_attr($gateway->get_id()); ?>' && order && order.should_collect_payment" class="wu-overflow">
<?php echo $field_content; ?>
</div>
<?php
echo ob_get_clean();
}
);
}
/**
* Returns an array with the list of gateways that support auto-renew.
*
* @since 2.0.0
* @return mixed
*/
public function get_auto_renewable_gateways() {
return (array) $this->auto_renewable_gateways;
}
}