forked from LiveCarta/PayPal-PHP-SDK
Webhook Validate Event Support
- Added validateWebhookEvent method - Updated Tests - Updated Samples
This commit is contained in:
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace PayPal\Api;
|
||||
|
||||
use PayPal\Common\PayPalModel;
|
||||
use PayPal\Common\PayPalResourceModel;
|
||||
use PayPal\Exception\PayPalConnectionException;
|
||||
use PayPal\Validation\ArgumentValidator;
|
||||
use PayPal\Api\WebhookEventList;
|
||||
use PayPal\Rest\ApiContext;
|
||||
use PayPal\Transport\PayPalRestCall;
|
||||
use PayPal\Validation\JsonValidator;
|
||||
|
||||
/**
|
||||
* Class WebhookEvent
|
||||
@@ -163,6 +163,44 @@ class WebhookEvent extends PayPalResourceModel
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates Received Event from Webhook, and returns the webhook event object. Because security verifications by verifying certificate chain is not enabled in PHP yet,
|
||||
* we need to fallback to default behavior of retrieving the ID attribute of the data, and make a separate GET call to PayPal APIs, to retrieve the data.
|
||||
* This is important to do again, as hacker could have faked the data, and the retrieved data cannot be trusted without either doing client side security validation, or making a separate call
|
||||
* to PayPal APIs to retrieve the actual data. This limits the hacker to mimick a fake data, as hacker wont be able to predict the Id correctly.
|
||||
*
|
||||
* NOTE: PLEASE DO NOT USE THE DATA PROVIDED IN WEBHOOK DIRECTLY, AS HACKER COULD PASS IN FAKE DATA. IT IS VERY IMPORTANT THAT YOU RETRIEVE THE ID AND MAKE A SEPARATE CALL TO PAYPAL API.
|
||||
*
|
||||
* @param string $body
|
||||
* @param ApiContext $apiContext
|
||||
* @param PayPalRestCall $restCall is the Rest Call Service that is used to make rest calls
|
||||
* @return WebhookEvent
|
||||
* @throws \InvalidArgumentException if input arguments are incorrect, or Id is not found.
|
||||
* @throws PayPalConnectionException if any exception from PayPal APIs other than not found is sent.
|
||||
*/
|
||||
public static function validateAndGetReceivedEvent($body, $apiContext = null, $restCall = null)
|
||||
{
|
||||
if ($body == null | empty($body)){
|
||||
throw new \InvalidArgumentException("Body cannot be null or empty");
|
||||
}
|
||||
if (!JsonValidator::validate($body, true)) {
|
||||
throw new \InvalidArgumentException("Request Body is not a valid JSON.");
|
||||
}
|
||||
$object = new WebhookEvent($body);
|
||||
if ($object->getId() == null) {
|
||||
throw new \InvalidArgumentException("Id attribute not found in JSON. Possible reason could be invalid JSON Object");
|
||||
}
|
||||
try {
|
||||
return self::get($object->getId(), $apiContext, $restCall);
|
||||
} catch(PayPalConnectionException $ex) {
|
||||
if ($ex->getCode() == 404) {
|
||||
// It means that the given webhook event Id is not found for this merchant.
|
||||
throw new \InvalidArgumentException("Webhook Event Id provided in the data is incorrect. This could happen if anyone other than PayPal is faking the incoming webhook data.");
|
||||
}
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Webhooks event resource identified by event_id. Can be used to retrieve the payload for an event.
|
||||
*
|
||||
|
||||
36
sample/notifications/ValidateWebhookEvent.php
Normal file
36
sample/notifications/ValidateWebhookEvent.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
$apiContext = require __DIR__ . '/../bootstrap.php';
|
||||
|
||||
// # Validate Webhook
|
||||
// PHP Currently does not support certificate chain validation, that is necessary to validate webhook directly, from received data
|
||||
// To resolve that, we need to use alternative, which includes making a GET call to obtain the data directly from PayPal.
|
||||
|
||||
//
|
||||
// ## Received Body from Webhook
|
||||
// Body received from webhook. This would be the data that you receive in the post request that comes from PayPal, to your webhook set URL.
|
||||
// This is a sample data, that represents the webhook event data.
|
||||
$bodyReceived = '{"id":"WH-36G56432PK518391U-9HW18392D95289106","create_time":"2015-06-01T20:21:13Z","resource_type":"sale","event_type":"PAYMENT.SALE.COMPLETED","summary":"Payment completed for $ 20.0 USD","resource":{"id":"2FY57107YS3937627","create_time":"2015-06-01T20:20:28Z","update_time":"2015-06-01T20:20:46Z","amount":{"total":"20.00","currency":"USD"},"payment_mode":"INSTANT_TRANSFER","state":"completed","protection_eligibility":"ELIGIBLE","protection_eligibility_type":"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE","parent_payment":"PAY-2SV945219E505370PKVWL5DA","transaction_fee":{"value":"0.88","currency":"USD"},"links":[{"href":"https://api.sandbox.paypal.com/v1/payments/sale/2FY57107YS3937627","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/payments/sale/2FY57107YS3937627/refund","rel":"refund","method":"POST"},{"href":"https://api.sandbox.paypal.com/v1/payments/payment/PAY-2SV945219E505370PKVWL5DA","rel":"parent_payment","method":"GET"}]},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-36G56432PK518391U-9HW18392D95289106","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-36G56432PK518391U-9HW18392D95289106/resend","rel":"resend","method":"POST"}]}';
|
||||
|
||||
/**
|
||||
* This is one way to receive the entire body that you received from PayPal webhook. This is one of the way to retrieve that information.
|
||||
* Just uncomment the below line to read the data from actual request.
|
||||
*/
|
||||
/** @var String $bodyReceived */
|
||||
// $bodyReceived = file_get_contents('php://input');
|
||||
|
||||
// ### Validate Received Event Method
|
||||
// Call the validateReceivedEvent() method with provided body, and apiContext object to validate
|
||||
try {
|
||||
/** @var \PayPal\Api\WebhookEvent $output */
|
||||
$output = \PayPal\Api\WebhookEvent::validateAndGetReceivedEvent($bodyReceived, $apiContext);
|
||||
} catch (Exception $ex) {
|
||||
// NOTE: PLEASE DO NOT USE RESULTPRINTER CLASS IN YOUR ORIGINAL CODE. FOR SAMPLE ONLY
|
||||
ResultPrinter::printError("Validate Received Webhook Event", "WebhookEvent", null, $bodyReceived, $ex);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// NOTE: PLEASE DO NOT USE RESULTPRINTER CLASS IN YOUR ORIGINAL CODE. FOR SAMPLE ONLY
|
||||
ResultPrinter::printResult("Validate Received Webhook Event", "WebhookEvent", $output->getId(), $bodyReceived, $output);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace PayPal\Test\Api;
|
||||
|
||||
use PayPal\Common\PayPalResourceModel;
|
||||
use PayPal\Exception\PayPalConnectionException;
|
||||
use PayPal\Validation\ArgumentValidator;
|
||||
use PayPal\Api\WebhookEventList;
|
||||
use PayPal\Rest\ApiContext;
|
||||
@@ -128,6 +129,48 @@ class WebhookEventTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertNotNull($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider mockProvider
|
||||
* @param WebhookEvent $obj
|
||||
*/
|
||||
public function testValidateWebhook($obj, $mockApiContext)
|
||||
{
|
||||
$mockPayPalRestCall = $this->getMockBuilder('\PayPal\Transport\PayPalRestCall')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$mockPayPalRestCall->expects($this->any())
|
||||
->method('execute')
|
||||
->will($this->returnValue(
|
||||
WebhookEventTest::getJson()
|
||||
));
|
||||
|
||||
$result = WebhookEvent::validateAndGetReceivedEvent('{"id":"123"}', $mockApiContext, $mockPayPalRestCall);
|
||||
//$result = $obj->get("eventId", $mockApiContext, $mockPayPalRestCall);
|
||||
$this->assertNotNull($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider mockProvider
|
||||
* @param WebhookEvent $obj
|
||||
* @param ApiContext $mockApiContext
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @expectedExceptionMessage Webhook Event Id provided in the data is incorrect. This could happen if anyone other than PayPal is faking the incoming webhook data.
|
||||
*/
|
||||
public function testValidateWebhook404($obj, $mockApiContext)
|
||||
{
|
||||
$mockPayPalRestCall = $this->getMockBuilder('\PayPal\Transport\PayPalRestCall')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$mockPayPalRestCall->expects($this->any())
|
||||
->method('execute')
|
||||
->will($this->throwException(new PayPalConnectionException(null, "404 not found", 404)));
|
||||
|
||||
$result = WebhookEvent::validateAndGetReceivedEvent('{"id":"123"}', $mockApiContext, $mockPayPalRestCall);
|
||||
$this->assertNotNull($result);
|
||||
}
|
||||
|
||||
public function mockProvider()
|
||||
{
|
||||
$obj = self::getObject();
|
||||
@@ -139,4 +182,57 @@ class WebhookEventTest extends \PHPUnit_Framework_TestCase
|
||||
array($obj, null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider mockProvider
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @expectedExceptionMessage Body cannot be null or empty
|
||||
*/
|
||||
public function testValidateWebhookNull($mockApiContext)
|
||||
{
|
||||
WebhookEvent::validateAndGetReceivedEvent(null, $mockApiContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider mockProvider
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @expectedExceptionMessage Body cannot be null or empty
|
||||
*/
|
||||
public function testValidateWebhookEmpty($mockApiContext)
|
||||
{
|
||||
WebhookEvent::validateAndGetReceivedEvent('', $mockApiContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider mockProvider
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @expectedExceptionMessage Request Body is not a valid JSON.
|
||||
*/
|
||||
public function testValidateWebhookInvalid($mockApiContext)
|
||||
{
|
||||
WebhookEvent::validateAndGetReceivedEvent('something-invalid', $mockApiContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider mockProvider
|
||||
* @param $mockApiContext ApiContext
|
||||
* @expectedException \PHPUnit_Framework_Error_Notice
|
||||
* @expectedExceptionMessage Missing Accessor: PayPal\Api\WebhookEvent:setValid. You might be using older version of SDK. If not, create an issue at https://github.com/paypal/PayPal-PHP-SDK/issues
|
||||
*/
|
||||
public function testValidateWebhookValidJSONWithMissingObject($obj, $mockApiContext)
|
||||
{
|
||||
WebhookEvent::validateAndGetReceivedEvent('{"valid":"json"}', $mockApiContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider mockProvider
|
||||
* @param $mockApiContext ApiContext
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @expectedExceptionMessage Id attribute not found in JSON. Possible reason could be invalid JSON Object
|
||||
*/
|
||||
public function testValidateWebhookValidJSONWithoutId($obj, $mockApiContext)
|
||||
{
|
||||
WebhookEvent::validateAndGetReceivedEvent('{"summary":"json"}', $mockApiContext);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user