settings = $this->get_configuration()->get_options(); if (empty($this->settings) || !isset($this->settings['enabled']) || !(bool) $this->settings['enabled']) { return; } // Don't load for change payment method page. if (isset($_GET['change_payment_method'])) { return; } add_action('template_redirect', [$this, 'set_session']); add_action('wc_ajax_payplug_create_order', [$this, 'ajax_create_order']); add_action('wc_ajax_applepay_update_payment', [$this, 'applepay_update_payment']); add_action('wc_ajax_applepay_get_order_totals', [$this, 'applepay_get_order_totals']); add_action('wc_ajax_payplug_order_review_url', [$this, 'ajax_create_payment']); add_action('wc_ajax_payplug_apple_pay_create_order_pay', [$this, 'ajax_apple_pay_create_order_pay']); add_action('wc_ajax_payplug_check_payment', [$this, 'check_payment']); add_action('wc_ajax_payplug_create_intent', [$this, 'create_payment_intent']); } /** * Sets the WC customer session if one is not set. * This is needed so nonces can be verified by AJAX Request. */ public function set_session(): void { if (!is_product() || (isset(WC()->session) && WC()->session->has_session())) { return; } $session_class = apply_filters('woocommerce_session_handler', 'WC_Session_Handler'); $wc_session = new $session_class(); if (version_compare(WC_VERSION, '3.3', '>=')) { $wc_session->init(); } $wc_session->set_customer_session_cookie(true); } /** * Create the woocommerce order in the BO */ public function ajax_create_order(): void { if (WC()->cart->is_empty()) { wp_send_json_error(__('Empty cart', 'payplug')); } if (!defined('WOOCOMMERCE_CHECKOUT')) { define('WOOCOMMERCE_CHECKOUT', true); } WC()->checkout()->process_checkout(); die(0); } /** * Process Apple Pay payment for an existing order on the order-pay page. */ public function ajax_apple_pay_create_order_pay(): void { if (!check_ajax_referer('woocommerce-process_checkout', 'woocommerce-process-checkout-nonce', false)) { wp_send_json([ 'result' => 'failure', 'messages' => '', ]); return; } $order_id = isset($_POST['order_id']) ? absint(wp_unslash($_POST['order_id'])) : 0; $order_key = isset($_POST['order_key']) ? wc_clean(wp_unslash($_POST['order_key'])) : ''; $order = $order_id ? wc_get_order($order_id) : null; if (!$order || !hash_equals($order->get_order_key(), $order_key)) { wp_send_json([ 'result' => 'failure', 'messages' => '', ]); return; } // This AJAX request's own URL never carries the order-pay query var (only the page // that triggered it does), but WC_Payment_Gateway::get_order_total() - used by this // plugin's own check_gateway() filter on woocommerce_available_payment_gateways to // enforce per-method amount permissions - reads that query var to know whether to use // the order's total or the (here empty, on order-pay) cart's. Left unset, it falls // back to a cart total of 0, which the amount-permission check then rejects, making // Apple Pay appear unavailable below. Setting it restores the normal, fully validated // availability check (API key, requirements, amount permissions, etc.). global $wp_query; $wp_query->set('order-pay', $order_id); $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if (!isset($available_gateways['apple_pay'])) { wp_send_json([ 'result' => 'failure', 'messages' => '', ]); return; } try { $result = $available_gateways['apple_pay']->process_payment($order_id); wp_send_json($result); } catch (\Exception $e) { wp_send_json([ 'result' => 'failure', 'messages' => '', ]); } } /** * Create the woocommerce order in the BO */ public function ajax_create_payment(): void { global $wp; $https_referer = wc_clean(wp_unslash($_POST['_wp_http_referer'] ?? '')); $path = wp_parse_url($https_referer) ?: []; $output = []; if (!empty($path['query'])) { wp_parse_str($path['query'], $output); } if (isset($output['order-pay'])) { $order_id = absint($output['order-pay']); } else { preg_match('/(?<=order-pay\/)\d*/', $path['path'] ?? '', $matches); $order_id = !empty($matches[0]) ? absint($matches[0]) : 0; } // The referer is client-supplied and can be spoofed: only trust it as an order-pay // request once the order it names is confirmed real and the key matches, exactly // like the order-pay AJAX flows below already require (create_payment_intent, // ajax_apple_pay_create_order_pay). Otherwise fall through as a regular checkout. $order = $order_id ? wc_get_order($order_id) : false; if (!$order instanceof \WC_Order || !hash_equals($order->get_order_key(), wc_clean(wp_unslash($output['key'] ?? '')))) { $order_id = 0; } // Order-pay repays an existing order, whose line items live on the order itself, // not the session cart - which is legitimately empty here (the customer already // completed checkout for it), so only require a non-empty cart on a fresh checkout. if (empty($order_id) && WC()->cart->is_empty()) { wp_send_json_error(__('Empty cart', 'payplug')); } if (!defined('WOOCOMMERCE_CHECKOUT')) { define('WOOCOMMERCE_CHECKOUT', true); } $payment_method = $_POST['payment_method']; if ($payment_method === 'payplug' || $payment_method === 'american_express') { $method = $this->get_configuration()->get_option('payment_methods.configuration.payplug.embedded_mode'); if ('integrated' != $method && 'popup' != $method) { $this->ajax_create_order(); } } else { $this->ajax_create_order(); } $this->process_order_payment($order_id, $payment_method); } /** * wordpress class-wc-checkout.php * We don't need all the other verifications, at this point customer already had the checkout and it's all valid, the order already exists * We can+t use WP method because it's protected * Process an order that does require payment. * * @since 3.0.0 * * @param int $order_id Order ID. * @param string $payment_method Payment method. */ protected function process_order_payment($order_id, $payment_method): void { // This AJAX request's own URL never carries the order-pay query var (only the page // that triggered it does), but WC_Payment_Gateway::get_order_total() - used by this // plugin's own check_gateway() filter on woocommerce_available_payment_gateways to // enforce per-method amount permissions - reads that query var to know whether to use // the order's total or the (here empty, on order-pay) cart's. Left unset, it falls // back to a cart total of 0, which the amount-permission check then rejects, making // every gateway appear unavailable below. Setting it restores the normal, fully // validated availability check (API key, requirements, amount permissions, etc.). global $wp_query; $wp_query->set('order-pay', $order_id); $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if (!isset($available_gateways[$payment_method])) { return; } // Store Order ID in session so it can be re-used after payment failure. WC()->session->set('order_awaiting_payment', $order_id); // Process Payment. $result = $available_gateways[$payment_method]->process_payment($order_id); // Redirect to success/confirmation/payment page. if (isset($result['result']) && 'success' === $result['result']) { $result['order_id'] = $order_id; $result = apply_filters('woocommerce_payment_successful_result', $result, $order_id); if (!wp_doing_ajax()) { // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect wp_redirect($result['redirect']); exit; } wp_send_json($result); } } /** * Update Payplug API Payment for Apple Pay */ public function applepay_update_payment(): void { $payment_id = $_POST['payment_id']; $apiService = new \Payplug\PayplugWoocommerce\Service\Api(); $mode = PayplugWoocommerceHelper::check_mode() ? 'live' : 'test'; $bearer_token = $apiService->get_bearer_token($mode); try { \Payplug\Payplug::init([ 'secretKey' => $bearer_token, 'apiVersion' => '2019-08-06', ]); } catch (\Exception $e) { $is_401 = (method_exists($e, 'getCode') && $e->getCode() == 401) || strpos($e->getMessage(), '401') !== false; if ($is_401 && $this->try_refresh_jwt($apiService, $mode)) { try { \Payplug\Payplug::init([ 'secretKey' => $apiService->get_bearer_token($mode), 'apiVersion' => '2019-08-06', ]); } catch (\Exception $e) { PayplugWoocommerceHelper::payplug_logout(); wp_send_json_error(__('You have been disconnected from the Payplug module (error 401)', 'payplug')); return; } } else { if ($is_401) { PayplugWoocommerceHelper::payplug_logout(); wp_send_json_error(__('You have been disconnected from the Payplug module (error 401)', 'payplug')); } else { wp_send_json_error($e->getMessage()); } return; } } $apple_pay = []; $apple_pay['payment_token'] = $_POST['payment_token']; $payment = \Payplug\Payment::retrieve($payment_id); $data = ['apple_pay' => $apple_pay]; $update = $payment->update($data); wp_send_json_success(['result' => $update->is_paid]); } public function applepay_get_order_totals(): void { try { wp_send_json_success(WC()->cart->total); } catch (\Exception $e) { PayplugGateway::log($e->getMessage()); wp_send_json_error($e->getMessage()); } } /** * Empty cart for Apple Pay on product page */ public function applepay_empty_cart(): void { try { WC()->cart->empty_cart(); wp_send_json_success(); } catch (\Exception $e) { wp_send_json_error([ 'message' => __('Your order was cancelled.', 'woocommerce'), ]); } } /** * Add the product on the current page to the cart for Apple Pay on product page */ public function applepay_add_to_cart(): void { try { if (!empty($_POST['product_id'])) { $product_id = $_POST['product_id']; } else { $product_id = $_POST['product_variation_id']; } $product_quantity = !empty($_POST['product_quantity']) ? $_POST['product_quantity'] : 1; WC()->cart->add_to_cart($product_id, $product_quantity); wp_send_json_success([ 'total' => WC()->cart->total - WC()->cart->shipping_total, ]); } catch (\Exception $e) { wp_send_json_error([ 'message' => __('Your order was cancelled.', 'woocommerce'), ]); } } /** * Limit string length. * * @param string $value * @param int $maxlength * * @return string */ public function limit_length($value, $maxlength = 100) { return (strlen($value) > $maxlength) ? substr($value, 0, $maxlength) : $value; } public function check_payment(): void { global $wpdb; $payment_id = $_POST['payment_id']; if (empty($payment_id)) { wp_send_json_error(__('Invalid request.', 'payplug')); } $apiService = new \Payplug\PayplugWoocommerce\Service\Api(); $mode = PayplugWoocommerceHelper::check_mode() ? 'live' : 'test'; $bearer_token = $apiService->get_bearer_token($mode); try { \Payplug\Payplug::init([ 'secretKey' => $bearer_token, 'apiVersion' => '2019-08-06', ]); $payment = \Payplug\Payment::retrieve($payment_id); } catch (\Exception $e) { $is_401 = (method_exists($e, 'getCode') && $e->getCode() == 401) || strpos($e->getMessage(), '401') !== false; if ($is_401) { if ($this->try_refresh_jwt($apiService, $mode)) { try { \Payplug\Payplug::init([ 'secretKey' => $apiService->get_bearer_token($mode), 'apiVersion' => '2019-08-06', ]); $payment = \Payplug\Payment::retrieve($payment_id); } catch (\Exception $e) { PayplugWoocommerceHelper::payplug_logout(); wp_send_json_error(__('You have been disconnected from the Payplug module (error 401)', 'payplug')); return; } } else { PayplugWoocommerceHelper::payplug_logout(); wp_send_json_error(__('You have been disconnected from the Payplug module (error 401)', 'payplug')); return; } } else { $order_id = $this->getOrderFromPaymentId($payment_id); $order = wc_get_order($order_id); PayplugGateway::log( sprintf( 'Order #%s : An error occurred while retrieving the payment data with the message : %s', $order_id, $e->getMessage() ) ); wp_send_json_error($e->getMessage()); } } $order_id = $this->getOrderFromPaymentId($payment_id); $order = wc_get_order($order_id); $return_url = esc_url_raw($order->get_checkout_order_received_url()); if ((isset($payment->failure)) && (!empty($payment->failure)) || ($payment->is_paid === false && is_null($payment->paid_at))) { $order->update_status('failed', __('Order cancelled by customer.', 'woocommerce')); wp_send_json_error( [ 'code' => isset($payment->failure->code) ? $payment->failure->code : 500, 'message' => !empty($payment->failure->message) ? $payment->failure->message : __('payplug_integrated_payment_error', 'payplug'), 'cancel_url' => esc_url_raw($order->get_cancel_order_url_raw()), ] ); } wp_send_json_success([ 'payment_id' => $payment->id, 'result' => 'success', 'redirect' => !empty($payment->hosted_payment->payment_url) ? $payment->hosted_payment->payment_url : $return_url, 'cancel' => !empty($payment->hosted_payment->cancel_url) ? $payment->hosted_payment->cancel_url : null, ]); } /** * Attempt to regenerate the JWT for the given mode and persist the new token. * * @param \Payplug\PayplugWoocommerce\Service\Api $apiService * @param string $mode * * @return bool True if a new JWT was successfully obtained and stored. */ private function try_refresh_jwt($apiService, $mode) { $configuration = $this->get_configuration(); $options = $configuration->get_options(); $oauth_client_data = isset($options['oauth_client_data']) ? json_decode($options['oauth_client_data'], true) : []; $client_data = isset($oauth_client_data[$mode]) ? $oauth_client_data[$mode] : []; if (empty($client_data)) { return false; } $new_jwt = $apiService->generate_jwt( isset($client_data['client_id']) ? $client_data['client_id'] : '', isset($client_data['client_secret']) ? $client_data['client_secret'] : '' ); if (empty($new_jwt) || !isset($new_jwt['access_token'])) { return false; } $jwt = isset($options['jwt']) ? json_decode($options['jwt'], true) : []; $jwt[$mode] = $new_jwt; $api_keys = isset($options['api_key']) ? json_decode($options['api_key'], true) : []; $api_keys[$mode] = $new_jwt['access_token']; $configuration->update_option('jwt', json_encode($jwt)); $configuration->update_option('api_key', json_encode($api_keys)); return true; } /** * @param $payment_id * * @return int|string|null */ private function getOrderFromPaymentId($payment_id) { global $wpdb; if (OrderUtil::custom_orders_table_usage_is_enabled()) { $orders = wc_get_orders( [ 'field_query' => [ [ 'key' => 'transaction_id', 'comparison' => $payment_id, ], ], ] ); $order = $orders[0]; $order_id = $order->get_id(); } else { $sql = 'SELECT post_id FROM $wpdb->postmeta WHERE meta_key = "_transaction_id" AND meta_value = %s'; $order_id = $wpdb->get_var( $wpdb->prepare( $sql, $payment_id ) ); } return $order_id; } public function create_payment_intent(): void { if (!check_ajax_referer('woocommerce-process_checkout', 'woocommerce-process-checkout-nonce', false)) { wp_send_json_error(__('Invalid order.', 'payplug'), 403); return; } $order_id = isset($_POST['order_id']) ? absint(wp_unslash($_POST['order_id'])) : 0; $this->gateway = $this->get_payplug_gateway(isset($_POST['gateway']) ? wc_clean(wp_unslash($_POST['gateway'])) : ''); $order = wc_get_order($order_id); if (!$order instanceof \WC_Order || !$this->gateway) { wp_send_json_error(__('Invalid order.', 'payplug')); return; } // On order-pay, closing the payment sheet should keep the customer on the order-pay // page, not cancel the order and send them to the cart like a fresh checkout attempt // would. $is_order_pay = is_wc_endpoint_url('order-pay') || !empty($_POST['order_pay_key']); if (!empty($_POST['order_pay_key'])) { $order_pay_key = wc_clean(wp_unslash($_POST['order_pay_key'])); if (!hash_equals($order->get_order_key(), $order_pay_key)) { wp_send_json_error(__('Invalid order.', 'payplug')); return; } } $cancel_url = $is_order_pay ? esc_url_raw($order->get_checkout_payment_url()) : esc_url_raw($order->get_cancel_order_url_raw()); $customer_id = PayplugWoocommerceHelper::is_pre_30() ? $order->customer_user : $order->get_customer_id(); $return_url = esc_url_raw($order->get_checkout_order_received_url()); $address_data = PayplugAddressData::from_order($order); $amount = (int) PayplugWoocommerceHelper::get_payplug_amount($order->get_total()); $amount = $this->gateway->validate_order_amount($amount); $payment_data = [ 'amount' => $amount, 'currency' => get_woocommerce_currency(), 'allow_save_card' => $this->gateway->save_card_available() && (int) $customer_id > 0, 'billing' => $address_data->get_billing(), 'shipping' => $address_data->get_shipping(), 'hosted_payment' => [ 'return_url' => $return_url, ], 'notification_url' => esc_url_raw(WC()->api_request_url('PayplugGateway')), 'metadata' => [ 'order_id' => $order_id, 'customer_id' => ((int) $customer_id > 0) ? $customer_id : 'guest', 'domain' => $this->limit_length(esc_url_raw(home_url()), 500), ], ]; if (PayplugWoocommerceHelper::is_checkout_block() && is_checkout()) { $payment_data['metadata']['woocommerce_block'] = 'CHECKOUT'; } elseif (PayplugWoocommerceHelper::is_cart_block() && is_cart()) { $payment_data['metadata']['woocommerce_block'] = 'CART'; } if ($this->gateway->id === 'apple_pay') { unset($payment_data['allow_save_card']); $payment_data['payment_method'] = $this->gateway->id; $payment_data['payment_context'] = [ 'apple_pay' => [ 'domain_name' => $this->gateway->domain_name, 'application_data' => base64_encode(json_encode([ 'apple_pay_domain' => $this->gateway->domain_name, ])), ], ]; $payment_data['hosted_payment']['cancel_url'] = $cancel_url; $payment_data['metadata']['applepay_workflow'] = 'checkout'; } $method = $this->get_configuration()->get_option('payment_methods.configuration.payplug.embedded_mode'); if ('integrated' == $method && $this->gateway->id === 'payplug') { $payment_data['initiator'] = 'PAYER'; $payment_data['integration'] = 'INTEGRATED_PAYMENT'; unset($payment_data['hosted_payment']['cancel_url']); } if ('popup' == $method && $this->gateway->id === 'american_express') { $payment_data['payment_method'] = $this->gateway->id; } /** * Filter the payment data before it's used * * @param array $payment_data * @param int $order_id * @param array $customer_details * @param PayplugAddressData $address_data */ $payment_data = apply_filters('payplug_gateway_payment_data', $payment_data, $order_id, [], $address_data); try { $payment = $this->gateway->payplug_api->payment_create($payment_data); } catch (HttpException $e) { PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, wc_print_r($e->getErrorObject(), true)), 'error'); wp_send_json_error(__('Payment processing failed. Please retry.', 'payplug')); return; } catch (\Exception $e) { PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, $e->getMessage()), 'error'); wp_send_json_error(__('Payment processing failed. Please retry.', 'payplug')); return; } // Save transaction id on the order PayplugWoocommerceHelper::is_pre_30() ? update_post_meta($order_id, '_transaction_id', $payment->id) : $order->set_transaction_id($payment->id); if (is_callable([$order, 'save'])) { $order->save(); } $metadata = PayplugWoocommerceHelper::extract_transaction_metadata($payment); PayplugWoocommerceHelper::save_transaction_metadata($order, $metadata); wp_send_json_success([ 'payment_id' => $payment->id, 'merchant_session' => isset($payment->payment_method['merchant_session']) ? $payment->payment_method['merchant_session'] : null, 'redirect' => !empty($payment->hosted_payment->payment_url) ? $payment->hosted_payment->payment_url : $return_url, 'cancel' => $cancel_url, ]); } /** * Returns an instantiated gateway. * * @return PayplugGateway */ protected function get_payplug_gateway($id) { if (!isset($this->gateway)) { $gateways = WC()->payment_gateways()->payment_gateways(); foreach ($gateways as $gateway) { if ($gateway->id === $id) { return $gateway; } } } } }