Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion inc/gateways/class-paypal-rest-gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Define the events we want to receive
$event_types = [
Expand Down
106 changes: 106 additions & 0 deletions tests/WP_Ultimo/Gateways/PayPal_REST_Gateway_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,112 @@
$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()
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -1963,7 +2069,7 @@

try {
$method->invoke($gateway, $fixtures['payment'], $fixtures['membership'], $fixtures['customer'], $cart, 'new');
} catch (\RuntimeException $e) {

Check warning on line 2072 in tests/WP_Ultimo/Gateways/PayPal_REST_Gateway_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Empty CATCH statement detected
// Expected — thrown by the filter to abort before wp_redirect().
}

Expand Down
Loading