This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,283 @@
<?php
/**
* WooPay
*
* @package WCPay\WooPay
*/
namespace WCPay\WooPay;
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface;
/**
* WooPay
*/
class WooPay_Adapted_Extensions extends IntegrationRegistry {
const POINTS_AND_REWARDS_PLUGIN = 'woocommerce-points-and-rewards';
const POINTS_AND_REWARDS_API = 'points-and-rewards';
const GIFT_CARDS_API = 'woocommerce-gift-cards';
const GIFT_CARDS_BLOCKS = 'wc-gift-cards-blocks';
/**
* Initializa WC Blocks regitered integrations.
*/
public function init() {
do_action( 'woocommerce_blocks_checkout_block_registration', $this );
}
/**
* Get WooPay adapted extensions settings and extra data needed on WooPay.
*
* @param string $email The user email the data will be loaded.
*
* @return array The extensions script data.
*/
public function get_adapted_extensions_data( $email ) {
$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 [];
}
$extension_settings = [];
$user = wp_get_current_user();
if ( ! is_user_logged_in() ) {
// If the user is a guest and has an account on the merchant site, load data
// from there to check if we need to verify their email on WooPay later.
$user_by_email = get_user_by( 'email', $email );
if ( false !== $user_by_email ) {
$user = $user_by_email;
}
}
// Points and Rewards.
if ( in_array( self::POINTS_AND_REWARDS_PLUGIN, $enabled_adapted_extensions, true ) ) {
$points_and_rewards_data = self::get_points_and_rewards_data( $user );
if ( null !== $points_and_rewards_data ) {
$extension_settings[ self::POINTS_AND_REWARDS_API ] = $points_and_rewards_data;
}
}
if ( in_array( self::GIFT_CARDS_API, $enabled_adapted_extensions, true ) ) {
$gift_cards_data = self::get_gift_cards_data( $user );
if ( null !== $gift_cards_data ) {
$extension_settings[ self::GIFT_CARDS_API ] = $gift_cards_data;
}
}
return $extension_settings;
}
/**
* Get Points and Rewards settings for WooPay.
*
* @param \WP_User $user The user the data will be loaded.
*
* @return array|null The Points and Rewards script data if installed.
*/
public function get_points_and_rewards_data( $user ) {
if (
empty( $this->registered_integrations[ self::POINTS_AND_REWARDS_API ] ) ||
! class_exists( 'WC_Points_Rewards_Manager' ) ||
! method_exists( 'WC_Points_Rewards_Manager', 'get_users_points' )
) {
return null;
}
$points_and_rewards_script_data = $this->registered_integrations[ self::POINTS_AND_REWARDS_API ]->get_script_data();
list( $points, $monetary_value ) = explode( ':', get_option( 'wc_points_rewards_redeem_points_ratio', '' ) );
$points = floatval( $points );
$monetary_value = floatval( $monetary_value );
$points_and_rewards_script_data['points_ratio'] = [
'points' => $points,
'monetary_value' => $monetary_value,
];
/**
* Check if the user has points to show the verify email alert.
*
* @psalm-suppress UndefinedClass
*/
$available_points_for_user = \WC_Points_Rewards_Manager::get_users_points( $user->ID );
if ( $available_points_for_user > 0 && $available_points_for_user > $points_and_rewards_script_data['minimum_points_amount'] ) {
// Only ask the user to verify email if they have available points.
$points_and_rewards_script_data['should_verify_email'] = ! is_user_logged_in();
$points_and_rewards_script_data['points_available'] = $available_points_for_user;
}
return $points_and_rewards_script_data;
}
/**
* Get Gift Cards settings for WooPay.
*
* @param \WP_User $user The user the data will be loaded.
*
* @return array|null The Gift Cards script data if installed.
*/
public function get_gift_cards_data( $user ) {
if (
empty( $this->registered_integrations[ self::GIFT_CARDS_BLOCKS ] ) ||
! function_exists( 'WC_GC' ) ||
! property_exists( WC_GC(), 'account' ) ||
! method_exists( WC_GC()->account, 'get_active_giftcards' )
) {
return null;
}
$gift_cards_script_data = $this->registered_integrations[ self::GIFT_CARDS_BLOCKS ]->get_script_data();
$gift_cards_script_data['should_verify_email'] = false;
if ( ! is_user_logged_in() ) {
// Verify if the user has Gift Card balance to ask them to verify email on WooPay.
$gift_cards = WC_GC()->account->get_active_giftcards( $user->ID );
$balance = 0;
foreach ( $gift_cards as $giftcard_data ) {
$balance += (float) $giftcard_data->get_balance();
}
if ( $balance > 0 ) {
$gift_cards_script_data['should_verify_email'] = true;
}
}
return $gift_cards_script_data;
}
/**
* The custom data from plugins to be used on WooPay,
* it's not an adapted extension because it doesn't
* use the email verification integration.
*
* @return array The custom data.
*/
public function get_extension_data() {
$extension_data = [];
if ( defined( 'WOOCOMMERCE_MULTICURRENCY_VERSION' ) ) {
$extension_data['woocommerce-multicurrency'] = [
'currency' => get_woocommerce_currency(),
];
}
if ( $this->is_affiliate_for_woocommerce_enabled() && function_exists( 'afwc_get_referrer_id' ) ) {
/**
* Suppress psalm warning.
*
* @psalm-suppress UndefinedFunction
*/
$extension_data['affiliate-for-woocommerce'] = [
'affiliate-user' => afwc_get_referrer_id(),
];
}
if ( $this->is_automate_woo_referrals_enabled() ) {
$advocate_id = $this->get_automate_woo_advocate_id_from_cookie();
$extension_data['automatewoo-referrals'] = [
'advocate_id' => $advocate_id,
];
}
return $extension_data;
}
/**
* Update order extension data after finishing
* an order on WooPay, this usually is needed
* for extensions which uses cookies when an
* order is finished.
*
* @param int $order_id The successful WooPay order.
*/
public function update_order_extension_data( $order_id ) {
if ( ! empty( $_GET['affiliate'] ) && // phpcs:ignore WordPress.Security.NonceVerification
$this->is_affiliate_for_woocommerce_enabled()
) {
$affiliate_id = (int) wc_clean( wp_unslash( $_GET['affiliate'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
if ( class_exists( '\AFWC_API' ) ) {
// phpcs:ignore
/**
* @psalm-suppress UndefinedClass
*/
$affiliate_api = \AFWC_API::get_instance();
$affiliate_api->track_conversion( $order_id, $affiliate_id, '', [ 'is_affiliate_eligible' => true ] );
}
}
}
/**
* Get WC Blocks registered integrations.
*
* @param IntegrationInterface $integration An instance of IntegrationInterface.
*
* @return boolean True means registered successfully.
*/
public function register( IntegrationInterface $integration ) {
$name = $integration->get_name();
if ( self::GIFT_CARDS_BLOCKS === $name || self::POINTS_AND_REWARDS_API === $name ) {
$this->registered_integrations[ $name ] = $integration;
}
return true;
}
/**
* Check if Affiliate for WooCommerce is enabled and
* its functions used on WCPay are available.
*
* @return boolean
*/
public function is_affiliate_for_woocommerce_enabled() {
return defined( 'AFWC_PLUGIN_FILE' ) &&
function_exists( 'afwc_get_referrer_id' ) &&
class_exists( 'AFWC_API' ) &&
method_exists( 'AFWC_API', 'get_instance' ) &&
method_exists( 'AFWC_API', 'track_conversion' );
}
/**
* Check if Automate Woo Referrals is enabled and
* its functions used on WCPay are available.
*
* @psalm-suppress UndefinedClass
* @psalm-suppress UndefinedFunction
*
* @return boolean
*/
private function is_automate_woo_referrals_enabled() {
return function_exists( 'AW_Referrals' ) &&
method_exists( AW_Referrals(), 'options' ) &&
AW_Referrals()->options()->type === 'link' &&
class_exists( '\AutomateWoo\Referrals\Referral_Manager' ) &&
method_exists( \AutomateWoo\Referrals\Referral_Manager::class, 'get_advocate_key_from_cookie' );
}
/**
* Get AutomateWoo advocate id from cookie.
*
* @psalm-suppress UndefinedClass
*
* @return string|null
*/
private function get_automate_woo_advocate_id_from_cookie() {
if ( class_exists( '\AutomateWoo\Referrals\Referral_Manager' ) ) {
$advocate_from_key_cookie = \AutomateWoo\Referrals\Referral_Manager::get_advocate_key_from_cookie();
return $advocate_from_key_cookie ? $advocate_from_key_cookie->get_advocate_id() : null;
}
return null;
}
}
@@ -0,0 +1,215 @@
<?php
/**
* Class WooPay_Webhooks
*
* @package WooCommerce\Payments
*/
namespace WCPay\WooPay;
use WC_Payments_Account;
use WC_Payments_API_Client;
use WCPay\Exceptions\API_Exception;
defined( 'ABSPATH' ) || exit;
/**
* This class introduces webhooks to delivery order updates to the associated
* orders in the woopay.
*
* WooPay Webhooks are enqueued to their associated actions, delivered, and logged.
*/
class WooPay_Order_Status_Sync {
const WCPAY_WEBHOOK_WOOPAY_ORDER_STATUS_CHANGED = 'wcpay_webhook_platform_checkout_order_status_changed';
/**
* WC_Payments_Account instance to get information about the account
*
* @var WC_Payments_Account
*/
private $account;
/**
* Client for making requests to the WooCommerce Payments API
*
* @var WC_Payments_API_Client
*/
protected $payments_api_client;
/**
* Setup webhook for the WooPay Order Status Sync.
*
* @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client.
* @param WC_Payments_Account $account - WooCommerce Payments account.
*/
public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account ) {
$this->payments_api_client = $payments_api_client;
$this->account = $account;
add_filter( 'woocommerce_webhook_topic_hooks', [ __CLASS__, 'add_topics' ], 20, 2 );
add_filter( 'woocommerce_webhook_payload', [ __CLASS__, 'create_payload' ], 10, 4 );
add_filter( 'woocommerce_valid_webhook_resources', [ __CLASS__, 'add_resource' ], 10, 1 );
add_filter( 'woocommerce_valid_webhook_events', [ __CLASS__, 'add_event' ], 10, 1 );
add_action( 'woocommerce_order_status_changed', [ __CLASS__, 'send_webhook' ], 10, 3 );
add_action( 'admin_init', [ $this, 'maybe_create_woopay_order_webhook' ], 10 );
}
/**
* Return the webhook name.
*
* @return string
*/
private static function get_webhook_name() {
return __( 'WCPay woopay order status sync', 'woocommerce-payments' );
}
/**
* Maybe create the WooPay webhook under certain conditions.
*/
public function maybe_create_woopay_order_webhook() {
if ( ! current_user_can( 'manage_woocommerce' ) || self::is_webhook_created() ) {
return;
}
if ( ! $this->account->is_stripe_account_valid() || $this->account->is_account_under_review() || $this->account->is_account_rejected() ) {
return;
}
$this->register_webhook();
}
/**
* Return true if webhook was already created.
*
* @return bool
*/
private static function is_webhook_created() {
return ! empty( self::get_webhook() );
}
/**
* Return array with the webhook id for the woopay order status sync.
*
* @return array
*/
public static function get_webhook() {
$data_store = \WC_Data_Store::load( 'webhook' );
$args = [
'search' => self::get_webhook_name(),
'status' => 'active',
'limit' => 1,
];
$webhooks = $data_store->search_webhooks( $args );
return $webhooks;
}
/**
* Register the webhook on WooCommerce.
*
* @return void
*/
private function register_webhook() {
$webhook = new \WC_Webhook();
$webhook->set_name( self::get_webhook_name() );
$webhook->set_user_id( get_current_user_id() );
$webhook->set_topic( 'order.status_changed' );
$webhook->set_secret( wp_generate_password( 50, false ) );
$webhook->set_delivery_url( WooPay_Utilities::get_woopay_rest_url( 'merchant-notification' ) );
$webhook->set_status( 'active' );
$webhook->save();
try {
$this->payments_api_client->update_woopay( [ 'webhook_secret' => $webhook->get_secret() ] );
} catch ( API_Exception $e ) {
$webhook->delete();
}
}
/**
* Add order webhook topic
*
* @param array $topic_hooks List of WooCommerce's standard webhook topics and hooks.
*/
public static function add_topics( $topic_hooks ) {
$topic_hooks['order.status_changed'][] = self::WCPAY_WEBHOOK_WOOPAY_ORDER_STATUS_CHANGED;
return $topic_hooks;
}
/**
* Setup payload for the webhook delivery.
*
* @param array $payload Data to be sent out by the webhook.
* @param string $resource_name Type/name of the resource.
* @param integer $resource_id ID of the resource.
* @param integer $id ID of the webhook.
*/
public static function create_payload( $payload, $resource_name, $resource_id, $id ) {
$webhook = wc_get_webhook( $id );
if ( 0 !== strpos( $webhook->get_delivery_url(), WooPay_Utilities::get_woopay_rest_url( 'merchant-notification' ) ) ) {
// This is not a WooPay webhook, so we don't need to modify the payload.
return $payload;
}
return [
'blog_id' => \Jetpack_Options::get_option( 'id' ),
'order_id' => $resource_id,
'order_status' => $payload['status'],
];
}
/**
* Add webhook resource for order.
*
* @param array $resources List of available resources.
*/
public static function add_resource( $resources ) {
$resources[] = 'order';
return $resources;
}
/**
* Undocumented function
*
* @param array $topic_events List of available topic events.
*/
public static function add_event( $topic_events ) {
$topic_events[] = 'status_changed';
return $topic_events;
}
/**
* Trigger webhook delivery.
*
* @param int $order_id Order id.
* @param string $previous_status the old WooCommerce order status.
* @param string $next_status the new WooCommerce order status.
* @return void
*/
public static function send_webhook( $order_id, $previous_status, $next_status ) {
$order = wc_get_order( $order_id );
if ( $order->get_meta( 'is_woopay' ) ) {
do_action( self::WCPAY_WEBHOOK_WOOPAY_ORDER_STATUS_CHANGED, $order_id, $next_status );
}
}
/**
* Removes the webhook if woopay is disabled.
*
* @return void
*/
public static function remove_webhook() {
if ( self::is_webhook_created() ) {
$webhook_id = self::get_webhook()[0];
$webhook = new \WC_Webhook( $webhook_id );
$webhook->delete();
}
}
}
@@ -0,0 +1,216 @@
<?php
/**
* Class WooPay_Scheduler.
*
* @package WooCommerce\Payments
*/
namespace WCPay\WooPay;
use WC_Payments_API_Client;
use WCPay\Logger;
/**
* This class adds a cron job that runs daily to check if is there any extensions incompatible with WooPay active,
* if one is found WooPay is disabled, also updates the list of extensions adapted to work with WooPay and the
* available countries.
*/
class WooPay_Scheduler {
const INVALID_EXTENSIONS_FOUND_OPTION_NAME = 'woopay_invalid_extension_found';
const INCOMPATIBLE_EXTENSIONS_LIST_OPTION_NAME = 'woopay_incompatible_extensions';
const ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME = 'woopay_enabled_adapted_extensions';
const ADAPTED_EXTENSIONS_LIST_OPTION_NAME = 'woopay_adapted_extensions';
/**
* WC_Payments_API_Client instance.
*
* @var WC_Payments_API_Client
*/
private $payments_api_client;
/**
* Constructor.
*
* @param WC_Payments_API_Client $payments_api_client The Payments API Client.
* @return void
*/
public function __construct( WC_Payments_API_Client $payments_api_client ) {
$this->payments_api_client = $payments_api_client;
}
/**
* Init the hooks.
*/
public function init() {
add_action( 'init', [ $this, 'schedule' ] );
add_action( 'validate_woopay_compatibility', [ $this, 'update_compatibility_and_maybe_show_incompatibility_warning' ] );
add_action( 'activated_plugin', [ $this, 'show_warning_when_incompatible_extension_is_enabled' ] );
add_action( 'deactivated_plugin', [ $this, 'hide_warning_when_incompatible_extension_is_disabled' ] );
add_action( 'woocommerce_woocommerce_payments_updated', [ $this, 'remove_legacy_schedule_action_name_on_update' ] );
register_deactivation_hook( WCPAY_PLUGIN_FILE, [ $this, 'remove_scheduler' ] );
}
/**
* Disables the scheduler when the plugin is disabled.
*/
public function remove_scheduler() {
wp_clear_scheduled_hook( 'validate_woopay_compatibility' );
}
/**
* Starts the cron job.
*/
public function schedule() {
if ( ! wp_next_scheduled( 'validate_woopay_compatibility' ) ) {
wp_schedule_event( time(), 'daily', 'validate_woopay_compatibility' );
}
}
/**
* Remove legacy action name on WCPay update.
*/
public function remove_legacy_schedule_action_name_on_update() {
if ( wp_next_scheduled( 'validate_incompatible_extensions' ) ) {
wp_clear_scheduled_hook( 'validate_incompatible_extensions' );
}
}
/**
* Updates the compability list.
*/
public function update_compatibility_and_maybe_show_incompatibility_warning() {
try {
$compatibility = $this->payments_api_client->get_woopay_compatibility();
$incompatible_extensions = isset( $compatibility['incompatible_extensions'] ) ? $compatibility['incompatible_extensions'] : [];
$adapted_extensions = isset( $compatibility['adapted_extensions'] ) ? $compatibility['adapted_extensions'] : [];
$available_countries = isset( $compatibility['available_countries'] ) ? $compatibility['available_countries'] : [];
$active_plugins = get_option( 'active_plugins', [] );
update_option( self::INCOMPATIBLE_EXTENSIONS_LIST_OPTION_NAME, $incompatible_extensions );
delete_option( self::INVALID_EXTENSIONS_FOUND_OPTION_NAME );
update_option( self::ADAPTED_EXTENSIONS_LIST_OPTION_NAME, $adapted_extensions );
delete_option( self::ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME );
if ( ! empty( $active_plugins ) && is_array( $active_plugins ) ) {
if ( count( $this->get_extensions_in_list( $active_plugins, $incompatible_extensions ) ) > 0 ) {
update_option( self::INVALID_EXTENSIONS_FOUND_OPTION_NAME, true );
}
}
$this->update_enabled_adapted_extensions( $active_plugins, $adapted_extensions );
$this->update_available_countries( $available_countries );
} catch ( \Exception $e ) {
Logger::error( 'Failed to decode WooPay incompatible extensions list. ' . $e );
}
}
/**
* Update the enable adapted extensions list.
*
* @param array $active_plugins The active plugins.
* @param array $adapted_extensions The adapted extensions list.
*/
public function update_enabled_adapted_extensions( $active_plugins, $adapted_extensions ) {
$enabled_adapted_extensions = $this->get_extensions_in_list( $active_plugins, $adapted_extensions );
if ( count( $enabled_adapted_extensions ) > 0 ) {
update_option( '_wcpay_feature_woopay_first_party_auth', 0 );
} else {
update_option( '_wcpay_feature_woopay_first_party_auth', 1 );
}
update_option( self::ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME, $enabled_adapted_extensions );
}
/**
* Update the available countries list.
*
* @param array $available_countries The available countries list.
*/
public function update_available_countries( $available_countries ) {
try {
if ( is_array( $available_countries ) ) {
update_option( WooPay_Utilities::AVAILABLE_COUNTRIES_OPTION_NAME, wp_json_encode( $available_countries ) );
}
} catch ( \Exception $e ) {
Logger::error( 'Failed to decode WooPay available countries. ' . $e );
}
}
/**
* Adds a warning to the WC Payments settings page.
*
* @param string $plugin The plugin being enabled.
*/
public function show_warning_when_incompatible_extension_is_enabled( $plugin ) {
$incompatible_extensions = get_option( self::INCOMPATIBLE_EXTENSIONS_LIST_OPTION_NAME, [] );
$adapted_extensions = get_option( self::ADAPTED_EXTENSIONS_LIST_OPTION_NAME, [] );
$plugin = $this->format_extension_name( $plugin );
$active_plugins = get_option( 'active_plugins', [] );
if ( count( $this->get_extensions_in_list( [ $plugin ], $incompatible_extensions ) ) > 0 ) {
update_option( self::INVALID_EXTENSIONS_FOUND_OPTION_NAME, true );
}
$this->update_enabled_adapted_extensions( $active_plugins, $adapted_extensions );
}
/**
* Removes the warning when the last incompatible extension is removed.
*
* @param string $plugin_being_deactivated The plugin name.
*/
public function hide_warning_when_incompatible_extension_is_disabled( $plugin_being_deactivated ) {
$incompatible_extensions = get_option( self::INCOMPATIBLE_EXTENSIONS_LIST_OPTION_NAME, [] );
$adapted_extensions = get_option( self::ADAPTED_EXTENSIONS_LIST_OPTION_NAME, [] );
$active_plugins = get_option( 'active_plugins', [] );
// Needs to remove the plugin being deactivated because WordPress only updates the list after this hook runs.
$active_plugins = array_diff( $active_plugins, [ $plugin_being_deactivated ] );
// Only deactivates the warning if there are no other incompatible extensions.
if ( count( $this->get_extensions_in_list( $active_plugins, $incompatible_extensions ) ) === 0 ) {
delete_option( self::INVALID_EXTENSIONS_FOUND_OPTION_NAME );
}
$this->update_enabled_adapted_extensions( $active_plugins, $adapted_extensions );
}
/**
* Get the list of extensions in a list.
*
* @param mixed $active_plugins list of active plugins.
* @param mixed $extensions list of extensions to find in active plugins.
*
* @return array
*/
public function get_extensions_in_list( $active_plugins, $extensions ) {
$plugins_in_list = [];
foreach ( $active_plugins as $plugin ) {
$plugin = $this->format_extension_name( $plugin );
if ( in_array( $plugin, $extensions, true ) ) {
$plugins_in_list[] = $plugin;
}
}
return $plugins_in_list;
}
/**
* Removes the folder and file extension from the plugin name.
*
* @param string $plugin the plugin main file name.
* @return string the plugin name.
*/
private function format_extension_name( $plugin ) {
$plugin = explode( '/', $plugin );
$plugin = end( $plugin );
return str_replace( '.php', '', $plugin );
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,145 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Class SessionHandler
*
* This is a copy of the Automattic\WooCommerce\StoreApi\SessionHandler class with the addition of an `init_session_cookie` method.
*
* @package WooCommerce\Payments
*/
namespace WCPay\Platform_Checkout;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
use WC_Session;
defined( 'ABSPATH' ) || exit;
/**
* SessionHandler class
*/
final class SessionHandler extends WC_Session {
/**
* Token from HTTP headers.
*
* @var string
*/
protected $token;
/**
* Table name for session data.
*
* @var string Custom session table name
*/
protected $table;
/**
* Expiration timestamp.
*
* @var int
*/
protected $session_expiration;
/**
* Constructor for the session class.
*/
public function __construct() {
$this->token = wc_clean( wp_unslash( $_SERVER['HTTP_CART_TOKEN'] ?? '' ) );
$this->table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions';
}
/**
* Note: This method was added to the original class for compatibility with WooPay.
*/
public function init_session_cookie() {
$this->init();
}
/**
* Init hooks and session data.
*/
public function init() {
$this->init_session_from_token();
add_action( 'shutdown', [ $this, 'save_data' ], 20 );
}
/**
* Process the token header to load the correct session.
*/
protected function init_session_from_token() {
$payload = JsonWebToken::get_parts( $this->token )->payload;
$this->_customer_id = $payload->user_id;
$this->session_expiration = $payload->exp;
$this->_data = (array) $this->get_session( $this->_customer_id, [] );
}
/**
* Return true if the current user has an active session,.
*
* @return bool
*/
public function has_session() {
return ! empty( $this->token );
}
/**
* Returns the session.
*
* @param string $customer_id Customer ID.
* @param mixed $default Default session value.
*
* @return string|array|bool
*/
public function get_session( $customer_id, $default = false ) {
global $wpdb;
// This mimics behaviour from default WC_Session_Handler class. There will be no sessions retrieved while WP setup is due.
if ( Constants::is_defined( 'WP_SETUP_CONFIG' ) ) {
return false;
}
$value = $wpdb->get_var(
$wpdb->prepare(
"SELECT session_value FROM $this->table WHERE session_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$customer_id
)
);
if ( is_null( $value ) ) {
$value = $default;
}
return maybe_unserialize( $value );
}
/**
* Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call.
*
* @return string
*/
private function get_cache_prefix() {
return \WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP );
}
/**
* Save data and delete user session.
*/
public function save_data() {
// Dirty if something changed - prevents saving nothing new.
if ( $this->_dirty ) {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"INSERT INTO $this->table (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d) ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->_customer_id,
maybe_serialize( $this->_data ),
$this->session_expiration
)
);
wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->session_expiration - time() );
$this->_dirty = false;
}
}
}
@@ -0,0 +1,62 @@
<?php
/**
* Class WooPay_Store_Api_Token
*
* @package WCPay\Platform_Checkout
*/
namespace WCPay\Platform_Checkout;
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractCartRoute;
if ( class_exists( AbstractCartRoute::class ) ) {
/**
* This class is used to get the cart token from the cart route.
*/
class WooPay_Store_Api_Token extends AbstractCartRoute {
/**
* Helper method to get the instance of the class.
*
* @return WooPay_Store_Api_Token The instance of the class.
*
* @psalm-suppress InvalidArgument Psalm thinks namespace is incorrect.
*/
public static function init() {
$formatters = new \Automattic\WooCommerce\StoreApi\Formatters();
$extend_schema = new \Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema( $formatters );
$schema_controller = new \Automattic\WooCommerce\StoreApi\SchemaController( $extend_schema );
return new self( $schema_controller, $schema_controller->get( 'cart' ) );
}
/**
* Get the path of this REST route.
*
* @throws \Exception Throws exception because this method is not meant to be implemented in this utility class.
*/
public function get_path() {
throw new \Exception( 'Not implemented' );
}
/**
* Get arguments for this REST route.
*
* @throws \Exception Throws exception because this method is not meant to be implemented in this utility class.
*/
public function get_args() {
throw new \Exception( 'Not implemented' );
}
//phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod
/**
* This function is used to get the cart token from the cart route.
*
* @return string The cart token.
* @psalm-suppress UndefinedMethod
*/
public function get_cart_token() {
// @phpstan-ignore-next-line.
return parent::get_cart_token();
}
}
}
@@ -0,0 +1,368 @@
<?php
/**
* WooPay
*
* @package WCPay\WooPay
*/
namespace WCPay\WooPay;
use WC_Payments_Features;
use WC_Payments_Subscriptions_Utilities;
use WCPay\Logger;
use WC_Geolocation;
use WC_Payments;
use Jetpack_Options;
/**
* WooPay
*/
class WooPay_Utilities {
use WC_Payments_Subscriptions_Utilities;
const AVAILABLE_COUNTRIES_OPTION_NAME = 'woocommerce_woocommerce_payments_woopay_available_countries';
const AVAILABLE_COUNTRIES_DEFAULT = '["US"]';
const DEFAULT_WOOPAY_URL = 'https://pay.woo.com';
/**
* Check various conditions to determine if we should enable woopay.
*
* @param \WC_Payment_Gateway_WCPay $gateway Gateway instance.
* @return boolean
*/
public function should_enable_woopay( $gateway ) {
$is_woopay_eligible = WC_Payments_Features::is_woopay_eligible(); // Feature flag.
$is_woopay_enabled = 'yes' === $gateway->get_option( 'platform_checkout', 'no' );
return $is_woopay_eligible && $is_woopay_enabled;
}
/**
* Checks various conditions to determine if WooPay should be enabled on the checkout page.
*
* This function should only be called when evaluating something for the checkout or cart page. The
* function will return false if you're on any other page.
*
* @return bool True if WooPay should be enabled, false otherwise.
*/
public function should_enable_woopay_on_cart_or_checkout(): bool {
if ( ! is_checkout() && ! has_block( 'woocommerce/checkout' ) && ! is_cart() && ! has_block( 'woocommerce/cart' ) ) {
// Wrong usage, this should only be called for the checkout or cart page.
return false;
}
if ( ! is_user_logged_in() ) {
// If there's a subscription product in the cart and the customer isn't logged in we
// should not enable WooPay since that situation is currently not supported.
// Note that this is mirrored in the WC_Payments_WooPay_Button_Handler class.
if ( class_exists( 'WC_Subscriptions_Cart' ) && \WC_Subscriptions_Cart::cart_contains_subscription() ) {
return false;
}
// If guest checkout is disabled and the customer isn't logged in we should not enable
// WooPay scripts since that situations is currently not supported.
// Note that this is mirrored in the WC_Payments_WooPay_Button_Handler class.
if ( ! $this->is_guest_checkout_enabled() ) {
return false;
}
}
return true;
}
/**
* Check conditions to determine if woopay express checkout is enabled.
*
* @return boolean
*/
public function is_woopay_express_checkout_enabled() {
return WC_Payments_Features::is_woopay_express_checkout_enabled() && $this->is_country_available( WC_Payments::get_gateway() ); // Feature flag.
}
/**
* Check conditions to determine if woopay first party auth is enabled.
*
* @return bool
*/
public function is_woopay_first_party_auth_enabled() {
return WC_Payments_Features::is_woopay_first_party_auth_enabled() && $this->is_country_available( WC_Payments::get_gateway() ); // Feature flag.
}
/**
* Determines if the WooPay email input hooks should be enabled.
*
* This function doesn't affect the appearance of the email input,
* only whether or not the email exists check or auto-redirection should be enabled.
*
* @return bool
*/
public function is_woopay_email_input_enabled() {
return apply_filters( 'wcpay_is_woopay_email_input_enabled', true );
}
/**
* Generates a hash based on the store's blog token, merchant ID, and the time step window.
*
* @return string
*/
public function get_woopay_request_signature() {
$store_blog_token = \Jetpack_Options::get_option( 'blog_token' );
$time_step_window = floor( time() / 30 );
return hash_hmac( 'sha512', \Jetpack_Options::get_option( 'id' ) . $time_step_window, $store_blog_token );
}
/**
* Check session to determine if we should create a platform customer.
*
* @return boolean
*/
public function should_save_platform_customer() {
$session_data = [];
if ( isset( WC()->session ) && method_exists( WC()->session, 'has_session' ) && WC()->session->has_session() ) {
$session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY );
}
$save_user_in_woopay_post = isset( $_POST['save_user_in_woopay'] ) && filter_var( wp_unslash( $_POST['save_user_in_woopay'] ), FILTER_VALIDATE_BOOLEAN ); // phpcs:ignore WordPress.Security.NonceVerification
$save_user_in_woopay_session = isset( $session_data['save_user_in_woopay'] ) && filter_var( $session_data['save_user_in_woopay'], FILTER_VALIDATE_BOOLEAN );
return $save_user_in_woopay_post || $save_user_in_woopay_session;
}
/**
* Get if WooPay is available on the user country.
*
* @return boolean
*/
public function is_country_available() {
if ( WC_Payments::mode()->is_test() ) {
return true;
}
$location_data = WC_Geolocation::geolocate_ip();
$available_countries = self::get_persisted_available_countries();
return in_array( $location_data['country'], $available_countries, true );
}
/**
* Get if WooPay is available on the store country.
*
* @return boolean
*/
public static function is_store_country_available() {
$store_base_location = wc_get_base_location();
if ( empty( $store_base_location['country'] ) ) {
return false;
}
$available_countries = self::get_persisted_available_countries();
return in_array( $store_base_location['country'], $available_countries, true );
}
/**
* Get phone number for creating woopay customer.
*
* @return mixed|string
*/
public function get_woopay_phone() {
$session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY );
if ( ! empty( $_POST['woopay_user_phone_field']['full'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return wc_clean( wp_unslash( $_POST['woopay_user_phone_field']['full'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
} elseif ( ! empty( $session_data['woopay_user_phone_field']['full'] ) ) {
return $session_data['woopay_user_phone_field']['full'];
}
return '';
}
/**
* Get the url marketing where the user have chosen marketing options.
*
* @return mixed|string
*/
public function get_woopay_source_url() {
$session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY );
if ( ! empty( $_POST['woopay_source_url'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return wc_clean( wp_unslash( $_POST['woopay_source_url'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
} elseif ( ! empty( $session_data['woopay_source_url'] ) ) {
return $session_data['woopay_source_url'];
}
return '';
}
/**
* Get if the request comes from blocks checkout.
*
* @return boolean
*/
public function get_woopay_is_blocks() {
$session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY );
$woopay_is_blocks_post = isset( $_POST['woopay_is_blocks'] ) && filter_var( wp_unslash( $_POST['woopay_is_blocks'] ), FILTER_VALIDATE_BOOLEAN ); // phpcs:ignore WordPress.Security.NonceVerification
$woopay_is_blocks_session = isset( $session_data['woopay_is_blocks'] ) && filter_var( $session_data['woopay_is_blocks'], FILTER_VALIDATE_BOOLEAN );
return $woopay_is_blocks_post || $woopay_is_blocks_session;
}
/**
* Get the user viewport.
*
* @return mixed|string
*/
public function get_woopay_viewport() {
$session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY );
if ( ! empty( $_POST['woopay_viewport'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return wc_clean( wp_unslash( $_POST['woopay_viewport'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
} elseif ( ! empty( $session_data['woopay_viewport'] ) ) {
return $session_data['woopay_viewport'];
}
return '';
}
/**
* Returns true if guest checkout is enabled, false otherwise.
*
* @return bool True if guest checkout is enabled, false otherwise.
*/
public function is_guest_checkout_enabled(): bool {
return 'yes' === get_option( 'woocommerce_enable_guest_checkout', 'no' );
}
/**
* Builds the WooPay rest url for a given endpoint
*
* @param string $endpoint the end point.
* @return string the endpoint full url.
*/
public static function get_woopay_rest_url( $endpoint ) {
return self::get_woopay_url() . '/wp-json/platform-checkout/v1/' . $endpoint;
}
/**
* Returns the WooPay url.
*
* @return string the WooPay url.
*/
public static function get_woopay_url() {
return defined( 'PLATFORM_CHECKOUT_HOST' ) ? PLATFORM_CHECKOUT_HOST : self::DEFAULT_WOOPAY_URL;
}
/**
* Get the store blog token.
*
* @return mixed|string the store blog token.
*/
public static function get_store_blog_token() {
if ( self::get_woopay_url() === self::DEFAULT_WOOPAY_URL ) {
// Using WooPay production: Use the blog token secret from the store blog.
return Jetpack_Options::get_option( 'blog_token' );
} elseif ( apply_filters( 'wcpay_woopay_use_blog_token', false ) ) {
// Requested to use the blog token secret from the store blog.
return Jetpack_Options::get_option( 'blog_token' );
} elseif ( defined( 'DEV_BLOG_TOKEN_SECRET' ) ) {
// Has a defined dev blog token secret: Use it.
return DEV_BLOG_TOKEN_SECRET;
} else {
Logger::log( __( 'WooPay blog_token is currently misconfigured.', 'woocommerce-payments' ) );
return '';
}
}
/**
* Return an array with encrypted and signed data.
*
* @param array $data The data to be encrypted and signed.
* @return array The encrypted and signed data.
*/
public static function encrypt_and_sign_data( $data ) {
$store_blog_token = self::get_store_blog_token();
if ( empty( $store_blog_token ) ) {
return [];
}
$message = wp_json_encode( $data );
// Generate an initialization vector (IV) for encryption.
$iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( 'aes-256-cbc' ) );
// Encrypt the JSON session.
$session_encrypted = openssl_encrypt( $message, 'aes-256-cbc', $store_blog_token, OPENSSL_RAW_DATA, $iv );
// Create an HMAC hash for data integrity.
$hash = hash_hmac( 'sha256', $session_encrypted, $store_blog_token );
$data = [
'session' => $session_encrypted,
'iv' => $iv,
'hash' => $hash,
];
return [
'blog_id' => Jetpack_Options::get_option( 'id' ),
'data' => array_map( 'base64_encode', $data ),
];
}
/**
* Decode encrypted and signed data and return it.
*
* @param array $data The session, iv, and hash data for the encryption.
* @return mixed The decoded data.
*/
public static function decrypt_signed_data( $data ) {
$store_blog_token = self::get_store_blog_token();
if ( empty( $store_blog_token ) ) {
return null;
}
// Decode the data.
$decoded_data_request = array_map( 'base64_decode', $data );
// Verify the HMAC hash before decryption to ensure data integrity.
$computed_hash = hash_hmac( 'sha256', $decoded_data_request['iv'] . $decoded_data_request['data'], $store_blog_token );
// If the hashes don't match, the message may have been tampered with.
if ( ! hash_equals( $computed_hash, $decoded_data_request['hash'] ) ) {
return null;
}
// Decipher the data using the blog token and the IV.
$decrypted_data = openssl_decrypt( $decoded_data_request['data'], 'aes-256-cbc', $store_blog_token, OPENSSL_RAW_DATA, $decoded_data_request['iv'] );
if ( false === $decrypted_data ) {
return null;
}
$decrypted_data = json_decode( $decrypted_data, true );
return $decrypted_data;
}
/**
* Get the persisted available countries.
*
* @return array
*/
private static function get_persisted_available_countries() {
$available_countries = json_decode( get_option( self::AVAILABLE_COUNTRIES_OPTION_NAME, self::AVAILABLE_COUNTRIES_DEFAULT ), true );
if ( ! is_array( $available_countries ) ) {
return json_decode( self::AVAILABLE_COUNTRIES_DEFAULT );
}
return $available_countries;
}
}
@@ -0,0 +1,94 @@
<?php
/**
* Class Create_And_Confirm_Intention_Test
*
* @package Checkout_Service
*/
namespace WCPay\WooPay\Service;
use WC_Payments_Features;
use WCPay\Core\Server\Request;
use WCPay\Core\Server\Request\WooPay_Create_And_Confirm_Intention;
use WCPay\Core\Server\Request\WooPay_Create_And_Confirm_Setup_Intention;
use WCPay\Payment_Information;
/**
* Checkout service class.
*/
class Checkout_Service {
/**
* Create woopay request from base create and confirm request.
*
* @param Request $base_request Base request.
* @param Payment_Information $payment_information Using saved payment method.
*
* @return WooPay_Create_And_Confirm_Intention
* @throws \WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception
*/
public function create_intention_request( Request $base_request, Payment_Information $payment_information ) {
$request = WooPay_Create_And_Confirm_Intention::extend( $base_request );
$request->set_has_woopay_subscription( '1' === $payment_information->get_order()->get_meta( '_woopay_has_subscription' ) );
$request->set_save_payment_method_to_platform( $payment_information->should_save_payment_method_to_platform() );
$request->set_is_platform_payment_method( $this->is_platform_payment_method( $payment_information ) );
return $request;
}
/**
* Create woopay setup and confirm intent request from base create and confirm request.
*
* @param Request $base_request Base request.
* @param Payment_Information $payment_information Using saved payment method.
* @param bool $save_in_platform_account Should save in platform account.
* @param bool $save_payment_method_to_platform Should save in platform.
*
* @return WooPay_Create_And_Confirm_Setup_Intention
* @throws \WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception
*/
public function create_and_confirm_setup_intention_request( Request $base_request, Payment_Information $payment_information, bool $save_in_platform_account, bool $save_payment_method_to_platform ) {
$request = WooPay_Create_And_Confirm_Setup_Intention::extend( $base_request );
$request->set_save_in_platform_account( $save_in_platform_account );
$request->set_save_payment_method_to_platform( $save_payment_method_to_platform );
$request->set_is_platform_payment_method( $this->is_platform_payment_method( $payment_information ) );
return $request;
}
/**
* Determine if current payment method is a platform payment method.
*
* @param Payment_Information $payment_information Payment information object used to determine if a saved payment method is being used as well as helps to determine
* if stripe platform account should be used or not.
*
* @return boolean True if it is a platform payment method.
*/
public function is_platform_payment_method( Payment_Information $payment_information ) {
// Return false for express checkout method.
if ( isset( $_POST['payment_request_type'] ) || isset( $_POST['express_payment_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return false;
}
$should_use_stripe_platform = \WC_Payments::get_payment_gateway_by_id( $payment_information->get_payment_method_stripe_id() )->should_use_stripe_platform_on_checkout_page();
// Make sure the payment method being charged was created in the platform.
if (
! $payment_information->is_using_saved_payment_method() &&
$should_use_stripe_platform
) {
// This payment method was created under the platform account.
return true;
}
return false;
}
/**
* Load filters.
*
* @return void
*/
public function init() {
add_filter( 'wcpay_create_and_confirm_intent_request', [ $this, 'create_intention_request' ], 10, 3 );
add_filter( 'wcpay_create_and_confirm_setup_intention_request', [ $this, 'create_and_confirm_setup_intention_request' ], 10, 4 );
}
}