diff --git a/src/Adapter/ApplepayAdapter.php b/src/Adapter/ApplepayAdapter.php new file mode 100644 index 00000000..cd33937a --- /dev/null +++ b/src/Adapter/ApplepayAdapter.php @@ -0,0 +1,163 @@ + + * + * @package UnzerSDK\Adapter + */ +namespace UnzerSDK\Adapter; + +use UnzerSDK\Constants\ApplepayValidationDomains; +use UnzerSDK\Exceptions\ApplepayMerchantValidationException; +use UnzerSDK\Resources\ExternalResources\ApplepaySession; +use UnzerSDK\Services\EnvironmentService; + +class ApplepayAdapter +{ + private $request; + + /** + * @param string $merchantValidationURL URL for merchant validation request + * @param ApplepaySession $applePaySession Containing applepay session data. + * @param string $merchantValidationCertificatePath Path to merchant identification certificate + * @param string|null $merchantValidationCertificateKeyChainPath + * + * @return string|null + * + * @throws ApplepayMerchantValidationException + */ + public function validateApplePayMerchant( + string $merchantValidationURL, + ApplepaySession $applePaySession, + string $merchantValidationCertificatePath, + ?string $merchantValidationCertificateKeyChainPath = null + ): ?string { + if (!$this->validMerchantValidationDomain($merchantValidationURL)) { + throw new ApplepayMerchantValidationException('Invalid URL used for merchantValidation request.'); + } + $payload = $applePaySession->jsonSerialize(); + $this->init( + $merchantValidationURL, + $payload, + $merchantValidationCertificatePath, + $merchantValidationCertificateKeyChainPath + ); + $sessionResponse = $this->execute(); + $this->close(); + return $sessionResponse; + } + + /** + * Check whether domain of merchantValidationURL is allowed for validation request. + * + * @param string $merchantValidationURL URL used for merchant validation request. + * + */ + public function validMerchantValidationDomain(string $merchantValidationURL): bool + { + $domain = explode('/', $merchantValidationURL)[2] ?? ''; + + $UrlList = ApplepayValidationDomains::ALLOWED_VALIDATION_URLS; + return in_array($domain, $UrlList); + } + + /** + * {@inheritDoc} + */ + public function init($url, $payload, $sslCert, $caCert = null): void + { + $timeout = EnvironmentService::getTimeout(); + $curlVerbose = EnvironmentService::isCurlVerbose(); + + $this->request = curl_init($url); + $this->setOption(CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + $this->setOption(CURLOPT_POST, 1); + $this->setOption(CURLOPT_DNS_USE_GLOBAL_CACHE, false); + $this->setOption(CURLOPT_POSTFIELDS, $payload); + $this->setOption(CURLOPT_FAILONERROR, false); + $this->setOption(CURLOPT_TIMEOUT, $timeout); + $this->setOption(CURLOPT_CONNECTTIMEOUT, $timeout); + $this->setOption(CURLOPT_HTTP200ALIASES, (array)400); + $this->setOption(CURLOPT_RETURNTRANSFER, 1); + $this->setOption(CURLOPT_SSL_VERIFYPEER, 1); + $this->setOption(CURLOPT_SSL_VERIFYHOST, 2); + $this->setOption(CURLOPT_VERBOSE, $curlVerbose); + $this->setOption(CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + + $this->setOption(CURLOPT_SSLCERT, $sslCert); + if (isset($caCert)) { + $this->setOption(CURLOPT_CAINFO, $caCert); + } + } + + /** + * Sets curl option. + * + * @param $name + * @param $value + */ + private function setOption($name, $value): void + { + curl_setopt($this->request, $name, $value); + } + + /** + * {@inheritDoc} + * + * @throws ApplepayMerchantValidationException + */ + public function execute(): ?string + { + $response = curl_exec($this->request); + $error = curl_error($this->request); + $errorNo = curl_errno($this->request); + + switch ($errorNo) { + case 0: + return $response; + break; + case CURLE_OPERATION_TIMEDOUT: + $errorMessage = 'Timeout: The Applepay API seems to be not available at the moment!'; + break; + default: + $errorMessage = $error . ' (curl_errno: ' . $errorNo . ').'; + break; + } + throw new ApplepayMerchantValidationException($errorMessage); + } + + /** + * @inheritDoc + */ + public function close(): void + { + curl_close($this->request); + } + + /** + * @inheritDoc + */ + public function getResponseCode(): string + { + return curl_getinfo($this->request, CURLINFO_HTTP_CODE); + } +} diff --git a/src/Constants/ApplepayValidationDomains.php b/src/Constants/ApplepayValidationDomains.php new file mode 100644 index 00000000..9f9824ab --- /dev/null +++ b/src/Constants/ApplepayValidationDomains.php @@ -0,0 +1,52 @@ + + * + * @package UnzerSDK\Constants + */ +namespace UnzerSDK\Constants; + +class ApplepayValidationDomains +{ + // URL list + public const ALLOWED_VALIDATION_URLS = [ + 'apple-pay-gateway.apple.com', + 'cn-apple-pay-gateway.apple.com', + 'apple-pay-gateway-nc-pod1.apple.com', + 'apple-pay-gateway-nc-pod2.apple.com', + 'apple-pay-gateway-nc-pod3.apple.com', + 'apple-pay-gateway-nc-pod4.apple.com', + 'apple-pay-gateway-nc-pod5.apple.com', + 'apple-pay-gateway-pr-pod1.apple.com', + 'apple-pay-gateway-pr-pod2.apple.com', + 'apple-pay-gateway-pr-pod3.apple.com', + 'apple-pay-gateway-pr-pod4.apple.com', + 'apple-pay-gateway-pr-pod5.apple.com', + 'cn-apple-pay-gateway-sh-pod1.apple.com', + 'cn-apple-pay-gateway-sh-pod2.apple.com', + 'cn-apple-pay-gateway-sh-pod3.apple.com', + 'cn-apple-pay-gateway-tj-pod1.apple.com', + 'cn-apple-pay-gateway-tj-pod2.apple.com', + 'cn-apple-pay-gateway-tj-pod3.apple.com', + 'apple-pay-gateway-cert.apple.com', + 'cn-apple-pay-gateway-cert.apple.com' + ]; +} diff --git a/src/Exceptions/ApplepayMerchantValidationException.php b/src/Exceptions/ApplepayMerchantValidationException.php new file mode 100644 index 00000000..ba61e821 --- /dev/null +++ b/src/Exceptions/ApplepayMerchantValidationException.php @@ -0,0 +1,33 @@ + + * + * @package UnzerSDK + * + */ + +namespace UnzerSDK\Exceptions; + +use Exception; + +class ApplepayMerchantValidationException extends Exception +{ +} diff --git a/src/Resources/ExternalResources/ApplepaySession.php b/src/Resources/ExternalResources/ApplepaySession.php new file mode 100644 index 00000000..8fb4a416 --- /dev/null +++ b/src/Resources/ExternalResources/ApplepaySession.php @@ -0,0 +1,133 @@ + + * + * @package UnzerSDK + * + */ + +namespace UnzerSDK\Resources\ExternalResources; + +class ApplepaySession +{ + /** + * This can be found in the Apple Developer Account + * + * @var string|null $merchantIdentifier + */ + private $merchantIdentifier; + + /** + * This is the Merchant-Name + * + * @var string|null $displayName + */ + private $displayName; + + /** + * This is the Domain Name which has been validated in the Apple Developer Account. + * + * @var string|null $domainName + */ + private $domainName; + + /** + * ApplepaySession constructor. + * + * @param string $merchantIdentifier + * @param string $displayName + * @param string $domainName + */ + public function __construct(string $merchantIdentifier, string $displayName, string $domainName) + { + $this->merchantIdentifier = $merchantIdentifier; + $this->displayName = $displayName; + $this->domainName = $domainName; + } + + /** + * Returns the json representation of this object's properties. + * + * @return false|string + */ + public function jsonSerialize() + { + $properties = get_object_vars($this); + return json_encode($properties, JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION); + } + + /** + * @return string|null + */ + public function getMerchantIdentifier(): ?string + { + return $this->merchantIdentifier; + } + + /** + * @param string|null $merchantIdentifier + * + * @return ApplepaySession + */ + public function setMerchantIdentifier(?string $merchantIdentifier): ApplepaySession + { + $this->merchantIdentifier = $merchantIdentifier; + return $this; + } + + /** + * @return string|null + */ + public function getDisplayName(): ?string + { + return $this->displayName; + } + + /** + * @param string|null $displayName + * + * @return ApplepaySession + */ + public function setDisplayName(?string $displayName): ApplepaySession + { + $this->displayName = $displayName; + return $this; + } + + /** + * @return string|null + */ + public function getDomainName(): ?string + { + return $this->domainName; + } + + /** + * @param string|null $domainName + * + * @return ApplepaySession + */ + public function setDomainName(?string $domainName): ApplepaySession + { + $this->domainName = $domainName; + return $this; + } +} diff --git a/src/Services/EnvironmentService.php b/src/Services/EnvironmentService.php index fbfdbaa9..4888a8d2 100755 --- a/src/Services/EnvironmentService.php +++ b/src/Services/EnvironmentService.php @@ -41,6 +41,9 @@ class EnvironmentService public const ENV_VAR_TEST_PRIVATE_KEY_NON_3DS = 'UNZER_PAPI_TEST_PRIVATE_KEY_NON_3DS'; public const ENV_VAR_TEST_PUBLIC_KEY_NON_3DS = 'UNZER_PAPI_TEST_PUBLIC_KEY_NON_3DS'; + public const ENV_VAR_TEST_APPLE_MERCHANT_CERTIFICATE = 'UNZER_APPLE_MERCHANT_CERTIFICATE_PATH'; + public const ENV_VAR_TEST_APPLE_CA_CERTIFICATE = 'UNZER_APPLE_CA_CERTIFICATE_PATH'; + private const ENV_VAR_NAME_TIMEOUT = 'UNZER_PAPI_TIMEOUT'; private const DEFAULT_TIMEOUT = 60; @@ -137,4 +140,24 @@ public static function getTestPublicKey($non3ds = false): string $key = stripslashes($_SERVER[$variableName] ?? ''); return empty($key) ? '' : $key; } + + /** + * Returns the apple merchant certificate path set via environment variable. + * + * @return string + */ + public static function getAppleMerchantCertificatePath(): string + { + return stripslashes($_SERVER[self::ENV_VAR_TEST_APPLE_MERCHANT_CERTIFICATE] ?? ''); + } + + /** + * Returns the CA certificate path set via environment variable. + * + * @return string + */ + public static function getAppleCaCertificatePath(): string + { + return stripslashes($_SERVER[self::ENV_VAR_TEST_APPLE_CA_CERTIFICATE] ?? ''); + } } diff --git a/test/integration/ApplepayAdapterTest.php b/test/integration/ApplepayAdapterTest.php new file mode 100644 index 00000000..98f002b7 --- /dev/null +++ b/test/integration/ApplepayAdapterTest.php @@ -0,0 +1,151 @@ + + * + * @package UnzerSDK + * + */ + +namespace UnzerSDK\test\integration; + +use UnzerSDK\Adapter\ApplepayAdapter; +use UnzerSDK\Exceptions\ApplepayMerchantValidationException; +use UnzerSDK\Resources\ExternalResources\ApplepaySession; +use UnzerSDK\Services\EnvironmentService; +use UnzerSDK\test\BaseIntegrationTest; + +class ApplepayAdapterTest extends BaseIntegrationTest +{ + private $merchantValidationUrl; + private $merchantValidationCertificatePath; + private $appleCaCertificatePath; + + public function domainShouldBeValidatedCorrectlyDP() + { + return [ + 'invalid: example.domain.com' => ['https://example.domain.com', false], + 'valid: https://apple-pay-gateway.apple.com/some/path' => ['https://apple-pay-gateway.apple.com/some/path', true], + 'valid: https://cn-apple-pay-gateway.apple.com' => ['https://cn-apple-pay-gateway.apple.com', true], + 'invalid: apple-pay-gateway-nc-pod1.apple.com' => ['apple-pay-gateway-nc-pod1.apple.com', false], + 'invalid: (empty)' => ['', false], + ]; + } + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->merchantValidationUrl = 'https://apple-pay-gateway-cert.apple.com/paymentservices/startSession'; + $this->merchantValidationCertificatePath = EnvironmentService::getAppleMerchantCertificatePath(); + $this->appleCaCertificatePath = EnvironmentService::getAppleCaCertificatePath(); + } + + /** + * test merchant validation request. + * + * @test + * + * @throws \UnzerSDK\Exceptions\ApplepayMerchantValidationException + */ + public function verifyMerchantValidationRequest(): void + { + $applepaySession = $this->createApplepaySession(); + $appleAdapter = new ApplepayAdapter(); + + $validationResponse = $appleAdapter->validateApplePayMerchant( + $this->merchantValidationUrl, + $applepaySession, + $this->merchantValidationCertificatePath, + $this->appleCaCertificatePath + ); + + $this->assertNotNull($validationResponse); + } + + /** + * test merchant validation request without ca certificate. + * + * @test + * + * @throws \UnzerSDK\Exceptions\ApplepayMerchantValidationException + */ + public function merchantValidationWorksWithoutCaCert(): void + { + $applepaySession = $this->createApplepaySession(); + $appleAdapter = new ApplepayAdapter(); + + $validationResponse = $appleAdapter->validateApplePayMerchant( + $this->merchantValidationUrl, + $applepaySession, + $this->merchantValidationCertificatePath + ); + + $this->assertNotNull($validationResponse); + } + + /** + * Merchant validation call should throw Exception if domain of Validation url is invalid. + * + * @test + * + */ + public function merchantValidationThrowsErrorForInvalidDomain(): void + { + $applepaySession = $this->createApplepaySession(); + $appleAdapter = new ApplepayAdapter(); + + $this->expectException(ApplepayMerchantValidationException::class); + $this->expectExceptionMessage('Invalid URL used for merchantValidation request.'); + + $appleAdapter->validateApplePayMerchant( + 'https://invalid.domain.com/some/path', + $applepaySession, + $this->merchantValidationCertificatePath + ); + } + + /** + * test merchant validation request without ca certificate. + * + * @dataProvider domainShouldBeValidatedCorrectlyDP + * @test + * + * @param mixed $validationUrl + * @param mixed $expectedResult + * + */ + public function domainShouldBeValidatedCorrectly($validationUrl, $expectedResult): void + { + $appleAdapter = new ApplepayAdapter(); + + $domainValidation = $appleAdapter->validMerchantValidationDomain($validationUrl); + $this->assertEquals($expectedResult, $domainValidation); + } + + /** + * @return ApplepaySession + */ + private function createApplepaySession(): ApplepaySession + { + return new ApplepaySession('merchantIdentifier', 'displayName', 'domainName'); + } +} diff --git a/test/unit/Adapter/ApplepaySessionTest.php b/test/unit/Adapter/ApplepaySessionTest.php new file mode 100644 index 00000000..84dfa01a --- /dev/null +++ b/test/unit/Adapter/ApplepaySessionTest.php @@ -0,0 +1,42 @@ + + * + * @package UnzerSDK + * + */ + +namespace UnzerSDK\test\unit\Adapter; + +use PHPUnit\Framework\TestCase; +use UnzerSDK\Resources\ExternalResources\ApplepaySession; + +class ApplepaySessionTest extends TestCase +{ + public function testJsonSerialize(): void + { + $applepaySession = new ApplepaySession('merchantIdentifier', 'displayName', 'domainName'); + $expectedJson = '{"merchantIdentifier": "merchantIdentifier", "displayName": "displayName", "domainName": "domainName"}'; + + $jsonSerialize = $applepaySession->jsonSerialize(); + $this->assertJsonStringEqualsJsonString($expectedJson, $jsonSerialize); + } +}