mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-06 12:25:15 +08:00
Merge branch 'trunk' into PCP-1389-pay-later-button-and-message-get-hidden-when-product-cart-checkout-value-is-outside-of-range
This commit is contained in:
commit
cc97f84acb
11 changed files with 630 additions and 9 deletions
|
@ -211,6 +211,15 @@ class PurchaseUnit {
|
|||
return $this->custom_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom ID.
|
||||
*
|
||||
* @param string $custom_id The value to set.
|
||||
*/
|
||||
public function set_custom_id( string $custom_id ): void {
|
||||
$this->custom_id = $custom_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the invoice id.
|
||||
*
|
||||
|
|
|
@ -127,4 +127,17 @@ class PartnerReferralsData {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the validation token to the return_url
|
||||
*
|
||||
* @param array $data The referral data.
|
||||
* @param string $token The token to be appended.
|
||||
* @return array
|
||||
*/
|
||||
public function append_onboarding_token( array $data, string $token ): array {
|
||||
$data['partner_config_override']['return_url'] =
|
||||
add_query_arg( 'ppcpToken', $token, $data['partner_config_override']['return_url'] );
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -258,6 +258,12 @@ class CreateOrderEndpoint implements EndpointInterface {
|
|||
} else {
|
||||
$this->purchase_unit = $this->purchase_unit_factory->from_wc_cart( null, $this->handle_shipping_in_paypal );
|
||||
|
||||
// Do not allow completion by webhooks when started via non-checkout buttons,
|
||||
// it is needed only for some APMs in checkout.
|
||||
if ( in_array( $data['context'], array( 'product', 'cart', 'cart-block' ), true ) ) {
|
||||
$this->purchase_unit->set_custom_id( '' );
|
||||
}
|
||||
|
||||
// The cart does not have any info about payment method, so we must handle free trial here.
|
||||
if ( (
|
||||
in_array( $payment_method, array( CreditCardGateway::ID, CardButtonGateway::ID ), true )
|
||||
|
|
318
modules/ppcp-onboarding/src/Helper/OnboardingUrl.php
Normal file
318
modules/ppcp-onboarding/src/Helper/OnboardingUrl.php
Normal file
|
@ -0,0 +1,318 @@
|
|||
<?php
|
||||
/**
|
||||
* Manages an Onboarding Url / Token to preserve /v2/customer/partner-referrals action_url integrity.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Onboarding\Helper
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Onboarding\Helper;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class OnboardingUrl
|
||||
*/
|
||||
class OnboardingUrl {
|
||||
|
||||
/**
|
||||
* The user ID to associate with the cache key
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $user_id;
|
||||
|
||||
/**
|
||||
* The cryptographically secure secret
|
||||
*
|
||||
* @var ?string
|
||||
*/
|
||||
private $secret = null;
|
||||
|
||||
/**
|
||||
* Unix Timestamp when token was generated
|
||||
*
|
||||
* @var ?int
|
||||
*/
|
||||
private $time = null;
|
||||
|
||||
/**
|
||||
* The "action_url" from /v2/customer/partner-referrals
|
||||
*
|
||||
* @var ?string
|
||||
*/
|
||||
private $url = null;
|
||||
|
||||
/**
|
||||
* The cache object
|
||||
*
|
||||
* @var Cache
|
||||
*/
|
||||
private $cache;
|
||||
|
||||
/**
|
||||
* The prefix for the cache key
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $cache_key_prefix;
|
||||
|
||||
/**
|
||||
* The TTL for the cache.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $cache_ttl = 3 * MONTH_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* The constructor
|
||||
*
|
||||
* @param Cache $cache The cache object to store the URL.
|
||||
* @param string $cache_key_prefix The prefix for the cache entry.
|
||||
* @param int $user_id User ID to associate the link with.
|
||||
*/
|
||||
public function __construct(
|
||||
Cache $cache,
|
||||
string $cache_key_prefix,
|
||||
int $user_id
|
||||
) {
|
||||
$this->cache = $cache;
|
||||
$this->cache_key_prefix = $cache_key_prefix;
|
||||
$this->user_id = $user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the token, if it's valid then delete it.
|
||||
* If it's invalid don't delete it, to prevent malicious requests from invalidating the token.
|
||||
*
|
||||
* @param Cache $cache The cache object where the URL is stored.
|
||||
* @param string $onboarding_token The token to validate.
|
||||
* @param int $user_id User ID to associate the link with.
|
||||
* @return bool
|
||||
*/
|
||||
public static function validate_token_and_delete( Cache $cache, string $onboarding_token, int $user_id ): bool {
|
||||
if ( ! $onboarding_token ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$token_data = json_decode( UrlHelper::url_safe_base64_decode( $onboarding_token ) ?: '', true );
|
||||
|
||||
if ( ! $token_data ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $token_data['u'] ) || ! isset( $token_data['k'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $token_data['u'] !== $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$onboarding_url = new self( $cache, $token_data['k'], $token_data['u'] );
|
||||
|
||||
if ( ! $onboarding_url->load() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ( $onboarding_url->token() ?: '' ) !== $onboarding_token ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$onboarding_url->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cached data if is valid and initialize object.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function load(): bool {
|
||||
if ( ! $this->cache->has( $this->cache_key() ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cached_data = $this->cache->get( $this->cache_key() );
|
||||
|
||||
if ( ! $this->validate_cache_data( $cached_data ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->secret = $cached_data['secret'];
|
||||
$this->time = $cached_data['time'];
|
||||
$this->url = $cached_data['url'];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init(): void {
|
||||
try {
|
||||
$this->secret = bin2hex( random_bytes( 16 ) );
|
||||
} catch ( \Exception $e ) {
|
||||
$this->secret = wp_generate_password( 16 );
|
||||
}
|
||||
|
||||
$this->time = time();
|
||||
$this->url = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates data from cache
|
||||
*
|
||||
* @param array $cache_data The data retrieved from the cache.
|
||||
* @return bool
|
||||
*/
|
||||
private function validate_cache_data( $cache_data ): bool {
|
||||
if ( ! is_array( $cache_data ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
! ( $cache_data['user_id'] ?? false )
|
||||
|| ! ( $cache_data['hash_check'] ?? false )
|
||||
|| ! ( $cache_data['secret'] ?? false )
|
||||
|| ! ( $cache_data['time'] ?? false )
|
||||
|| ! ( $cache_data['url'] ?? false )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $cache_data['user_id'] !== $this->user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detect if salt has changed.
|
||||
if ( $cache_data['hash_check'] !== wp_hash( '' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we want we can also validate time for expiration eventually.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL
|
||||
*
|
||||
* @return string
|
||||
* @throws RuntimeException Throws in case the URL isn't initialized.
|
||||
*/
|
||||
public function get(): string {
|
||||
if ( null === $this->url ) {
|
||||
throw new RuntimeException( 'Object not initialized.' );
|
||||
}
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Token
|
||||
*
|
||||
* @return string
|
||||
* @throws RuntimeException Throws in case the object isn't initialized.
|
||||
*/
|
||||
public function token(): string {
|
||||
if (
|
||||
null === $this->secret
|
||||
|| null === $this->time
|
||||
|| null === $this->user_id
|
||||
) {
|
||||
throw new RuntimeException( 'Object not initialized.' );
|
||||
}
|
||||
|
||||
// Trim the hash to make sure the token isn't too long.
|
||||
$hash = substr(
|
||||
wp_hash(
|
||||
implode(
|
||||
'|',
|
||||
array(
|
||||
$this->cache_key_prefix,
|
||||
$this->user_id,
|
||||
$this->secret,
|
||||
$this->time,
|
||||
)
|
||||
)
|
||||
),
|
||||
0,
|
||||
32
|
||||
);
|
||||
|
||||
$token = wp_json_encode(
|
||||
array(
|
||||
'k' => $this->cache_key_prefix,
|
||||
'u' => $this->user_id,
|
||||
'h' => $hash,
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $token ) {
|
||||
throw new RuntimeException( 'Unable to generate token.' );
|
||||
}
|
||||
|
||||
return UrlHelper::url_safe_base64_encode( $token );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL
|
||||
*
|
||||
* @param string $url The URL to store in the cache.
|
||||
* @return void
|
||||
*/
|
||||
public function set( string $url ): void {
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the URL and related data in cache
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function persist(): void {
|
||||
if (
|
||||
null === $this->secret
|
||||
|| null === $this->time
|
||||
|| null === $this->user_id
|
||||
|| null === $this->url
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->cache->set(
|
||||
$this->cache_key(),
|
||||
array(
|
||||
'hash_check' => wp_hash( '' ), // To detect if salt has changed.
|
||||
'secret' => $this->secret,
|
||||
'time' => $this->time,
|
||||
'user_id' => $this->user_id,
|
||||
'url' => $this->url,
|
||||
),
|
||||
$this->cache_ttl
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the token from cache
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete(): void {
|
||||
$this->cache->delete( $this->cache_key() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the compiled cache key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function cache_key(): string {
|
||||
return implode( '_', array( $this->cache_key_prefix, $this->user_id ) );
|
||||
}
|
||||
|
||||
}
|
42
modules/ppcp-onboarding/src/Helper/UrlHelper.php
Normal file
42
modules/ppcp-onboarding/src/Helper/UrlHelper.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
/**
|
||||
* Provides Helper functions for URL handling
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Onboarding\Helper
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Onboarding\Helper;
|
||||
|
||||
/**
|
||||
* Class OnboardingUrl
|
||||
*/
|
||||
class UrlHelper {
|
||||
|
||||
/**
|
||||
* Does a base64 encode of a string safe to be used on a URL
|
||||
*
|
||||
* @param string $string The string to be encoded.
|
||||
* @return string
|
||||
*/
|
||||
public static function url_safe_base64_encode( string $string ): string {
|
||||
//phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
$encoded_string = base64_encode( $string );
|
||||
$url_safe_string = str_replace( array( '+', '/' ), array( '-', '_' ), $encoded_string );
|
||||
return rtrim( $url_safe_string, '=' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a base64 decode of a string URL safe string
|
||||
*
|
||||
* @param string $url_safe_string The string to be decoded.
|
||||
* @return false|string
|
||||
*/
|
||||
public static function url_safe_base64_decode( string $url_safe_string ) {
|
||||
$padded_string = str_pad( $url_safe_string, strlen( $url_safe_string ) % 4, '=', STR_PAD_RIGHT );
|
||||
$encoded_string = str_replace( array( '-', '_' ), array( '+', '/' ), $padded_string );
|
||||
//phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
return base64_decode( $encoded_string );
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
|
||||
/**
|
||||
|
@ -96,14 +97,24 @@ class OnboardingRenderer {
|
|||
|
||||
$environment = $is_production ? 'production' : 'sandbox';
|
||||
$product = 'PPCP' === $data['products'][0] ? 'ppcp' : 'express_checkout';
|
||||
if ( $this->cache->has( $environment . '-' . $product ) ) {
|
||||
return $this->cache->get( $environment . '-' . $product );
|
||||
$cache_key = $environment . '-' . $product;
|
||||
|
||||
$onboarding_url = new OnboardingUrl( $this->cache, $cache_key, get_current_user_id() );
|
||||
|
||||
if ( $onboarding_url->load() ) {
|
||||
return $onboarding_url->get() ?: '';
|
||||
}
|
||||
|
||||
$onboarding_url->init();
|
||||
|
||||
$data = $this->partner_referrals_data
|
||||
->append_onboarding_token( $data, $onboarding_url->token() ?: '' );
|
||||
|
||||
$url = $is_production ? $this->production_partner_referrals->signup_link( $data ) : $this->sandbox_partner_referrals->signup_link( $data );
|
||||
$url = add_query_arg( $args, $url );
|
||||
|
||||
$this->cache->set( $environment . '-' . $product, $url, 3 * MONTH_IN_SECONDS );
|
||||
$onboarding_url->set( $url );
|
||||
$onboarding_url->persist();
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
|
|
@ -9,12 +9,15 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
|
||||
|
||||
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
|
||||
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
|
||||
use WooCommerce\PayPalCommerce\Http\RedirectorInterface;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\State;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus;
|
||||
|
@ -168,7 +171,7 @@ class SettingsListener {
|
|||
* Listens if the merchant ID should be updated.
|
||||
*/
|
||||
public function listen_for_merchant_id(): void {
|
||||
if ( ! $this->is_valid_site_request() || $this->state->current_state() === State::STATE_ONBOARDED ) {
|
||||
if ( ! $this->is_valid_site_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -177,17 +180,23 @@ class SettingsListener {
|
|||
* phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
* phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
*/
|
||||
if ( ! isset( $_GET['merchantIdInPayPal'] ) || ! isset( $_GET['merchantId'] ) ) {
|
||||
if ( ! isset( $_GET['merchantIdInPayPal'] ) || ! isset( $_GET['merchantId'] ) || ! isset( $_GET['ppcpToken'] ) ) {
|
||||
return;
|
||||
}
|
||||
$merchant_id = sanitize_text_field( wp_unslash( $_GET['merchantIdInPayPal'] ) );
|
||||
$merchant_email = sanitize_text_field( wp_unslash( $_GET['merchantId'] ) );
|
||||
|
||||
$merchant_id = sanitize_text_field( wp_unslash( $_GET['merchantIdInPayPal'] ) );
|
||||
$merchant_email = sanitize_text_field( wp_unslash( $_GET['merchantId'] ) );
|
||||
$onboarding_token = sanitize_text_field( wp_unslash( $_GET['ppcpToken'] ) );
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
$this->settings->set( 'merchant_id', $merchant_id );
|
||||
$this->settings->set( 'merchant_email', $merchant_email );
|
||||
|
||||
if ( ! OnboardingUrl::validate_token_and_delete( $this->signup_link_cache, $onboarding_token, get_current_user_id() ) ) {
|
||||
$this->onboarding_redirect( false );
|
||||
}
|
||||
|
||||
$is_sandbox = $this->settings->has( 'sandbox_on' ) && $this->settings->get( 'sandbox_on' );
|
||||
if ( $is_sandbox ) {
|
||||
$this->settings->set( 'merchant_id_sandbox', $merchant_id );
|
||||
|
@ -203,8 +212,23 @@ class SettingsListener {
|
|||
*/
|
||||
do_action( 'woocommerce_paypal_payments_onboarding_before_redirect' );
|
||||
|
||||
$redirect_url = $this->get_onboarding_redirect_url();
|
||||
if ( ! $this->settings->has( 'client_id' ) || ! $this->settings->get( 'client_id' ) ) {
|
||||
$this->onboarding_redirect( false );
|
||||
}
|
||||
|
||||
$this->onboarding_redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the onboarding URL.
|
||||
*
|
||||
* @param bool $success Should redirect to the success or error URL.
|
||||
* @return void
|
||||
*/
|
||||
private function onboarding_redirect( bool $success = true ): void {
|
||||
$redirect_url = $this->get_onboarding_redirect_url();
|
||||
|
||||
if ( ! $success ) {
|
||||
$redirect_url = add_query_arg( 'ppcp-onboarding-error', '1', $redirect_url );
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue