diff --git a/app/code/Magento/Paypal/Controller/Payflow/ReturnUrl.php b/app/code/Magento/Paypal/Controller/Payflow/ReturnUrl.php
index d2a14febe54dd..5a32269b1b795 100644
--- a/app/code/Magento/Paypal/Controller/Payflow/ReturnUrl.php
+++ b/app/code/Magento/Paypal/Controller/Payflow/ReturnUrl.php
@@ -18,6 +18,10 @@
*/
class ReturnUrl extends Payflow implements CsrfAwareActionInterface, HttpGetActionInterface
{
+ private const ORDER_INCREMENT_ID = 'INVNUM';
+
+ private const SILENT_POST_HASH = 'secure_silent_post_hash';
+
/**
* @var array of allowed order states on frontend
*/
@@ -63,23 +67,18 @@ public function execute()
$this->_view->loadLayout(false);
/** @var \Magento\Checkout\Block\Onepage\Success $redirectBlock */
$redirectBlock = $this->_view->getLayout()->getBlock($this->_redirectBlockName);
-
- if ($this->_checkoutSession->getLastRealOrderId()) {
- /** @var \Magento\Sales\Model\Order $order */
- $order = $this->_orderFactory->create()->loadByIncrementId($this->_checkoutSession->getLastRealOrderId());
-
- if ($order->getIncrementId()) {
- if ($this->checkOrderState($order)) {
- $redirectBlock->setData('goto_success_page', true);
+ $order = $this->getOrderFromRequest();
+ if ($order) {
+ if ($this->checkOrderState($order)) {
+ $redirectBlock->setData('goto_success_page', true);
+ } else {
+ if ($this->checkPaymentMethod($order)) {
+ $gotoSection = $this->_cancelPayment((string)$this->getRequest()->getParam('RESPMSG'));
+ $redirectBlock->setData('goto_section', $gotoSection);
+ $redirectBlock->setData('error_msg', __('Your payment has been declined. Please try again.'));
} else {
- if ($this->checkPaymentMethod($order)) {
- $gotoSection = $this->_cancelPayment((string)$this->getRequest()->getParam('RESPMSG'));
- $redirectBlock->setData('goto_section', $gotoSection);
- $redirectBlock->setData('error_msg', __('Your payment has been declined. Please try again.'));
- } else {
- $redirectBlock->setData('goto_section', false);
- $redirectBlock->setData('error_msg', __('Requested payment method does not match with order.'));
- }
+ $redirectBlock->setData('goto_section', false);
+ $redirectBlock->setData('error_msg', __('Requested payment method does not match with order.'));
}
}
}
@@ -87,6 +86,29 @@ public function execute()
$this->_view->renderLayout();
}
+ /**
+ * Returns an order from request.
+ *
+ * @return Order|null
+ */
+ private function getOrderFromRequest(): ?Order
+ {
+ $orderId = $this->getRequest()->getParam(self::ORDER_INCREMENT_ID);
+ if (!$orderId) {
+ return null;
+ }
+
+ $order = $this->_orderFactory->create()->loadByIncrementId($orderId);
+ $storedHash = (string)$order->getPayment()->getAdditionalInformation(self::SILENT_POST_HASH);
+ $requestHash = (string)$this->getRequest()->getParam('USER2');
+ if (empty($storedHash) || empty($requestHash) || !hash_equals($storedHash, $requestHash)) {
+ return null;
+ }
+ $this->_checkoutSession->setLastRealOrderId($orderId);
+
+ return $order;
+ }
+
/**
* Check order state
*
diff --git a/app/code/Magento/Paypal/Plugin/TransparentSessionChecker.php b/app/code/Magento/Paypal/Plugin/TransparentSessionChecker.php
index d53fd183c1942..5d950f6c346e5 100644
--- a/app/code/Magento/Paypal/Plugin/TransparentSessionChecker.php
+++ b/app/code/Magento/Paypal/Plugin/TransparentSessionChecker.php
@@ -20,6 +20,8 @@ class TransparentSessionChecker
*/
private $disableSessionUrls = [
'paypal/transparent/redirect',
+ 'paypal/payflowadvanced/returnUrl',
+ 'paypal/payflow/returnUrl',
'paypal/hostedpro/return',
];
diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php
index 2b1d6526ab2a8..5e27a33d1201b 100644
--- a/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php
+++ b/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php
@@ -32,6 +32,8 @@ class ReturnUrlTest extends TestCase
{
const LAST_REAL_ORDER_ID = '000000001';
+ const SILENT_POST_HASH = 'abcdfg';
+
/**
* @var ReturnUrl
*/
@@ -142,7 +144,7 @@ protected function setUp(): void
$this->checkoutSession = $this->getMockBuilder(Session::class)
->disableOriginalConstructor()
- ->setMethods(['getLastRealOrderId', 'getLastRealOrder', 'restoreQuote'])
+ ->setMethods(['setLastRealOrderId', 'getLastRealOrder', 'restoreQuote'])
->getMock();
$this->paymentFailures = $this->getMockBuilder(PaymentFailuresInterface::class)
@@ -177,8 +179,15 @@ public function testExecuteAllowedOrderState($state)
$this->withLayout();
$this->withOrder(self::LAST_REAL_ORDER_ID, $state);
- $this->checkoutSession->method('getLastRealOrderId')
- ->willReturn(self::LAST_REAL_ORDER_ID);
+ $this->request->method('getParam')
+ ->willReturnMap([
+ ['INVNUM', self::LAST_REAL_ORDER_ID],
+ ['USER2', self::SILENT_POST_HASH],
+ ]);
+
+ $this->checkoutSession->expects($this->once())
+ ->method('setLastRealOrderId')
+ ->with(self::LAST_REAL_ORDER_ID);
$this->block->method('setData')
->with('goto_success_page', true)
@@ -202,6 +211,45 @@ public function allowedOrderStateDataProvider()
];
}
+ /**
+ * Checks a test case when silent post hash validation fails.
+ *
+ * @param string $requestHash
+ * @param string $orderHash
+ * @dataProvider invalidHashVariations
+ */
+ public function testFailedHashValidation(string $requestHash, string $orderHash)
+ {
+ $this->withLayout();
+ $this->withOrder(self::LAST_REAL_ORDER_ID, Order::STATE_PROCESSING, $orderHash);
+
+ $this->request->method('getParam')
+ ->willReturnMap([
+ ['INVNUM', self::LAST_REAL_ORDER_ID],
+ ['USER2', $requestHash],
+ ]);
+
+ $this->checkoutSession->expects($this->never())
+ ->method('setLastRealOrderId')
+ ->with(self::LAST_REAL_ORDER_ID);
+
+ $this->returnUrl->execute();
+ }
+
+ /**
+ * Gets list of allowed order states.
+ *
+ * @return array
+ */
+ public function invalidHashVariations()
+ {
+ return [
+ ['requestHash' => '', 'orderHash' => self::SILENT_POST_HASH],
+ ['requestHash' => self::SILENT_POST_HASH, 'orderHash' => ''],
+ ['requestHash' => 'abcd', 'orderHash' => 'dcba'],
+ ];
+ }
+
/**
* Checks a test case when action processes order with not allowed state.
*
@@ -218,8 +266,11 @@ public function testExecuteNotAllowedOrderState($state, $restoreQuote, $expected
$this->withCheckoutSession(self::LAST_REAL_ORDER_ID, $restoreQuote);
$this->request->method('getParam')
- ->with('RESPMSG')
- ->willReturn($errMessage);
+ ->willReturnMap([
+ ['RESPMSG', $errMessage],
+ ['INVNUM', self::LAST_REAL_ORDER_ID],
+ ['USER2', self::SILENT_POST_HASH],
+ ]);
$this->payment->method('getMethod')
->willReturn(Config::METHOD_PAYFLOWLINK);
@@ -261,8 +312,14 @@ public function testCheckRejectByPaymentMethod()
$this->withLayout();
$this->withOrder(self::LAST_REAL_ORDER_ID, Order::STATE_NEW);
- $this->checkoutSession->method('getLastRealOrderId')
- ->willReturn(self::LAST_REAL_ORDER_ID);
+ $this->checkoutSession->expects($this->once())
+ ->method('setLastRealOrderId')
+ ->with(self::LAST_REAL_ORDER_ID);
+ $this->request->method('getParam')
+ ->willReturnMap([
+ ['INVNUM', self::LAST_REAL_ORDER_ID],
+ ['USER2', self::SILENT_POST_HASH],
+ ]);
$this->withBlockContent(false, 'Requested payment method does not match with order.');
@@ -285,8 +342,11 @@ public function testCheckXSSEscaped($errorMsg, $errorMsgEscaped)
$this->withCheckoutSession(self::LAST_REAL_ORDER_ID, true);
$this->request->method('getParam')
- ->with('RESPMSG')
- ->willReturn($errorMsg);
+ ->willReturnMap([
+ ['RESPMSG', $errorMsg],
+ ['INVNUM', self::LAST_REAL_ORDER_ID],
+ ['USER2', self::SILENT_POST_HASH],
+ ]);
$this->checkoutHelper->method('cancelCurrentOrder')
->with(self::equalTo($errorMsgEscaped));
@@ -323,8 +383,11 @@ public function testCheckAdvancedAcceptingByPaymentMethod()
$this->withCheckoutSession(self::LAST_REAL_ORDER_ID, true);
$this->request->method('getParam')
- ->with('RESPMSG')
- ->willReturn('message');
+ ->willReturnMap([
+ ['RESPMSG', 'message'],
+ ['INVNUM', self::LAST_REAL_ORDER_ID],
+ ['USER2', self::SILENT_POST_HASH],
+ ]);
$this->withBlockContent('paymentMethod', 'Your payment has been declined. Please try again.');
@@ -347,9 +410,10 @@ public function testCheckAdvancedAcceptingByPaymentMethod()
*
* @param string $incrementId
* @param string $state
+ * @param string $hash
* @return void
*/
- private function withOrder($incrementId, $state)
+ private function withOrder($incrementId, $state, $hash = self::SILENT_POST_HASH)
{
$this->orderFactory->method('create')
->willReturn($this->order);
@@ -366,6 +430,8 @@ private function withOrder($incrementId, $state)
$this->order->method('getPayment')
->willReturn($this->payment);
+ $this->payment->method('getAdditionalInformation')
+ ->willReturn($hash);
}
/**
@@ -390,8 +456,8 @@ private function withLayout()
*/
private function withCheckoutSession($orderId, $restoreQuote)
{
- $this->checkoutSession->method('getLastRealOrderId')
- ->willReturn($orderId);
+ $this->checkoutSession->method('setLastRealOrderId')
+ ->with($orderId);
$this->checkoutSession->method('getLastRealOrder')
->willReturn($this->order);
diff --git a/app/code/Magento/Paypal/etc/csp_whitelist.xml b/app/code/Magento/Paypal/etc/csp_whitelist.xml
index f9296332910e6..ee83e2864ab89 100644
--- a/app/code/Magento/Paypal/etc/csp_whitelist.xml
+++ b/app/code/Magento/Paypal/etc/csp_whitelist.xml
@@ -36,6 +36,14 @@
www.paypal.com
www.sandbox.paypal.com
+ pilot-payflowlink.paypal.com
+
+
+
+
+ www.paypal.com
+ www.sandbox.paypal.com
+ pilot-payflowlink.paypal.com
diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js
index 7fb94a7e2348e..bd779567a39b5 100644
--- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js
+++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js
@@ -74,6 +74,7 @@ define([
if (this.iframeIsLoaded) {
document.getElementById(this.getCode() + '-iframe')
.contentWindow.location.reload();
+ this.paymentReady(false);
}
this.paymentReady(true);
diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_canceled.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_canceled.php
new file mode 100644
index 0000000000000..31c666c8fc84b
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_canceled.php
@@ -0,0 +1,22 @@
+requireDataFixture('Magento/Sales/_files/order.php');
+
+$objectManager = Bootstrap::getObjectManager();
+/** @var Order $order */
+$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001');
+/** @var OrderManagementInterface $orderManagement */
+$orderManagement = $objectManager->create(OrderManagementInterface::class);
+$orderManagement->place($order);
+$orderManagement->cancel($order->getEntityId());
diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_canceled_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_canceled_rollback.php
new file mode 100644
index 0000000000000..5ac1e380561b4
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_canceled_rollback.php
@@ -0,0 +1,30 @@
+get(OrderRepositoryInterface::class);
+/** @var OrderInterface $order */
+$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001');
+/** @var Registry $registry */
+$registry = $objectManager->get(Registry::class);
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+$orderRepository->delete($order);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
+
+Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php');