Implement connection-listener for merchant info

This commit is contained in:
Philipp Stracker 2024-12-10 18:23:30 +01:00
parent 4d2c9fce10
commit 377e85a2d0
No known key found for this signature in database
6 changed files with 218 additions and 54 deletions

View file

@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
return array(
'settings.url' => static function ( ContainerInterface $container ) : string {
@ -138,11 +139,24 @@ return array(
return in_array( $country, $eligible_countries, true );
},
'settings.handler.connection-listener' => static function ( ContainerInterface $container ) : ConnectionListener {
$page_id = $container->has( 'wcgateway.current-ppcp-settings-page-id' ) ? $container->get( 'wcgateway.current-ppcp-settings-page-id' ) : '';
return new ConnectionListener(
$page_id,
$container->get( 'settings.data.common' ),
$container->get( 'settings.service.onboarding-url-manager' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache {
return new Cache( 'ppcp-paypal-signup-link' );
},
'settings.service.onboarding-url-manager' => static function ( ContainerInterface $container ) : OnboardingUrlManager {
return new OnboardingUrlManager();
return new OnboardingUrlManager(
$container->get( 'settings.service.signup-link-cache' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.connection-url-generators' => static function ( ContainerInterface $container ) : array {
// Define available environments.
@ -162,7 +176,6 @@ return array(
$generators[ $environment ] = new ConnectionUrlGenerator(
$config['partner_referrals'],
$container->get( 'api.repository.partner-referrals-data' ),
$container->get( 'settings.service.signup-link-cache' ),
$environment,
$container->get( 'settings.service.onboarding-url-manager' ),
$container->get( 'woocommerce.logger.woocommerce' )

View file

@ -12,8 +12,18 @@ namespace WooCommerce\PayPalCommerce\Settings\Handler;
use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
use Psr\Log\LoggerInterface;
class ConnectionHandler {
/**
* Provides a listener that handles merchant-connection requests.
*
* Those connection requests are made after the merchant logs into their PayPal
* account (inside the login popup). At the last step, they see a "Return to
* Store" button.
* Clicking that button triggers the merchant-connection request.
*/
class ConnectionListener {
/**
* ID of the current settings page; empty if not on a PayPal settings page.
*
@ -35,6 +45,13 @@ class ConnectionHandler {
*/
private OnboardingUrlManager $url_manager;
/**
* Logger instance, mainly used for debugging purposes.
*
* @var LoggerInterface
*/
private LoggerInterface $logger;
/**
* ID of the current user, set by the process() method.
*
@ -48,11 +65,13 @@ class ConnectionHandler {
* @param string $settings_page_id Current plugin settings page ID.
* @param CommonSettings $settings Access to saved connection details.
* @param OnboardingUrlManager $url_manager Get OnboardingURL instances.
* @param ?LoggerInterface $logger The logger, for debugging purposes.
*/
public function __construct( string $settings_page_id, CommonSettings $settings, OnboardingUrlManager $url_manager ) {
public function __construct( string $settings_page_id, CommonSettings $settings, OnboardingUrlManager $url_manager, LoggerInterface $logger = null ) {
$this->settings_page_id = $settings_page_id;
$this->settings = $settings;
$this->url_manager = $url_manager;
$this->logger = $logger ?: new NullLogger();
// Initialize as "guest", the real ID is provided via process().
$this->user_id = 0;
@ -71,9 +90,20 @@ class ConnectionHandler {
return;
}
$token = $this->get_token_from_request( $request );
if ( ! $this->url_manager->validate_token_and_delete( $token, $this->user_id ) ) {
return;
}
$data = $this->extract_data( $request );
if ( ! $data ) {
return;
}
$this->logger->info( 'Found merchant data in request', $data );
$this->store_data(
$data['use_sandbox'],
$data['is_sandbox'],
$data['merchant_id'],
$data['merchant_email']
);
@ -96,32 +126,38 @@ class ConnectionHandler {
return false;
}
// Requirement 3: The params are present and not empty - 'merchantIdInPayPal' - 'merchantId' - 'ppcpToken'
$required_params = array(
'merchantIdInPayPal',
'merchantId',
'ppcpToken',
);
foreach ( $required_params as $param ) {
if ( empty( $request[ $param ] ) ) {
return false;
}
}
return true;
}
/**
* Checks, if the connection token is valid.
* Extract the merchant details (ID & email) from the request details.
*
* If the token is valid, it is *instantly invalidated* by this check: It's
* not possible to verify the same token twice.
* @param array $request The full request details.
*
* @param string $token The token to verify.
*
* @return bool True, if the token is valid.
* @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys,
* or an empty array on failure.
*/
protected function is_token_valid( string $token ) : bool {
// $valid = OnboardingUrl::validate_token_and_delete( $this->cache, $token, $user_id )
// OR OnboardingUrl::validate_previous_token( $this->cache, $token, $user_id )
return true;
}
protected function extract_data( array $request ) : array {
// $merchant_id: $request['merchantIdInPayPal'] (!), sanitize: sanitize_text_field( wp_unslash() )
// $merchant_email: $request['merchantId'] (!), sanitize: $this->sanitize_merchant_email()
$this->logger->info( 'Extracting connection data from request...' );
$merchant_id = $this->get_merchant_id_from_request( $request );
$merchant_email = $this->get_merchant_email_from_request( $request );
if ( ! $merchant_id || ! $merchant_email ) {
return array();
}
return array(
'is_sandbox' => $this->settings->get_sandbox(),
@ -130,26 +166,76 @@ class ConnectionHandler {
);
}
/**
* Persist the merchant details to the database.
*
* @param bool $is_sandbox Whether the details are for a sandbox account.
* @param string $merchant_id The anonymized merchant ID.
* @param string $merchant_email The merchant's email.
*/
protected function store_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void {
$this->logger->info( "Save merchant details to the DB: $merchant_email ($merchant_id)" );
$this->settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email );
$this->settings->save();
}
/**
* Returns the sanitized connection token from the incoming request.
*
* @param array $request Full request details.
*
* @return string The sanitized token, or an empty string.
*/
protected function get_token_from_request( array $request ) : string {
return $this->sanitize_string( $request['ppcpToken'] ?? '' );
}
/**
* Returns the sanitized merchant ID from the incoming request.
*
* @param array $request Full request details.
*
* @return string The sanitized merchant ID, or an empty string.
*/
protected function get_merchant_id_from_request( array $request ) : string {
return $this->sanitize_string( $request['merchantIdInPayPal'] ?? '' );
}
/**
* Returns the sanitized merchant email from the incoming request.
*
* Note that the email is provided via the argument "merchantId", which
* looks incorrect at first, but PayPal uses the email address as merchant
* IDm and offers a more anonymous ID via the "merchantIdInPayPal" argument.
*
* @param array $request Full request details.
*
* @return string The sanitized merchant email, or an empty string.
*/
protected function get_merchant_email_from_request( array $request ) : string {
return $this->sanitize_merchant_email( $request['merchantId'] ?? '' );
}
/**
* Sanitizes a request-argument for processing.
*
* @param string $value Value from the request argument.
*
* @return string Sanitized value.
*/
protected function sanitize_string( string $value ) : string {
return trim( sanitize_text_field( wp_unslash( $value ) ) );
}
/**
* Sanitizes the merchant's email address for processing.
*
* @param string $email The plain email.
*
* @return string
* @return string Sanitized email address.
*/
private function sanitize_merchant_email( string $email ) : string {
protected function sanitize_merchant_email( string $email ) : string {
return sanitize_text_field( str_replace( ' ', '+', $email ) );
}
/**
* Persist the merchant details to the database.
*
* @param bool $is_sandbox
* @param string $merchant_id
* @param string $merchant_email
*/
protected function store_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void {
$this->settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email );
$this->settings->save();
}
}

View file

@ -37,13 +37,6 @@ class ConnectionUrlGenerator {
*/
protected PartnerReferralsData $referrals_data;
/**
* The cache
*
* @var Cache
*/
protected Cache $cache;
/**
* Manages access to OnboardingUrl instances
*
@ -63,7 +56,7 @@ class ConnectionUrlGenerator {
*
* @var LoggerInterface
*/
private $logger;
private LoggerInterface $logger;
/**
* Constructor for the ConnectionUrlGenerator class.
@ -72,8 +65,6 @@ class ConnectionUrlGenerator {
*
* @param PartnerReferrals $partner_referrals PartnerReferrals for URL generation.
* @param PartnerReferralsData $referrals_data Default partner referrals data.
* @param Cache $cache The cache object used for storing and
* retrieving data.
* @param string $environment Environment that is used to generate the URL.
* ['production'|'sandbox'].
* @param OnboardingUrlManager $url_manager Manages access to OnboardingUrl instances.
@ -82,14 +73,12 @@ class ConnectionUrlGenerator {
public function __construct(
PartnerReferrals $partner_referrals,
PartnerReferralsData $referrals_data,
Cache $cache,
string $environment,
OnboardingUrlManager $url_manager,
?LoggerInterface $logger = null
) {
$this->partner_referrals = $partner_referrals;
$this->referrals_data = $referrals_data;
$this->cache = $cache;
$this->environment = $environment;
$this->url_manager = $url_manager;
$this->logger = $logger ?: new NullLogger();
@ -119,7 +108,7 @@ class ConnectionUrlGenerator {
public function generate( array $products = array() ) : string {
$cache_key = $this->cache_key( $products );
$user_id = get_current_user_id();
$onboarding_url = $this->url_manager->get( $this->cache, $cache_key, $user_id );
$onboarding_url = $this->url_manager->get( $cache_key, $user_id );
$cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key );
if ( $cached_url ) {

View file

@ -9,7 +9,9 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Service;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
// TODO: Replace the OnboardingUrl with a new implementation for this module.
use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
@ -25,16 +27,75 @@ use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
* without having to re-write all token-related details just yet.
*/
class OnboardingUrlManager {
/**
* Cache instance for onboarding token.
*
* @var Cache
*/
private Cache $cache;
/**
* Logger instance, mainly used for debugging purposes.
*
* @var LoggerInterface
*/
private LoggerInterface $logger;
/**
* Constructor.
*
* @param Cache $cache Cache instance for onboarding token.
* @param ?LoggerInterface $logger The logger, for debugging purposes.
*/
public function __construct( Cache $cache, LoggerInterface $logger = null ) {
$this->cache = $cache;
$this->logger = $logger ?: new NullLogger();
}
/**
* Returns a new Onboarding Url instance.
*
* @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.
*
* @return OnboardingUrl
*/
public function get( Cache $cache, string $cache_key_prefix, int $user_id ) : OnboardingUrl {
return new OnboardingUrl( $cache, $cache_key_prefix, $user_id );
public function get( string $cache_key_prefix, int $user_id ) : OnboardingUrl {
return new OnboardingUrl( $this->cache, $cache_key_prefix, $user_id );
}
/**
* Validates the authentication token; if it's valid, the token is instantly
* invalidated (deleted), so it cannot be validated again.
*
* @param string $token The token to validate.
* @param int $user_id User ID who generated the token.
*
* @return bool True, if the token is valid. False otherwise.
*/
public function validate_token_and_delete( string $token, int $user_id ) : bool {
if ( $user_id < 1 || strlen( $token ) < 10 ) {
return false;
}
$log_token = ( (string) substr( $token, 0, 2 ) ) . '...' . ( (string) substr( $token, - 6 ) );
$this->logger->debug( 'Validating onboarding ppcpToken: ' . $log_token );
if ( OnboardingUrl::validate_token_and_delete( $this->cache, $token, $user_id ) ) {
$this->logger->info( 'Validated onboarding ppcpToken: ' . $log_token );
return true;
}
if ( OnboardingUrl::validate_previous_token( $this->cache, $token, $user_id ) ) {
// TODO: Do we need this here? Previous logic was to reload the page without doing anything in this case.
$this->logger->info( 'Validated previous token, silently redirecting: ' . $log_token );
return true;
}
$this->logger->error( 'Failed to validate onboarding ppcpToken: ' . $log_token );
return false;
}
}

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
@ -85,7 +86,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
}
);
$endpoint = $container->get( 'settings.switch-ui.endpoint' );
$endpoint = $container->get( 'settings.switch-ui.endpoint' ) ? $container->get( 'settings.switch-ui.endpoint' ) : null;
assert( $endpoint instanceof SwitchSettingsUiEndpoint );
add_action(
@ -189,6 +190,17 @@ class SettingsModule implements ServiceModule, ExecutableModule {
}
);
add_action(
'admin_init',
static function () use ( $container ) : void {
$connection_handler = $container->get( 'settings.handler.connection-listener' );
assert( $connection_handler instanceof ConnectionListener );
// @phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no nonce; sanitation done by the handler
$connection_handler->process( get_current_user_id(), $_GET );
}
);
return true;
}

View file

@ -653,7 +653,10 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$listener = $container->get( 'wcgateway.settings.listener' );
assert( $listener instanceof SettingsListener );
$use_new_ui = $container->get( 'wcgateway.settings.admin-settings-enabled' );
if ( ! $use_new_ui ) {
$listener->listen_for_merchant_id();
}
try {
$listener->listen_for_vaulting_enabled();