diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index efef7f1..9222224 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -161,10 +161,26 @@ jobs: name: Validate database schema run: (cd tests/Application && bin/console doctrine:schema:validate) - - - name: Run PHPSpec + - name: Run PHPStan + run: vendor/bin/phpstan analyse -c phpstan.neon -l 8 src/ + + - name: Run ECS + run: vendor/bin/ecs check src + + - name: Run PHPSpec run: vendor/bin/phpspec run --ansi -f progress --no-interaction - - - name: Run PHPUnit - run: vendor/bin/phpunit --colors=always + - name: Load fixtures in test application + run: (cd tests/Application && bin/console sylius:fixtures:load -n) + + - name: Failed build Slack notification + uses: rtCamp/action-slack-notify@v2 + if: ${{ failure() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') }} + env: + SLACK_CHANNEL: ${{ secrets.FAILED_BUILD_SLACK_CHANNEL }} + SLACK_COLOR: ${{ job.status }} + SLACK_ICON: https://github.com/rtCamp.png?size=48 + SLACK_MESSAGE: ':x:' + SLACK_TITLE: Failed build on ${{ github.event.repository.name }} repository + SLACK_USERNAME: ${{ secrets.FAILED_BUILD_SLACK_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.FAILED_BUILD_SLACK_WEBHOOK }} diff --git a/.gitignore b/.gitignore index 5cefda9..986c41a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,8 @@ # Symfony CLI https://symfony.com/doc/current/setup/symfony_server.html#different-php-settings-per-project /.php-version /php.ini + +###> friendsofphp/php-cs-fixer ### +/.php-cs-fixer.php +/.php-cs-fixer.cache +###< friendsofphp/php-cs-fixer ### diff --git a/composer.json b/composer.json index edae82a..6ef64b8 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "license": "MIT", "require": { "php": "^8.1", + "doctrine/annotations": "^1.14", "sylius/sylius": ">=1.12.13 || ~1.13.0", "sylius/mailer-bundle": "^1.8 || ^2.0@beta", "symfony/webpack-encore-bundle": "^1.15" @@ -16,6 +17,7 @@ "require-dev": { "behat/behat": "^3.6.1", "behat/mink-selenium2-driver": "^1.4", + "bitbag/coding-standard": "^3.0", "dmore/behat-chrome-extension": "^1.3", "dmore/chrome-mink-driver": "^2.7", "friends-of-behat/mink": "^1.8", @@ -26,9 +28,9 @@ "friends-of-behat/suite-settings-extension": "^1.0", "friends-of-behat/symfony-extension": "^2.1", "friends-of-behat/variadic-extension": "^1.3", - "phpspec/phpspec": "^7.2", + "phpspec/phpspec": "^7.5", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.8.1", + "phpstan/phpstan": "^1.10", "phpstan/phpstan-doctrine": "1.3.40", "phpstan/phpstan-strict-rules": "^1.3.0", "phpstan/phpstan-webmozart-assert": "^1.2.0", diff --git a/phpstan.neon b/phpstan.neon index 2235744..3256831 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,7 @@ parameters: level: max reportUnmatchedIgnoredErrors: false checkMissingIterableValueType: false + checkGenericClassInNonGenericObjectType: false paths: - src - tests/Behat @@ -16,3 +17,4 @@ parameters: ignoreErrors: - '/Parameter #1 \$configuration of method Symfony\\Component\\DependencyInjection\\Extension\\Extension::processConfiguration\(\) expects Symfony\\Component\\Config\\Definition\\ConfigurationInterface, Symfony\\Component\\Config\\Definition\\ConfigurationInterface\|null given\./' + - '#Property .+::\$requestStack is never read, only written\.#' diff --git a/spec/Action/CaptureActionSpec.php b/spec/Action/CaptureActionSpec.php new file mode 100644 index 0000000..b33bf3e --- /dev/null +++ b/spec/Action/CaptureActionSpec.php @@ -0,0 +1,117 @@ +beConstructedWith($signatureResolver); + } + + public function it_should_return_true_when_request_and_model_is_valid( + Capture $request, + ArrayObject $arrayObject, + ): void { + $request->getModel()->willReturn($arrayObject); + + $this->supports($request)->shouldReturn(true); + } + + public function it_should_return_false_when_model_is_invalid( + Capture $request, + ): void { + $request->getModel()->willReturn(null); + + $this->supports($request)->shouldReturn(false); + } + + public function it_should_return_false_when_request_is_not_a_capture_instance( + Request $request, + ): void { + $this->supports($request)->shouldReturn(false); + } + + public function it_executes_correctly_with_valid_request( + Capture $request, + OrderInterface $order, + PaymentInterface $payment, + PaymentSecurityTokenInterface $token, + AddressInterface $address, + CustomerInterface $customer, + ImojeApi $apiClass, + SignatureResolverInterface $signatureResolver, + ): void { + $data = ['statusImoje' => ImojeApiInterface::NEW_STATUS, 'paymentId' => 123, 'tokenHash' => '1234sdcsdfxz']; + $request->getModel()->willReturn(new ArrayObject($data)); + + $request->getToken()->willReturn('1234sdcsdfxz'); + $request->getFirstModel()->willReturn($payment); + $request->getToken()->willReturn($token); + + $payment->getOrder()->willReturn($order); + $payment->getId()->willReturn(123); + + $token->getHash()->willReturn('1234sdcsdfxz'); + + $apiClass->getServiceKey()->willReturn('1234sdcsdfxz'); + $order->getBillingAddress()->willReturn($address); + $order->getCustomer()->willReturn($customer); + + $apiClass->getApiUrl()->willReturn('http://example.com/'); + + $apiClass->getServiceId()->willReturn('1'); + $apiClass->getMerchantId()->willReturn('1'); + $order->getTotal()->willReturn(1000); + $order->getCurrencyCode()->willReturn('EUR'); + $order->getNumber()->willReturn('123'); + $address->getFirstName()->willReturn('John Doe'); + $address->getLastName()->willReturn('Smith'); + $token->getAfterUrl()->willReturn('http://example.com/'); + $customer->getEmail()->willReturn('john@doe.com'); + + $orderData = [ + 'serviceId' => '1', + 'merchantId' => '1', + 'amount' => 1000, + 'currency' => 'EUR', + 'orderId' => '123', + 'customerFirstName' => 'John Doe', + 'customerLastName' => 'Smith', + 'urlReturn' => 'http://example.com/', + 'customerEmail' => 'john@doe.com', + ]; + + $signatureResolver->createSignature($orderData, '1234sdcsdfxz') + ->willReturn('signature'); + + $request->setModel(new ArrayObject($data))->shouldBeCalled(); + + $this->setApi($apiClass); + $this->shouldThrow(HttpPostRedirect::class) + ->during('execute', [$request]); + } +} diff --git a/spec/Action/ConvertPaymentActionSpec.php b/spec/Action/ConvertPaymentActionSpec.php new file mode 100644 index 0000000..1883e8b --- /dev/null +++ b/spec/Action/ConvertPaymentActionSpec.php @@ -0,0 +1,108 @@ +shouldHaveType(ConvertPaymentAction::class); + } + + function it_implements_imoje_gateway_factory_interface(): void + { + $this->shouldHaveType(ActionInterface::class); + } + + public function it_sets_result_from_payment_details_with_non_empty_details( + Convert $request, + PaymentInterface $payment, + ): void { + $request->getSource()->willReturn($payment); + $payment->getDetails()->willReturn(['field' => '123']); + $request->setResult(['field' => '123'])->shouldBeCalled(); + + $this->execute($request); + } + + public function it_sets_empty_result_when_payment_details_are_empty( + Convert $request, + PaymentInterface $payment, + ): void { + $payment->getDetails()->willReturn([]); + $request->getSource()->willReturn($payment); + $request->setResult([])->shouldBeCalled(); + + $this->execute($request); + } + + public function it_sets_result_when_payment_details_contain_null( + Convert $request, + PaymentInterface $payment, + ): void { + $payment->getDetails()->willReturn(['key' => null]); + $request->getSource()->willReturn($payment); + $request->setResult(['key' => null])->shouldBeCalled(); + + $this->execute($request); + } + + public function it_should_return_true_when_getTo_and_source_is_valid( + Convert $request, + PaymentInterface $payment, + ): void { + $request->getSource()->willReturn($payment); + $request->getTo()->willReturn('array'); + + $this->supports($request)->shouldReturn(true); + } + + public function it_should_return_false_when_source_is_invalid( + Convert $request, + ): void { + $request->getSource()->willReturn(null); + $request->getTo()->willReturn('array'); + + $this->supports($request)->shouldReturn(false); + } + + public function it_should_return_false_when_getTo_is_invalid( + Convert $request, + PaymentInterface $payment, + ): void { + $request->getSource()->willReturn($payment); + $request->getTo()->willReturn('object'); + + $this->supports($request)->shouldReturn(false); + } + + public function it_should_return_false_when_getTo_and_source_is_invalid( + Convert $request, + ): void { + $request->getSource()->willReturn(null); + $request->getTo()->willReturn('object'); + + $this->supports($request)->shouldReturn(false); + } + + public function it_should_return_false_when_request_invalid( + Request $request, + ): void { + $this->supports($request)->shouldReturn(false); + } +} diff --git a/spec/Action/NotifyActionSpec.php b/spec/Action/NotifyActionSpec.php new file mode 100644 index 0000000..246c315 --- /dev/null +++ b/spec/Action/NotifyActionSpec.php @@ -0,0 +1,124 @@ +beConstructedWith( + $requestStack, + $signatureResolver, + ); + } + + public function it_should_return_true_when_request_and_request_data_is_valid( + Notify $request, + ArrayObject $arrayObject, + SignatureResolverInterface $signatureResolver, + ImojeApi $api, + Request $httpRequest, + RequestStack $requestStack, + ): void { + $request->getModel()->willReturn($arrayObject); + $requestStack->getCurrentRequest() + ->willReturn($httpRequest); + $api->getServiceKey()->willReturn('1234sdcsdfxz'); + $signatureResolver->verifySignature($httpRequest, '1234sdcsdfxz') + ->willReturn(true); + + $this->setApi($api); + $this->supports($request)->shouldReturn(true); + } + + public function it_should_return_false_when_request_and_signature_are_invalid( + Notify $request, + ArrayObject $arrayObject, + SignatureResolverInterface $signatureResolver, + ImojeApi $api, + Request $httpRequest, + RequestStack $requestStack, + ): void { + $request->getModel()->willReturn($arrayObject); + $requestStack->getCurrentRequest()->willReturn($httpRequest); + $api->getServiceKey()->willReturn('1234sdcsdfxz'); + $signatureResolver->verifySignature($httpRequest, '1234sdcsdfxz') + ->willReturn(false); + + $this->setApi($api); + $this->supports($request)->shouldReturn(false); + } + + public function it_should_return_false_when_request_is_empty( + Notify $request, + ImojeApi $api, + RequestStack $requestStack, + Request $httpRequest, + ): void { + $request->getModel()->willReturn(null); + $requestStack->getCurrentRequest()->willReturn($httpRequest); + $api->getServiceKey()->willReturn(null); + + $this->supports($request)->shouldReturn(false); + } + + public function it_should_return_false_when_request_is_invalid_and( + Notify $request, + RequestStack $requestStack, + Request $httpRequest, + ): void { + $request->getModel()->willReturn(null); + $requestStack->getCurrentRequest()->willReturn($httpRequest); + + $this->supports($httpRequest)->shouldReturn(false); + } + + public function it_sets_model_status_from_notification_data( + Notify $request, + ArrayObject $arrayObject, + SignatureResolverInterface $signatureResolver, + ImojeApi $api, + Request $httpRequest, + RequestStack $requestStack, + ): void { + $requestStack->getCurrentRequest()->willReturn($httpRequest); + $api->getServiceKey()->willReturn('1234sdcsdfxz'); + $signatureResolver->verifySignature($httpRequest, '1234sdcsdfxz') + ->willReturn(true); + $notificationData = ['transaction' => ['status' => 'new', 'paymentId' => 1, 'tokenHash' => '1234sdcsdfxz']]; + $jsonNotificationData = json_encode($notificationData); + $httpRequest->getContent()->willReturn($jsonNotificationData); + $request->getModel()->willReturn(new ArrayObject([ + 'status' => 'new', + 'paymentId' => 1, + 'tokenHash' => '1234sdcsdfxz', + ])); + $request->setModel(new ArrayObject([ + 'status' => 'new', + 'paymentId' => 1, + 'tokenHash' => '1234sdcsdfxz', + 'statusImoje' => 'new', + ]))->willReturn($arrayObject); + + $this->setApi($api); + $this->execute($request); + } +} diff --git a/spec/Action/StatusActionSpec.php b/spec/Action/StatusActionSpec.php new file mode 100644 index 0000000..0cf0f2d --- /dev/null +++ b/spec/Action/StatusActionSpec.php @@ -0,0 +1,135 @@ +shouldHaveType(StatusAction::class); + } + + public function it_should_implement_interface(): void + { + $this->shouldImplement(ActionInterface::class); + } + + public function it_should_return_new_status( + GetStatusInterface $request, + ): void { + $data = ['statusImoje' => ImojeApiInterface::NEW_STATUS, 'paymentId' => 1]; + + $request->getModel()->willReturn(new ArrayCollection($data)); + + $request->markNew()->shouldBeCalled(); + + $this->execute($request); + } + + public function it_should_return_pending_status( + GetStatusInterface $request, + ): void { + $data = ['statusImoje' => ImojeApiInterface::PENDING_STATUS, 'paymentId' => 1]; + + $request->getModel()->willReturn(new ArrayCollection($data)); + $request->markPending()->shouldBeCalled(); + + $this->execute($request); + } + + public function it_should_return_cancelled_status( + GetStatusInterface $request, + ): void { + $data = ['statusImoje' => ImojeApiInterface::CANCELLED_STATUS, 'paymentId' => 1, 'tokenHash' => 'dfgdsgxcvxcerf234']; + + $request->getModel()->willReturn(new ArrayCollection($data)); + $request->markCanceled()->shouldBeCalled(); + $data['tokenHash'] = ''; + + $request->setModel(new ArrayCollection($data))->shouldBeCalled(); + + $this->execute($request); + } + + public function it_should_return_rejected_status( + GetStatusInterface $request, + ): void { + $data = ['statusImoje' => ImojeApiInterface::REJECTED_STATUS, 'paymentId' => 1, 'tokenHash' => 'dfgdsgxcvxcerf234']; + $request->getModel()->willReturn(new ArrayCollection($data)); + $request->markFailed()->shouldBeCalled(); + $data['tokenHash'] = ''; + + $request->setModel(new ArrayCollection($data))->shouldBeCalled(); + + $this->execute($request); + } + + public function it_should_return_settled_status( + GetStatusInterface $request, + ): void { + $data = ['statusImoje' => ImojeApiInterface::SETTLED_STATUS, 'paymentId' => 1, 'tokenHash' => 'dfgdsgxcvxcerf234']; + $request->getModel()->willReturn(new ArrayCollection($data)); + + $request->markCaptured()->shouldBeCalled(); + + $this->execute($request); + } + + public function it_should_return_unknown_status( + GetStatusInterface $request, + ): void { + $data = ['statusImoje' => 'test', 'paymentId' => 1, 'tokenHash' => 'dfgdsgxcvxcerf234']; + $request->getModel()->willReturn(new ArrayCollection($data)); + + $request->markUnknown()->shouldBeCalled(); + + $this->execute($request); + } + + function it_throws_exception_if_request_not_supported( + GetStatusInterface $request, + ): void { + $this->shouldThrow(RequestNotSupportedException::class)->during('execute', [$request]); + } + + function it_returns_true_if_request_is_valid( + GetStatusInterface $request, + ArrayAccess $model, + ): void { + $request->getModel()->willReturn($model); + + $this->supports($request)->shouldBe(true); + } + + function it_returns_false_if_request_model_is_empty( + GetStatusInterface $request, + ): void { + $request->getModel()->willReturn(null); + + $this->supports($request)->shouldBe(false); + } + + function it_returns_false_if_request_class_not_instanceof_GetStatusInterface( + Request $request, + ): void { + $this->supports($request)->shouldBe(false); + } +} diff --git a/spec/ImojeGatewayFactorySpec.php b/spec/ImojeGatewayFactorySpec.php new file mode 100644 index 0000000..0a1c73d --- /dev/null +++ b/spec/ImojeGatewayFactorySpec.php @@ -0,0 +1,28 @@ +shouldHaveType(ImojeGatewayFactory::class); + } + + function it_implements_imoje_gateway_factory_interface(): void + { + $this->shouldHaveType(GatewayFactoryInterface::class); + } +} diff --git a/spec/Provider/PaymentTokenProviderSpec.php b/spec/Provider/PaymentTokenProviderSpec.php new file mode 100644 index 0000000..f8b2dd7 --- /dev/null +++ b/spec/Provider/PaymentTokenProviderSpec.php @@ -0,0 +1,93 @@ +beConstructedWith($orderRepository, $paymentTokenRepository); + } + + public function it_is_initializable(): void + { + $this->shouldHaveType(PaymentTokenProvider::class); + } + + public function it_should_return_token_correctly( + Request $request, + OrderInterface $order, + PaymentSecurityTokenInterface $token, + PaymentInterface $payment, + RepositoryInterface $orderRepository, + RepositoryInterface $paymentTokenRepository, + ): void { + $orderNumber = 500; + $tokenHash = '3423423453fsxzc'; + + $transactionData = [ + 'transaction' => [ + 'orderId' => $orderNumber, + 'tokenHash' => $tokenHash, + ], + ]; + + $request->getContent()->willReturn(json_encode($transactionData)); + $orderRepository->findOneBy(['number' => $orderNumber])->willReturn($order); + $order->getPayments() + ->willReturn(new ArrayCollection([ + $payment->getWrappedObject(), + ])); + $payment->getState()->willReturn(PaymentInterface::STATE_NEW); + $payment->getDetails()->willReturn(['tokenHash' => $tokenHash]); + $paymentTokenRepository->findOneBy(['hash' => $tokenHash])->willReturn($token); + + $this->provideToken($request)->shouldReturn($token); + } + + public function it_should_return_null_if_paymentTokenRepository_is_not_called( + Request $request, + OrderInterface $order, + PaymentInterface $payment, + RepositoryInterface $orderRepository, + ): void { + $orderNumber = 500; + $tokenHash = '3423423453fsxzc'; + $transactionData = [ + 'transaction' => [ + 'orderId' => $orderNumber, + 'tokenHash' => $tokenHash, + ], + ]; + $request->getContent()->willReturn(json_encode($transactionData)); + $orderRepository->findOneBy(['number' => $orderNumber]) + ->willReturn($order); + $order->getPayments() + ->willReturn(new ArrayCollection([ + $payment->getWrappedObject(), + ])); + $payment->getState()->willReturn(PaymentInterface::STATE_CANCELLED); + $payment->getDetails()->willReturn(['tokenHash' => $tokenHash]); + + $this->provideToken($request)->shouldReturn(null); + } +} diff --git a/spec/Resolver/SignatureResolverSpec.php b/spec/Resolver/SignatureResolverSpec.php new file mode 100644 index 0000000..02ad24c --- /dev/null +++ b/spec/Resolver/SignatureResolverSpec.php @@ -0,0 +1,95 @@ +shouldHaveType(SignatureResolver::class); + } + + public function it_should_sort_fields_and_build_data_string(): void + { + $fields = [ + 'field1' => 'value1', + 'field2' => 'value2', + ]; + $serviceKey = 'adasvcx3412'; + $expectedDataString = 'field1=value1&field2=value2'; + $expectedHash = hash(ImojeApiInterface::HASHING_ALGORITHM, $expectedDataString . $serviceKey) . ';' . ImojeApiInterface::HASHING_ALGORITHM; + + $this->createSignature($fields, $serviceKey)->shouldReturn($expectedHash); + } + + public function it_should_return_hash_with_service_key_only_when_fields_are_empty(): void + { + $fields = []; + $serviceKey = 'adasvcx3412'; + $expectedHash = hash(ImojeApiInterface::HASHING_ALGORITHM, $serviceKey) . ';' . ImojeApiInterface::HASHING_ALGORITHM; + + $this->createSignature($fields, $serviceKey)->shouldReturn($expectedHash); + } + + public function it_should_return_hash_without_service_key_when_service_key_is_empty(): void + { + $fields = [ + 'field1' => 'value1', + 'field2' => 'value2', + ]; + $serviceKey = ''; + $expectedDataString = 'field1=value1&field2=value2'; + $expectedHash = hash(ImojeApiInterface::HASHING_ALGORITHM, $expectedDataString) . ';' . ImojeApiInterface::HASHING_ALGORITHM; + + $this->createSignature($fields, $serviceKey)->shouldReturn($expectedHash); + } + + public function it_should_return_true_if_signatures_match( + Request $request, + ): void { + $serviceKey = 'adasvcx3412'; + $body = 'test.jpg'; + $exampleHash = hash('sha256', sprintf('test.jpg%s', $serviceKey)); + $headerSignature = sprintf('alg=sha256;signature=%s', $exampleHash); + $request->getContent()->willReturn($body); + + $request->headers = new \Symfony\Component\HttpFoundation\HeaderBag(['X-Imoje-Signature' => $headerSignature]); + + $this->verifySignature($request, $serviceKey)->shouldBe(true); + } + + public function it_should_return_false_if_signatures_not_match( + Request $request, + ): void { + $serviceKey = 'adasvcx3412'; + $body = 'test2.jpg'; + $exampleHash = hash('sha256', sprintf('test.jpg%s', $serviceKey)); + $headerSignature = sprintf('alg=sha256;signature=%s', $exampleHash); + $request->getContent()->willReturn($body); + + $request->headers = new \Symfony\Component\HttpFoundation\HeaderBag(['X-Imoje-Signature' => $headerSignature]); + + $this->verifySignature($request, $serviceKey)->shouldBe(false); + } + + public function it_should_return_false_if_content_is_empty( + Request $request, + ): void { + $serviceKey = ''; + $body = ''; + $exampleHash = ''; + $headerSignature = sprintf('alg=sha256;signature=%s', $exampleHash); + $request->getContent()->willReturn($body); + + $request->headers = new \Symfony\Component\HttpFoundation\HeaderBag(['X-Imoje-Signature' => $headerSignature]); + + $this->verifySignature($request, $serviceKey)->shouldBe(false); + } +} diff --git a/src/Action/CaptureAction.php b/src/Action/CaptureAction.php index a791b2a..70b1f4d 100644 --- a/src/Action/CaptureAction.php +++ b/src/Action/CaptureAction.php @@ -1,5 +1,11 @@ apiClass = ImojeApi::class; } - public function execute($request) + public function execute($request): void { RequestNotSupportedException::assertSupports($this, $request); $model = $request->getModel(); @@ -50,15 +56,15 @@ public function execute($request) throw new HttpPostRedirect( $this->api->getApiUrl(), - $orderData + $orderData, ); } public function supports($request): bool { return - $request instanceof Capture - && $request->getModel() instanceof ArrayObject; + $request instanceof Capture && + $request->getModel() instanceof ArrayObject; } private function prepareOrderData(OrderInterface $order, PaymentSecurityTokenInterface $token): array @@ -73,10 +79,10 @@ private function prepareOrderData(OrderInterface $order, PaymentSecurityTokenInt $orderData['amount'] = $order->getTotal(); $orderData['currency'] = $order->getCurrencyCode(); $orderData['orderId'] = $order->getNumber(); - $orderData['customerFirstName'] = $billingAddress->getFirstName(); - $orderData['customerLastName'] = $billingAddress->getLastName(); + $orderData['customerFirstName'] = $billingAddress?->getFirstName(); + $orderData['customerLastName'] = $billingAddress?->getLastName(); $orderData['urlReturn'] = $token->getAfterUrl(); - $orderData['customerEmail'] = $customer->getEmail(); + $orderData['customerEmail'] = $customer?->getEmail(); $orderData['signature'] = $this->signatureResolver->createSignature($orderData, $this->api->getServiceKey()); return $orderData; diff --git a/src/Action/ConvertPaymentAction.php b/src/Action/ConvertPaymentAction.php index 4ad81b5..be187f2 100644 --- a/src/Action/ConvertPaymentAction.php +++ b/src/Action/ConvertPaymentAction.php @@ -1,5 +1,11 @@ setResult((array) $details); } - public function supports($request) + public function supports($request): bool { return $request instanceof Convert && diff --git a/src/Action/NotifyAction.php b/src/Action/NotifyAction.php index f641c20..294c81e 100644 --- a/src/Action/NotifyAction.php +++ b/src/Action/NotifyAction.php @@ -1,5 +1,11 @@ request->getContent(), true); + if (null == $this->request) { + throw new \Exception('Request is empty'); + } + + /** @var string $content */ + $content = $this->request->getContent(); + $notificationData = json_decode($content, true); $transactionData = $notificationData['transaction']; $model = $request->getModel(); @@ -44,9 +56,13 @@ public function execute($request): void public function supports($request): bool { + if (null == $this->request) { + return false; + } + return - $request instanceof Notify - && $request->getModel() instanceof ArrayObject - && $this->signatureResolver->verifySignature($this->request, $this->api->getServiceKey()); + $request instanceof Notify && + $request->getModel() instanceof ArrayObject && + $this->signatureResolver->verifySignature($this->request, $this->api->getServiceKey()); } } diff --git a/src/Action/StatusAction.php b/src/Action/StatusAction.php index 05d0444..d088982 100644 --- a/src/Action/StatusAction.php +++ b/src/Action/StatusAction.php @@ -1,5 +1,11 @@ getModel() instanceof ArrayAccess - ; + ; } } diff --git a/src/Api/ImojeApi.php b/src/Api/ImojeApi.php index b4cb51b..64395fe 100644 --- a/src/Api/ImojeApi.php +++ b/src/Api/ImojeApi.php @@ -1,5 +1,11 @@ getContent()) { + if ('' !== $request->getContent()) { return new Response('', Response::HTTP_NO_CONTENT); } $paymentToken = $this->paymentTokenProvider->provideToken($request); - if (null !== $paymentToken) { - $notifyToken = $this->payum->getHttpRequestVerifier()->verify($this->createRequestWithToken($request, $paymentToken)); - $gateway = $this->payum->getGateway($notifyToken->getGatewayName()); - - $gateway->execute(new Notify($notifyToken)); - - return new JsonResponse(['status' => 'ok']); - } else { + if (null === $paymentToken) { throw new NotFoundHttpException('Payment token not found'); } + + $notifyToken = $this->payum->getHttpRequestVerifier()->verify($this->createRequestWithToken($request, $paymentToken)); + $gateway = $this->payum->getGateway($notifyToken->getGatewayName()); + + $gateway->execute(new Notify($notifyToken)); + + return new JsonResponse(['status' => 'ok']); } private function createRequestWithToken( Request $request, - PaymentSecurityTokenInterface $token + PaymentSecurityTokenInterface $token, ): Request { $request = Request::create( $token->getTargetUrl(), @@ -49,11 +58,11 @@ private function createRequestWithToken( $request->cookies->all(), $request->files->all(), $request->server->all(), - $request->getContent() + $request->getContent(), ); $request->attributes->add([ - 'payum_token' => $token->getHash() + 'payum_token' => $token->getHash(), ]); return $request; diff --git a/src/DependencyInjection/BitBagSyliusImojeExtension.php b/src/DependencyInjection/BitBagSyliusImojeExtension.php index eb6b443..684b70e 100644 --- a/src/DependencyInjection/BitBagSyliusImojeExtension.php +++ b/src/DependencyInjection/BitBagSyliusImojeExtension.php @@ -1,5 +1,11 @@ add('environment', ChoiceType::class, [ + ->add( + 'environment', + ChoiceType::class, + [ 'choices' => [ 'bitbag.imoje_plugin.configuration.production' => ImojeApiInterface::PRODUCTION_ENVIRONMENT, 'bitbag.imoje_plugin.configuration.sandbox' => ImojeApiInterface::SANDBOX_ENVIRONMENT, ], - 'label' => 'bitbag.imoje_plugin.configuration.environment' - ] + 'label' => 'bitbag.imoje_plugin.configuration.environment', + ], ) ->add('merchant_id', TextType::class, [ 'label' => 'bitbag.imoje_plugin.configuration.merchant_id', @@ -30,10 +39,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) new NotBlank( [ 'message' => 'bitbag.imoje_plugin.configuration.merchant_id.not_blank', - 'groups' => ['sylius'] - ] + 'groups' => ['sylius'], + ], ), - ] + ], ]) ->add('service_id', TextType::class, [ 'label' => 'bitbag.imoje_plugin.configuration.service_id', @@ -41,10 +50,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) new NotBlank( [ 'message' => 'bitbag.imoje_plugin.configuration.service_id.not_blank', - 'groups' => ['sylius'] - ] + 'groups' => ['sylius'], + ], ), - ] + ], ]) ->add('service_key', TextType::class, [ 'label' => 'bitbag.imoje_plugin.configuration.service_key', @@ -52,10 +61,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) new NotBlank( [ 'message' => 'bitbag.imoje_plugin.configuration.service_key.not_blank', - 'groups' => ['sylius'] - ] + 'groups' => ['sylius'], + ], ), - ] + ], ]) ->add('authorization_token', TextType::class, [ 'label' => 'bitbag.imoje_plugin.configuration.authorization_token', @@ -63,10 +72,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) new NotBlank( [ 'message' => 'bitbag.imoje_plugin.configuration.authorization_token.not_blank', - 'groups' => ['sylius'] - ] + 'groups' => ['sylius'], + ], ), - ] + ], ]); } } diff --git a/src/ImojeGatewayFactory.php b/src/ImojeGatewayFactory.php index fbd9779..b83a070 100644 --- a/src/ImojeGatewayFactory.php +++ b/src/ImojeGatewayFactory.php @@ -1,5 +1,11 @@ defaults( [ 'payum.factory_name' => 'imoje', - 'payum.factory_title' => 'Imoje' - ] + 'payum.factory_title' => 'Imoje', + ], ); if (false === (bool) $config['payum.api']) { diff --git a/src/Provider/PaymentTokenProvider.php b/src/Provider/PaymentTokenProvider.php index c4ae7f5..bbdde64 100644 --- a/src/Provider/PaymentTokenProvider.php +++ b/src/Provider/PaymentTokenProvider.php @@ -1,9 +1,16 @@ getContent(); $content = json_decode($content, true); $transactionData = $content['transaction']; + /** @var OrderInterface $order */ $order = $this->getOrder($transactionData); - foreach ($order->getPayments() as $payment) { + /** @var Collection $payments */ + $payments = $order->getPayments(); + + foreach ($payments as $payment) { $model = $payment->getDetails(); $tokenHash = $model['tokenHash'] ?? null; if ( - null !== $tokenHash - && $payment->getState() !== PaymentInterface::STATE_CANCELLED - && $payment->getState() !== PaymentInterface::STATE_FAILED + null !== $tokenHash && + $payment->getState() !== PaymentInterface::STATE_CANCELLED && + $payment->getState() !== PaymentInterface::STATE_FAILED ) { return $this->getToken($tokenHash); } @@ -45,11 +58,17 @@ public function provideToken(Request $request): ?PaymentSecurityTokenInterface private function getOrder(array $transactionData): ?OrderInterface { - return $this->orderRepository->findOneBy(['number' => $transactionData['orderId']]); + /** @var OrderInterface|null $order */ + $order = $this->orderRepository->findOneBy(['number' => $transactionData['orderId']]); + + return $order; } private function getToken(string $hash): ?PaymentSecurityTokenInterface { - return $this->paymentTokenRepository->findOneBy(['hash' => $hash]); + /** @var PaymentSecurityTokenInterface|null $token */ + $token = $this->paymentTokenRepository->findOneBy(['hash' => $hash]); + + return $token; } } diff --git a/src/Provider/PaymentTokenProviderInterface.php b/src/Provider/PaymentTokenProviderInterface.php index a02861f..8ba4481 100644 --- a/src/Provider/PaymentTokenProviderInterface.php +++ b/src/Provider/PaymentTokenProviderInterface.php @@ -1,5 +1,11 @@ $value], '', '&'); $key = ''; } + return $key !== '' ? "$key=$value" : $value; }, array_keys($fields), $fields); @@ -28,12 +36,16 @@ public function createSignature(array $fields, string $serviceKey): string public function verifySignature(Request $request, string $serviceKey): bool { + /** @var string $headerSignature */ $headerSignature = $request->headers->get('X-Imoje-Signature'); $body = $request->getContent(); $parts = []; parse_str(str_replace([';', '='], ['&', '='], $headerSignature), $parts); + Assert::keyExists($parts, 'alg'); + Assert::string($parts['alg']); + $ownSignature = hash($parts['alg'], $body . $serviceKey); return $ownSignature === $parts['signature']; diff --git a/src/Resolver/SignatureResolverInterface.php b/src/Resolver/SignatureResolverInterface.php index ee07c15..1722aee 100644 --- a/src/Resolver/SignatureResolverInterface.php +++ b/src/Resolver/SignatureResolverInterface.php @@ -1,5 +1,11 @@