Merge pull request #1239 from woocommerce/PCP-1481-webhook-storage

Fix webhook issues when switching sandbox, and delete all webhooks when unsubscribing
This commit is contained in:
Emili Castells 2023-03-16 09:17:56 +01:00 committed by GitHub
commit aea0963812
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 106 additions and 72 deletions

View file

@ -175,12 +175,12 @@ class WebhookEndpoint {
* *
* @param Webhook $hook The webhook to delete. * @param Webhook $hook The webhook to delete.
* *
* @return bool
* @throws RuntimeException If the request fails. * @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/ */
public function delete( Webhook $hook ): bool { public function delete( Webhook $hook ): void {
if ( ! $hook->id() ) { if ( ! $hook->id() ) {
return false; return;
} }
$bearer = $this->bearer->bearer(); $bearer = $this->bearer->bearer();
@ -198,7 +198,18 @@ class WebhookEndpoint {
__( 'Not able to delete the webhook.', 'woocommerce-paypal-payments' ) __( 'Not able to delete the webhook.', 'woocommerce-paypal-payments' )
); );
} }
return wp_remote_retrieve_response_code( $response ) === 204;
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
$json = null;
if ( is_array( $response ) ) {
$json = json_decode( $response['body'] );
}
throw new PayPalApiException(
$json,
$status_code
);
}
} }
/** /**

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Onboarding;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
/** /**
* Exposes and handles REST routes related to onboarding. * Exposes and handles REST routes related to onboarding.
@ -249,7 +250,7 @@ class OnboardingRESTController {
} }
$webhook_registrar = $this->container->get( 'webhook.registrar' ); $webhook_registrar = $this->container->get( 'webhook.registrar' );
$webhook_registrar->unregister(); assert( $webhook_registrar instanceof WebhookRegistrar );
$webhook_registrar->register(); $webhook_registrar->register();
return array(); return array();

View file

@ -20,7 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper; use WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Webhooks\WebhookInfoStorage; use WooCommerce\PayPalCommerce\Webhooks\WebhookEventStorage;
/** /**
* Class StatusReportModule * Class StatusReportModule
@ -62,7 +62,7 @@ class StatusReportModule implements ModuleInterface {
$messages_apply = $c->get( 'button.helper.messages-apply' ); $messages_apply = $c->get( 'button.helper.messages-apply' );
$last_webhook_storage = $c->get( 'webhook.last-webhook-storage' ); $last_webhook_storage = $c->get( 'webhook.last-webhook-storage' );
assert( $last_webhook_storage instanceof WebhookInfoStorage ); assert( $last_webhook_storage instanceof WebhookEventStorage );
$billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' );
assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint );

View file

@ -322,16 +322,6 @@ class SettingsListener {
} }
$this->settings->persist(); $this->settings->persist();
if ( $credentials_change_status ) {
if ( in_array(
$credentials_change_status,
array( self::CREDENTIALS_ADDED, self::CREDENTIALS_CHANGED ),
true
) ) {
$this->webhook_registrar->register();
}
}
if ( $this->cache->has( PayPalBearer::CACHE_KEY ) ) { if ( $this->cache->has( PayPalBearer::CACHE_KEY ) ) {
$this->cache->delete( PayPalBearer::CACHE_KEY ); $this->cache->delete( PayPalBearer::CACHE_KEY );
} }
@ -345,7 +335,7 @@ class SettingsListener {
} }
$redirect_url = false; $redirect_url = false;
if ( self::CREDENTIALS_ADDED === $credentials_change_status ) { if ( self::CREDENTIALS_UNCHANGED !== $credentials_change_status ) {
$redirect_url = $this->get_onboarding_redirect_url(); $redirect_url = $this->get_onboarding_redirect_url();
} }

View file

@ -1,3 +1,5 @@
import {setVisibleByClass} from "../../../ppcp-button/resources/js/modules/Helper/Hiding"
document.addEventListener( document.addEventListener(
'DOMContentLoaded', 'DOMContentLoaded',
() => { () => {
@ -147,5 +149,25 @@ document.addEventListener(
simulateBtn.prop('disabled', false); simulateBtn.prop('disabled', false);
} }
}); });
const sandboxCheckbox = document.querySelector('#ppcp-sandbox_on');
if (sandboxCheckbox) {
const setWebhooksVisibility = (show) => {
[
'#field-webhook_status_heading',
'#field-webhooks_list',
'#field-webhooks_resubscribe',
'#field-webhooks_simulate',
].forEach(selector => {
setVisibleByClass(selector, show, 'hide');
});
};
const serverSandboxState = PayPalCommerceGatewayWebhooksStatus.environment === 'sandbox';
setWebhooksVisibility(serverSandboxState === sandboxCheckbox.checked);
sandboxCheckbox.addEventListener('click', () => {
setWebhooksVisibility(serverSandboxState === sandboxCheckbox.checked);
});
}
} }
); );

View file

@ -167,7 +167,8 @@ return array(
'webhook.status.assets' => function( ContainerInterface $container ) : WebhooksStatusPageAssets { 'webhook.status.assets' => function( ContainerInterface $container ) : WebhooksStatusPageAssets {
return new WebhooksStatusPageAssets( return new WebhooksStatusPageAssets(
$container->get( 'webhook.module-url' ), $container->get( 'webhook.module-url' ),
$container->get( 'ppcp.asset-version' ) $container->get( 'ppcp.asset-version' ),
$container->get( 'onboarding.environment' )
); );
}, },
@ -198,8 +199,8 @@ return array(
); );
}, },
'webhook.last-webhook-storage' => static function ( ContainerInterface $container ): WebhookInfoStorage { 'webhook.last-webhook-storage' => static function ( ContainerInterface $container ): WebhookEventStorage {
return new WebhookInfoStorage( $container->get( 'webhook.last-webhook-storage.key' ) ); return new WebhookEventStorage( $container->get( 'webhook.last-webhook-storage.key' ) );
}, },
'webhook.last-webhook-storage.key' => static function ( ContainerInterface $container ): string { 'webhook.last-webhook-storage.key' => static function ( ContainerInterface $container ): string {
return 'ppcp-last-webhook'; return 'ppcp-last-webhook';

View file

@ -62,8 +62,6 @@ class ResubscribeEndpoint {
// Validate nonce. // Validate nonce.
$this->request_data->read_request( $this->nonce() ); $this->request_data->read_request( $this->nonce() );
$this->registrar->unregister();
if ( ! $this->registrar->register() ) { if ( ! $this->registrar->register() ) {
wp_send_json_error( 'Webhook subscription failed.', 500 ); wp_send_json_error( 'Webhook subscription failed.', 500 );
return false; return false;

View file

@ -77,11 +77,11 @@ class IncomingWebhookEndpoint {
private $simulation; private $simulation;
/** /**
* The last webhook info storage. * The last webhook event storage.
* *
* @var WebhookInfoStorage * @var WebhookEventStorage
*/ */
private $last_webhook_storage; private $last_webhook_event_storage;
/** /**
* IncomingWebhookEndpoint constructor. * IncomingWebhookEndpoint constructor.
@ -92,7 +92,7 @@ class IncomingWebhookEndpoint {
* @param bool $verify_request Whether requests need to be verified or not. * @param bool $verify_request Whether requests need to be verified or not.
* @param WebhookEventFactory $webhook_event_factory The webhook event factory. * @param WebhookEventFactory $webhook_event_factory The webhook event factory.
* @param WebhookSimulation $simulation The simulation handler. * @param WebhookSimulation $simulation The simulation handler.
* @param WebhookInfoStorage $last_webhook_storage The last webhook info storage. * @param WebhookEventStorage $last_webhook_event_storage The last webhook event storage.
* @param RequestHandler ...$handlers The handlers, which process a request in the end. * @param RequestHandler ...$handlers The handlers, which process a request in the end.
*/ */
public function __construct( public function __construct(
@ -102,18 +102,18 @@ class IncomingWebhookEndpoint {
bool $verify_request, bool $verify_request,
WebhookEventFactory $webhook_event_factory, WebhookEventFactory $webhook_event_factory,
WebhookSimulation $simulation, WebhookSimulation $simulation,
WebhookInfoStorage $last_webhook_storage, WebhookEventStorage $last_webhook_event_storage,
RequestHandler ...$handlers RequestHandler ...$handlers
) { ) {
$this->webhook_endpoint = $webhook_endpoint; $this->webhook_endpoint = $webhook_endpoint;
$this->webhook = $webhook; $this->webhook = $webhook;
$this->handlers = $handlers; $this->handlers = $handlers;
$this->logger = $logger; $this->logger = $logger;
$this->verify_request = $verify_request; $this->verify_request = $verify_request;
$this->webhook_event_factory = $webhook_event_factory; $this->webhook_event_factory = $webhook_event_factory;
$this->last_webhook_storage = $last_webhook_storage; $this->last_webhook_event_storage = $last_webhook_event_storage;
$this->simulation = $simulation; $this->simulation = $simulation;
} }
/** /**
@ -186,7 +186,7 @@ class IncomingWebhookEndpoint {
public function handle_request( \WP_REST_Request $request ): \WP_REST_Response { public function handle_request( \WP_REST_Request $request ): \WP_REST_Response {
$event = $this->event_from_request( $request ); $event = $this->event_from_request( $request );
$this->last_webhook_storage->save( $event ); $this->last_webhook_event_storage->save( $event );
if ( $this->simulation->is_simulation_event( $event ) ) { if ( $this->simulation->is_simulation_event( $event ) ) {
$this->logger->info( 'Received simulated webhook.' ); $this->logger->info( 'Received simulated webhook.' );

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Status\Assets; namespace WooCommerce\PayPalCommerce\Webhooks\Status\Assets;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint; use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulateEndpoint; use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulateEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulationStateEndpoint; use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulationStateEndpoint;
@ -33,18 +34,28 @@ class WebhooksStatusPageAssets {
*/ */
private $version; private $version;
/**
* The environment object.
*
* @var Environment
*/
private $environment;
/** /**
* WebhooksStatusPageAssets constructor. * WebhooksStatusPageAssets constructor.
* *
* @param string $module_url The URL to the module. * @param string $module_url The URL to the module.
* @param string $version The assets version. * @param string $version The assets version.
* @param Environment $environment The environment object.
*/ */
public function __construct( public function __construct(
string $module_url, string $module_url,
string $version string $version,
Environment $environment
) { ) {
$this->module_url = untrailingslashit( $module_url ); $this->module_url = untrailingslashit( $module_url );
$this->version = $version; $this->version = $version;
$this->environment = $environment;
} }
/** /**
@ -103,6 +114,7 @@ class WebhooksStatusPageAssets {
'tooLongDelayMessage' => __( 'Looks like the webhook cannot be received. Check that your website is accessible from the internet.', 'woocommerce-paypal-payments' ), 'tooLongDelayMessage' => __( 'Looks like the webhook cannot be received. Check that your website is accessible from the internet.', 'woocommerce-paypal-payments' ),
), ),
), ),
'environment' => $this->environment->current_environment(),
); );
} }

View file

@ -12,9 +12,9 @@ namespace WooCommerce\PayPalCommerce\Webhooks;
use WooCommerce\PayPalCommerce\ApiClient\Entity\WebhookEvent; use WooCommerce\PayPalCommerce\ApiClient\Entity\WebhookEvent;
/** /**
* Class WebhookInfoStorage * Class WebhookEventStorage
*/ */
class WebhookInfoStorage { class WebhookEventStorage {
/** /**
* The WP option key. * The WP option key.

View file

@ -152,7 +152,6 @@ class WebhookModule implements ModuleInterface {
add_action( add_action(
'init', 'init',
function () use ( $registrar ) { function () use ( $registrar ) {
$registrar->unregister();
$registrar->register(); $registrar->register();
} }
); );

View file

@ -45,11 +45,11 @@ class WebhookRegistrar {
private $rest_endpoint; private $rest_endpoint;
/** /**
* The last webhook info storage. * The last webhook event storage.
* *
* @var WebhookInfoStorage * @var WebhookEventStorage
*/ */
private $last_webhook_storage; private $last_webhook_event_storage;
/** /**
* The logger. * The logger.
@ -64,22 +64,22 @@ class WebhookRegistrar {
* @param WebhookFactory $webhook_factory The Webhook factory. * @param WebhookFactory $webhook_factory The Webhook factory.
* @param WebhookEndpoint $endpoint The Webhook endpoint. * @param WebhookEndpoint $endpoint The Webhook endpoint.
* @param IncomingWebhookEndpoint $rest_endpoint The WordPress Rest API endpoint. * @param IncomingWebhookEndpoint $rest_endpoint The WordPress Rest API endpoint.
* @param WebhookInfoStorage $last_webhook_storage The last webhook info storage. * @param WebhookEventStorage $last_webhook_event_storage The last webhook event storage.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
WebhookFactory $webhook_factory, WebhookFactory $webhook_factory,
WebhookEndpoint $endpoint, WebhookEndpoint $endpoint,
IncomingWebhookEndpoint $rest_endpoint, IncomingWebhookEndpoint $rest_endpoint,
WebhookInfoStorage $last_webhook_storage, WebhookEventStorage $last_webhook_event_storage,
LoggerInterface $logger LoggerInterface $logger
) { ) {
$this->webhook_factory = $webhook_factory; $this->webhook_factory = $webhook_factory;
$this->endpoint = $endpoint; $this->endpoint = $endpoint;
$this->rest_endpoint = $rest_endpoint; $this->rest_endpoint = $rest_endpoint;
$this->last_webhook_storage = $last_webhook_storage; $this->last_webhook_event_storage = $last_webhook_event_storage;
$this->logger = $logger; $this->logger = $logger;
} }
/** /**
@ -88,6 +88,8 @@ class WebhookRegistrar {
* @return bool * @return bool
*/ */
public function register(): bool { public function register(): bool {
$this->unregister();
$webhook = $this->webhook_factory->for_url_and_events( $webhook = $this->webhook_factory->for_url_and_events(
$this->rest_endpoint->url(), $this->rest_endpoint->url(),
$this->rest_endpoint->handled_event_types() $this->rest_endpoint->handled_event_types()
@ -102,7 +104,7 @@ class WebhookRegistrar {
self::KEY, self::KEY,
$created->to_array() $created->to_array()
); );
$this->last_webhook_storage->clear(); $this->last_webhook_event_storage->clear();
$this->logger->info( 'Webhooks subscribed.' ); $this->logger->info( 'Webhooks subscribed.' );
return true; return true;
} catch ( RuntimeException $error ) { } catch ( RuntimeException $error ) {
@ -113,27 +115,23 @@ class WebhookRegistrar {
/** /**
* Unregister webhooks with PayPal. * Unregister webhooks with PayPal.
*
* @return bool
*/ */
public function unregister(): bool { public function unregister(): void {
$data = (array) get_option( self::KEY, array() );
if ( ! $data ) {
return false;
}
try { try {
$webhook = $this->webhook_factory->from_array( $data ); $webhooks = $this->endpoint->list();
$success = $this->endpoint->delete( $webhook ); foreach ( $webhooks as $webhook ) {
try {
$this->endpoint->delete( $webhook );
} catch ( RuntimeException $deletion_error ) {
$this->logger->error( "Failed to delete webhook {$webhook->id()}: {$deletion_error->getMessage()}" );
}
}
} catch ( RuntimeException $error ) { } catch ( RuntimeException $error ) {
$this->logger->error( 'Failed to delete webhooks: ' . $error->getMessage() ); $this->logger->error( 'Failed to delete webhooks: ' . $error->getMessage() );
return false;
} }
if ( $success ) { delete_option( self::KEY );
delete_option( self::KEY ); $this->last_webhook_event_storage->clear();
$this->last_webhook_storage->clear(); $this->logger->info( 'Webhooks deleted.' );
$this->logger->info( 'Webhooks deleted.' );
}
return $success;
} }
} }

View file

@ -85,6 +85,8 @@ class SettingsListenerTest extends ModularTestCase
$dcc_status_cache->shouldReceive('has') $dcc_status_cache->shouldReceive('has')
->andReturn(false); ->andReturn(false);
expect('wp_safe_redirect')->once();
$testee->listen(); $testee->listen();
} }
} }