From 36a96c4bde8a70233e3a58e410b59a3018b0776e Mon Sep 17 00:00:00 2001 From: Marvin Muxfeld Date: Tue, 9 Jul 2024 13:38:34 +0200 Subject: [PATCH] PISHPS-328: added ApplePayValidationAllowList, subscriber to list default saleschannel urls, integrated allow list into applepay validate route --- .../ApplePayDirect/ApplePayDirect.php | 43 ++++++- ...tionUrlAllowListCanNotBeEmptyException.php | 12 ++ ...ayValidationUrlNotInAllowListException.php | 12 ++ .../ApplePayValidationUrlAllowListGateway.php | 47 ++++++++ .../ApplePayValidationUrlAllowListItem.php | 50 ++++++++ .../ApplePayValidationUrlAllowList.php | 69 +++++++++++ .../ApplePayValidationUrlSanitizer.php | 29 +++++ .../Subscriber/ApplePayConfigSubscriber.php | 110 ++++++++++++++++++ .../ApplePayDirectControllerBase.php | 1 + src/Resources/config/config.xml | 9 ++ src/Resources/config/services/components.xml | 16 +++ ...lePayValidationUrlAllowListGatewayTest.php | 36 ++++++ .../ApplePayValidationUrlAllowListTest.php | 41 +++++++ .../ApplePayValidationUrlSanitizerTest.php | 37 ++++++ .../ApplePayDirect/ApplePayDirectTest.php | 87 +++++++++++--- 15 files changed, 583 insertions(+), 16 deletions(-) create mode 100644 src/Components/ApplePayDirect/Exceptions/ApplePayValidationUrlAllowListCanNotBeEmptyException.php create mode 100644 src/Components/ApplePayDirect/Exceptions/ApplePayValidationUrlNotInAllowListException.php create mode 100644 src/Components/ApplePayDirect/Gateways/ApplePayValidationUrlAllowListGateway.php create mode 100644 src/Components/ApplePayDirect/Models/ApplePayValidationUrlAllowListItem.php create mode 100644 src/Components/ApplePayDirect/Models/Collections/ApplePayValidationUrlAllowList.php create mode 100644 src/Components/ApplePayDirect/Services/ApplePayValidationUrlSanitizer.php create mode 100644 src/Components/ApplePayDirect/Subscriber/ApplePayConfigSubscriber.php create mode 100644 tests/PHPUnit/Components/ApplePayDirect/Gateways/ApplePayValidationUrlAllowListGatewayTest.php create mode 100644 tests/PHPUnit/Components/ApplePayDirect/Models/Collections/ApplePayValidationUrlAllowListTest.php create mode 100644 tests/PHPUnit/Components/ApplePayDirect/Services/ApplePayValidationUrlSanitizerTest.php diff --git a/src/Components/ApplePayDirect/ApplePayDirect.php b/src/Components/ApplePayDirect/ApplePayDirect.php index 013768326..f02204b59 100644 --- a/src/Components/ApplePayDirect/ApplePayDirect.php +++ b/src/Components/ApplePayDirect/ApplePayDirect.php @@ -2,10 +2,14 @@ namespace Kiener\MolliePayments\Components\ApplePayDirect; +use Kiener\MolliePayments\Components\ApplePayDirect\Exceptions\ApplePayValidationUrlAllowListCanNotBeEmptyException; +use Kiener\MolliePayments\Components\ApplePayDirect\Exceptions\ApplePayValidationUrlNotInAllowListException; +use Kiener\MolliePayments\Components\ApplePayDirect\Gateways\ApplePayValidationUrlAllowListGateway; use Kiener\MolliePayments\Components\ApplePayDirect\Models\ApplePayCart; use Kiener\MolliePayments\Components\ApplePayDirect\Services\ApplePayDomainVerificationService; use Kiener\MolliePayments\Components\ApplePayDirect\Services\ApplePayFormatter; use Kiener\MolliePayments\Components\ApplePayDirect\Services\ApplePayShippingBuilder; +use Kiener\MolliePayments\Components\ApplePayDirect\Services\ApplePayValidationUrlSanitizer; use Kiener\MolliePayments\Facade\MolliePaymentDoPay; use Kiener\MolliePayments\Factory\MollieApiFactory; use Kiener\MolliePayments\Handler\Method\ApplePayPayment; @@ -105,6 +109,16 @@ class ApplePayDirect */ private $repoOrderAdresses; + /** + * @var ApplePayValidationUrlAllowListGateway + */ + private $validationUrlAllowListGateway; + + /** + * @var ApplePayValidationUrlSanitizer + */ + private $validationUrlSanitizer; + /** * @param ApplePayDomainVerificationService $domainFileDownloader @@ -121,8 +135,10 @@ class ApplePayDirect * @param ShopService $shopService * @param OrderService $orderService * @param OrderAddressRepositoryInterface $repoOrderAdresses + * @param ApplePayValidationUrlAllowListGateway $validationUrlAllowListGateway + * @param ApplePayValidationUrlSanitizer $validationUrlSanitizer */ - public function __construct(ApplePayDomainVerificationService $domainFileDownloader, ApplePayPayment $paymentHandler, MolliePaymentDoPay $molliePayments, CartServiceInterface $cartService, ApplePayFormatter $formatter, ApplePayShippingBuilder $shippingBuilder, SettingsService $pluginSettings, CustomerService $customerService, PaymentMethodRepository $repoPaymentMethods, CartBackupService $cartBackupService, MollieApiFactory $mollieApiFactory, ShopService $shopService, OrderService $orderService, OrderAddressRepositoryInterface $repoOrderAdresses) + public function __construct(ApplePayDomainVerificationService $domainFileDownloader, ApplePayPayment $paymentHandler, MolliePaymentDoPay $molliePayments, CartServiceInterface $cartService, ApplePayFormatter $formatter, ApplePayShippingBuilder $shippingBuilder, SettingsService $pluginSettings, CustomerService $customerService, PaymentMethodRepository $repoPaymentMethods, CartBackupService $cartBackupService, MollieApiFactory $mollieApiFactory, ShopService $shopService, OrderService $orderService, OrderAddressRepositoryInterface $repoOrderAdresses, ApplePayValidationUrlAllowListGateway $validationUrlAllowListGateway, ApplePayValidationUrlSanitizer $validationUrlSanitizer) { $this->domainFileDownloader = $domainFileDownloader; $this->paymentHandler = $paymentHandler; @@ -138,6 +154,8 @@ public function __construct(ApplePayDomainVerificationService $domainFileDownloa $this->shopService = $shopService; $this->orderService = $orderService; $this->repoOrderAdresses = $repoOrderAdresses; + $this->validationUrlAllowListGateway = $validationUrlAllowListGateway; + $this->validationUrlSanitizer = $validationUrlSanitizer; } @@ -473,6 +491,29 @@ public function createPayment(OrderEntity $order, string $shopwareReturnUrl, str return $paymentData->getMollieID(); } + /** + * @param string $validationUrl + * @throws ApplePayValidationUrlAllowListCanNotBeEmptyException + * @throws ApplePayValidationUrlNotInAllowListException + * @return string + */ + public function validateValidationUrl(string $validationUrl): string + { + $allowList = $this->validationUrlAllowListGateway->getAllowList(); + + if ($allowList->isEmpty()) { + throw new ApplePayValidationUrlAllowListCanNotBeEmptyException(); + } + + $validationUrl = $this->validationUrlSanitizer->sanitizeValidationUrl($validationUrl); + + if ($allowList->contains($validationUrl) === false) { + throw new ApplePayValidationUrlNotInAllowListException($validationUrl); + } + + return $validationUrl; + } + /** * @param Cart $cart * @return ApplePayCart diff --git a/src/Components/ApplePayDirect/Exceptions/ApplePayValidationUrlAllowListCanNotBeEmptyException.php b/src/Components/ApplePayDirect/Exceptions/ApplePayValidationUrlAllowListCanNotBeEmptyException.php new file mode 100644 index 000000000..c7a7ed58b --- /dev/null +++ b/src/Components/ApplePayDirect/Exceptions/ApplePayValidationUrlAllowListCanNotBeEmptyException.php @@ -0,0 +1,12 @@ +systemConfigService = $systemConfigService; + } + + /** + * Get the ApplePayValidationUrlAllowList + * + * @return ApplePayValidationUrlAllowList + */ + public function getAllowList(): ApplePayValidationUrlAllowList + { + $allowList = $this->systemConfigService->get('MolliePayments.config.ApplePayValidationAllowList'); + + if (is_string($allowList) === false || empty($allowList)) { + return ApplePayValidationUrlAllowList::create(); + } + + $allowList = trim($allowList); + + $items = explode(',', $allowList); + $items = array_map([ApplePayValidationUrlAllowListItem::class, 'create'], $items); + + return ApplePayValidationUrlAllowList::create(...$items); + } +} diff --git a/src/Components/ApplePayDirect/Models/ApplePayValidationUrlAllowListItem.php b/src/Components/ApplePayDirect/Models/ApplePayValidationUrlAllowListItem.php new file mode 100644 index 000000000..54f02ff9f --- /dev/null +++ b/src/Components/ApplePayDirect/Models/ApplePayValidationUrlAllowListItem.php @@ -0,0 +1,50 @@ +value = $value; + } + + public static function create(string $value): self + { + if (empty($value)) { + throw new \InvalidArgumentException(sprintf('The value of %s must not be empty', self::class)); + } + + if (strpos($value, 'http') !== 0) { + $value = 'https://' . $value; + } + + if (substr($value, -1) !== '/') { + $value .= '/'; + } + + return new self($value); + } + + /** + * Compare the value with the given value + * + * @param string $value value that will be compared + * @return bool + */ + public function equals(string $value): bool + { + return $this->value === $value; + } +} diff --git a/src/Components/ApplePayDirect/Models/Collections/ApplePayValidationUrlAllowList.php b/src/Components/ApplePayDirect/Models/Collections/ApplePayValidationUrlAllowList.php new file mode 100644 index 000000000..f7b049de7 --- /dev/null +++ b/src/Components/ApplePayDirect/Models/Collections/ApplePayValidationUrlAllowList.php @@ -0,0 +1,69 @@ +allowList = $allowList; + } + + /** + * Create a new ApplePayAllowList + * + * @param ApplePayValidationUrlAllowListItem ...$items + * @return ApplePayValidationUrlAllowList + */ + public static function create(ApplePayValidationUrlAllowListItem ...$items): self + { + return new self($items); + } + + /** + * Check if the given value is in the allow list + * + * @param string $value + * @return bool + */ + public function contains(string $value): bool + { + foreach ($this->allowList as $item) { + if ($item->equals($value)) { + return true; + } + } + + return false; + } + + /** + * @return bool + */ + public function isEmpty(): bool + { + return count($this) === 0; + } + + /** + * @inheritDoc + */ + public function count(): int + { + return count($this->allowList); + } +} diff --git a/src/Components/ApplePayDirect/Services/ApplePayValidationUrlSanitizer.php b/src/Components/ApplePayDirect/Services/ApplePayValidationUrlSanitizer.php new file mode 100644 index 000000000..2e4be7584 --- /dev/null +++ b/src/Components/ApplePayDirect/Services/ApplePayValidationUrlSanitizer.php @@ -0,0 +1,29 @@ +systemConfigService = $systemConfigService; + $this->salesChannelDomainRepository = $salesChannelDomainRepository; + } + + /** + * Registers the events the subscriber listens to. + */ + public static function getSubscribedEvents() + { + return [ + PluginPostInstallEvent::class => 'setDefaultConfig', + PluginPostUpdateEvent::class => 'setDefaultConfig', + ]; + } + + /** + * Sets the default configuration for Apple Pay validation allow list. + * + * This method retrieves all sales channel domains and constructs a comma-separated + * list of their URLs. It then sets this list as the default value for the + * Apple Pay validation allow list configuration. + * + * @return void + */ + public function setDefaultConfig(): void + { + // Check if the Apple Pay validation allow list is already set. + if ($this->validationAllowListIsEmpty() === false) { + return; // Ensuring to not overwrite the existing configuration. + } + + // Create a new context with a system source. + $context = new Context(new SystemSource()); + + // Define criteria to fetch sales channel domains. + $criteria = new Criteria(); + $criteria->addAssociation('salesChannel'); + + // Fetch sales channel domains using the repository. + $domains = $this->salesChannelDomainRepository->search($criteria, $context); + $usedDomains = []; + + foreach ($domains as $domain) { + if (!$domain instanceof SalesChannelDomainEntity) { + continue; + } + $usedDomains[] = $domain->getUrl(); + } + + if (count($usedDomains)) { + // Convert the array of URLs to a comma-separated string. + $usedDomainsString = implode(',', $usedDomains); + + // Set the configuration value for the Apple Pay validation allow list. + $this->systemConfigService->set('MolliePayments.config.ApplePayValidationAllowList', $usedDomainsString); + } + } + + /** + * Checks if the Apple Pay validation allow list is empty. + * + * @return bool + */ + private function validationAllowListIsEmpty(): bool + { + $allowList = $this->systemConfigService->get('MolliePayments.config.ApplePayValidationAllowList'); + return empty($allowList); + } +} diff --git a/src/Controller/StoreApi/ApplePayDirect/ApplePayDirectControllerBase.php b/src/Controller/StoreApi/ApplePayDirect/ApplePayDirectControllerBase.php index a12b367b0..6299b3c35 100644 --- a/src/Controller/StoreApi/ApplePayDirect/ApplePayDirectControllerBase.php +++ b/src/Controller/StoreApi/ApplePayDirect/ApplePayDirectControllerBase.php @@ -117,6 +117,7 @@ public function createPaymentSession(RequestDataBag $data, SalesChannelContext $ throw new \Exception('Please provide a validation url!'); } + $validationURL = $this->applePay->validateValidationUrl($validationURL); $session = $this->applePay->createPaymentSession($validationURL, $context); return new CreateSessionResponse($session); diff --git a/src/Resources/config/config.xml b/src/Resources/config/config.xml index 2184638f7..a9a64eea5 100644 --- a/src/Resources/config/config.xml +++ b/src/Resources/config/config.xml @@ -152,6 +152,15 @@ + + ApplePayValidationAllowList + + + + Enter the domains that are allowed for Apple Pay validation, separated by commas. + Geben Sie die Domains ein, die für die Apple Pay Validierung zugelassen sind, getrennt durch Kommas. + Voer de domeinen in die zijn toegestaan voor Apple Pay validatie, gescheiden door komma's. + createCustomersAtMollie diff --git a/src/Resources/config/services/components.xml b/src/Resources/config/services/components.xml index 2d20b69df..88d7a4ae9 100644 --- a/src/Resources/config/services/components.xml +++ b/src/Resources/config/services/components.xml @@ -5,6 +5,13 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + + + + + + @@ -39,6 +46,8 @@ + + @@ -47,6 +56,13 @@ + + + + + + + diff --git a/tests/PHPUnit/Components/ApplePayDirect/Gateways/ApplePayValidationUrlAllowListGatewayTest.php b/tests/PHPUnit/Components/ApplePayDirect/Gateways/ApplePayValidationUrlAllowListGatewayTest.php new file mode 100644 index 000000000..345da8849 --- /dev/null +++ b/tests/PHPUnit/Components/ApplePayDirect/Gateways/ApplePayValidationUrlAllowListGatewayTest.php @@ -0,0 +1,36 @@ +service = $this->createMock(SystemConfigService::class); + $this->gateway = new ApplePayValidationUrlAllowListGateway($this->service); + } + + public function testProvidesEmptyAllowList(): void + { + $this->service->expects($this->once())->method('get')->willReturn(''); + $allowList = $this->gateway->getAllowList(); + + $this->assertTrue($allowList->isEmpty()); + } + + public function testProvidesAllowList(): void + { + $allowListString = 'https://example.com,https://example-url.org'; + $this->service->expects($this->once())->method('get')->willReturn($allowListString); + $allowList = $this->gateway->getAllowList(); + + $this->assertFalse($allowList->isEmpty()); + $this->assertCount(2, $allowList); + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Components/ApplePayDirect/Models/Collections/ApplePayValidationUrlAllowListTest.php b/tests/PHPUnit/Components/ApplePayDirect/Models/Collections/ApplePayValidationUrlAllowListTest.php new file mode 100644 index 000000000..4653c11e9 --- /dev/null +++ b/tests/PHPUnit/Components/ApplePayDirect/Models/Collections/ApplePayValidationUrlAllowListTest.php @@ -0,0 +1,41 @@ +assertTrue($allowList->contains('https://example.com')); + $this->assertTrue($allowList->contains('https://example-url.org')); + $this->assertFalse($allowList->contains('https://example-url.net')); + } + + public function canDetermineIfListIsEmpty(): void + { + $allowList = ApplePayValidationUrlAllowList::create(); + + $this->assertTrue($allowList->isEmpty()); + } + + public function testProvidesCount(): void + { + $allowList = ApplePayValidationUrlAllowList::create( + ApplePayValidationUrlAllowListItem::create('https://example.com'), + ApplePayValidationUrlAllowListItem::create('https://example-url.org') + ); + + $this->assertCount(2, $allowList); + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Components/ApplePayDirect/Services/ApplePayValidationUrlSanitizerTest.php b/tests/PHPUnit/Components/ApplePayDirect/Services/ApplePayValidationUrlSanitizerTest.php new file mode 100644 index 000000000..a6b72193d --- /dev/null +++ b/tests/PHPUnit/Components/ApplePayDirect/Services/ApplePayValidationUrlSanitizerTest.php @@ -0,0 +1,37 @@ +sanitizer = new ApplePayValidationUrlSanitizer(); + } + + /** + * @dataProvider sanitationTestDataProvider + */ + public function testProvidesSanitizedUrl(string $url, string $expected): void + { + $sanitizedUrl = $this->sanitizer->sanitizeValidationUrl($url); + + $this->assertEquals($expected, $sanitizedUrl); + } + + public function sanitationTestDataProvider(): array + { + return [ + 'keeps http value if provided' => ['http://example.com', 'http://example.com/'], + 'keeps https value if provided' => ['https://example.com', 'https://example.com/'], + 'adds https to beginning of string if missing' => ['example.com', 'https://example.com/'], + 'adds trailing slash if missing' => ['https://example.com', 'https://example.com/'], + 'adds https to beginning of string and trailing slash if missing' => ['example.com', 'https://example.com/'], + ]; + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Service/ApplePayDirect/ApplePayDirectTest.php b/tests/PHPUnit/Service/ApplePayDirect/ApplePayDirectTest.php index 45fbc9fe1..364997d0d 100644 --- a/tests/PHPUnit/Service/ApplePayDirect/ApplePayDirectTest.php +++ b/tests/PHPUnit/Service/ApplePayDirect/ApplePayDirectTest.php @@ -3,9 +3,15 @@ namespace Kiener\MolliePayments\Tests\Service\ApplePayDirect; use Kiener\MolliePayments\Components\ApplePayDirect\ApplePayDirect; +use Kiener\MolliePayments\Components\ApplePayDirect\Exceptions\ApplePayValidationUrlAllowListCanNotBeEmptyException; +use Kiener\MolliePayments\Components\ApplePayDirect\Exceptions\ApplePayValidationUrlNotInAllowListException; +use Kiener\MolliePayments\Components\ApplePayDirect\Gateways\ApplePayValidationUrlAllowListGateway; +use Kiener\MolliePayments\Components\ApplePayDirect\Models\ApplePayValidationUrlAllowListItem; +use Kiener\MolliePayments\Components\ApplePayDirect\Models\Collections\ApplePayValidationUrlAllowList; use Kiener\MolliePayments\Components\ApplePayDirect\Services\ApplePayDomainVerificationService; use Kiener\MolliePayments\Components\ApplePayDirect\Services\ApplePayFormatter; use Kiener\MolliePayments\Components\ApplePayDirect\Services\ApplePayShippingBuilder; +use Kiener\MolliePayments\Components\ApplePayDirect\Services\ApplePayValidationUrlSanitizer; use Kiener\MolliePayments\Facade\MolliePaymentDoPay; use Kiener\MolliePayments\Factory\MollieApiFactory; use Kiener\MolliePayments\Handler\Method\ApplePayPayment; @@ -41,22 +47,18 @@ class ApplePayDirectTest extends TestCase { use MockTrait; - /** - * This test verifies that our Apple Pay Cart is correctly - * built from a provided Shopware Cart object. - */ - public function testBuildApplePayCart(): void - { - $swCart = $this->buildShopwareCart(); + private SalesChannelContext $scContext; - /** @var SalesChannelContext $scContext */ - $scContext = $this->createDummyMock(SalesChannelContext::class, $this); + private $validationUrlAllowListGateway; + private ApplePayDirect $applePay; + protected function setUp(): void + { + $swCart = $this->buildShopwareCart(); - $fakeCartService = new FakeCartService($swCart, $scContext); + $this->scContext = $this->createDummyMock(SalesChannelContext::class, $this); - /** @var ShippingMethodService $shippingMethodService */ - $shippingMethodService = $this->createDummyMock(ShippingMethodService::class, $this); + $fakeCartService = new FakeCartService($swCart, $this->scContext); /** @var ApplePayDomainVerificationService $domainVerification */ $domainVerification = $this->createDummyMock(ApplePayDomainVerificationService::class, $this); @@ -97,8 +99,11 @@ public function testBuildApplePayCart(): void /** @var OrderAddressRepository $repoOrderAdresses */ $repoOrderAdresses = $this->createDummyMock(OrderAddressRepository::class, $this); + $this->validationUrlAllowListGateway = $this->createDummyMock(ApplePayValidationUrlAllowListGateway::class, $this); - $applePay = new ApplePayDirect( + $validationUrlSanitizer = new ApplePayValidationUrlSanitizer(); + + $this->applePay = new ApplePayDirect( $domainVerification, $payment, $doPay, @@ -112,10 +117,19 @@ public function testBuildApplePayCart(): void $apiFactory, $shopService, $orderService, - $repoOrderAdresses + $repoOrderAdresses, + $this->validationUrlAllowListGateway, + $validationUrlSanitizer ); + } - $apCart = $applePay->getCart($scContext); + /** + * This test verifies that our Apple Pay Cart is correctly + * built from a provided Shopware Cart object. + */ + public function testBuildApplePayCart(): void + { + $apCart = $this->applePay->getCart($this->scContext); $this->assertEquals(34.99, $apCart->getAmount()); $this->assertEquals(5, $apCart->getTaxes()->getPrice()); @@ -131,6 +145,49 @@ public function testBuildApplePayCart(): void $this->assertEquals(1, $apCart->getShippings()[0]->getQuantity()); } + public function testThrowsExceptionWhenAllowListIsEmpty(): void + { + $this->validationUrlAllowListGateway->expects($this->once()) + ->method('getAllowList') + ->willReturn(ApplePayValidationUrlAllowList::create()); + + $this->expectException(ApplePayValidationUrlAllowListCanNotBeEmptyException::class); + $this->expectExceptionMessage('The Apple Pay validation URL allow list can not be empty. Please check the configuration.'); + + $this->applePay->validateValidationUrl('https://example.com'); + } + + public function testThrowsExceptionWhenUrlIsNotInAllowList(): void + { + $allowList = ApplePayValidationUrlAllowList::create( + ApplePayValidationUrlAllowListItem::create('https://example.com/') + ); + $this->validationUrlAllowListGateway->expects($this->once()) + ->method('getAllowList') + ->willReturn($allowList); + + $testUrl = 'https://example.org/'; + + $this->expectException(ApplePayValidationUrlNotInAllowListException::class); + $this->expectExceptionMessage(sprintf('The given URL %s is not in the Apple Pay validation URL allow list.', $testUrl)); + + $this->applePay->validateValidationUrl($testUrl); + } + + public function testProvidesValidValidationUrl(): void + { + $allowList = ApplePayValidationUrlAllowList::create( + ApplePayValidationUrlAllowListItem::create($expected = 'https://example.com/') + ); + + $this->validationUrlAllowListGateway->expects($this->once()) + ->method('getAllowList') + ->willReturn($allowList); + + $actual = $this->applePay->validateValidationUrl($expected); + + $this->assertSame($expected, $actual); + } /** * @return Cart