舒舒服服水电费多少发多少*(^&*(
<?php
/**
* Class WooPay_Session.
*
* @package WooCommerce\Payments
*/
namespace WCPay\WooPay;
use Automattic\Jetpack\Connection\Rest_Authentication;
use Automattic\WooCommerce\StoreApi\RoutesController;
use Automattic\WooCommerce\StoreApi\StoreApi;
use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
use Jetpack_Options;
use WCPay\Blocks_Data_Extractor;
use WCPay\Logger;
use WCPay\Platform_Checkout\SessionHandler;
use WCPay\Platform_Checkout\WooPay_Store_Api_Token;
use WCPay\WooPay\WooPay_Scheduler;
use WC_Customer;
use WC_Payments;
use WC_Payments_Customer_Service;
use WC_Payments_Features;
use WCPay\MultiCurrency\MultiCurrency;
use WP_REST_Request;
/**
* Class responsible for handling woopay sessions.
* This class should be loaded as soon as possible so the correct session is loaded.
* So don't load it in the WC_Payments::init() function.
*/
class WooPay_Session {
const STORE_API_NAMESPACE_PATTERN = '@^(wc/store(/v[\d]+)?|store-api)$@';
const WOOPAY_SESSION_KEY = 'woopay-user-data';
/**
* Init the hooks.
*
* @return void
*/
public static function init() {
add_filter( 'determine_current_user', [ __CLASS__, 'determine_current_user_for_woopay' ], 20 );
add_filter( 'woocommerce_session_handler', [ __CLASS__, 'add_woopay_store_api_session_handler' ], 20 );
add_action( 'woocommerce_order_payment_status_changed', [ __CLASS__, 'woopay_order_payment_status_changed' ] );
add_action( 'woopay_restore_order_customer_id', [ __CLASS__, 'restore_order_customer_id_from_requests_with_verified_email' ] );
add_filter( 'woocommerce_order_needs_payment', [ __CLASS__, 'woopay_trial_subscriptions_handler' ], 20, 3 );
register_deactivation_hook( WCPAY_PLUGIN_FILE, [ __CLASS__, 'run_and_remove_woopay_restore_order_customer_id_schedules' ] );
add_filter( 'automatewoo/referrals/referred_order_advocate', [ __CLASS__, 'automatewoo_refer_a_friend_referral_from_parameter' ] );
}
/**
* This filter is used to add a custom session handler before processing Store API request callbacks.
* This is only necessary because the Store API SessionHandler currently doesn't provide an `init_session_cookie` method.
*
* @param string $default_session_handler The default session handler class name.
*
* @return string The session handler class name.
*/
public static function add_woopay_store_api_session_handler( $default_session_handler ) {
$cart_token = wc_clean( wp_unslash( $_SERVER['HTTP_CART_TOKEN'] ?? null ) );
if (
$cart_token &&
self::is_request_from_woopay() &&
\WC_Payments_Utils::is_store_api_request() &&
class_exists( JsonWebToken::class ) &&
JsonWebToken::validate( $cart_token, '@' . wp_salt() )
) {
return SessionHandler::class;
}
return $default_session_handler;
}
/**
* Sets the current user as the user sent via the api from WooPay if present.
*
* @param \WP_User|null|int $user user to be used during the request.
*
* @return \WP_User|null|int
*/
public static function determine_current_user_for_woopay( $user ) {
if ( ! self::is_request_from_woopay() || ! \WC_Payments_Utils::is_store_api_request() ) {
return $user;
}
if ( ! self::is_woopay_enabled() ) {
return $user;
}
// Validate that the request is signed properly.
if ( ! self::has_valid_request_signature() ) {
Logger::log( __( 'WooPay request is not signed correctly.', 'woocommerce-payments' ) );
wp_die( esc_html__( 'WooPay request is not signed correctly.', 'woocommerce-payments' ), 401 );
}
add_filter( 'wcpay_is_woopay_store_api_request', '__return_true' );
$cart_token_user_id = self::get_user_id_from_cart_token();
if ( null === $cart_token_user_id ) {
return $user;
}
return $cart_token_user_id;
}
/**
* Returns the user ID from the cart token.
*
* @return int|null The User ID or null if there's no cart token in the request.
*/
public static function get_user_id_from_cart_token() {
$payload = self::get_payload_from_cart_token();
if ( null === $payload ) {
return null;
}
$session_handler = new SessionHandler();
$session_data = $session_handler->get_session( $payload->user_id );
$customer = maybe_unserialize( $session_data['customer'] );
// If the token is already authenticated, return the customer ID.
if ( is_numeric( $customer['id'] ) && intval( $customer['id'] ) > 0 ) {
return intval( $customer['id'] );
}
$woopay_verified_email_address = self::get_woopay_verified_email_address();
$enabled_adapted_extensions = get_option( WooPay_Scheduler::ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME, [] );
// If the email is verified on WooPay, matches session email (set during the redirection),
// and the store has an adapted extension installed,
// return the user to get extension data without authentication.
if ( ( is_countable( $enabled_adapted_extensions ) ? count( $enabled_adapted_extensions ) : 0 ) > 0 && null !== $woopay_verified_email_address && ! empty( $customer['email'] ) ) {
$user = get_user_by( 'email', $woopay_verified_email_address );
if ( $woopay_verified_email_address === $customer['email'] && $user ) {
// Remove Gift Cards session cache to load account gift cards.
add_filter( 'woocommerce_gc_account_session_timeout_minutes', '__return_false' );
return $user->ID;
}
}
return null;
}
/**
* Update order data for extensions which uses cookies,
* also prevent set order customer ID on requests with
* email verified to skip the login screen on the TYP.
* After 10 minutes, the customer ID will be restored
* and the user will need to login to access the TYP.
*
* @param int $order_id The order ID being updated.
*/
public static function woopay_order_payment_status_changed( $order_id ) {
if ( ! self::is_woopay_enabled() ) {
return;
}
if ( ! self::is_request_from_woopay() || ! \WC_Payments_Utils::is_store_api_request() ) {
return;
}
$woopay_adapted_extensions = new WooPay_Adapted_Extensions();
$woopay_adapted_extensions->update_order_extension_data( $order_id );
$woopay_verified_email_address = self::get_woopay_verified_email_address();
if ( null === $woopay_verified_email_address ) {
return;
}
$enabled_adapted_extensions = get_option( WooPay_Scheduler::ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME, [] );
if ( ( is_countable( $enabled_adapted_extensions ) ? count( $enabled_adapted_extensions ) : 0 ) === 0 ) {
return;
}
$payload = self::get_payload_from_cart_token();
if ( null === $payload ) {
return;
}
$order = wc_get_order( $order_id );
// Guest users user_id on the cart token payload looks like "t_hash" and the order
// customer id is 0, logged in users is the real user id in both cases.
$user_is_logged_in = $payload->user_id === $order->get_customer_id();
if ( ! $user_is_logged_in && $woopay_verified_email_address === $order->get_billing_email() ) {
$order->add_meta_data( 'woopay_merchant_customer_id', $order->get_customer_id(), true );
$order->set_customer_id( 0 );
$order->save();
wp_schedule_single_event( time() + 10 * MINUTE_IN_SECONDS, 'woopay_restore_order_customer_id', [ $order_id ] );
}
}
/**
* Restore the order customer ID after 10 minutes
* on requests with email verified.
*
* @param \WC_Order $order_id The order ID being updated.
*/
public static function restore_order_customer_id_from_requests_with_verified_email( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order->meta_exists( 'woopay_merchant_customer_id' ) ) {
return;
}
$order->set_customer_id( $order->get_meta( 'woopay_merchant_customer_id' ) );
$order->delete_meta_data( 'woopay_merchant_customer_id' );
$order->save();
}
/**
* Restore all WooPay verified email orders customer ID
* and disable the schedules when plugin is disabled.
*/
public static function run_and_remove_woopay_restore_order_customer_id_schedules() {
// WooCommerce is disabled when disabling WCPay.
if ( ! function_exists( 'wc_get_orders' ) ) {
return;
}
$args = [
'meta_key' => 'woopay_merchant_customer_id', //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'return' => 'ids',
];
$order_ids = wc_get_orders( $args );
if ( ! empty( $order_ids ) ) {
foreach ( $order_ids as $order_id ) {
self::restore_order_customer_id_from_requests_with_verified_email( $order_id );
}
}
wp_clear_scheduled_hook( 'woopay_restore_order_customer_id' );
}
/**
* Fix for AutomateWoo - Refer A Friend Add-on
* plugin when using link referrals.
*
* @param int $advocate_id The advocate ID.
*
* @return false|int|mixed The advocate ID or false if the request is not from WooPay.
*/
public static function automatewoo_refer_a_friend_referral_from_parameter( $advocate_id ) {
if ( ! self::is_request_from_woopay() || ! \WC_Payments_Utils::is_store_api_request() ) {
return $advocate_id;
}
if ( ! self::is_woopay_enabled() ) {
return $advocate_id;
}
if ( empty( $_GET['automatewoo_referral_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return false;
}
$automatewoo_referral = (int) wc_clean( wp_unslash( $_GET['automatewoo_referral_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
return $automatewoo_referral;
}
/**
* Process trial subscriptions for WooPay.
*
* @param bool $needs_payment If the order needs payment.
* @param \WC_Order $order The order.
* @param array $valid_order_statuses The valid order statuses.
*/
public static function woopay_trial_subscriptions_handler( $needs_payment, $order, $valid_order_statuses ) {
if ( ! self::is_request_from_woopay() || ! \WC_Payments_Utils::is_store_api_request() ) {
return $needs_payment;
}
if ( ! self::is_woopay_enabled() ) {
return $needs_payment;
}
if ( ! class_exists( 'WC_Subscriptions_Cart' ) || $order->get_total() > 0 ) {
return $needs_payment;
}
if ( \WC_Subscriptions_Cart::cart_contains_subscription() ) {
return true;
}
return $needs_payment;
}
/**
* Returns the payload from a cart token.
*
* @return object|null The cart token payload if it's valid.
*/
private static function get_payload_from_cart_token() {
if ( ! isset( $_SERVER['HTTP_CART_TOKEN'] ) ) {
return null;
}
if ( ! class_exists( JsonWebToken::class ) ) {
return null;
}
$cart_token = wc_clean( wp_unslash( $_SERVER['HTTP_CART_TOKEN'] ) );
if ( $cart_token && JsonWebToken::validate( $cart_token, '@' . wp_salt() ) ) {
$payload = JsonWebToken::get_parts( $cart_token )->payload;
if ( empty( $payload ) ) {
return null;
}
// Store API namespace is used as the token issuer.
if ( ! preg_match( self::STORE_API_NAMESPACE_PATTERN, $payload->iss ) ) {
return null;
}
return $payload;
}
return null;
}
/**
* Returns the encrypted session request for the frontend.
*
* @return array The encrypted session request or an empty array if the server is not eligible for encryption.
*/
public static function get_frontend_init_session_request() {
if ( ! extension_loaded( 'openssl' ) || ! function_exists( 'openssl_encrypt' ) ) {
return [];
}
// phpcs:disable WordPress.Security.NonceVerification.Missing
$order_id = ! empty( $_POST['order_id'] ) ? absint( wp_unslash( $_POST['order_id'] ) ) : null;
$key = ! empty( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : null;
$billing_email = ! empty( $_POST['billing_email'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_email'] ) ) : null;
// phpcs:enable
// phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, Generic.Arrays.DisallowLongArraySyntax.Found
$appearance = ! empty( $_POST['appearance'] ) ? self::array_map_recursive( array( __CLASS__, 'sanitize_string' ), $_POST['appearance'] ) : null;
$session = self::get_init_session_request( $order_id, $key, $billing_email, null, $appearance );
return WooPay_Utilities::encrypt_and_sign_data( $session );
}
/**
* Retrieves cart data from the current session.
*
* If the request doesn't come from WooPay, this uses the same strategy in
* `hydrate_from_api` on the Checkout Block to retrieve cart data.
*
* @param bool $is_pay_for_order Whether the request is for a pay-for-order session.
* @param int|null $order_id Pay-for-order order ID.
* @param string|null $key Pay-for-order key.
* @param string|null $billing_email Pay-for-order billing email.
* @param WP_REST_Request|null $woopay_request The WooPay request object.
*
* @return array The cart data.
*/
private static function get_cart_data( $is_pay_for_order, $order_id, $key, $billing_email, $woopay_request ) {
if ( ! $woopay_request ) {
return ! $is_pay_for_order
? rest_preload_api_request( [], '/wc/store/v1/cart' )['/wc/store/v1/cart']['body']
: rest_preload_api_request( [], '/wc/store/v1/order/' . rawurlencode( $order_id ) . '?key=' . rawurlencode( $key ) . '&billing_email=' . rawurlencode( $billing_email ) )[ '/wc/store/v1/order/' . rawurlencode( $order_id ) . '?key=' . rawurlencode( $key ) . '&billing_email=' . rawurlencode( $billing_email ) ]['body'];
}
$cart_request = new WP_REST_Request( 'GET', '/wc/store/v1/cart' );
$cart_request->set_header( 'Cart-Token', $woopay_request->get_header( 'cart_token' ) );
return rest_do_request( $cart_request )->get_data();
}
/**
* Retrieves checkout data from the current session.
*
* If the request doesn't come from WooPay, this uses the same strategy in
* `hydrate_from_api` on the Checkout Block to retrieve checkout data.
*
* @param WP_REST_Request $woopay_request The WooPay request object.
* @return mixed The checkout data.
*/
private static function get_checkout_data( $woopay_request ) {
add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
if ( ! $woopay_request ) {
$preloaded_checkout_data = rest_preload_api_request( [], '/wc/store/v1/checkout' );
$checkout_data = isset( $preloaded_checkout_data['/wc/store/v1/checkout'] ) ? $preloaded_checkout_data['/wc/store/v1/checkout']['body'] : '';
} else {
$checkout_request = new WP_REST_Request( 'GET', '/wc/store/v1/checkout' );
$checkout_request->set_header( 'Cart-Token', $woopay_request->get_header( 'cart_token' ) );
$checkout_data = rest_do_request( $checkout_request )->get_data();
}
remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
return $checkout_data;
}
/**
* Retrieves the user email from the current session.
*
* @param \WP_User $user The user object.
* @return string The user email.
*/
public static function get_user_email( $user ) {
if ( ! empty( $_POST['email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return sanitize_email( wp_unslash( $_POST['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
}
if ( ! empty( $_GET['email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return sanitize_email( wp_unslash( $_GET['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
}
if ( ! empty( $_POST['encrypted_data'] ) && is_array( $_POST['encrypted_data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
// phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$decrypted_data = WooPay_Utilities::decrypt_signed_data( $_POST['encrypted_data'] );
if ( ! empty( $decrypted_data['user_email'] ) ) {
return sanitize_email( wp_unslash( $decrypted_data['user_email'] ) );
}
}
// Get the email from the customer object if it's available.
if ( ! empty( WC()->customer ) ) {
$billing_email = WC()->customer->get_billing_email();
if ( ! empty( $billing_email ) ) {
return $billing_email;
}
$customer_email = WC()->customer->get_email();
if ( ! empty( $customer_email ) ) {
return $customer_email;
}
}
// As a last resort, we try to get the email from the customer logged in the store.
if ( $user->exists() ) {
return $user->user_email;
}
return '';
}
/**
* Returns the initial session request data.
*
* @param int|null $order_id Pay-for-order order ID.
* @param string|null $key Pay-for-order key.
* @param string|null $billing_email Pay-for-order billing email.
* @param WP_REST_Request|null $woopay_request The WooPay request object.
* @param array $appearance Merchant appearance.
* @return array The initial session request data without email and user_session.
*/
public static function get_init_session_request( $order_id = null, $key = null, $billing_email = null, $woopay_request = null, $appearance = null ) {
$user = wp_get_current_user();
$is_pay_for_order = null !== $order_id;
$order = wc_get_order( $order_id );
$customer_id = WC_Payments::get_customer_service()->get_customer_id_by_user_id( $user->ID );
if ( null === $customer_id ) {
// create customer.
$customer_data = WC_Payments_Customer_Service::map_customer_data( null, new WC_Customer( $user->ID ) );
$customer_id = WC_Payments::get_customer_service()->create_customer_for_user( $user, $customer_data );
}
if ( WC_Payments_Features::is_customer_multi_currency_enabled() && 0 !== $user->ID ) {
// Multicurrency selection is stored on user meta when logged in and WC session when logged out.
// This code just makes sure that currency selection is available on WC session for WooPay.
$currency = get_user_meta( $user->ID, MultiCurrency::CURRENCY_META_KEY, true );
$currency_code = strtoupper( $currency );
if ( ! empty( $currency_code ) && WC()->session ) {
WC()->session->set( MultiCurrency::CURRENCY_SESSION_KEY, $currency_code );
}
}
$account_id = WC_Payments::get_account_service()->get_stripe_account_id();
$site_logo_id = get_theme_mod( 'custom_logo' );
$site_logo_url = $site_logo_id ? ( wp_get_attachment_image_src( $site_logo_id, 'full' )[0] ?? '' ) : '';
$woopay_store_logo = WC_Payments::get_gateway()->get_option( 'platform_checkout_store_logo' );
$store_logo = $site_logo_url;
if ( ! empty( $woopay_store_logo ) ) {
$store_logo = get_rest_url( null, 'wc/v3/payments/file/' . $woopay_store_logo );
}
include_once WCPAY_ABSPATH . 'includes/compat/blocks/class-blocks-data-extractor.php';
$blocks_data_extractor = new Blocks_Data_Extractor();
$cart_data = self::get_cart_data( $is_pay_for_order, $order_id, $key, $billing_email, $woopay_request );
$checkout_data = self::get_checkout_data( $woopay_request );
$email = self::get_user_email( $user );
if ( $woopay_request ) {
$order_id = $checkout_data['order_id'] ?? null;
}
$request = [
'wcpay_version' => WCPAY_VERSION_NUMBER,
'user_id' => $user->ID,
'customer_id' => $customer_id,
'session_nonce' => self::create_woopay_nonce( $user->ID ),
'store_api_token' => self::init_store_api_token(),
'email' => $email,
'store_data' => [
'store_name' => get_bloginfo( 'name' ),
'store_logo' => $store_logo,
'custom_message' => self::get_formatted_custom_terms(),
'blog_id' => Jetpack_Options::get_option( 'id' ),
'blog_url' => get_site_url(),
'blog_checkout_url' => ! $is_pay_for_order ? wc_get_checkout_url() : $order->get_checkout_payment_url(),
'blog_shop_url' => get_permalink( wc_get_page_id( 'shop' ) ),
'blog_timezone' => wp_timezone_string(),
'store_api_url' => self::get_store_api_url(),
'account_id' => $account_id,
'test_mode' => WC_Payments::mode()->is_test(),
'capture_method' => empty( WC_Payments::get_gateway()->get_option( 'manual_capture' ) ) || 'no' === WC_Payments::get_gateway()->get_option( 'manual_capture' ) ? 'automatic' : 'manual',
'is_subscriptions_plugin_active' => WC_Payments::get_gateway()->is_subscriptions_plugin_active(),
'woocommerce_tax_display_cart' => get_option( 'woocommerce_tax_display_cart' ),
'ship_to_billing_address_only' => wc_ship_to_billing_address_only(),
'return_url' => ! $is_pay_for_order ? wc_get_cart_url() : $order->get_checkout_payment_url(),
'blocks_data' => $blocks_data_extractor->get_data(),
'checkout_schema_namespaces' => $blocks_data_extractor->get_checkout_schema_namespaces(),
'optional_fields_status' => self::get_option_fields_status(),
],
'user_session' => null,
'preloaded_requests' => ! $is_pay_for_order ? [
'cart' => $cart_data,
'checkout' => $checkout_data,
] : [
'cart' => $cart_data,
'checkout' => [
'order_id' => $order_id, // This is a workaround for the checkout order error. https://github.com/woocommerce/woocommerce-blocks/blob/04f36065b34977f02079e6c2c8cb955200a783ff/assets/js/blocks/checkout/block.tsx#L81-L83.
],
],
'tracks_user_identity' => WC_Payments::woopay_tracker()->tracks_get_identity(),
'appearance' => $appearance,
];
$woopay_adapted_extensions = new WooPay_Adapted_Extensions();
$request['extension_data'] = $woopay_adapted_extensions->get_extension_data();
if ( ! empty( $email ) ) {
// Save email in session to skip TYP verify email and check if
// WooPay verified email matches.
WC()->customer->set_billing_email( $email );
WC()->customer->save();
$woopay_adapted_extensions->init();
$request['adapted_extensions'] = $woopay_adapted_extensions->get_adapted_extensions_data( $email );
if ( ! is_user_logged_in() && count( $request['adapted_extensions'] ) > 0 ) {
$store_user_email_registered = get_user_by( 'email', $email );
if ( $store_user_email_registered ) {
$request['email_verified_session_nonce'] = self::create_woopay_nonce( $store_user_email_registered->ID );
}
}
}
return $request;
}
/**
* Recursively map an array.
*
* @param callable $callback The sanitize_text_field function.
* @param array $array The nested array.
*
* @return array A new appearance array.
*/
private static function array_map_recursive( $callback, $array ) {
$func = function ( $item ) use ( &$func, &$callback ) {
return is_array( $item ) ? array_map( $func, $item ) : call_user_func( $callback, $item );
};
return array_map( $func, $array );
}
/**
* Sanitize a string.
*
* @param string $item A string.
*
* @return string The sanitized string.
*/
private static function sanitize_string( $item ) {
return sanitize_text_field( wp_unslash( $item ) );
}
/**
* Used to initialize woopay session.
*
* @return void
*/
public static function ajax_init_woopay() {
$is_nonce_valid = check_ajax_referer( 'wcpay_init_woopay_nonce', false, false );
if ( ! $is_nonce_valid ) {
wp_send_json_error(
__( 'You aren’t authorized to do that.', 'woocommerce-payments' ),
403
);
}
$order_id = ! empty( $_POST['order_id'] ) ? absint( wp_unslash( $_POST['order_id'] ) ) : null;
$key = ! empty( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : null;
$billing_email = ! empty( $_POST['billing_email'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_email'] ) ) : null;
$appearance = ! empty( $_POST['appearance'] ) ? self::array_map_recursive( array( __CLASS__, 'sanitize_string' ), $_POST['appearance'] ) : null; // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, Generic.Arrays.DisallowLongArraySyntax.Found
$body = self::get_init_session_request( $order_id, $key, $billing_email, null, $appearance );
$body['user_session'] = isset( $_REQUEST['user_session'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['user_session'] ) ) : null;
$args = [
'url' => WooPay_Utilities::get_woopay_rest_url( 'init' ),
'method' => 'POST',
'timeout' => 30,
'body' => wp_json_encode( $body ),
'headers' => [
'Content-Type' => 'application/json',
],
];
/**
* Suppress psalm error from Jetpack Connection namespacing WP_Error.
*
* @psalm-suppress UndefinedDocblockClass
*/
$response = \Automattic\Jetpack\Connection\Client::remote_request( $args, wp_json_encode( $body ) );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
Logger::error( 'HTTP_REQUEST_ERROR ' . var_export( $response, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
// Respond with same message platform would respond with on failure.
$response_body_json = wp_json_encode( [ 'result' => 'failure' ] );
} else {
$response_body_json = wp_remote_retrieve_body( $response );
}
Logger::log( $response_body_json );
wp_send_json( json_decode( $response_body_json ) );
}
/**
* Used to initialize woopay session on frontend
*
* @return void
*/
public static function ajax_get_woopay_session() {
$is_nonce_valid = check_ajax_referer( 'woopay_session_nonce', false, false );
if ( ! $is_nonce_valid ) {
wp_send_json_error(
__( 'You aren’t authorized to do that.', 'woocommerce-payments' ),
403
);
}
$blog_id = Jetpack_Options::get_option( 'id' );
if ( empty( $blog_id ) ) {
wp_send_json_error(
__( 'Could not determine the blog ID.', 'woocommerce-payments' ),
503
);
}
wp_send_json( self::get_frontend_init_session_request() );
}
/**
* Save the blocks checkout phone number in session.
*
* @return void
*/
public static function ajax_set_woopay_phone_number() {
$is_nonce_valid = check_ajax_referer( 'woopay_session_nonce', false, false );
if ( ! $is_nonce_valid ) {
wp_send_json_error(
__( 'You aren’t authorized to do that.', 'woocommerce-payments' ),
403
);
}
if ( ! ( isset( WC()->session ) && WC()->session->has_session() ) ) {
WC()->session->set_customer_session_cookie( true );
}
if ( ! empty( $_POST['empty'] ) && filter_var( wp_unslash( $_POST['empty'] ), FILTER_VALIDATE_BOOLEAN ) ) {
WC()->session->__unset( self::WOOPAY_SESSION_KEY );
wp_send_json_success();
return;
}
$data = [
'save_user_in_woopay' => filter_var( wp_unslash( $_POST['save_user_in_woopay'] ), FILTER_VALIDATE_BOOLEAN ),
'woopay_source_url' =>
wc_clean( wp_unslash( $_POST['woopay_source_url'] ) ),
'woopay_is_blocks' => filter_var( wp_unslash( $_POST['save_user_in_woopay'] ), FILTER_VALIDATE_BOOLEAN ),
'woopay_viewport' => wc_clean( wp_unslash( $_POST['woopay_viewport'] ) ),
'woopay_user_phone_field' => [
'full' => wc_clean( wp_unslash( $_POST['woopay_user_phone_field']['full'] ) ),
],
];
WC()->session->set( self::WOOPAY_SESSION_KEY, $data );
wp_send_json_success();
}
/**
* Used to initialize woopay session on frontend
*
* @return void
*/
public static function ajax_get_woopay_minimum_session_data() {
$is_nonce_valid = check_ajax_referer( 'woopay_session_nonce', false, false );
if ( ! $is_nonce_valid ) {
wp_send_json_error(
__( 'You aren’t authorized to do that.', 'woocommerce-payments' ),
403
);
}
$blog_id = Jetpack_Options::get_option( 'id' );
if ( empty( $blog_id ) ) {
wp_send_json_error(
__( 'Could not determine the blog ID.', 'woocommerce-payments' ),
503
);
}
wp_send_json( self::get_woopay_minimum_session_data() );
}
/**
* Return WooPay minimum session data.
*
* @return array Array of minimum session data used by WooPay or false on failures.
*/
public static function get_woopay_minimum_session_data() {
if ( ! extension_loaded( 'openssl' ) || ! function_exists( 'openssl_encrypt' ) ) {
return [];
}
$blog_id = Jetpack_Options::get_option( 'id' );
if ( empty( $blog_id ) ) {
return [];
}
$data = [
'wcpay_version' => WCPAY_VERSION_NUMBER,
'blog_id' => $blog_id,
'blog_rest_url' => get_rest_url(),
'blog_checkout_url' => wc_get_checkout_url(),
'session_nonce' => self::create_woopay_nonce( get_current_user_id() ),
'store_api_token' => self::init_store_api_token(),
];
return WooPay_Utilities::encrypt_and_sign_data( $data );
}
/**
* Returns true if the request that's currently being processed is from WooPay, false
* otherwise.
*
* @return bool True if request is from WooPay.
*/
public static function is_request_from_woopay(): bool {
return isset( $_SERVER['HTTP_USER_AGENT'] ) && 'WooPay' === $_SERVER['HTTP_USER_AGENT'];
}
/**
* Returns true if the request that's currently being processed is signed with the blog token.
*
* @return bool True if the request signature is valid.
*/
public static function has_valid_request_signature() {
return apply_filters( 'wcpay_woopay_is_signed_with_blog_token', Rest_Authentication::is_signed_with_blog_token() );
}
/**
* Get the WooPay verified email address from the header.
*
* @return string|null The WooPay verified email address if it's set.
*/
private static function get_woopay_verified_email_address() {
$has_woopay_verified_email_address = isset( $_SERVER['HTTP_X_WOOPAY_VERIFIED_EMAIL_ADDRESS'] );
return $has_woopay_verified_email_address ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WOOPAY_VERIFIED_EMAIL_ADDRESS'] ) ) : null;
}
/**
* Returns true if WooPay is enabled, false otherwise.
*
* @return bool True if WooPay is enabled, false otherwise.
*/
private static function is_woopay_enabled(): bool {
// There were previously instances of this function being called too early. While those should be resolved, adding this defensive check as well.
if ( ! class_exists( WC_Payments_Features::class ) || ! class_exists( WC_Payments::class ) || is_null( WC_Payments::get_gateway() ) ) {
return false;
}
return WC_Payments_Features::is_woopay_eligible() && 'yes' === WC_Payments::get_gateway()->get_option( 'platform_checkout', 'no' );
}
/**
* Initializes the WooPay_Store_Api_Token class and returns the Cart token.
*
* @return string The Cart Token.
*/
private static function init_store_api_token() {
$cart_route = WooPay_Store_Api_Token::init();
return $cart_route->get_cart_token();
}
/**
* Retrieves the Store API URL.
*
* @return string
*/
private static function get_store_api_url() {
if ( class_exists( StoreApi::class ) && class_exists( RoutesController::class ) ) {
try {
$cart = StoreApi::container()->get( RoutesController::class )->get( 'cart' );
$store_api_url = method_exists( $cart, 'get_namespace' ) ? $cart->get_namespace() : 'wc/store';
} catch ( \Exception $e ) {
$store_api_url = 'wc/store';
}
}
return get_rest_url( null, $store_api_url ?? 'wc/store' );
}
/**
* WooPay requests to the merchant API does not include a cookie, so the token
* is always empty. This function creates a nonce that can be used without
* a cookie.
*
* @param int $uid The uid to be used for the nonce. Most likely the user ID.
* @return false|string
*/
private static function create_woopay_nonce( int $uid ) {
$action = 'wc_store_api';
$token = '';
$i = wp_nonce_tick( $action );
return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
}
/**
* Gets the custom message from the settings and replaces the placeholders with the correct links.
*
* @return string The custom message with the placeholders replaced.
*/
private static function get_formatted_custom_terms() {
$custom_message = WC_Payments::get_gateway()->get_option( 'platform_checkout_custom_message' );
$terms_value = wc_terms_and_conditions_page_id() ?
'<a href="' . get_permalink( wc_terms_and_conditions_page_id() ) . '">' . __( 'Terms of Service', 'woocommerce-payments' ) . '</a>' :
__( 'Terms of Service', 'woocommerce-payments' );
$privacy_policy_value = wc_privacy_policy_page_id() ?
'<a href="' . get_permalink( wc_privacy_policy_page_id() ) . '">' . __( 'Privacy Policy', 'woocommerce-payments' ) . '</a>' :
__( 'Privacy Policy', 'woocommerce-payments' );
$replacement_map = [
'[terms_of_service_link]' => $terms_value,
'[terms]' => $terms_value,
'[privacy_policy_link]' => $privacy_policy_value,
'[privacy_policy]' => $privacy_policy_value,
];
return str_replace( array_keys( $replacement_map ), array_values( $replacement_map ), $custom_message );
}
/**
* Returns the status of checkout optional/required address fields.
*
* @return array The status of the checkout fields.
*/
private static function get_option_fields_status() {
// Shortcode checkout options.
$company = get_option( 'woocommerce_checkout_company_field', 'optional' );
$address_2 = get_option( 'woocommerce_checkout_address_2_field', 'optional' );
$phone = get_option( 'woocommerce_checkout_phone_field', 'required' );
$has_terms_and_condition_page = ! empty( get_option( 'woocommerce_terms_page_id', null ) );
$terms_and_conditions = wp_kses_post( wc_replace_policy_page_link_placeholders( wc_get_terms_and_conditions_checkbox_text() ) );
$has_privacy_policy_page = ! empty( get_option( 'wp_page_for_privacy_policy', null ) );
$custom_below_place_order_button_text = self::get_formatted_custom_terms();
$below_place_order_button_text = $custom_below_place_order_button_text;
$show_terms_checkbox = false;
// Blocks checkout options. To get the blocks checkout options, we need
// to parse the checkout page content because the options are stored
// in the blocks HTML as a JSON.
$checkout_page_id = get_option( 'woocommerce_checkout_page_id' );
$checkout_page = get_post( $checkout_page_id );
/*
* Will show the terms checkbox if the terms page is set.
* Will show the checkbox even when the text is loaded from the custom field or the policy page field.
*/
if ( $has_terms_and_condition_page && $terms_and_conditions ) {
$show_terms_checkbox = true;
if ( ! $below_place_order_button_text ) {
$below_place_order_button_text = $terms_and_conditions;
}
}
if ( ! $below_place_order_button_text && $has_privacy_policy_page ) {
$show_terms_checkbox = false;
$below_place_order_button_text = wp_kses_post( wc_replace_policy_page_link_placeholders( wc_get_privacy_policy_text( 'checkout' ) ) );
}
if ( empty( $checkout_page ) ) {
return [
'company' => $company,
'address_2' => $address_2,
'phone' => $phone,
'terms_checkbox' => $show_terms_checkbox,
'custom_terms' => $below_place_order_button_text,
];
}
$checkout_page_blocks = parse_blocks( $checkout_page->post_content );
$checkout_block_index = array_search( 'woocommerce/checkout', array_column( $checkout_page_blocks, 'blockName' ), true );
// If we can find the index, it means the merchant checkout page is using blocks checkout.
if ( false !== $checkout_block_index ) {
$below_place_order_button_text = $custom_below_place_order_button_text;
$company = 'optional';
$address_2 = 'optional';
$phone = 'optional';
if ( ! empty( $checkout_page_blocks[ $checkout_block_index ]['attrs'] ) ) {
$checkout_block_attrs = $checkout_page_blocks[ $checkout_block_index ]['attrs'];
if ( ! empty( $checkout_block_attrs['requireCompanyField'] ) ) {
$company = 'required';
}
if ( ! empty( $checkout_block_attrs['requirePhoneField'] ) ) {
$phone = 'required';
}
// showCompanyField is undefined by default.
if ( empty( $checkout_block_attrs['showCompanyField'] ) ) {
$company = 'hidden';
}
if ( isset( $checkout_block_attrs['showApartmentField'] ) && false === $checkout_block_attrs['showApartmentField'] ) {
$address_2 = 'hidden';
}
if ( isset( $checkout_block_attrs['showPhoneField'] ) && false === $checkout_block_attrs['showPhoneField'] ) {
$phone = 'hidden';
}
}
$fields_block = self::get_inner_block( $checkout_page_blocks[ $checkout_block_index ], 'woocommerce/checkout-fields-block' );
$terms_block = self::get_inner_block( $fields_block, 'woocommerce/checkout-terms-block' );
$show_terms_checkbox = false;
$below_place_order_button_text = '';
if ( $terms_block ) {
$show_terms_checkbox = isset( $terms_block['attrs']['checkbox'] ) && $terms_block['attrs']['checkbox'];
$below_place_order_button_text = self::get_blocks_terms_and_conditions_text( $terms_block, $show_terms_checkbox );
}
}
return [
'company' => $company,
'address_2' => $address_2,
'phone' => $phone,
'terms_checkbox' => $show_terms_checkbox,
'custom_terms' => $below_place_order_button_text,
];
}
/**
* Gets the blocks terms and conditions text.
*
* @param array $terms_block the terms block.
* @param bool $show_terms_checkbox whether the terms checkbox is shown.
* @return string
*/
private static function get_blocks_terms_and_conditions_text( $terms_block, $show_terms_checkbox ) {
if ( isset( $terms_block['attrs']['text'] ) && ! empty( $terms_block['attrs']['text'] ) ) {
return $terms_block['attrs']['text'];
}
$privacy_page_link = get_privacy_policy_url();
$privacy_page_link = $privacy_page_link ? '<a href="' . $privacy_page_link . '" target="_blank">' . __( 'Privacy Policy', 'woocommerce-payments' ) . '</a>' : __( 'Privacy Policy', 'woocommerce-payments' );
$terms_page_id = wc_terms_and_conditions_page_id();
$terms_page_link = '';
if ( $terms_page_id ) {
$terms_page_link = get_permalink( $terms_page_id );
}
$terms_page_link = $terms_page_link ? '<a href="' . $terms_page_link . '" target="_blank">' . __( 'Terms and Conditions', 'woocommerce-payments' ) . '</a>' : __( 'Terms and Conditions', 'woocommerce-payments' );
if ( $show_terms_checkbox ) {
return sprintf(
/* translators: %1$s terms page link, %2$s privacy page link. */
__( 'You must accept our %1$s and %2$s to continue with your purchase.', 'woocommerce-payments' ),
$terms_page_link,
$privacy_page_link
);
}
return sprintf(
/* translators: %1$s terms page link, %2$s privacy page link. */
__( 'By proceeding with your purchase you agree to our %1$s and %2$s', 'woocommerce-payments' ),
$terms_page_link,
$privacy_page_link
);
}
/**
* Searches for an inner block with the given name.
*
* @param array $current_block A block that contains child blocks.
* @param string $inner_block_name The name of a child block.
* @return array|null
*/
private static function get_inner_block( $current_block, $inner_block_name ) {
if ( ! isset( $current_block['innerBlocks'] ) ) {
return;
}
$inner_block_index = array_search(
$inner_block_name,
array_column(
$current_block['innerBlocks'],
'blockName'
),
true
);
if ( ! $inner_block_index || ! isset( $current_block['innerBlocks'][ $inner_block_index ] ) ) {
return;
}
return $current_block['innerBlocks'][ $inner_block_index ];
}
}