Файловый менеджер - Редактировать - /home/tuudkjt/globeasy/wp-includes/ID3/Gateways.zip
Назад
PK !m.]��� PayPal/Constants.phpnu ��� <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Gateways\PayPal; /** * PayPal Gateway Constants. * * Provides constants for PayPal payment statuses, intents, and other PayPal-related values. * * @since 10.5.0 */ class Constants { /** * PayPal proxy request timeout. */ const WPCOM_PROXY_REQUEST_TIMEOUT = 60; /** * PayPal payment statuses. */ const STATUS_COMPLETED = 'COMPLETED'; const STATUS_APPROVED = 'APPROVED'; const STATUS_CAPTURED = 'CAPTURED'; const STATUS_AUTHORIZED = 'AUTHORIZED'; const STATUS_PAYER_ACTION_REQUIRED = 'PAYER_ACTION_REQUIRED'; const VOIDED = 'VOIDED'; /** * PayPal payment intents. */ const INTENT_CAPTURE = 'CAPTURE'; const INTENT_AUTHORIZE = 'AUTHORIZE'; /** * PayPal payment actions. */ const PAYMENT_ACTION_CAPTURE = 'capture'; const PAYMENT_ACTION_AUTHORIZE = 'authorize'; /** * PayPal shipping preferences. */ const SHIPPING_NO_SHIPPING = 'NO_SHIPPING'; const SHIPPING_GET_FROM_FILE = 'GET_FROM_FILE'; const SHIPPING_SET_PROVIDED_ADDRESS = 'SET_PROVIDED_ADDRESS'; /** * PayPal user actions. */ const USER_ACTION_PAY_NOW = 'PAY_NOW'; /** * Maximum lengths for PayPal fields. */ const PAYPAL_ORDER_ITEM_NAME_MAX_LENGTH = 127; const PAYPAL_INVOICE_ID_MAX_LENGTH = 127; const PAYPAL_ADDRESS_LINE_MAX_LENGTH = 300; const PAYPAL_COUNTRY_CODE_LENGTH = 2; const PAYPAL_STATE_MAX_LENGTH = 300; const PAYPAL_CITY_MAX_LENGTH = 120; const PAYPAL_POSTAL_CODE_MAX_LENGTH = 60; const PAYPAL_LOCALE_MAX_LENGTH = 10; /** * Supported payment sources. */ const PAYMENT_SOURCE_PAYPAL = 'paypal'; const PAYMENT_SOURCE_VENMO = 'venmo'; const PAYMENT_SOURCE_PAYLATER = 'paylater'; const SUPPORTED_PAYMENT_SOURCES = array( self::PAYMENT_SOURCE_PAYPAL, self::PAYMENT_SOURCE_VENMO, self::PAYMENT_SOURCE_PAYLATER ); /** * Fields to redact from logs. * * @var array */ const FIELDS_TO_REDACT = array( 'given_name', 'surname', 'full_name', 'address_line_1', 'address_line_2', 'admin_area_1', 'admin_area_2', 'postal_code', 'phone', 'phone_number', 'national_number', ); /** * List of currencies supported by PayPal (Orders API V2). * * @var array<string> */ const SUPPORTED_CURRENCIES = array( 'AUD', // Australian Dollar. 'BRL', // Brazilian Real. 'CAD', // Canadian Dollar. 'CNY', // Chinese Renminbi. 'CZK', // Czech Koruna. 'DKK', // Danish Krone. 'EUR', // Euro. 'HKD', // Hong Kong Dollar. 'HUF', // Hungarian Forint. 'ILS', // Israeli New Shekel. 'JPY', // Japanese Yen. 'MYR', // Malaysian Ringgit. 'MXN', // Mexican Peso. 'TWD', // New Taiwan Dollar. 'NZD', // New Zealand Dollar. 'NOK', // Norwegian Krone. 'PHP', // Philippine Peso. 'PLN', // Polish Złoty. 'GBP', // Pound Sterling. 'SGD', // Singapore Dollar. 'SEK', // Swedish Krona. 'CHF', // Swiss Franc. 'THB', // Thai Baht. 'USD', // United States Dollar. 'RUB', // Russian Ruble. ); /** * PayPal authorization already captured issue code. * * @var string */ const PAYPAL_ISSUE_AUTHORIZATION_ALREADY_CAPTURED = 'AUTHORIZATION_ALREADY_CAPTURED'; /** * PayPal account locked or closed issue code. * * @var string */ const PAYPAL_ISSUE_PAYEE_ACCOUNT_LOCKED_OR_CLOSED = 'PAYEE_ACCOUNT_LOCKED_OR_CLOSED'; /** * PayPal account restricted issue code. * * @var string */ const PAYPAL_ISSUE_PAYEE_ACCOUNT_RESTRICTED = 'PAYEE_ACCOUNT_RESTRICTED'; /** * Meta key for storing PayPal payment status in order meta. * * @var string * * @since 10.5.0 */ public const PAYPAL_ORDER_META_STATUS = '_paypal_status'; /** * Meta key for storing PayPal capture ID in order meta. * * @var string * * @since 10.5.0 */ public const PAYPAL_ORDER_META_CAPTURE_ID = '_paypal_capture_id'; /** * Meta key for storing PayPal authorization ID in order meta. * * @var string * * @since 10.5.0 */ public const PAYPAL_ORDER_META_AUTHORIZATION_ID = '_paypal_authorization_id'; /** * Meta key for storing PayPal authorization checked flag in order meta. * * @var string * * @since 10.5.0 */ public const PAYPAL_ORDER_META_AUTHORIZATION_CHECKED = '_paypal_authorization_checked'; /** * Meta key for storing PayPal order ID in order meta. * * @var string * * @since 10.5.0 */ public const PAYPAL_ORDER_META_ORDER_ID = '_paypal_order_id'; /** * Meta key for storing PayPal addresses updated flag in order meta. * * @var string * * @since 10.5.0 */ public const PAYPAL_ORDER_META_ADDRESSES_UPDATED = '_paypal_addresses_updated'; /** * Meta key for storing PayPal payment source in order meta. * * @var string * * @since 10.5.0 */ public const PAYPAL_ORDER_META_PAYMENT_SOURCE = '_paypal_payment_source'; /** * Meta key for storing PayPal shipping callback token in order meta. * * @var string * * @since 10.5.0 */ public const PAYPAL_ORDER_META_SHIPPING_CALLBACK_TOKEN = '_paypal_shipping_callback_token'; } PK !m.]�l� � PayPal/AddressRequirements.phpnu ��� <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Gateways\PayPal; /** * Class AddressRequirements * * This helper class checks country-specific address requirements. * This was built based on {@see https://developer.paypal.com/api/rest/reference/orders/v2/country-address-requirements/} */ class AddressRequirements { /** * The single instance of the class. * * @var AddressRequirements */ protected static $instance = null; /** * Countries that require a city in the address. * * @var array */ private const COUNTRIES_REQUIRING_CITY = array( 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CS', 'CU', 'CV', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TL', 'TM', 'TN', 'TO', 'TP', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'YU', 'ZA', 'ZM', 'ZW', 'ZR', 'C2', ); /** * Countries that require a postal code in the address. * * @var array */ private const COUNTRIES_REQUIRING_POSTAL_CODE = array( 'AR', 'AT', 'AU', 'BR', 'BT', 'CA', 'C2', 'CC', 'CH', 'CN', 'DE', 'DK', 'EH', 'ES', 'FK', 'FM', 'FO', 'FR', 'GB', 'GL', 'GM', 'IT', 'JP', 'KG', 'KI', 'KM', 'MR', 'MX', 'NE', 'NL', 'NO', 'NR', 'NU', 'NF', 'PL', 'PM', 'PN', 'SE', 'SG', 'SH', 'SJ', 'SM', 'SR', 'TF', 'TH', 'TK', 'TV', 'UM', 'US', 'VA', 'WF', 'YT', ); /** * Get class instance. * * @return AddressRequirements Instance. */ final public static function instance() { if ( null === static::$instance ) { static::$instance = new static(); } return static::$instance; } /** * Check if a country requires a city in the address. * * @param string $country_code The ISO 3166-1 alpha-2 country code. * @return bool */ public function country_requires_city( string $country_code ) { return in_array( strtoupper( $country_code ), self::COUNTRIES_REQUIRING_CITY, true ); } /** * Check if a country requires a postal code in the address. * * @param string $country_code The ISO 3166-1 alpha-2 country code. * @return bool */ public function country_requires_postal_code( string $country_code ) { return in_array( strtoupper( $country_code ), self::COUNTRIES_REQUIRING_POSTAL_CODE, true ); } } PK !m.]hb $ $ PayPal/Buttons.phpnu ��� <?php /** * Class Automattic\WooCommerce\Gateways\PayPal\Buttons file. * * @package WooCommerce\Gateways */ declare(strict_types=1); namespace Automattic\WooCommerce\Gateways\PayPal; use Automattic\WooCommerce\Proxies\LegacyProxy; use Automattic\WooCommerce\Gateways\PayPal\Request as PayPalRequest; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Handles PayPal Buttons. * * @since 10.5.0 */ class Buttons { /** * The option for the client-id. * * @var string */ private const CLIENT_ID_OPTION = 'woocommerce_paypal_client_id'; /** * The gateway instance. * * @var \WC_Gateway_Paypal */ private \WC_Gateway_Paypal $gateway; /** * Whether the gateway should use Orders v2 API. * * @var bool */ private bool $enabled; /** * The request instance. * * @var PayPalRequest */ private PayPalRequest $request; /** * Constructor. * * @param \WC_Gateway_Paypal $gateway The gateway instance. */ public function __construct( \WC_Gateway_Paypal $gateway ) { $this->gateway = $gateway; $this->request = new PayPalRequest( $this->gateway ); $this->enabled = $this->gateway->should_use_orders_v2() && 'yes' === $this->gateway->get_option( 'paypal_buttons', 'yes' ); } /** * Get the options for the PayPal buttons. * * @since 10.5.0 * * @return array */ public function get_options(): array { $common_options = $this->get_common_options(); $options = array( 'partner-attribution-id' => 'Woo_Cart_CoreUpgrade', 'page-type' => $this->get_page_type(), ); return array_merge( $common_options, $options ); } /** * Get the common attributes for the PayPal JS SDK script and modules. * * @since 10.5.0 * * @return array */ public function get_common_options(): array { $intent = $this->gateway->get_option( 'paymentaction' ) === 'authorization' ? 'authorize' : 'capture'; return array( 'client-id' => $this->get_client_id(), 'components' => 'buttons,funding-eligibility,messages', 'disable-funding' => 'card,applepay', 'enable-funding' => 'venmo,paylater', 'currency' => get_woocommerce_currency(), 'intent' => $intent, 'merchant-id' => $this->gateway->email, ); } /** * Get the client-id for the PayPal buttons. * * @since 10.5.0 * * @return string|null The PayPal client-id, or null if the request fails. */ public function get_client_id(): ?string { if ( ! $this->gateway->should_use_orders_v2() ) { return null; } $option_key = self::CLIENT_ID_OPTION . ( $this->gateway->testmode ? '_sandbox' : '_live' ); $client_id = get_option( $option_key, null ); if ( empty( $client_id ) ) { $client_id = $this->request->fetch_paypal_client_id(); if ( empty( $client_id ) ) { return null; } update_option( $option_key, $client_id ); } return $client_id; } /** * Get the page type for the PayPal buttons. * * @since 10.5.0 * * @return string */ public function get_page_type(): string { $page_type = 'checkout'; if ( is_cart() || has_block( 'woocommerce/cart' ) ) { $page_type = 'cart'; } elseif ( is_product() ) { $page_type = 'product-details'; } return $page_type; } /** * Whether PayPal Buttons is enabled. * * @since 10.5.0 * * @return bool */ public function is_enabled(): bool { return $this->enabled; } /** * Get the current page URL, to be used for App Switch. * Limited to checkout, cart, and product pages for security. * * @since 10.5.0 * * @return string */ public function get_current_page_for_app_switch(): string { // If checkout, cart or product page, return the current page URL. if ( wc_get_container()->get( LegacyProxy::class )->call_function( 'is_checkout' ) || is_cart() || is_product() ) { return get_permalink( get_the_ID() ); } return ''; } } PK !m.]���& & PayPal/Helper.phpnu ��� <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Gateways\PayPal; use WC_Order; use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants; defined( 'ABSPATH' ) || exit; /** * PayPal Helper Class * * Helper methods for PayPal gateway operations including order validation, * data redaction, and address updates. * * @since 10.5.0 */ class Helper { /** * Check if the PayPal gateway is enabled. * * @return bool */ public static function is_paypal_gateway_available(): bool { $settings = get_option( 'woocommerce_paypal_settings', array() ); $enabled = isset( $settings['enabled'] ) && 'yes' === $settings['enabled']; $should_load = isset( $settings['_should_load'] ) && 'yes' === $settings['_should_load']; return $enabled && $should_load; } /** * Check if the merchant is eligible for migration from WPS to PPCP. * * @return bool */ public static function is_orders_v2_migration_eligible(): bool { $settings = get_option( 'woocommerce_paypal_settings', array() ); // If API keys are set, the merchant is not eligible for migration // as they may be using features that cannot be seamlessly migrated. $is_test_mode = isset( $settings['testmode'] ) && 'yes' === $settings['testmode']; $api_username = $is_test_mode ? ( $settings['sandbox_api_username'] ?? null ) : ( $settings['api_username'] ?? null ); $api_password = $is_test_mode ? ( $settings['sandbox_api_password'] ?? null ) : ( $settings['api_password'] ?? null ); $api_signature = $is_test_mode ? ( $settings['sandbox_api_signature'] ?? null ) : ( $settings['api_signature'] ?? null ); return empty( $api_username ) && empty( $api_password ) && empty( $api_signature ); } /** * Get the WC order from the PayPal custom ID. * * @param string $custom_id The custom ID string from the PayPal order. * @return WC_Order|null */ public static function get_wc_order_from_paypal_custom_id( string $custom_id ): ?WC_Order { if ( '' === $custom_id ) { return null; } $data = json_decode( $custom_id, true ); if ( ! is_array( $data ) ) { return null; } $order_id = $data['order_id'] ?? null; if ( ! $order_id ) { return null; } $order = wc_get_order( $order_id ); if ( ! $order instanceof \WC_Order ) { return null; } // Validate the order key. $order_key = $data['order_key'] ?? null; if ( $order_key !== $order->get_order_key() ) { return null; } return $order; } /** * Remove PII (Personally Identifiable Information) from data for logging. * * This function recursively traverses the data array and redacts sensitive information * while preserving the structure for debugging purposes. * * @param mixed $data The data to remove PII from (array, string, or other types). * @return mixed The data with PII redacted. */ public static function redact_data( $data ) { if ( ! is_array( $data ) ) { return $data; } $redacted_data = array(); foreach ( $data as $key => $value ) { // Skip redacting the payee information as it belongs to the store merchant. if ( 'payee' === $key ) { $redacted_data[ $key ] = $value; continue; } // Mask the email address. if ( 'email_address' === $key || 'email' === $key ) { $redacted_data[ $key ] = self::mask_email( (string) $value ); continue; } if ( is_array( $value ) ) { $redacted_data[ $key ] = self::redact_data( $value ); } elseif ( in_array( $key, Constants::FIELDS_TO_REDACT, true ) ) { $redacted_data[ $key ] = '[redacted]'; } else { // Keep non-PII data as is. $redacted_data[ $key ] = $value; } } return $redacted_data; } /** * Mask email address before @ keeping the full domain. * * @param string $email The email address to mask. * @return string The masked email address or original input if invalid. */ public static function mask_email( string $email ): string { if ( empty( $email ) ) { return $email; } $parts = explode( '@', $email, 2 ); if ( count( $parts ) !== 2 || empty( $parts[0] ) || empty( $parts[1] ) ) { return $email; } list( $local, $domain ) = $parts; if ( strlen( $local ) <= 3 ) { $masked_local = str_repeat( '*', strlen( $local ) ); } else { $masked_local = substr( $local, 0, 2 ) . str_repeat( '*', max( 1, strlen( $local ) - 3 ) ) . substr( $local, -1 ); } return $masked_local . '@' . $domain; } /** * Update the addresses in the order. * * @param WC_Order|null $order The order object. * @param array $paypal_order_details The PayPal order details. * @return void */ public static function update_addresses_in_order( ?WC_Order $order, array $paypal_order_details ): void { if ( empty( $order ) || empty( $paypal_order_details ) ) { return; } // Bail early if '_paypal_addresses_updated' is 'yes', meaning the addresses update already have been successful. if ( 'yes' === $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_ADDRESSES_UPDATED, true ) ) { return; } // Update the shipping information. $full_name = $paypal_order_details['purchase_units'][0]['shipping']['name']['full_name'] ?? ''; if ( ! empty( $full_name ) ) { $name_parts = explode( ' ', $full_name, 2 ); $approximate_first_name = $name_parts[0] ?? ''; $approximate_last_name = isset( $name_parts[1] ) ? $name_parts[1] : ''; $order->set_shipping_first_name( $approximate_first_name ); $order->set_shipping_last_name( $approximate_last_name ); } $shipping_address = $paypal_order_details['purchase_units'][0]['shipping']['address'] ?? array(); if ( ! empty( $shipping_address ) ) { $order->set_shipping_country( $shipping_address['country_code'] ?? '' ); $order->set_shipping_postcode( $shipping_address['postal_code'] ?? '' ); $order->set_shipping_state( $shipping_address['admin_area_1'] ?? '' ); $order->set_shipping_city( $shipping_address['admin_area_2'] ?? '' ); $order->set_shipping_address_1( $shipping_address['address_line_1'] ?? '' ); $order->set_shipping_address_2( $shipping_address['address_line_2'] ?? '' ); } // Update the billing information. $full_name = $paypal_order_details['payer']['name'] ?? array(); $email = $paypal_order_details['payer']['email_address'] ?? ''; if ( ! empty( $full_name ) ) { $order->set_billing_first_name( $full_name['given_name'] ?? '' ); $order->set_billing_last_name( $full_name['surname'] ?? '' ); $order->set_billing_email( $email ); } $billing_address = $paypal_order_details['payer']['address'] ?? array(); if ( ! empty( $billing_address ) ) { $order->set_billing_country( $billing_address['country_code'] ?? '' ); $order->set_billing_postcode( $billing_address['postal_code'] ?? '' ); $order->set_billing_state( $billing_address['admin_area_1'] ?? '' ); $order->set_billing_city( $billing_address['admin_area_2'] ?? '' ); $order->set_billing_address_1( $billing_address['address_line_1'] ?? '' ); $order->set_billing_address_2( $billing_address['address_line_2'] ?? '' ); } $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_ADDRESSES_UPDATED, 'yes' ); $order->save(); } } PK !m.]�5&r�, �, ! PayPal/TransactAccountManager.phpnu ��� <?php declare(strict_types=1); namespace Automattic\WooCommerce\Gateways\PayPal; use Automattic\Jetpack\Connection\Client as Jetpack_Connection_Client; use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * PayPal TransactAccountManager Class * * Handles Transact account management. * * @since 10.5.0 */ final class TransactAccountManager { /** * The API version for the proxy endpoint. * * @var int * * @since 10.5.0 */ private const WPCOM_PROXY_ENDPOINT_API_VERSION = 2; /** * Transact provider type, for provider onboarding. * * @var string * * @since 10.5.0 */ private const TRANSACT_PROVIDER_TYPE = 'paypal_standard'; /** * Cache key for the merchant account in live mode. * * @var string * * @since 10.5.0 */ private const TRANSACT_MERCHANT_ACCOUNT_CACHE_KEY_LIVE = 'woocommerce_paypal_transact_merchant_account_live'; /** * Cache key for the merchant account in test mode. * * @var string * * @since 10.5.0 */ private const TRANSACT_MERCHANT_ACCOUNT_CACHE_KEY_TEST = 'woocommerce_paypal_transact_merchant_account_test'; /** * Cache key for the provider account in live mode. * * @var string * * @since 10.5.0 */ private const TRANSACT_PROVIDER_ACCOUNT_CACHE_KEY_LIVE = 'woocommerce_paypal_transact_provider_account_live'; /** * Cache key for the provider account in test mode. * * @var string * * @since 10.5.0 */ private const TRANSACT_PROVIDER_ACCOUNT_CACHE_KEY_TEST = 'woocommerce_paypal_transact_provider_account_test'; /** * The expiry time for the Transact account cache. * * @var int * * @since 10.5.0 */ private const TRANSACT_ACCOUNT_CACHE_EXPIRY = 60 * 60 * 24; // 24 hours. /** * Paypal gateway object. * * @var \WC_Gateway_Paypal */ private \WC_Gateway_Paypal $gateway; /** * Constructor. * * @param \WC_Gateway_Paypal $gateway Paypal gateway object. */ public function __construct( \WC_Gateway_Paypal $gateway ) { $this->gateway = $gateway; } /** * Onboard the merchant with the Transact platform. * * @return void * * @since 10.5.0 */ public function do_onboarding(): void { // Check that we have a PayPal email. This is required for processing payments -- // used as the payee email. Only begin onboarding if this minimum requirement is met. if ( empty( $this->gateway->email ) ) { return; } // Register with Jetpack if not already connected. $jetpack_connection_manager = $this->gateway->get_jetpack_connection_manager(); if ( ! $jetpack_connection_manager ) { \WC_Gateway_Paypal::log( 'Jetpack connection manager not found.', 'error' ); return; } if ( ! $jetpack_connection_manager->is_connected() ) { $result = $jetpack_connection_manager->try_registration(); if ( is_wp_error( $result ) ) { \WC_Gateway_Paypal::log( 'Jetpack registration failed: ' . $result->get_error_message(), 'error' ); return; } } // Fetch (cached) or create the Transact merchant and provider accounts. $merchant_account_data = $this->get_transact_account_data( 'merchant' ); if ( empty( $merchant_account_data ) ) { $merchant_account = $this->create_merchant_account(); if ( empty( $merchant_account ) ) { \WC_Gateway_Paypal::log( 'Transact merchant onboarding failed.', 'error' ); return; } // Cache the merchant account data. $this->update_transact_account_cache( $this->get_cache_key( 'merchant' ), $merchant_account ); } $provider_account_data = $this->get_transact_account_data( 'provider' ); if ( empty( $provider_account_data ) ) { $provider_account = $this->create_provider_account(); if ( ! $provider_account ) { \WC_Gateway_Paypal::log( 'Transact provider onboarding failed.', 'error' ); return; } // Cache the provider account data. $this->update_transact_account_cache( $this->get_cache_key( 'provider' ), $provider_account ); } // Set an extra flag to indicate that we've completed onboarding, // so we can do inexpensive early returns for checkers like // WC_Gateway_Paypal::should_use_orders_v2(). $this->gateway->set_transact_onboarding_complete(); } /** * Get the Transact account (merchant or provider) data. Performs a fetch if the account * is not in cache or expired. * * @since 10.5.0 * * @param string $account_type The type of account to get (merchant or provider). * @return mixed Returns null if the transact account cannot be retrieved. */ public function get_transact_account_data( string $account_type ) { $cache_key = $this->get_cache_key( $account_type ); // Get transact account from cache. If not found, fetch/create it. $transact_account = $this->get_transact_account_from_cache( $cache_key ); if ( empty( $transact_account ) ) { $transact_account = 'merchant' === $account_type ? $this->fetch_merchant_account() : $this->fetch_provider_account(); // Fetch failed. if ( empty( $transact_account ) ) { return null; } // Update cache. $this->update_transact_account_cache( $cache_key, $transact_account ); } return $transact_account; } /** * Get the cache key for the transact account. * * @since 10.5.0 * * @param string $account_type The type of account to get (merchant or provider). * @return string|null The cache key, or null if the account type is invalid. */ private function get_cache_key( string $account_type ): ?string { if ( 'merchant' === $account_type ) { return $this->gateway->testmode ? self::TRANSACT_MERCHANT_ACCOUNT_CACHE_KEY_TEST : self::TRANSACT_MERCHANT_ACCOUNT_CACHE_KEY_LIVE; } if ( 'provider' === $account_type ) { return $this->gateway->testmode ? self::TRANSACT_PROVIDER_ACCOUNT_CACHE_KEY_TEST : self::TRANSACT_PROVIDER_ACCOUNT_CACHE_KEY_LIVE; } return null; } /** * Fetch the merchant account from the Transact platform. * * @since 10.5.0 * * @return array|null The API response body, or null if the request fails. */ private function fetch_merchant_account(): ?array { $site_id = \Jetpack_Options::get_option( 'id' ); if ( ! $site_id ) { return null; } $request_body = array( 'test_mode' => $this->gateway->testmode, ); $response = $this->send_transact_api_request( 'GET', sprintf( '/sites/%d/transact/account', $site_id ), $request_body ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return null; } $response_data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $response_data['public_id'] ) ) { return null; } return array( 'public_id' => $response_data['public_id'] ); } /** * Fetch the provider account from the Transact platform. * * @since 10.5.0 * * @return bool True if the provider account exists, false otherwise. */ private function fetch_provider_account(): bool { $site_id = \Jetpack_Options::get_option( 'id' ); if ( ! $site_id ) { return false; } $request_body = array( 'test_mode' => $this->gateway->testmode, 'provider_type' => self::TRANSACT_PROVIDER_TYPE, ); $response = $this->send_transact_api_request( 'GET', sprintf( '/sites/%d/transact/account/%s', $site_id, self::TRANSACT_PROVIDER_TYPE ), $request_body ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return false; } // Provider account response only returns an empty onboarding link, // which we do not need. return true; } /** * Create the merchant account with the Transact platform. * * @since 10.5.0 * * @return array|null The API response body, or null if the request fails. */ private function create_merchant_account(): ?array { $site_id = \Jetpack_Options::get_option( 'id' ); if ( ! $site_id ) { return null; } $request_body = array( 'test_mode' => $this->gateway->testmode ); $response = $this->send_transact_api_request( 'POST', sprintf( '/sites/%d/transact/account', $site_id ), $request_body ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return null; } $response_data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $response_data['public_id'] ) ) { \WC_Gateway_Paypal::log( 'Transact merchant account creation failed. Response body: ' . wc_print_r( $response_data, true ) ); return null; } return array( 'public_id' => $response_data['public_id'] ); } /** * Create the provider account with the Transact platform. * * @since 10.5.0 * * @return bool True if the provider account creation was successful, false otherwise. */ private function create_provider_account(): bool { $site_id = \Jetpack_Options::get_option( 'id' ); if ( ! $site_id ) { return false; } $request_body = array( 'test_mode' => $this->gateway->testmode, 'provider_type' => self::TRANSACT_PROVIDER_TYPE, ); $response = $this->send_transact_api_request( 'POST', sprintf( '/sites/%d/transact/account/%s/onboard', $site_id, self::TRANSACT_PROVIDER_TYPE ), $request_body ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return false; } // Provider account response only returns an empty onboarding link, // which we do not need. return true; } /** * Update the transact account (merchant or provider) cache. * * @since 10.5.0 * * @param string|null $cache_key The cache key to update. * @param array|bool $account_data The transact account data. * * @return void */ private function update_transact_account_cache( string $cache_key, $account_data ): void { $expires = time() + self::TRANSACT_ACCOUNT_CACHE_EXPIRY; update_option( $cache_key, array( 'account' => $account_data, 'expiry' => $expires, ) ); } /** * Get the transact account (merchant or provider) from the database cache. * * @since 10.5.0 * * @param string $cache_key The cache key to get the account. * @return mixed The transact account data, or null if the cache is * empty or expired. */ private function get_transact_account_from_cache( string $cache_key ) { $transact_account = get_option( $cache_key, null ); if ( empty( $transact_account ) || ( isset( $transact_account['expiry'] ) && $transact_account['expiry'] < time() ) ) { return null; } return $transact_account['account'] ?? null; } /** * Send a request to the Transact platform. * * @since 10.5.0 * * @param string $method The HTTP method to use. * @param string $endpoint The endpoint to request. * @param array $request_body The request body. * * @return array|\WP_Error The API response body, or null if the request fails. */ private function send_transact_api_request( string $method, string $endpoint, array $request_body ) { if ( 'GET' === $method ) { $endpoint .= '?' . http_build_query( $request_body ); } $response = Jetpack_Connection_Client::wpcom_json_api_request_as_blog( $endpoint, (string) self::WPCOM_PROXY_ENDPOINT_API_VERSION, array( 'headers' => array( 'Content-Type' => 'application/json' ), 'method' => $method, 'timeout' => PayPalConstants::WPCOM_PROXY_REQUEST_TIMEOUT, ), 'GET' === $method ? null : wp_json_encode( $request_body ), 'wpcom' ); return $response; } } PK !m.]���� � PayPal/Request.phpnu ��� <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Gateways\PayPal; use Exception; use WC_Order; use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants; use Automattic\WooCommerce\Gateways\PayPal\AddressRequirements as PayPalAddressRequirements; use Automattic\WooCommerce\Enums\OrderStatus; use Automattic\Jetpack\Connection\Client as Jetpack_Connection_Client; defined( 'ABSPATH' ) || exit; /** * PayPal Request Class * * Handles PayPal API requests for creating orders, authorizing/capturing payments, * and fetching PayPal order details using the Orders v2 API. * * @since 10.5.0 */ class Request { /** * The PayPal gateway instance. * * @var \WC_Gateway_Paypal */ private \WC_Gateway_Paypal $gateway; /** * The API version for the proxy endpoint. * * @var string */ private const WPCOM_PROXY_ENDPOINT_API_VERSION = '2'; /** * The base for the proxy REST endpoint. * * @var string */ private const WPCOM_PROXY_REST_BASE = 'transact/paypal_standard/proxy'; /** * Proxy REST endpoints. * * @var string */ private const WPCOM_PROXY_ORDER_ENDPOINT = 'order'; private const WPCOM_PROXY_PAYMENT_CAPTURE_ENDPOINT = 'payment/capture'; private const WPCOM_PROXY_PAYMENT_AUTHORIZE_ENDPOINT = 'payment/authorize'; private const WPCOM_PROXY_PAYMENT_CAPTURE_AUTH_ENDPOINT = 'payment/capture_auth'; private const WPCOM_PROXY_CLIENT_ID_ENDPOINT = 'client_id'; /** * Constructor. * * @param \WC_Gateway_Paypal $gateway Paypal gateway object. */ public function __construct( \WC_Gateway_Paypal $gateway ) { $this->gateway = $gateway; } /** * Create a PayPal order using the Orders v2 API. * * This method creates a PayPal order and returns the order details including * the approval URL where customers will be redirected to complete payment. * * @param WC_Order $order Order object. * @param string $payment_source The payment source. * @param array $js_sdk_params Extra parameters for a PayPal JS SDK (Buttons) request. * @return array|null * @throws Exception If the PayPal order creation fails. */ public function create_paypal_order( WC_Order $order, string $payment_source = PayPalConstants::PAYMENT_SOURCE_PAYPAL, array $js_sdk_params = array() ): ?array { $paypal_debug_id = null; // While PayPal JS SDK can return 'paylater' as the payment source in the createOrder callback, // Orders v2 API does not accept it. We will use 'paypal' instead. // Accepted payment_source values for Orders v2: // https://developer.paypal.com/docs/api/orders/v2/#orders_create!ct=application/json&path=payment_source&t=request. if ( PayPalConstants::PAYMENT_SOURCE_PAYLATER === $payment_source ) { $payment_source = PayPalConstants::PAYMENT_SOURCE_PAYPAL; } try { $request_body = array( 'test_mode' => $this->gateway->testmode, 'order' => $this->get_paypal_create_order_request_params( $order, $payment_source, $js_sdk_params ), ); $response = $this->send_wpcom_proxy_request( 'POST', self::WPCOM_PROXY_ORDER_ENDPOINT, $request_body ); if ( is_wp_error( $response ) ) { throw new Exception( 'PayPal order creation failed. Response error: ' . $response->get_error_message() ); } if ( ! is_array( $response ) ) { throw new Exception( 'PayPal order creation failed. Invalid response type.' ); } $http_code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $response_data = json_decode( $body, true ); $response_array = is_array( $response_data ) ? $response_data : array(); /** * Fires after receiving a response from PayPal order creation. * * This hook allows extensions to react to PayPal API responses, such as * displaying admin notices or logging response data. * * Note: This hook fires on EVERY order creation attempt (success or failure), * and can be called multiple times for the same order if retried. Extensions * hooking this should be idempotent and check order state/meta before taking * action to avoid duplicate processing. * * @since 10.4.0 * * @param int|string $http_code The HTTP status code from the PayPal API response. * @param array $response_data The decoded response data from the PayPal API * @param WC_Order $order The WooCommerce order object. */ do_action( 'woocommerce_paypal_standard_order_created_response', $http_code, $response_array, $order ); if ( ! in_array( $http_code, array( 200, 201 ), true ) ) { $paypal_debug_id = isset( $response_data['debug_id'] ) ? $response_data['debug_id'] : null; throw new Exception( 'PayPal order creation failed. Response status: ' . $http_code . '. Response body: ' . $body ); } $redirect_url = null; if ( empty( $js_sdk_params['is_js_sdk_flow'] ) ) { // We only need an approve link for the classic, redirect flow. $redirect_url = $this->get_approve_link( $http_code, $response_data ); if ( empty( $redirect_url ) ) { throw new Exception( 'PayPal order creation failed. Missing approval link.' ); } } // Save the PayPal order ID to the order. $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_ORDER_ID, $response_data['id'] ); // Save the PayPal order status to the order. $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_STATUS, $response_data['status'] ); // Remember the payment source: payment_source is not patchable. // If the payment source is changed, we need to create a new PayPal order. $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_PAYMENT_SOURCE, $payment_source ); $order->save(); return array( 'id' => $response_data['id'], 'redirect_url' => $redirect_url, ); } catch ( Exception $e ) { \WC_Gateway_Paypal::log( $e->getMessage() ); if ( $paypal_debug_id ) { $order->add_order_note( sprintf( /* translators: %1$s: PayPal debug ID */ __( 'PayPal order creation failed. PayPal debug ID: %1$s', 'woocommerce' ), $paypal_debug_id ) ); } return null; } } /** * Get PayPal order details. * * @param string $paypal_order_id The ID of the PayPal order. * @return array * @throws Exception If the PayPal order details request fails. * @throws Exception If the PayPal order details are not found. */ public function get_paypal_order_details( string $paypal_order_id ): array { $request_body = array( 'test_mode' => $this->gateway->testmode, ); $response = $this->send_wpcom_proxy_request( 'GET', self::WPCOM_PROXY_ORDER_ENDPOINT . '/' . $paypal_order_id, $request_body ); if ( is_wp_error( $response ) ) { throw new Exception( 'PayPal order details request failed: ' . esc_html( $response->get_error_message() ) ); } $http_code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $response_data = json_decode( $body, true ); if ( 200 !== $http_code ) { $debug_id = isset( $response_data['debug_id'] ) ? $response_data['debug_id'] : null; $message = 'PayPal order details request failed. HTTP ' . (int) $http_code . ( $debug_id ? '. Debug ID: ' . $debug_id : '' ); throw new Exception( esc_html( $message ) ); } return $response_data; } /** * Authorize or capture a PayPal payment using the Orders v2 API. * * This method authorizes or captures a PayPal payment and updates the order status. * * @param WC_Order|null $order Order object. * @param string|null $action_url The URL to authorize or capture the payment. * @param string $action The action to perform. Either 'authorize' or 'capture'. * @return void * @throws Exception If the PayPal payment authorization or capture fails. */ public function authorize_or_capture_payment( ?WC_Order $order, ?string $action_url, string $action = PayPalConstants::PAYMENT_ACTION_CAPTURE ): void { if ( ! $order ) { \WC_Gateway_Paypal::log( 'Order not found to authorize or capture payment.' ); return; } $paypal_debug_id = null; $paypal_order_id = $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_ORDER_ID ); if ( ! $paypal_order_id ) { \WC_Gateway_Paypal::log( 'PayPal order ID not found. Cannot ' . $action . ' payment.' ); return; } if ( ! $action_url || ! filter_var( $action_url, FILTER_VALIDATE_URL ) ) { \WC_Gateway_Paypal::log( 'Invalid or missing action URL. Cannot ' . $action . ' payment.' ); return; } // Skip if the payment is already captured. if ( PayPalConstants::STATUS_COMPLETED === $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_STATUS, true ) ) { \WC_Gateway_Paypal::log( 'PayPal payment is already captured. Skipping capture. Order ID: ' . $order->get_id() ); return; } try { if ( PayPalConstants::PAYMENT_ACTION_CAPTURE === $action ) { $endpoint = self::WPCOM_PROXY_PAYMENT_CAPTURE_ENDPOINT; $request_body = array( 'capture_url' => $action_url, 'paypal_order_id' => $paypal_order_id, 'test_mode' => $this->gateway->testmode, ); } else { $endpoint = self::WPCOM_PROXY_PAYMENT_AUTHORIZE_ENDPOINT; $request_body = array( 'authorize_url' => $action_url, 'paypal_order_id' => $paypal_order_id, 'test_mode' => $this->gateway->testmode, ); } $response = $this->send_wpcom_proxy_request( 'POST', $endpoint, $request_body ); if ( is_wp_error( $response ) ) { throw new Exception( 'PayPal ' . $action . ' payment request failed. Response error: ' . $response->get_error_message() ); } $http_code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $response_data = json_decode( $body, true ); if ( 200 !== $http_code && 201 !== $http_code ) { $paypal_debug_id = isset( $response_data['debug_id'] ) ? $response_data['debug_id'] : null; throw new Exception( 'PayPal ' . $action . ' payment failed. Response status: ' . $http_code . '. Response body: ' . $body ); } } catch ( Exception $e ) { \WC_Gateway_Paypal::log( $e->getMessage() ); $note_message = sprintf( /* translators: %1$s: Action, %2$s: PayPal order ID */ __( 'PayPal %1$s payment failed. PayPal Order ID: %2$s', 'woocommerce' ), $action, $paypal_order_id ); // Add debug ID to the note if available. if ( $paypal_debug_id ) { $note_message .= sprintf( /* translators: %s: PayPal debug ID */ __( '. PayPal debug ID: %s', 'woocommerce' ), $paypal_debug_id ); } $order->add_order_note( $note_message ); $order->update_status( OrderStatus::FAILED ); $order->save(); } } /** * Capture a PayPal payment that has been authorized. * * @param WC_Order|null $order Order object. * @return void * @throws Exception If the PayPal payment capture fails. */ public function capture_authorized_payment( ?WC_Order $order ): void { if ( ! $order ) { \WC_Gateway_Paypal::log( 'Order not found to capture authorized payment.' ); return; } $paypal_order_id = $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_ORDER_ID, true ); // Skip if the PayPal Order ID is not found. This means the order was not created via the Orders v2 API. if ( ! $paypal_order_id ) { \WC_Gateway_Paypal::log( 'PayPal Order ID not found to capture authorized payment. Order ID: ' . $order->get_id() ); return; } $capture_id = $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_CAPTURE_ID, true ); // Skip if the payment is already captured. if ( $capture_id ) { \WC_Gateway_Paypal::log( 'PayPal payment is already captured. PayPal capture ID: ' . $capture_id . '. Order ID: ' . $order->get_id() ); return; } $paypal_status = $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_STATUS, true ); // Skip if the payment is already captured. if ( PayPalConstants::STATUS_CAPTURED === $paypal_status || PayPalConstants::STATUS_COMPLETED === $paypal_status ) { \WC_Gateway_Paypal::log( 'PayPal payment is already captured. Skipping capture. Order ID: ' . $order->get_id() ); return; } // Skip if the payment requires payer action. if ( PayPalConstants::STATUS_PAYER_ACTION_REQUIRED === $paypal_status ) { \WC_Gateway_Paypal::log( 'PayPal payment requires payer action. Skipping capture. Order ID: ' . $order->get_id() ); return; } // Skip if the payment is voided. if ( PayPalConstants::VOIDED === $paypal_status ) { \WC_Gateway_Paypal::log( 'PayPal payment voided. Skipping capture. Order ID: ' . $order->get_id() ); return; } $authorization_id = $this->get_authorization_id_for_capture( $order ); if ( ! $authorization_id ) { \WC_Gateway_Paypal::log( 'Authorization ID not found to capture authorized payment. Order ID: ' . $order->get_id() ); return; } $paypal_debug_id = null; $http_code = null; try { $request_body = array( 'test_mode' => $this->gateway->testmode, 'authorization_id' => $authorization_id, 'paypal_order_id' => $paypal_order_id, ); $response = $this->send_wpcom_proxy_request( 'POST', self::WPCOM_PROXY_PAYMENT_CAPTURE_AUTH_ENDPOINT, $request_body ); if ( is_wp_error( $response ) ) { throw new Exception( 'PayPal capture payment request failed. Response error: ' . $response->get_error_message() ); } $http_code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $response_data = json_decode( $body, true ); $issue = isset( $response_data['details'][0]['issue'] ) ? $response_data['details'][0]['issue'] : ''; $auth_already_captured = 422 === $http_code && PayPalConstants::PAYPAL_ISSUE_AUTHORIZATION_ALREADY_CAPTURED === $issue; if ( 200 !== $http_code && 201 !== $http_code && ! $auth_already_captured ) { $paypal_debug_id = isset( $response_data['debug_id'] ) ? $response_data['debug_id'] : null; throw new Exception( 'PayPal capture payment failed. Response status: ' . $http_code . '. Response body: ' . $body ); } // Set custom status for successful capture response, or if the authorization was already captured. $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_STATUS, PayPalConstants::STATUS_CAPTURED ); $order->save(); } catch ( Exception $e ) { \WC_Gateway_Paypal::log( $e->getMessage() ); $note_message = sprintf( __( 'PayPal capture authorized payment failed', 'woocommerce' ), ); // Scenario 1: Capture auth API call returned 404 (authorization object does not exist). // If the authorization ID is not found (404 response), set the '_paypal_authorization_checked' flag. // This flag indicates that we've made an API call to capture PayPal payment and no authorization object was found with this authorization ID. // This prevents repeated API calls for orders that have no authorization data. if ( 404 === $http_code ) { $paypal_dashboard_url = $this->gateway->testmode ? 'https://www.sandbox.paypal.com/unifiedtransactions' : 'https://www.paypal.com/unifiedtransactions'; $note_message .= sprintf( /* translators: %1$s: Authorization ID, %2$s: open link tag, %3$s: close link tag */ __( '. Authorization ID: %1$s not found. Please log into your %2$sPayPal account%3$s to capture the payment', 'woocommerce' ), esc_html( $authorization_id ), '<a href="' . esc_url( $paypal_dashboard_url ) . '" target="_blank">', '</a>' ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_AUTHORIZATION_CHECKED, 'yes' ); } if ( $paypal_debug_id ) { $note_message .= sprintf( /* translators: %s: PayPal debug ID */ __( '. PayPal debug ID: %s', 'woocommerce' ), $paypal_debug_id ); } $order->add_order_note( $note_message ); $order->save(); } } /** * Get the authorization ID for the PayPal payment. * * @param WC_Order $order Order object. * @return string|null */ private function get_authorization_id_for_capture( WC_Order $order ): ?string { $paypal_order_id = $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_ORDER_ID, true ); $authorization_id = $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_AUTHORIZATION_ID, true ); $capture_id = $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_CAPTURE_ID, true ); // If the PayPal order ID is not found or the capture ID is already set, return null. if ( ! $paypal_order_id || ! empty( $capture_id ) ) { return null; } // If '_paypal_authorization_checked' is set to 'yes', it means we've already made an API call to PayPal // and confirmed that no authorization object exists. This flag is set in two scenarios: // 1. Capture auth API call returned 404 (authorization object does not exist with the authorization ID). // 2. Order details API call returned empty authorization array (authorization object does not exist for this PayPal order). // Return null to avoid repeated API calls for orders that have no authorization data. if ( 'yes' === $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_AUTHORIZATION_CHECKED, true ) ) { return null; } // If the authorization ID is not found, try to retrieve it from the PayPal order details. if ( empty( $authorization_id ) ) { \WC_Gateway_Paypal::log( 'Authorization ID not found, trying to retrieve from PayPal order details as a fallback for backwards compatibility. Order ID: ' . $order->get_id() ); try { $order_data = $this->get_paypal_order_details( $paypal_order_id ); $authorization_data = $this->get_latest_transaction_data( $order_data['purchase_units'][0]['payments']['authorizations'] ?? array() ); $capture_data = $this->get_latest_transaction_data( $order_data['purchase_units'][0]['payments']['captures'] ?? array() ); // If the payment is already captured, store the capture ID and status, and return null as there is no authorization ID that needs to be captured. if ( $capture_data && isset( $capture_data['id'] ) ) { $capture_id = $capture_data['id']; $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_CAPTURE_ID, $capture_id ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_STATUS, $capture_data['status'] ?? PayPalConstants::STATUS_CAPTURED ); $order->save(); \WC_Gateway_Paypal::log( 'Storing capture ID from Paypal. Order ID: ' . $order->get_id() . '; capture ID: ' . $capture_id ); return null; } if ( $authorization_data && isset( $authorization_data['id'], $authorization_data['status'] ) ) { // If the payment is already captured, return null as there is no authorization ID that needs to be captured. if ( PayPalConstants::STATUS_CAPTURED === $authorization_data['status'] ) { $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_STATUS, PayPalConstants::STATUS_CAPTURED ); $order->save(); return null; } $authorization_id = $authorization_data['id']; $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_AUTHORIZATION_ID, $authorization_id ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_STATUS, PayPalConstants::STATUS_AUTHORIZED ); \WC_Gateway_Paypal::log( 'Storing authorization ID from Paypal. Order ID: ' . $order->get_id() . '; authorization ID: ' . $authorization_id ); $order->save(); } else { // Scenario 2: Order details API call returned empty authorization array (authorization object does not exist). // Store '_paypal_authorization_checked' flag to prevent repeated API calls. // This flag indicates that we've made an API call to get PayPal order details and confirmed no authorization object exists. \WC_Gateway_Paypal::log( 'Authorization ID not found in PayPal order details. Order ID: ' . $order->get_id() ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_AUTHORIZATION_CHECKED, 'yes' ); $order->save(); return null; } } catch ( Exception $e ) { \WC_Gateway_Paypal::log( 'Error retrieving authorization ID from PayPal order details. Order ID: ' . $order->get_id() . '. Error: ' . $e->getMessage() ); return null; } } return $authorization_id; } /** * Get the latest item from the authorizations or captures array based on update_time. * * @param array $items Array of authorizations or captures. * @return array|null The latest authorization or capture or null if array is empty or no valid update_time found. */ private function get_latest_transaction_data( array $items ): ?array { if ( empty( $items ) ) { return null; } $latest_item = null; $latest_time = null; foreach ( $items as $item ) { if ( empty( $item['update_time'] ) ) { continue; } if ( null === $latest_time || $item['update_time'] > $latest_time ) { $latest_time = $item['update_time']; $latest_item = $item; } } return $latest_item; } /** * Get the approve link from the response data. * * @param int|string $http_code The HTTP code of the response. * @param array $response_data The response data. * @return string|null */ private function get_approve_link( $http_code, array $response_data ): ?string { // See https://developer.paypal.com/docs/api/orders/v2/#orders_create. if ( isset( $response_data['status'] ) && PayPalConstants::STATUS_PAYER_ACTION_REQUIRED === $response_data['status'] ) { $rel = 'payer-action'; } else { $rel = 'approve'; } foreach ( $response_data['links'] as $link ) { if ( $rel === $link['rel'] && 'GET' === $link['method'] && filter_var( $link['href'], FILTER_VALIDATE_URL ) ) { return esc_url_raw( $link['href'] ); } } return null; } /** * Build the request parameters for the PayPal create-order request. * * @param WC_Order $order Order object. * @param string $payment_source The payment source. * @param array $js_sdk_params Extra parameters for a PayPal JS SDK (Buttons) request. * @return array * * @throws Exception If the order items cannot be built. */ private function get_paypal_create_order_request_params( WC_Order $order, string $payment_source, array $js_sdk_params ): array { $payee_email = sanitize_email( (string) $this->gateway->get_option( 'email' ) ); $shipping_preference = $this->get_paypal_shipping_preference( $order ); /** * Filter the supported currencies for PayPal. * * @since 2.0.0 * * @param array $supported_currencies Array of supported currency codes. * @return array */ $supported_currencies = apply_filters( 'woocommerce_paypal_supported_currencies', PayPalConstants::SUPPORTED_CURRENCIES ); if ( ! in_array( strtoupper( $order->get_currency() ), $supported_currencies, true ) ) { throw new Exception( 'Currency is not supported by PayPal. Order ID: ' . esc_html( (string) $order->get_id() ) ); } $purchase_unit_amount = $this->get_paypal_order_purchase_unit_amount( $order ); if ( $purchase_unit_amount['value'] <= 0 ) { // If we cannot build purchase unit amount (e.g. negative or zero order total), // we should not proceed with the create-order request. throw new Exception( 'Cannot build PayPal order purchase unit amount. Order total is not valid. Order ID: ' . esc_html( (string) $order->get_id() ) . ', Total: ' . esc_html( (string) $purchase_unit_amount['value'] ) ); } $order_items = $this->get_paypal_order_items( $order ); $src_locale = get_locale(); // If the locale is longer than PayPal's string limit (10). if ( strlen( $src_locale ) > PayPalConstants::PAYPAL_LOCALE_MAX_LENGTH ) { // Keep only the main language and region parts. $locale_parts = explode( '_', $src_locale ); if ( count( $locale_parts ) > 2 ) { $src_locale = $locale_parts[0] . '_' . $locale_parts[1]; } } $params = array( 'intent' => $this->get_paypal_order_intent(), 'payment_source' => array( $payment_source => array( 'experience_context' => array( 'user_action' => PayPalConstants::USER_ACTION_PAY_NOW, 'shipping_preference' => $shipping_preference, // Customer redirected here on approval. 'return_url' => $this->normalize_url_for_paypal( add_query_arg( 'utm_nooverride', '1', $this->gateway->get_return_url( $order ) ) ), // Customer redirected here on cancellation. 'cancel_url' => $this->normalize_url_for_paypal( $order->get_cancel_order_url_raw() ), // Convert WordPress locale format (e.g., 'en_US') to PayPal's expected format (e.g., 'en-US'). 'locale' => str_replace( '_', '-', $src_locale ), 'app_switch_preference' => array( 'launch_paypal_app' => true, ), ), ), ), 'purchase_units' => array( array( 'custom_id' => $this->get_paypal_order_custom_id( $order ), 'amount' => $purchase_unit_amount, 'invoice_id' => $this->limit_length( $this->gateway->get_option( 'invoice_prefix' ) . $order->get_order_number(), PayPalConstants::PAYPAL_INVOICE_ID_MAX_LENGTH ), 'items' => $order_items, 'payee' => array( 'email_address' => $payee_email, ), ), ), ); if ( ! in_array( $shipping_preference, array( PayPalConstants::SHIPPING_NO_SHIPPING, PayPalConstants::SHIPPING_SET_PROVIDED_ADDRESS, ), true ) ) { $shipping_callback_token = $this->generate_shipping_callback_token( $order ); $callback_url = add_query_arg( 'token', $shipping_callback_token, rest_url( 'wc/v3/paypal-standard/update-shipping' ) ); $params['payment_source'][ $payment_source ]['experience_context']['order_update_callback_config'] = array( 'callback_events' => array( 'SHIPPING_ADDRESS', 'SHIPPING_OPTIONS' ), 'callback_url' => $this->normalize_url_for_paypal( $callback_url ), ); } // If the request is from PayPal JS SDK (Buttons), we need a cancel URL that is compatible with App Switch. if ( ! empty( $js_sdk_params['is_js_sdk_flow'] ) && ! empty( $js_sdk_params['app_switch_request_origin'] ) ) { // App Switch may open a new tab, so we cannot rely on client-side data. // We need to pass the order ID manually. // See https://developer.paypal.com/docs/checkout/standard/customize/app-switch/#resume-flow. $request_origin = $js_sdk_params['app_switch_request_origin']; // Check if $request_origin is a valid URL, and matches the current site. $origin_parts = wp_parse_url( $request_origin ); $site_parts = wp_parse_url( get_site_url() ); $is_valid_url = filter_var( $request_origin, FILTER_VALIDATE_URL ); $is_expected_scheme = isset( $origin_parts['scheme'], $site_parts['scheme'] ) && strcasecmp( $origin_parts['scheme'], $site_parts['scheme'] ) === 0; $is_expected_host = isset( $origin_parts['host'], $site_parts['host'] ) && strcasecmp( $origin_parts['host'], $site_parts['host'] ) === 0; if ( $is_valid_url && $is_expected_scheme && $is_expected_host ) { $cancel_url = add_query_arg( array( 'order_id' => $order->get_id(), ), $request_origin ); $params['payment_source'][ $payment_source ]['experience_context']['cancel_url'] = $this->normalize_url_for_paypal( $cancel_url ); } } $shipping = $this->get_paypal_order_shipping( $order ); if ( $shipping ) { $params['purchase_units'][0]['shipping'] = $shipping; } return $params; } /** * Get the amount data for the PayPal order purchase unit field. * * @param WC_Order|null $order Order object. * @return array */ public function get_paypal_order_purchase_unit_amount( ?WC_Order $order ): array { if ( ! $order ) { return array(); } $currency = $order->get_currency(); return array( 'currency_code' => $currency, 'value' => wc_format_decimal( $order->get_total(), wc_get_price_decimals() ), 'breakdown' => array( 'item_total' => array( 'currency_code' => $currency, 'value' => wc_format_decimal( $this->get_paypal_order_items_subtotal( $order ), wc_get_price_decimals() ), ), 'shipping' => array( 'currency_code' => $currency, 'value' => wc_format_decimal( $order->get_shipping_total(), wc_get_price_decimals() ), ), 'tax_total' => array( 'currency_code' => $currency, 'value' => wc_format_decimal( $order->get_total_tax(), wc_get_price_decimals() ), ), 'discount' => array( 'currency_code' => $currency, 'value' => wc_format_decimal( $order->get_discount_total(), wc_get_price_decimals() ), ), ), ); } /** * Build the custom ID for the PayPal order. The custom ID will be used by the proxy for webhook forwarding, * and by later steps to identify the order. * * @param WC_Order $order Order object. * @return string * @throws Exception If the custom ID is too long. */ private function get_paypal_order_custom_id( WC_Order $order ): string { $custom_id = wp_json_encode( array( 'order_id' => $order->get_id(), 'order_key' => $order->get_order_key(), // Endpoint for the proxy to forward webhooks to. 'site_url' => home_url(), 'site_id' => class_exists( '\Jetpack_Options' ) ? \Jetpack_Options::get_option( 'id' ) : null, 'v' => defined( 'WC_VERSION' ) ? WC_VERSION : WC()->version, ) ); if ( false === $custom_id ) { throw new Exception( 'Failed to encode custom ID.' ); } if ( strlen( $custom_id ) > 255 ) { throw new Exception( 'PayPal order custom ID is too long. Max length is 255 chars.' ); } return $custom_id ? $custom_id : ''; } /** * Get the order items for the PayPal create-order request. * Returns an empty array if any of the items (amount, quantity) are invalid. * * @param WC_Order $order Order object. * @return array */ private function get_paypal_order_items( WC_Order $order ): array { $items = array(); foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) { $item_amount = $this->get_paypal_order_item_amount( $order, $item ); if ( $item_amount < 0 ) { // PayPal does not accept negative item amounts in the items breakdown, so we return an empty list. return array(); } $quantity = $item->get_quantity(); // PayPal does not accept zero or fractional quantities. if ( ! is_numeric( $quantity ) || $quantity <= 0 || floor( $quantity ) != $quantity ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual return array(); } $items[] = array( 'name' => $this->limit_length( $item->get_name(), PayPalConstants::PAYPAL_ORDER_ITEM_NAME_MAX_LENGTH ), 'quantity' => $item->get_quantity(), 'unit_amount' => array( 'currency_code' => $order->get_currency(), // Use the subtotal before discounts. 'value' => wc_format_decimal( $item_amount, wc_get_price_decimals() ), ), ); } return $items; } /** * Get the subtotal for all items, before discounts. * * @param WC_Order $order Order object. * @return float */ private function get_paypal_order_items_subtotal( WC_Order $order ): float { $total = 0; foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) { $total += wc_add_number_precision( $this->get_paypal_order_item_amount( $order, $item ) * $item->get_quantity(), false ); } return wc_remove_number_precision( $total ); } /** * Get the amount for a specific order item. * * @param WC_Order $order Order object. * @param \WC_Order_Item $item Order item. * @return float */ private function get_paypal_order_item_amount( WC_Order $order, \WC_Order_Item $item ): float { if ( 'fee' === $item->get_type() && $item instanceof \WC_Order_Item_Fee ) { return (float) $item->get_amount(); } return (float) $order->get_item_subtotal( $item, $include_tax = false, $rounding_enabled = false ); } /** * Get the value for the intent field in the create-order request. * * @return string */ private function get_paypal_order_intent(): string { $payment_action = $this->gateway->get_option( 'paymentaction' ); if ( 'authorization' === $payment_action ) { return PayPalConstants::INTENT_AUTHORIZE; } return PayPalConstants::INTENT_CAPTURE; } /** * Get the shipping preference for the PayPal create-order request. * * @param WC_Order $order Order object. * @return string */ private function get_paypal_shipping_preference( WC_Order $order ): string { if ( ! $order->needs_shipping() ) { return PayPalConstants::SHIPPING_NO_SHIPPING; } $address_override = $this->gateway->get_option( 'address_override' ) === 'yes'; return $address_override ? PayPalConstants::SHIPPING_SET_PROVIDED_ADDRESS : PayPalConstants::SHIPPING_GET_FROM_FILE; } /** * Get the shipping information for the PayPal create-order request. * * @param WC_Order $order Order object. * @return array|null Returns null if the shipping is not required, * or the address is not set, or is incomplete. */ private function get_paypal_order_shipping( WC_Order $order ): ?array { if ( ! $order->needs_shipping() ) { return null; } $address_type = 'yes' === $this->gateway->get_option( 'send_shipping' ) ? 'shipping' : 'billing'; $full_name = trim( $order->{"get_formatted_{$address_type}_full_name"}() ); $address_line_1 = trim( $order->{"get_{$address_type}_address_1"}() ); $address_line_2 = trim( $order->{"get_{$address_type}_address_2"}() ); $state = trim( $order->{"get_{$address_type}_state"}() ); $city = trim( $order->{"get_{$address_type}_city"}() ); $postcode = trim( $order->{"get_{$address_type}_postcode"}() ); $country = trim( $order->{"get_{$address_type}_country"}() ); // If we do not have the complete address, // e.g. PayPal Buttons on product pages, we should not set the 'shipping' param // for the create-order request, otherwise it will fail. // Shipping information will be updated by the shipping callback handlers. // Country is a required field. if ( empty( $country ) ) { return null; } // Make sure the country code is in the correct format. $raw_country = $country; $country = $this->normalize_paypal_order_shipping_country_code( $raw_country ); if ( ! $country ) { \WC_Gateway_Paypal::log( sprintf( 'Could not identify a correct country code. Raw value: %s', $raw_country ), 'error' ); return null; } // Validate required fields based on country-specific address requirements. // phpcs:ignore Generic.Commenting.Todo.TaskFound // TODO: The container call can be removed once we migrate this class to the `src` folder. $address_requirements = wc_get_container()->get( PayPalAddressRequirements::class )::instance(); if ( empty( $city ) && $address_requirements->country_requires_city( $country ) ) { \WC_Gateway_Paypal::log( sprintf( 'City is required for country: %s', $country ), 'error' ); return null; } if ( empty( $postcode ) && $address_requirements->country_requires_postal_code( $country ) ) { \WC_Gateway_Paypal::log( sprintf( 'Postal code is required for country: %s', $country ), 'error' ); return null; } return array( 'name' => array( 'full_name' => $full_name, ), 'address' => array( 'address_line_1' => $this->limit_length( $address_line_1, PayPalConstants::PAYPAL_ADDRESS_LINE_MAX_LENGTH ), 'address_line_2' => $this->limit_length( $address_line_2, PayPalConstants::PAYPAL_ADDRESS_LINE_MAX_LENGTH ), 'admin_area_1' => $this->limit_length( $state, PayPalConstants::PAYPAL_STATE_MAX_LENGTH ), 'admin_area_2' => $this->limit_length( $city, PayPalConstants::PAYPAL_CITY_MAX_LENGTH ), 'postal_code' => $this->limit_length( $postcode, PayPalConstants::PAYPAL_POSTAL_CODE_MAX_LENGTH ), 'country_code' => strtoupper( $country ), ), ); } /** * Generate and store a shipping callback token for the order. * The token is stored in the database cache and can be validated later. * * @param WC_Order $order The order object. * @return string The generated token. */ private function generate_shipping_callback_token( WC_Order $order ): string { $token = bin2hex( random_bytes( 32 ) ); // Store the token in order meta for validation. $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_SHIPPING_CALLBACK_TOKEN, $token ); $order->save(); return $token; } /** * Normalize PayPal order shipping country code. * * @param string $country_code Country code to normalize. * @return string|null */ private function normalize_paypal_order_shipping_country_code( string $country_code ): ?string { // Normalize to uppercase. $code = strtoupper( trim( (string) $country_code ) ); // Check if it's a valid alpha-2 code. if ( strlen( $code ) === PayPalConstants::PAYPAL_COUNTRY_CODE_LENGTH ) { if ( WC()->countries->country_exists( $code ) ) { return $code; } \WC_Gateway_Paypal::log( sprintf( 'Invalid country code: %s', $code ) ); return null; } // Log when we get an unexpected country code length. \WC_Gateway_Paypal::log( sprintf( 'Unexpected country code length (%d) for country: %s', strlen( $code ), $code ) ); // Truncate to the expected maximum length (3). $max_country_code_length = PayPalConstants::PAYPAL_COUNTRY_CODE_LENGTH + 1; if ( strlen( $code ) > $max_country_code_length ) { $code = substr( $code, 0, $max_country_code_length ); } // Check if it's a valid alpha-3 code. $alpha2 = WC()->countries->get_country_from_alpha_3_code( $code ); if ( null === $alpha2 ) { \WC_Gateway_Paypal::log( sprintf( 'Invalid alpha-3 country code: %s', $code ) ); } return $alpha2; } /** * Normalize a URL for PayPal. PayPal requires absolute URLs with protocol. * * @param string $url The URL to check. * @return string Normalized URL. */ private function normalize_url_for_paypal( string $url ): string { // Replace encoded ampersand with actual ampersand. // In some cases, the URL may contain encoded ampersand but PayPal expects the actual ampersand. // PayPal request fails if the URL contains encoded ampersand. $url = str_replace( '&', '&', $url ); // If the URL is already the home URL, return it. if ( strpos( $url, home_url() ) === 0 ) { return esc_url_raw( $url ); } // Return the URL if it is already absolute (contains ://). if ( strpos( $url, '://' ) !== false ) { return esc_url_raw( $url ); } $home_url = untrailingslashit( home_url() ); // If the URL is relative (starts with /), prepend the home URL. if ( strpos( $url, '/' ) === 0 ) { return esc_url_raw( $home_url . $url ); } // Prepend home URL with a slash. return esc_url_raw( $home_url . '/' . $url ); } /** * Fetch the PayPal client-id from the Transact platform. * * @return string|null The PayPal client-id, or null if the request fails. * @throws Exception If the request fails. */ public function fetch_paypal_client_id(): ?string { try { $request_body = array( 'test_mode' => $this->gateway->testmode, ); $response = $this->send_wpcom_proxy_request( 'GET', self::WPCOM_PROXY_CLIENT_ID_ENDPOINT, $request_body ); if ( is_wp_error( $response ) ) { throw new Exception( 'Failed to fetch the client ID. Response error: ' . $response->get_error_message() ); } $http_code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $response_data = json_decode( $body, true ); if ( 200 !== $http_code ) { throw new Exception( 'Failed to fetch the client ID. Response status: ' . $http_code . '. Response body: ' . $body ); } return $response_data['client_id'] ?? null; } catch ( Exception $e ) { \WC_Gateway_Paypal::log( $e->getMessage() ); return null; } } /** * Send a request to the API proxy. * * @param string $method The HTTP method to use. * @param string $endpoint The endpoint to request. * @param array $request_body The request body. * * @return array|\WP_Error The API response body, or WP_Error if the request fails. * @throws Exception If the site ID is not found. */ private function send_wpcom_proxy_request( string $method, string $endpoint, array $request_body ) { $site_id = \Jetpack_Options::get_option( 'id' ); if ( ! $site_id ) { \WC_Gateway_Paypal::log( sprintf( 'Site ID not found. Cannot send request to %s.', $endpoint ) ); throw new Exception( 'Site ID not found. Cannot send proxy request.' ); } if ( 'GET' === $method ) { $endpoint .= '?' . http_build_query( $request_body ); } $response = Jetpack_Connection_Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/%s/%s', $site_id, self::WPCOM_PROXY_REST_BASE, $endpoint ), self::WPCOM_PROXY_ENDPOINT_API_VERSION, array( 'headers' => array( 'Content-Type' => 'application/json', 'User-Agent' => 'TransactGateway/woocommerce/' . WC()->version, ), 'method' => $method, 'timeout' => PayPalConstants::WPCOM_PROXY_REQUEST_TIMEOUT, ), 'GET' === $method ? null : wp_json_encode( $request_body ), 'wpcom' ); return $response; } /** * Limit length of an arg. * * @param string $text Text to limit. * @param integer $limit Limit size in characters. * @return string */ private function limit_length( string $text, int $limit = 127 ): string { $str_limit = $limit - 3; if ( function_exists( 'mb_strimwidth' ) ) { if ( mb_strlen( $text ) > $limit ) { $text = mb_strimwidth( $text, 0, $str_limit ) . '...'; } } elseif ( strlen( $text ) > $limit ) { $text = substr( $text, 0, $str_limit ) . '...'; } return $text; } } PK !m.]}K�%�% �% PayPal/WebhookHandler.phpnu ��� <?php declare(strict_types=1); namespace Automattic\WooCommerce\Gateways\PayPal; use Automattic\WooCommerce\Enums\OrderStatus; use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants; use Automattic\WooCommerce\Gateways\PayPal\Helper as PayPalHelper; use Automattic\WooCommerce\Gateways\PayPal\Request as PayPalRequest; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class WebhookHandler file. * * Handles webhook events. * * @since 10.5.0 */ class WebhookHandler { /** * Process the webhook event. * * @since 10.5.0 * * @param \WP_REST_Request $request The request object. * @return void */ public function process_webhook( \WP_REST_Request $request ): void { $data = $request->get_json_params(); if ( ! is_array( $data ) || empty( $data['event_type'] ) || empty( $data['resource'] ) ) { \WC_Gateway_Paypal::log( 'Invalid PayPal webhook payload: ' . wc_print_r( $data, true ) ); return; } \WC_Gateway_Paypal::log( 'Webhook received: ' . wc_print_r( PayPalHelper::redact_data( $data ), true ) ); switch ( $data['event_type'] ) { case 'CHECKOUT.ORDER.APPROVED': $this->process_checkout_order_approved( $data ); break; case 'PAYMENT.CAPTURE.PENDING': $this->process_payment_capture_pending( $data ); break; case 'PAYMENT.CAPTURE.COMPLETED': $this->process_payment_capture_completed( $data ); break; case 'PAYMENT.AUTHORIZATION.CREATED': $this->process_payment_authorization_created( $data ); break; default: \WC_Gateway_Paypal::log( 'Unhandled PayPal webhook event: ' . wc_print_r( PayPalHelper::redact_data( $data ), true ) ); break; } } /** * Process the CHECKOUT.ORDER.APPROVED webhook event. * * @since 10.5.0 * * @param array $event The webhook event data. * @return void */ private function process_checkout_order_approved( array $event ): void { $custom_id = $event['resource']['purchase_units'][0]['custom_id'] ?? ''; $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $custom_id ); if ( ! $order ) { \WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) ); return; } // Skip if the payment is already processed. $paypal_status = $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_STATUS, true ); if ( in_array( $paypal_status, array( PayPalConstants::STATUS_COMPLETED, PayPalConstants::STATUS_APPROVED ), true ) ) { return; } $status = $event['resource']['status'] ?? null; $paypal_order_id = $event['resource']['id'] ?? null; if ( PayPalConstants::STATUS_APPROVED === $status ) { \WC_Gateway_Paypal::log( 'PayPal payment approved. Order ID: ' . $order->get_id() ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_STATUS, $status ); // Clear the shipping callback token by setting it to an empty string. // This is done to prevent the token from being used again for the same order. // We are not deleting the meta key as we use the existence of the meta key to determine if the token was ever generated for this order. $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_SHIPPING_CALLBACK_TOKEN, '' ); $order->save(); $order->add_order_note( sprintf( /* translators: %1$s: PayPal order ID */ __( 'PayPal payment approved. PayPal Order ID: %1$s', 'woocommerce' ), $paypal_order_id ) ); $order->save(); // Update the addresses in the order with the addresses from the PayPal order details. PayPalHelper::update_addresses_in_order( $order, $event['resource'] ); // Authorize or capture the payment after approval. $paypal_intent = $event['resource']['intent'] ?? null; $links = $event['resource']['links'] ?? null; $action = PayPalConstants::INTENT_CAPTURE === $paypal_intent ? PayPalConstants::PAYMENT_ACTION_CAPTURE : PayPalConstants::PAYMENT_ACTION_AUTHORIZE; $this->authorize_or_capture_payment( $order, $links, $action ); } else { // This is unexpected for a CHECKOUT.ORDER.APPROVED event. \WC_Gateway_Paypal::log( 'PayPal payment approval failed. Order ID: ' . $order->get_id() . ' Status: ' . $status ); $order->add_order_note( sprintf( /* translators: %1$s: PayPal order ID, %2$s: Status */ __( 'PayPal payment approval failed. PayPal Order ID: %1$s. Status: %2$s', 'woocommerce' ), $paypal_order_id, $status ) ); } } /** * Process the PAYMENT.CAPTURE.COMPLETED webhook event. * * @since 10.5.0 * * @param array $event The webhook event data. * @return void */ private function process_payment_capture_completed( array $event ): void { $custom_id = $event['resource']['custom_id'] ?? ''; $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $custom_id ); if ( ! $order ) { \WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) ); return; } // Skip if the payment is already processed. if ( PayPalConstants::STATUS_COMPLETED === $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_STATUS, true ) ) { return; } $transaction_id = $event['resource']['id'] ?? null; $status = $event['resource']['status'] ?? null; $order->set_transaction_id( $transaction_id ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_CAPTURE_ID, $transaction_id ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_STATUS, $status ); $order->payment_complete(); $order->add_order_note( sprintf( /* translators: %1$s: Transaction ID */ __( 'PayPal payment captured. Transaction ID: %1$s.', 'woocommerce' ), $transaction_id ) ); $order->save(); } /** * Process the PAYMENT.CAPTURE.PENDING webhook event. * * @since 10.5.0 * * @param array $event The webhook event data. * @return void */ private function process_payment_capture_pending( array $event ): void { $custom_id = $event['resource']['custom_id'] ?? ''; $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $custom_id ); if ( ! $order ) { \WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) ); return; } // Skip if the payment is already processed. if ( PayPalConstants::STATUS_COMPLETED === $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_STATUS, true ) ) { return; } $transaction_id = $event['resource']['id'] ?? null; $status = $event['resource']['status'] ?? null; $reason = $event['resource']['status_details']['reason'] ?? 'Unknown'; $order->set_transaction_id( $transaction_id ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_CAPTURE_ID, $transaction_id ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_STATUS, $status ); /* translators: %s: reason */ $order->update_status( OrderStatus::ON_HOLD, sprintf( __( 'Payment pending (reason: %s).', 'woocommerce' ), $reason ) ); $order->save(); } /** * Process the PAYMENT.AUTHORIZATION.CREATED webhook event. * * @since 10.5.0 * * @param array $event The webhook event data. * @return void */ private function process_payment_authorization_created( array $event ): void { $custom_id = $event['resource']['custom_id'] ?? ''; $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $custom_id ); if ( ! $order ) { \WC_Gateway_Paypal::log( 'Invalid order. Custom ID: ' . wc_print_r( $custom_id, true ) ); return; } // Skip if the payment is already processed. if ( PayPalConstants::STATUS_COMPLETED === $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_STATUS, true ) ) { return; } $transaction_id = $event['resource']['id'] ?? null; $order->set_transaction_id( $transaction_id ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_AUTHORIZATION_ID, $transaction_id ); $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_STATUS, PayPalConstants::STATUS_AUTHORIZED ); $order->add_order_note( sprintf( /* translators: %1$s: Transaction ID */ __( 'PayPal payment authorized. Transaction ID: %1$s. Change payment status to processing or complete to capture funds.', 'woocommerce' ), $transaction_id ) ); $order->update_status( OrderStatus::ON_HOLD ); $order->save(); } /** * Capture the payment. * * @since 10.5.0 * * @param \WC_Order $order The order object. * @param array $links The links from the webhook event. * @param string $action The action to perform (capture or authorize). * @return void */ private function authorize_or_capture_payment( \WC_Order $order, array $links, string $action ): void { $action_url = $this->get_action_url( $links, $action ); $payment_gateways = WC()->payment_gateways()->payment_gateways(); if ( ! isset( $payment_gateways['paypal'] ) ) { \WC_Gateway_Paypal::log( 'PayPal gateway is not available.' ); return; } $gateway = $payment_gateways['paypal']; $paypal_request = new PayPalRequest( $gateway ); $paypal_request->authorize_or_capture_payment( $order, $action_url, $action ); } /** * Get the action URL from the links. * * @since 10.5.0 * * @param array $links The links from the webhook event. * @param string $action The action to perform (capture or authorize). * @return string|null */ private function get_action_url( array $links, string $action ): ?string { $action_url = null; foreach ( $links as $link ) { if ( $action === $link['rel'] && 'POST' === $link['method'] && filter_var( $link['href'], FILTER_VALIDATE_URL ) ) { $action_url = esc_url_raw( $link['href'] ); break; } } return $action_url; } } PK !m.]�Q�$G( G( PayPal/Notices.phpnu ��� <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Gateways\PayPal; use WC_Order; use Automattic\WooCommerce\Gateways\PayPal\Helper as PayPalHelper; defined( 'ABSPATH' ) || exit; /** * PayPal Notices Class * * Handles admin notices for PayPal gateway including migration notices, * account restriction warnings, and currency support notifications. * * @since 10.5.0 */ class Notices { /** * The name of the notice for PayPal migration. * * @since 10.5.0 * @var string */ private const PAYPAL_MIGRATION_NOTICE = 'paypal_migration_completed'; /** * The name of the notice for PayPal account restriction. * * @since 10.5.0 * @var string */ private const PAYPAL_ACCOUNT_RESTRICTED_NOTICE = 'paypal_account_restricted'; /** * The name of the notice for PayPal unsupported currency. * * @since 10.5.0 * @var string */ private const PAYPAL_UNSUPPORTED_CURRENCY_NOTICE = 'paypal_unsupported_currency'; /** * PayPal account restriction issue codes from PayPal API. * * @since 10.5.0 * @var array */ protected const PAYPAL_ACCOUNT_RESTRICTION_ISSUES = array( Constants::PAYPAL_ISSUE_PAYEE_ACCOUNT_LOCKED_OR_CLOSED, Constants::PAYPAL_ISSUE_PAYEE_ACCOUNT_RESTRICTED, ); /** * The PayPal gateway instance. * * @var \WC_Gateway_Paypal */ private $gateway; /** * Constructor. */ public function __construct() { $this->gateway = \WC_Gateway_Paypal::get_instance(); if ( ! $this->gateway ) { return; } // Only register admin notice hooks in the admin area. if ( is_admin() ) { add_action( 'admin_notices', array( $this, 'add_paypal_notices' ) ); // Use admin_head to inject notice on payments settings page. // This bypasses the suppress_admin_notices() function which removes all admin_notices hooks on the payments page. // This is a workaround to avoid the notice being suppressed by the suppress_admin_notices() function. add_action( 'admin_head', array( $this, 'add_paypal_notices_on_payments_settings_page' ) ); } } /** * Add PayPal Standard notices. * * @since 10.5.0 * @return void */ public function add_paypal_notices(): void { // Show only to users who can manage the site. if ( ! current_user_can( 'manage_woocommerce' ) && ! current_user_can( 'manage_options' ) ) { return; } // Skip if the gateway is not available or the merchant has not been onboarded. if ( ! PayPalHelper::is_paypal_gateway_available() || ! $this->gateway->should_use_orders_v2() ) { return; } $this->add_paypal_migration_notice(); $this->add_paypal_account_restricted_notice(); $this->add_paypal_unsupported_currency_notice(); } /** * Add PayPal notices on the payments settings page. * * @since 10.5.0 * @return void */ public function add_paypal_notices_on_payments_settings_page(): void { global $current_tab, $current_section; $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; $is_payments_settings_page = 'woocommerce_page_wc-settings' === $screen_id && 'checkout' === $current_tab && empty( $current_section ); // Only add the notice from this callback on the payments settings page. if ( ! $is_payments_settings_page ) { return; } $this->add_paypal_notices(); } /** * Add notice warning about the migration to PayPal Payments. * * @since 10.5.0 * @return void */ public function add_paypal_migration_notice(): void { // Skip if the notice has been dismissed. if ( $this->is_notice_dismissed( self::PAYPAL_MIGRATION_NOTICE ) ) { return; } $doc_url = 'https://woocommerce.com/document/woocommerce-paypal-payments/paypal-payments-upgrade-guide/'; $dismiss_url = $this->get_dismiss_url( self::PAYPAL_MIGRATION_NOTICE ); $message = sprintf( /* translators: 1: opening <a> tag, 2: closing </a> tag */ esc_html__( 'WooCommerce has upgraded your PayPal integration from PayPal Standard to PayPal Payments (PPCP), for a more reliable and modern checkout experience. If you do not prefer the upgraded integration in WooCommerce, we recommend switching to %1$sPayPal Payments%2$s extension.', 'woocommerce' ), '<a href="' . esc_url( $doc_url ) . '" target="_blank" rel="noopener noreferrer">', '</a>', ); $notice_html = '<div class="notice notice-warning is-dismissible">' . '<a class="woocommerce-message-close notice-dismiss" style="text-decoration: none;" href="' . esc_url( $dismiss_url ) . '" aria-label="' . esc_attr__( 'Dismiss this notice', 'woocommerce' ) . '"></a>' . '<p>' . $message . '</p>' . '</div>'; echo wp_kses_post( $notice_html ); } /** * Add notice warning about PayPal account restriction. * * @since 10.5.0 * @return void */ private function add_paypal_account_restricted_notice(): void { // Skip if there's no account restriction flag. if ( ! $this->has_account_restriction_flag() ) { return; } // Skip if the notice has been dismissed. if ( $this->is_notice_dismissed( self::PAYPAL_ACCOUNT_RESTRICTED_NOTICE ) ) { return; } $support_url = 'https://www.paypal.com/smarthelp/contact-us'; $dismiss_url = $this->get_dismiss_url( self::PAYPAL_ACCOUNT_RESTRICTED_NOTICE ); $message = sprintf( /* translators: 1: opening <a> tag, 2: closing </a> tag */ esc_html__( 'Your PayPal account has been restricted by PayPal. This may prevent customers from completing payments. Please %1$scontact PayPal support%2$s to resolve this issue and restore full functionality to your account.', 'woocommerce' ), '<a href="' . esc_url( $support_url ) . '" target="_blank" rel="noopener noreferrer">', '</a>', ); $notice_html = '<div class="notice notice-error is-dismissible">' . '<a class="woocommerce-message-close notice-dismiss" style="text-decoration: none;" href="' . esc_url( $dismiss_url ) . '" aria-label="' . esc_attr__( 'Dismiss this notice', 'woocommerce' ) . '"></a>' . '<p><strong>' . esc_html__( 'PayPal Account Restricted', 'woocommerce' ) . '</strong></p>' . '<p>' . $message . '</p>' . '</div>'; echo wp_kses_post( $notice_html ); } /** * Add notice warning when PayPal does not support the store's currency. * * @since 10.5.0 * @return void */ private function add_paypal_unsupported_currency_notice(): void { $currency = get_woocommerce_currency(); // Skip if the currency is supported by PayPal. if ( $this->gateway->is_valid_for_use() ) { return; } // Skip if the notice has been dismissed. if ( $this->is_notice_dismissed( self::PAYPAL_UNSUPPORTED_CURRENCY_NOTICE ) ) { return; } $dismiss_url = $this->get_dismiss_url( self::PAYPAL_UNSUPPORTED_CURRENCY_NOTICE ); $message = sprintf( /* translators: %s: Currency code */ esc_html__( 'PayPal Standard does not support your store currency (%s).', 'woocommerce' ), $currency ); $notice_html = '<div class="notice notice-error is-dismissible">' . '<a class="woocommerce-message-close notice-dismiss" style="text-decoration: none;" href="' . esc_url( $dismiss_url ) . '" aria-label="' . esc_attr__( 'Dismiss this notice', 'woocommerce' ) . '"></a>' . '<p>' . $message . '</p>' . '</div>'; echo wp_kses_post( $notice_html ); } /** * Get the dismiss URL for a notice. * * @since 10.5.0 * @param string $notice_name The name of the notice. * @return string */ private function get_dismiss_url( string $notice_name ): string { return wp_nonce_url( add_query_arg( 'wc-hide-notice', $notice_name ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ); } /** * Check if the notice has been dismissed. * * User meta keys for notice dismissals: * - dismissed_paypal_migration_completed_notice * - dismissed_paypal_account_restricted_notice * - dismissed_paypal_unsupported_currency_notice * * The meta keys are set by WC_Admin_Notices when the notice is dismissed by the user. * * @since 10.5.0 * @param string $notice_name The name of the notice. * @return bool */ private function is_notice_dismissed( string $notice_name ): bool { return (bool) get_user_meta( get_current_user_id(), 'dismissed_' . $notice_name . '_notice', true ); } /** * Check if there's a flag indicating PayPal account restriction. * * @since 10.5.0 * @return bool */ private function has_account_restriction_flag(): bool { return 'yes' === get_option( 'woocommerce_paypal_account_restricted_status', 'no' ); } /** * Set the flag indicating PayPal account restriction. * * @since 10.5.0 * @return void */ public static function set_account_restriction_flag(): void { if ( 'no' === get_option( 'woocommerce_paypal_account_restricted_status', 'no' ) ) { update_option( 'woocommerce_paypal_account_restricted_status', 'yes', false ); } } /** * Clear the flag indicating PayPal account restriction. * * @since 10.5.0 * @return void */ public static function clear_account_restriction_flag(): void { if ( 'yes' === get_option( 'woocommerce_paypal_account_restricted_status', 'no' ) ) { update_option( 'woocommerce_paypal_account_restricted_status', 'no' ); } } /** * Handle PayPal order response to manage account restriction notices. * * @since 10.5.0 * @param int|string $http_code The HTTP status code from the PayPal API response. * @param array $response_data The decoded response data from the PayPal API. * @param WC_Order $order The WooCommerce order object. * @return void */ public static function manage_account_restriction_flag_for_notice( $http_code, array $response_data, WC_Order $order ): void { // Clear the restriction flag on successful responses. if ( in_array( (int) $http_code, array( 200, 201 ), true ) ) { self::clear_account_restriction_flag(); return; } if ( empty( $response_data ) ) { return; } // Set the restriction flag for account-related errors. if ( 422 === (int) $http_code ) { $issue = isset( $response_data['details'][0]['issue'] ) ? $response_data['details'][0]['issue'] : ''; if ( in_array( $issue, self::PAYPAL_ACCOUNT_RESTRICTION_ISSUES, true ) ) { \WC_Gateway_Paypal::log( 'PayPal account restriction flag set due to issues when handling the order: ' . $order->get_id() ); self::set_account_restriction_flag(); } } } } PK !m.]��� PayPal/Constants.phpnu ��� PK !m.]�l� � X PayPal/AddressRequirements.phpnu ��� PK !m.]hb $ $ K$ PayPal/Buttons.phpnu ��� PK !m.]���& & �3 PayPal/Helper.phpnu ��� PK !m.]�5&r�, �, ! P PayPal/TransactAccountManager.phpnu ��� PK !m.]���� � <} PayPal/Request.phpnu ��� PK !m.]}K�%�% �% f! PayPal/WebhookHandler.phpnu ��� PK !m.]�Q�$G( G( WG PayPal/Notices.phpnu ��� PK � �o
| ver. 1.6 |
Github
|
.
| PHP 8.1.34 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка