diff --git a/inc/gateways/class-paypal-rest-gateway.php b/inc/gateways/class-paypal-rest-gateway.php index 7bfb5956..63ef4279 100644 --- a/inc/gateways/class-paypal-rest-gateway.php +++ b/inc/gateways/class-paypal-rest-gateway.php @@ -2326,7 +2326,26 @@ public function install_webhook() { wu_save_setting("paypal_rest_{$mode_prefix}_webhook_id", ''); } - $webhook_url = $this->get_webhook_listener_url(); + $webhook_url = esc_url_raw($this->get_webhook_listener_url()); + $webhook_scheme = strtolower((string) wp_parse_url($webhook_url, PHP_URL_SCHEME)); + $webhook_host = wp_parse_url($webhook_url, PHP_URL_HOST); + + // PayPal only delivers webhooks to public HTTPS endpoints. Detect a + // misconfigured WordPress URL before sending PayPal an invalid request. + if ('https' !== $webhook_scheme || empty($webhook_host)) { + $error = new \WP_Error( + 'wu_paypal_webhook_requires_https', + sprintf( + /* translators: %s: the currently configured webhook listener URL. */ + __('PayPal requires a public HTTPS webhook URL. Update the WordPress Network Site URL to HTTPS or define WU_GATEWAY_LISTENER_URL with a public HTTPS URL, then retry. Current listener: %s', 'ultimate-multisite'), + $webhook_url ?: __('not available', 'ultimate-multisite') + ) + ); + + $this->log($error->get_error_message(), LogLevel::ERROR); + + return $error; + } // Define the events we want to receive $event_types = [ diff --git a/tests/WP_Ultimo/Gateways/PayPal_REST_Gateway_Test.php b/tests/WP_Ultimo/Gateways/PayPal_REST_Gateway_Test.php index 3ad78400..b9274a37 100644 --- a/tests/WP_Ultimo/Gateways/PayPal_REST_Gateway_Test.php +++ b/tests/WP_Ultimo/Gateways/PayPal_REST_Gateway_Test.php @@ -620,6 +620,112 @@ public function test_maybe_install_webhook_skips_inactive_gateway(): void { $this->assertTrue(true); } + // ------------------------------------------------------------------------- + // install_webhook() + // ------------------------------------------------------------------------- + + /** + * Test webhook installation rejects a non-HTTPS listener before calling PayPal. + */ + public function test_install_webhook_rejects_http_listener_url(): void { + + $gateway = new class() extends PayPal_REST_Gateway { + + public function get_webhook_listener_url() { + + return 'http://example.test/?wu-gateway=paypal-rest'; + } + }; + + $gateway->init(); + + $result = $gateway->install_webhook(); + + $this->assertInstanceOf(\WP_Error::class, $result); + $this->assertEquals('wu_paypal_webhook_requires_https', $result->get_error_code()); + $this->assertStringContainsString('HTTPS', $result->get_error_message()); + } + + /** + * Test webhook installation rejects an HTTPS listener without a host. + */ + public function test_install_webhook_rejects_https_listener_url_without_host(): void { + + $gateway = new class() extends PayPal_REST_Gateway { + + public function get_webhook_listener_url() { + + return 'https:///listener'; + } + }; + + $gateway->init(); + + $result = $gateway->install_webhook(); + + $this->assertInstanceOf(\WP_Error::class, $result); + $this->assertEquals('wu_paypal_webhook_requires_https', $result->get_error_code()); + } + + /** + * Test webhook installation sends the payload required by PayPal's API schema. + */ + public function test_install_webhook_sends_https_url_and_event_types(): void { + + $gateway = new class() extends PayPal_REST_Gateway { + + public function get_webhook_listener_url() { + + return 'https://example.test/?wu-gateway=paypal-rest'; + } + }; + + $gateway->init(); + + $reflection = new \ReflectionClass($gateway); + $prop = $reflection->getProperty('access_token'); + $prop->setValue($gateway, 'test-access-token'); + + $captured_request = []; + + add_filter( + 'pre_http_request', + function ($preempt, $args, $url) use (&$captured_request) { + + $captured_request = [ + 'args' => $args, + 'url' => $url, + ]; + + return [ + 'response' => [ + 'code' => 201, + 'message' => 'Created', + ], + 'body' => wp_json_encode(['id' => 'WH-test-id']), + 'headers' => [], + 'cookies' => [], + ]; + }, + 10, + 3 + ); + + $result = $gateway->install_webhook(); + + remove_all_filters('pre_http_request'); + + $payload = json_decode($captured_request['args']['body'], true); + + $this->assertTrue($result); + $this->assertStringEndsWith('/v1/notifications/webhooks', $captured_request['url']); + $this->assertEquals('POST', $captured_request['args']['method']); + $this->assertEquals('https://example.test/?wu-gateway=paypal-rest', $payload['url']); + $this->assertNotEmpty($payload['event_types']); + $this->assertContains(['name' => 'BILLING.SUBSCRIPTION.PAYMENT.FAILED'], $payload['event_types']); + $this->assertContains(['name' => 'PAYMENT.SALE.COMPLETED'], $payload['event_types']); + } + // ------------------------------------------------------------------------- // is_currency_supported() // -------------------------------------------------------------------------